mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 04:09:28 +00:00
[html] split extension (for #45900)
This commit is contained in:
489
extensions/html-language-features/server/src/htmlServerMain.ts
Normal file
489
extensions/html-language-features/server/src/htmlServerMain.ts
Normal file
@@ -0,0 +1,489 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import {
|
||||
createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, RequestType,
|
||||
DocumentRangeFormattingRequest, Disposable, DocumentSelector, TextDocumentPositionParams, ServerCapabilities,
|
||||
Position, CompletionTriggerKind, ConfigurationRequest, ConfigurationParams, DidChangeWorkspaceFoldersNotification,
|
||||
WorkspaceFolder, DocumentColorRequest, ColorInformation, ColorPresentationRequest
|
||||
} from 'vscode-languageserver';
|
||||
import { TextDocument, Diagnostic, DocumentLink, SymbolInformation, CompletionList } from 'vscode-languageserver-types';
|
||||
import { getLanguageModes, LanguageModes, Settings } 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, runSafeAsync } from './utils/runner';
|
||||
import { doComplete as emmetDoComplete, updateExtensionsPath as updateEmmetExtensionsPath, getEmmetCompletionParticipants } from 'vscode-emmet-helper';
|
||||
import { getPathCompletionParticipant } from './modes/pathCompletion';
|
||||
|
||||
import { FoldingRangesRequest, FoldingProviderServerCapabilities } from './protocol/foldingProvider.proposed';
|
||||
import { getFoldingRegions } from './modes/htmlFolding';
|
||||
|
||||
namespace TagCloseRequest {
|
||||
export const type: RequestType<TextDocumentPositionParams, string | null, any, any> = new RequestType('html/tag');
|
||||
}
|
||||
|
||||
// Create a connection for the server
|
||||
const connection: IConnection = createConnection();
|
||||
|
||||
console.log = connection.console.log.bind(connection.console);
|
||||
console.error = connection.console.error.bind(connection.console);
|
||||
|
||||
process.on('unhandledRejection', (e: any) => {
|
||||
console.error(formatError(`Unhandled exception`, e));
|
||||
});
|
||||
process.on('uncaughtException', (e: any) => {
|
||||
console.error(formatError(`Unhandled exception`, e));
|
||||
});
|
||||
|
||||
// Create a simple text document manager. The text document manager
|
||||
// supports full document sync only
|
||||
const documents: TextDocuments = new TextDocuments();
|
||||
// Make the text document manager listen on the connection
|
||||
// for open, change and close text document events
|
||||
documents.listen(connection);
|
||||
|
||||
let workspaceFolders: WorkspaceFolder[] = [];
|
||||
|
||||
var languageModes: LanguageModes;
|
||||
|
||||
let clientSnippetSupport = false;
|
||||
let clientDynamicRegisterSupport = false;
|
||||
let scopedSettingsSupport = false;
|
||||
let workspaceFoldersSupport = false;
|
||||
|
||||
var 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) {
|
||||
let scopeUri = textDocument.uri;
|
||||
let 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(void 0);
|
||||
}
|
||||
|
||||
let emmetSettings: any = {};
|
||||
let currentEmmetExtensionsPath: string;
|
||||
const emmetTriggerCharacters = ['!', '.', '}', ':', '*', '$', ']', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
|
||||
// After the server has started the client sends an initilize request. The server receives
|
||||
// in the passed params the rootPath of the workspace plus the client capabilites
|
||||
connection.onInitialize((params: InitializeParams): InitializeResult => {
|
||||
let initializationOptions = params.initializationOptions;
|
||||
|
||||
workspaceFolders = (<any>params).workspaceFolders;
|
||||
if (!Array.isArray(workspaceFolders)) {
|
||||
workspaceFolders = [];
|
||||
if (params.rootPath) {
|
||||
workspaceFolders.push({ name: '', uri: uri.file(params.rootPath).toString() });
|
||||
}
|
||||
}
|
||||
|
||||
languageModes = getLanguageModes(initializationOptions ? initializationOptions.embeddedLanguages : { css: true, javascript: true });
|
||||
documents.onDidClose(e => {
|
||||
languageModes.onDocumentRemoved(e.document);
|
||||
});
|
||||
connection.onShutdown(() => {
|
||||
languageModes.dispose();
|
||||
});
|
||||
|
||||
function hasClientCapability(...keys: string[]) {
|
||||
let c = <any>params.capabilities;
|
||||
for (let i = 0; c && i < keys.length; i++) {
|
||||
c = c[keys[i]];
|
||||
}
|
||||
return !!c;
|
||||
}
|
||||
|
||||
clientSnippetSupport = hasClientCapability('textDocument', 'completion', 'completionItem', 'snippetSupport');
|
||||
clientDynamicRegisterSupport = hasClientCapability('workspace', 'symbol', 'dynamicRegistration');
|
||||
scopedSettingsSupport = hasClientCapability('workspace', 'configuration');
|
||||
workspaceFoldersSupport = hasClientCapability('workspace', 'workspaceFolders');
|
||||
let capabilities: ServerCapabilities & FoldingProviderServerCapabilities = {
|
||||
// Tell the client that the server works in FULL text document sync mode
|
||||
textDocumentSync: documents.syncKind,
|
||||
completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: [...emmetTriggerCharacters, '.', ':', '<', '"', '=', '/'] } : undefined,
|
||||
hoverProvider: true,
|
||||
documentHighlightProvider: true,
|
||||
documentRangeFormattingProvider: false,
|
||||
documentLinkProvider: { resolveProvider: false },
|
||||
documentSymbolProvider: true,
|
||||
definitionProvider: true,
|
||||
signatureHelpProvider: { triggerCharacters: ['('] },
|
||||
referencesProvider: true,
|
||||
colorProvider: true,
|
||||
foldingProvider: true
|
||||
};
|
||||
return { capabilities };
|
||||
});
|
||||
|
||||
connection.onInitialized((p) => {
|
||||
if (workspaceFoldersSupport) {
|
||||
connection.client.register(DidChangeWorkspaceFoldersNotification.type);
|
||||
|
||||
connection.onNotification(DidChangeWorkspaceFoldersNotification.type, e => {
|
||||
let toAdd = e.event.added;
|
||||
let toRemove = e.event.removed;
|
||||
let updatedFolders = [];
|
||||
if (workspaceFolders) {
|
||||
for (let 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
languageModes.getAllModes().forEach(m => {
|
||||
if (m.configure) {
|
||||
m.configure(change.settings);
|
||||
}
|
||||
});
|
||||
documents.all().forEach(triggerValidation);
|
||||
|
||||
// dynamically enable & disable the formatter
|
||||
if (clientDynamicRegisterSupport) {
|
||||
let enableFormatter = globalSettings && globalSettings.html && globalSettings.html.format && globalSettings.html.format.enable;
|
||||
if (enableFormatter) {
|
||||
if (!formatterRegistration) {
|
||||
let documentSelector: DocumentSelector = [{ language: 'html' }, { language: 'handlebars' }]; // don't register razor, the formatter does more harm than good
|
||||
formatterRegistration = connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector });
|
||||
}
|
||||
} else if (formatterRegistration) {
|
||||
formatterRegistration.then(r => r.dispose());
|
||||
formatterRegistration = null;
|
||||
}
|
||||
}
|
||||
|
||||
emmetSettings = globalSettings.emmet || {};
|
||||
if (currentEmmetExtensionsPath !== emmetSettings['extensionsPath']) {
|
||||
currentEmmetExtensionsPath = emmetSettings['extensionsPath'];
|
||||
const workspaceUri = (workspaceFolders && workspaceFolders.length === 1) ? uri.parse(workspaceFolders[0].uri) : null;
|
||||
updateEmmetExtensionsPath(currentEmmetExtensionsPath, workspaceUri ? workspaceUri.fsPath : undefined);
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
let 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) {
|
||||
let 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 {
|
||||
let version = textDocument.version;
|
||||
let diagnostics: Diagnostic[] = [];
|
||||
if (textDocument.languageId === 'html') {
|
||||
let modes = languageModes.getAllModesInDocument(textDocument);
|
||||
let settings = await getDocumentSettings(textDocument, () => modes.some(m => !!m.doValidation));
|
||||
textDocument = documents.get(textDocument.uri);
|
||||
if (textDocument && textDocument.version === version) { // check no new version has come in after in after the async op
|
||||
modes.forEach(mode => {
|
||||
if (mode.doValidation && isValidationEnabled(mode.getId(), settings)) {
|
||||
pushAll(diagnostics, mode.doValidation(textDocument, settings));
|
||||
}
|
||||
});
|
||||
connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e));
|
||||
}
|
||||
}
|
||||
|
||||
let cachedCompletionList: CompletionList | null;
|
||||
const hexColorRegex = /^#[\d,a-f,A-F]{1,6}$/;
|
||||
connection.onCompletion(async (textDocumentPosition, token) => {
|
||||
return runSafeAsync(async () => {
|
||||
const document = documents.get(textDocumentPosition.textDocument.uri);
|
||||
const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
|
||||
if (!mode || !mode.doComplete) {
|
||||
return { isIncomplete: true, items: [] };
|
||||
}
|
||||
const doComplete = mode.doComplete!;
|
||||
|
||||
if (cachedCompletionList
|
||||
&& !cachedCompletionList.isIncomplete
|
||||
&& (mode.getId() === 'html' || mode.getId() === 'css')
|
||||
&& textDocumentPosition.context
|
||||
&& textDocumentPosition.context.triggerKind === CompletionTriggerKind.TriggerForIncompleteCompletions
|
||||
) {
|
||||
let result: CompletionList = emmetDoComplete(document, textDocumentPosition.position, mode.getId(), emmetSettings);
|
||||
if (result && result.items) {
|
||||
result.items.push(...cachedCompletionList.items);
|
||||
} else {
|
||||
result = cachedCompletionList;
|
||||
cachedCompletionList = null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (mode.getId() !== 'html') {
|
||||
/* __GDPR__
|
||||
"html.embbedded.complete" : {
|
||||
"languageId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
connection.telemetry.logEvent({ key: 'html.embbedded.complete', value: { languageId: mode.getId() } });
|
||||
}
|
||||
|
||||
cachedCompletionList = null;
|
||||
const emmetCompletionList = CompletionList.create([], false);
|
||||
const pathCompletionList = CompletionList.create([], false);
|
||||
|
||||
const emmetCompletionParticipant = getEmmetCompletionParticipants(document, textDocumentPosition.position, mode.getId(), emmetSettings, emmetCompletionList);
|
||||
const completionParticipants = [emmetCompletionParticipant];
|
||||
// Ideally, fix this in the Language Service side
|
||||
// Check participants' methods before calling them
|
||||
if (mode.getId() === 'html') {
|
||||
const pathCompletionParticipant = getPathCompletionParticipant(document, workspaceFolders, pathCompletionList);
|
||||
completionParticipants.push(pathCompletionParticipant);
|
||||
}
|
||||
|
||||
let settings = await getDocumentSettings(document, () => doComplete.length > 2);
|
||||
let result = doComplete(document, textDocumentPosition.position, settings, completionParticipants);
|
||||
if (!result) {
|
||||
result = pathCompletionList;
|
||||
} else {
|
||||
result.items.push(...pathCompletionList.items);
|
||||
}
|
||||
if (emmetCompletionList.isIncomplete) {
|
||||
cachedCompletionList = result;
|
||||
if (hexColorRegex.test(emmetCompletionList.items[0].label) && result.items.some(x => x.label === emmetCompletionList.items[0].label)) {
|
||||
emmetCompletionList.items.shift();
|
||||
}
|
||||
return CompletionList.create([...emmetCompletionList.items, ...result.items], emmetCompletionList.isIncomplete || result.isIncomplete);
|
||||
}
|
||||
return result;
|
||||
|
||||
}, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onCompletionResolve((item, token) => {
|
||||
return runSafe(() => {
|
||||
let data = item.data;
|
||||
if (data && data.languageId && data.uri) {
|
||||
let mode = languageModes.getMode(data.languageId);
|
||||
let 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(() => {
|
||||
let document = documents.get(textDocumentPosition.textDocument.uri);
|
||||
let 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(() => {
|
||||
let document = documents.get(documentHighlightParams.textDocument.uri);
|
||||
let 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(() => {
|
||||
let document = documents.get(definitionParams.textDocument.uri);
|
||||
let 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(() => {
|
||||
let document = documents.get(referenceParams.textDocument.uri);
|
||||
let 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(() => {
|
||||
let document = documents.get(signatureHelpParms.textDocument.uri);
|
||||
let 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 runSafeAsync(async () => {
|
||||
let document = documents.get(formatParams.textDocument.uri);
|
||||
let settings = await getDocumentSettings(document, () => true);
|
||||
if (!settings) {
|
||||
settings = globalSettings;
|
||||
}
|
||||
let unformattedTags: string = settings && settings.html && settings.html.format && settings.html.format.unformatted || '';
|
||||
let enabledModes = { css: !unformattedTags.match(/\bstyle\b/), javascript: !unformattedTags.match(/\bscript\b/) };
|
||||
|
||||
return format(languageModes, document, formatParams.range, formatParams.options, settings, enabledModes);
|
||||
}, [], `Error while formatting range for ${formatParams.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onDocumentLinks((documentLinkParam, token) => {
|
||||
return runSafe(() => {
|
||||
let document = documents.get(documentLinkParam.textDocument.uri);
|
||||
let links: DocumentLink[] = [];
|
||||
if (document) {
|
||||
let documentContext = getDocumentContext(document.uri, workspaceFolders);
|
||||
languageModes.getAllModesInDocument(document).forEach(m => {
|
||||
if (m.findDocumentLinks) {
|
||||
pushAll(links, m.findDocumentLinks(document, documentContext));
|
||||
}
|
||||
});
|
||||
}
|
||||
return links;
|
||||
}, [], `Error while document links for ${documentLinkParam.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onDocumentSymbol((documentSymbolParms, token) => {
|
||||
return runSafe(() => {
|
||||
let document = documents.get(documentSymbolParms.textDocument.uri);
|
||||
let symbols: SymbolInformation[] = [];
|
||||
languageModes.getAllModesInDocument(document).forEach(m => {
|
||||
if (m.findDocumentSymbols) {
|
||||
pushAll(symbols, m.findDocumentSymbols(document));
|
||||
}
|
||||
});
|
||||
return symbols;
|
||||
}, [], `Error while computing document symbols for ${documentSymbolParms.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onRequest(DocumentColorRequest.type, (params, token) => {
|
||||
return runSafe(() => {
|
||||
let infos: ColorInformation[] = [];
|
||||
let document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
languageModes.getAllModesInDocument(document).forEach(m => {
|
||||
if (m.findDocumentColors) {
|
||||
pushAll(infos, m.findDocumentColors(document));
|
||||
}
|
||||
});
|
||||
}
|
||||
return infos;
|
||||
}, [], `Error while computing document colors for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onRequest(ColorPresentationRequest.type, (params, token) => {
|
||||
return runSafe(() => {
|
||||
let document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
let 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(() => {
|
||||
let document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
let pos = params.position;
|
||||
if (pos.character > 0) {
|
||||
let 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.onRequest(FoldingRangesRequest.type, (params, token) => {
|
||||
return runSafe(() => {
|
||||
let document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
return getFoldingRegions(languageModes, document, params.maxRanges, token);
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing folding regions for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
|
||||
// Listen on the connection
|
||||
connection.listen();
|
||||
@@ -0,0 +1,83 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { TextDocument } from 'vscode-languageserver';
|
||||
|
||||
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 = void 0;
|
||||
if (cleanupIntervalTimeInSec > 0) {
|
||||
cleanupInterval = setInterval(() => {
|
||||
let cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000;
|
||||
let uris = Object.keys(languageModels);
|
||||
for (let uri of uris) {
|
||||
let languageModelInfo = languageModels[uri];
|
||||
if (languageModelInfo.cTime < cutoffTime) {
|
||||
delete languageModels[uri];
|
||||
nModels--;
|
||||
}
|
||||
}
|
||||
}, cleanupIntervalTimeInSec * 1000);
|
||||
}
|
||||
|
||||
return {
|
||||
get(document: TextDocument): T {
|
||||
let version = document.version;
|
||||
let languageId = document.languageId;
|
||||
let languageModelInfo = languageModels[document.uri];
|
||||
if (languageModelInfo && languageModelInfo.version === version && languageModelInfo.languageId === languageId) {
|
||||
languageModelInfo.cTime = Date.now();
|
||||
return languageModelInfo.languageModel;
|
||||
}
|
||||
let 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 (let uri in languageModels) {
|
||||
let languageModelInfo = languageModels[uri];
|
||||
if (languageModelInfo.cTime < oldestTime) {
|
||||
oldestUri = uri;
|
||||
oldestTime = languageModelInfo.cTime;
|
||||
}
|
||||
}
|
||||
if (oldestUri) {
|
||||
delete languageModels[oldestUri];
|
||||
nModels--;
|
||||
}
|
||||
}
|
||||
return languageModel;
|
||||
|
||||
},
|
||||
onDocumentRemoved(document: TextDocument) {
|
||||
let uri = document.uri;
|
||||
if (languageModels[uri]) {
|
||||
delete languageModels[uri];
|
||||
nModels--;
|
||||
}
|
||||
},
|
||||
dispose() {
|
||||
if (typeof cleanupInterval !== 'undefined') {
|
||||
clearInterval(cleanupInterval);
|
||||
cleanupInterval = void 0;
|
||||
languageModels = {};
|
||||
nModels = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache';
|
||||
import { TextDocument, Position, Range } from 'vscode-languageserver-types';
|
||||
import { getCSSLanguageService, Stylesheet, ICompletionParticipant } from 'vscode-css-languageservice';
|
||||
import { LanguageMode, Settings } from './languageModes';
|
||||
import { HTMLDocumentRegions, CSS_STYLE_RULE } from './embeddedSupport';
|
||||
import { Color } from 'vscode-languageserver';
|
||||
import { extractAbbreviation } from 'vscode-emmet-helper';
|
||||
|
||||
export function getCSSMode(documentRegions: LanguageModelCache<HTMLDocumentRegions>): LanguageMode {
|
||||
let cssLanguageService = getCSSLanguageService();
|
||||
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';
|
||||
},
|
||||
configure(options: any) {
|
||||
cssLanguageService.configure(options && options.css);
|
||||
},
|
||||
doValidation(document: TextDocument, settings?: Settings) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.doValidation(embedded, cssStylesheets.get(embedded), settings && settings.css);
|
||||
},
|
||||
doComplete(document: TextDocument, position: Position, settings?: Settings, registeredCompletionParticipants?: ICompletionParticipant[]) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
const stylesheet = cssStylesheets.get(embedded);
|
||||
|
||||
if (registeredCompletionParticipants) {
|
||||
const nonEmmetCompletionParticipants = [];
|
||||
// Css Emmet completions in html files are provided no matter where the cursor is inside the embedded css document
|
||||
// Mimic the same here, until we solve the issue of css language service not able to parse complete embedded documents when there are errors
|
||||
for (let i = 0; i < registeredCompletionParticipants.length; i++) {
|
||||
if (typeof (<any>registeredCompletionParticipants[i]).getId === 'function' && (<any>registeredCompletionParticipants[i]).getId() === 'emmet') {
|
||||
const extractedResults = extractAbbreviation(document, position, { lookAhead: false, syntax: 'css' });
|
||||
if (extractedResults && extractedResults.abbreviation) {
|
||||
registeredCompletionParticipants[i].onCssProperty({ propertyName: extractedResults.abbreviation, range: extractedResults.abbreviationRange });
|
||||
}
|
||||
} else {
|
||||
nonEmmetCompletionParticipants.push(registeredCompletionParticipants[i]);
|
||||
}
|
||||
}
|
||||
cssLanguageService.setCompletionParticipants(nonEmmetCompletionParticipants);
|
||||
}
|
||||
return cssLanguageService.doComplete(embedded, position, stylesheet);
|
||||
},
|
||||
setCompletionParticipants(registeredCompletionParticipants: ICompletionParticipant[]) {
|
||||
cssLanguageService.setCompletionParticipants(registeredCompletionParticipants);
|
||||
},
|
||||
doHover(document: TextDocument, position: Position) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.doHover(embedded, position, cssStylesheets.get(embedded));
|
||||
},
|
||||
findDocumentHighlight(document: TextDocument, position: Position) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.findDocumentHighlights(embedded, position, cssStylesheets.get(embedded));
|
||||
},
|
||||
findDocumentSymbols(document: TextDocument) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.findDocumentSymbols(embedded, cssStylesheets.get(embedded)).filter(s => s.name !== CSS_STYLE_RULE);
|
||||
},
|
||||
findDefinition(document: TextDocument, position: Position) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.findDefinition(embedded, position, cssStylesheets.get(embedded));
|
||||
},
|
||||
findReferences(document: TextDocument, position: Position) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.findReferences(embedded, position, cssStylesheets.get(embedded));
|
||||
},
|
||||
findDocumentColors(document: TextDocument) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.findDocumentColors(embedded, cssStylesheets.get(embedded));
|
||||
},
|
||||
getColorPresentations(document: TextDocument, color: Color, range: Range) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.getColorPresentations(embedded, cssStylesheets.get(embedded), color, range);
|
||||
},
|
||||
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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
|
||||
import { TextDocument, Position, LanguageService, TokenType, Range } from 'vscode-html-languageservice';
|
||||
|
||||
export interface LanguageRange extends Range {
|
||||
languageId: string | undefined;
|
||||
attributeValue?: boolean;
|
||||
}
|
||||
|
||||
export interface HTMLDocumentRegions {
|
||||
getEmbeddedDocument(languageId: string, ignoreAttributeValues?: boolean): TextDocument;
|
||||
getLanguageRanges(range: Range): LanguageRange[];
|
||||
getLanguageAtPosition(position: Position): string | undefined;
|
||||
getLanguagesInDocument(): string[];
|
||||
getImportedScripts(): string[];
|
||||
}
|
||||
|
||||
export var 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)["']/.test(scanner.getTokenText())) {
|
||||
languageIdFromType = 'javascript';
|
||||
} else {
|
||||
languageIdFromType = void 0;
|
||||
}
|
||||
} 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,94 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { TextDocument, Range, TextEdit, FormattingOptions, Position } from 'vscode-languageserver-types';
|
||||
import { LanguageModes, Settings, LanguageModeRange } from './languageModes';
|
||||
import { pushAll } from '../utils/arrays';
|
||||
import { isEOL } from '../utils/strings';
|
||||
|
||||
export 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 = 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 = 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 = 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,185 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
import { TextDocument, CancellationToken, Position } from 'vscode-languageserver';
|
||||
import { LanguageService as HTMLLanguageService, TokenType, Range } from 'vscode-html-languageservice';
|
||||
|
||||
import { FoldingRangeType, FoldingRange, FoldingRangeList } from '../protocol/foldingProvider.proposed';
|
||||
import { LanguageModes } from './languageModes';
|
||||
import { binarySearch } from '../utils/arrays';
|
||||
|
||||
export function getFoldingRegions(languageModes: LanguageModes, document: TextDocument, maxRanges: number | undefined, cancellationToken: CancellationToken | null): FoldingRangeList {
|
||||
let htmlMode = languageModes.getMode('html');
|
||||
let range = Range.create(Position.create(0, 0), Position.create(document.lineCount, 0));
|
||||
let ranges: FoldingRange[] = [];
|
||||
if (htmlMode && htmlMode.getFoldingRanges) {
|
||||
ranges.push(...htmlMode.getFoldingRanges(document, range));
|
||||
}
|
||||
let modeRanges = languageModes.getModesInRange(document, range);
|
||||
for (let modeRange of modeRanges) {
|
||||
let mode = modeRange.mode;
|
||||
if (mode && mode !== htmlMode && mode.getFoldingRanges && !modeRange.attributeValue) {
|
||||
ranges.push(...mode.getFoldingRanges(document, modeRange));
|
||||
}
|
||||
}
|
||||
if (maxRanges && ranges.length > maxRanges) {
|
||||
ranges = limitRanges(ranges, maxRanges);
|
||||
}
|
||||
return { ranges };
|
||||
}
|
||||
|
||||
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 = void 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
return ranges.filter((r, index) => (typeof nestingLevels[index] === 'number') && nestingLevels[index] < maxLevel);
|
||||
}
|
||||
|
||||
export const EMPTY_ELEMENTS: string[] = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'];
|
||||
|
||||
export function isEmptyElement(e: string): boolean {
|
||||
return !!e && binarySearch(EMPTY_ELEMENTS, e.toLowerCase(), (s1: string, s2: string) => s1.localeCompare(s2)) >= 0;
|
||||
}
|
||||
|
||||
export function getHTMLFoldingRegions(htmlLanguageService: HTMLLanguageService, document: TextDocument, range: Range): FoldingRange[] {
|
||||
const scanner = htmlLanguageService.createScanner(document.getText());
|
||||
let token = scanner.scan();
|
||||
let ranges: FoldingRange[] = [];
|
||||
let stack: { startLine: number, tagName: string }[] = [];
|
||||
let lastTagName = null;
|
||||
let prevStart = -1;
|
||||
|
||||
function addRange(range: FoldingRange) {
|
||||
ranges.push(range);
|
||||
prevStart = range.startLine;
|
||||
}
|
||||
|
||||
while (token !== TokenType.EOS) {
|
||||
switch (token) {
|
||||
case TokenType.StartTag: {
|
||||
let tagName = scanner.getTokenText();
|
||||
let startLine = document.positionAt(scanner.getTokenOffset()).line;
|
||||
stack.push({ startLine, tagName });
|
||||
lastTagName = tagName;
|
||||
break;
|
||||
}
|
||||
case TokenType.EndTag: {
|
||||
lastTagName = scanner.getTokenText();
|
||||
break;
|
||||
}
|
||||
case TokenType.StartTagClose:
|
||||
if (!lastTagName || !isEmptyElement(lastTagName)) {
|
||||
break;
|
||||
}
|
||||
// fallthrough
|
||||
case TokenType.EndTagClose:
|
||||
case TokenType.StartTagSelfClose: {
|
||||
let i = stack.length - 1;
|
||||
while (i >= 0 && stack[i].tagName !== lastTagName) {
|
||||
i--;
|
||||
}
|
||||
if (i >= 0) {
|
||||
let stackElement = stack[i];
|
||||
stack.length = i;
|
||||
let line = document.positionAt(scanner.getTokenOffset()).line;
|
||||
let startLine = stackElement.startLine;
|
||||
let endLine = line - 1;
|
||||
if (endLine > startLine && prevStart !== startLine) {
|
||||
addRange({ startLine, endLine });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TokenType.Comment: {
|
||||
let startLine = document.positionAt(scanner.getTokenOffset()).line;
|
||||
let text = scanner.getTokenText();
|
||||
let m = text.match(/^\s*#(region\b)|(endregion\b)/);
|
||||
if (m) {
|
||||
if (m[1]) { // start pattern match
|
||||
stack.push({ startLine, tagName: '' }); // empty tagName marks region
|
||||
} else {
|
||||
let i = stack.length - 1;
|
||||
while (i >= 0 && stack[i].tagName.length) {
|
||||
i--;
|
||||
}
|
||||
if (i >= 0) {
|
||||
let stackElement = stack[i];
|
||||
stack.length = i;
|
||||
let endLine = startLine;
|
||||
startLine = stackElement.startLine;
|
||||
if (endLine > startLine && prevStart !== startLine) {
|
||||
addRange({ startLine, endLine, type: FoldingRangeType.Region });
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let endLine = document.positionAt(scanner.getTokenOffset() + scanner.getTokenLength()).line;
|
||||
if (startLine < endLine) {
|
||||
addRange({ startLine, endLine, type: FoldingRangeType.Comment });
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
token = scanner.scan();
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { getLanguageModelCache } from '../languageModelCache';
|
||||
import { LanguageService as HTMLLanguageService, HTMLDocument, DocumentContext, FormattingOptions, HTMLFormatConfiguration, ICompletionParticipant } from 'vscode-html-languageservice';
|
||||
import { TextDocument, Position, Range } from 'vscode-languageserver-types';
|
||||
import { LanguageMode, Settings } from './languageModes';
|
||||
|
||||
import { FoldingRange } from '../protocol/foldingProvider.proposed';
|
||||
import { getHTMLFoldingRegions } from './htmlFolding';
|
||||
|
||||
export function getHTMLMode(htmlLanguageService: HTMLLanguageService): LanguageMode {
|
||||
let globalSettings: Settings = {};
|
||||
let htmlDocuments = getLanguageModelCache<HTMLDocument>(10, 60, document => htmlLanguageService.parseHTMLDocument(document));
|
||||
let completionParticipants: ICompletionParticipant[] = [];
|
||||
return {
|
||||
getId() {
|
||||
return 'html';
|
||||
},
|
||||
configure(options: any) {
|
||||
globalSettings = options;
|
||||
},
|
||||
doComplete(document: TextDocument, position: Position, settings: Settings = globalSettings, registeredCompletionParticipants?: ICompletionParticipant[]) {
|
||||
if (registeredCompletionParticipants) {
|
||||
completionParticipants = registeredCompletionParticipants;
|
||||
}
|
||||
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);
|
||||
htmlLanguageService.setCompletionParticipants(completionParticipants);
|
||||
|
||||
return htmlLanguageService.doComplete(document, position, htmlDocument, options);
|
||||
},
|
||||
setCompletionParticipants(registeredCompletionParticipants: any[]) {
|
||||
completionParticipants = registeredCompletionParticipants;
|
||||
},
|
||||
doHover(document: TextDocument, position: Position) {
|
||||
return htmlLanguageService.doHover(document, position, htmlDocuments.get(document));
|
||||
},
|
||||
findDocumentHighlight(document: TextDocument, position: Position) {
|
||||
return htmlLanguageService.findDocumentHighlights(document, position, htmlDocuments.get(document));
|
||||
},
|
||||
findDocumentLinks(document: TextDocument, documentContext: DocumentContext) {
|
||||
return htmlLanguageService.findDocumentLinks(document, documentContext);
|
||||
},
|
||||
findDocumentSymbols(document: TextDocument) {
|
||||
return htmlLanguageService.findDocumentSymbols(document, htmlDocuments.get(document));
|
||||
},
|
||||
format(document: TextDocument, range: Range, formatParams: FormattingOptions, settings: Settings = globalSettings) {
|
||||
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);
|
||||
},
|
||||
getFoldingRanges(document: TextDocument, range: Range): FoldingRange[] {
|
||||
return getHTMLFoldingRegions(htmlLanguageService, document, range);
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
onDocumentRemoved(document: TextDocument) {
|
||||
htmlDocuments.onDocumentRemoved(document);
|
||||
},
|
||||
dispose() {
|
||||
htmlDocuments.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function merge(src: any, dst: any): any {
|
||||
for (var key in src) {
|
||||
if (src.hasOwnProperty(key)) {
|
||||
dst[key] = src[key];
|
||||
}
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
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 } from 'vscode-languageserver-types';
|
||||
import { LanguageMode, Settings } from './languageModes';
|
||||
import { getWordAtText, startsWith, isWhitespaceOnly, repeat } from '../utils/strings';
|
||||
import { HTMLDocumentRegions } from './embeddedSupport';
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import { join } from 'path';
|
||||
import { FoldingRange, FoldingRangeType } from '../protocol/foldingProvider.proposed';
|
||||
|
||||
const FILE_NAME = 'vscode://javascript/1'; // the same 'file' is used for all contents
|
||||
const JQUERY_D_TS = join(__dirname, '../../lib/jquery.d.ts');
|
||||
|
||||
const JS_WORD_REGEX = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g;
|
||||
|
||||
export function getJavascriptMode(documentRegions: LanguageModelCache<HTMLDocumentRegions>): LanguageMode {
|
||||
let jsDocuments = getLanguageModelCache<TextDocument>(10, 60, document => documentRegions.get(document).getEmbeddedDocument('javascript'));
|
||||
|
||||
let compilerOptions: ts.CompilerOptions = { allowNonTsExtensions: true, allowJs: true, lib: ['lib.es6.d.ts'], target: ts.ScriptTarget.Latest, moduleResolution: ts.ModuleResolutionKind.Classic };
|
||||
let currentTextDocument: TextDocument;
|
||||
let scriptFileVersion: number = 0;
|
||||
function updateCurrentTextDocument(doc: TextDocument) {
|
||||
if (!currentTextDocument || doc.uri !== currentTextDocument.uri || doc.version !== currentTextDocument.version) {
|
||||
currentTextDocument = jsDocuments.get(doc);
|
||||
scriptFileVersion++;
|
||||
}
|
||||
}
|
||||
const host: ts.LanguageServiceHost = {
|
||||
getCompilationSettings: () => compilerOptions,
|
||||
getScriptFileNames: () => [FILE_NAME, JQUERY_D_TS],
|
||||
getScriptKind: () => ts.ScriptKind.JS,
|
||||
getScriptVersion: (fileName: string) => {
|
||||
if (fileName === FILE_NAME) {
|
||||
return String(scriptFileVersion);
|
||||
}
|
||||
return '1'; // default lib an jquery.d.ts are static
|
||||
},
|
||||
getScriptSnapshot: (fileName: string) => {
|
||||
let text = '';
|
||||
if (startsWith(fileName, 'vscode:')) {
|
||||
if (fileName === FILE_NAME) {
|
||||
text = currentTextDocument.getText();
|
||||
}
|
||||
} else {
|
||||
text = ts.sys.readFile(fileName) || '';
|
||||
}
|
||||
return {
|
||||
getText: (start, end) => text.substring(start, end),
|
||||
getLength: () => text.length,
|
||||
getChangeRange: () => void 0
|
||||
};
|
||||
},
|
||||
getCurrentDirectory: () => '',
|
||||
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options)
|
||||
};
|
||||
let jsLanguageService = ts.createLanguageService(host);
|
||||
|
||||
let globalSettings: Settings = {};
|
||||
|
||||
return {
|
||||
getId() {
|
||||
return 'javascript';
|
||||
},
|
||||
configure(options: any) {
|
||||
globalSettings = options;
|
||||
},
|
||||
doValidation(document: TextDocument): Diagnostic[] {
|
||||
updateCurrentTextDocument(document);
|
||||
const syntaxDiagnostics = jsLanguageService.getSyntacticDiagnostics(FILE_NAME);
|
||||
const semanticDiagnostics = jsLanguageService.getSemanticDiagnostics(FILE_NAME);
|
||||
return syntaxDiagnostics.concat(semanticDiagnostics).map((diag: ts.Diagnostic): Diagnostic => {
|
||||
return {
|
||||
range: convertRange(currentTextDocument, diag),
|
||||
severity: DiagnosticSeverity.Error,
|
||||
source: 'js',
|
||||
message: ts.flattenDiagnosticMessageText(diag.messageText, '\n')
|
||||
};
|
||||
});
|
||||
},
|
||||
doComplete(document: TextDocument, position: Position): CompletionList {
|
||||
updateCurrentTextDocument(document);
|
||||
let offset = currentTextDocument.offsetAt(position);
|
||||
let completions = jsLanguageService.getCompletionsAtPosition(FILE_NAME, offset, { includeExternalModuleExports: false, includeInsertTextCompletions: false });
|
||||
if (!completions) {
|
||||
return { isIncomplete: false, items: [] };
|
||||
}
|
||||
let replaceRange = convertRange(currentTextDocument, getWordAtText(currentTextDocument.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: 'javascript',
|
||||
uri: document.uri,
|
||||
offset: offset
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
},
|
||||
doResolve(document: TextDocument, item: CompletionItem): CompletionItem {
|
||||
updateCurrentTextDocument(document);
|
||||
let details = jsLanguageService.getCompletionEntryDetails(FILE_NAME, item.data.offset, item.label, undefined, undefined);
|
||||
if (details) {
|
||||
item.detail = ts.displayPartsToString(details.displayParts);
|
||||
item.documentation = ts.displayPartsToString(details.documentation);
|
||||
delete item.data;
|
||||
}
|
||||
return item;
|
||||
},
|
||||
doHover(document: TextDocument, position: Position): Hover | null {
|
||||
updateCurrentTextDocument(document);
|
||||
let info = jsLanguageService.getQuickInfoAtPosition(FILE_NAME, currentTextDocument.offsetAt(position));
|
||||
if (info) {
|
||||
let contents = ts.displayPartsToString(info.displayParts);
|
||||
return {
|
||||
range: convertRange(currentTextDocument, info.textSpan),
|
||||
contents: MarkedString.fromPlainText(contents)
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
doSignatureHelp(document: TextDocument, position: Position): SignatureHelp | null {
|
||||
updateCurrentTextDocument(document);
|
||||
let signHelp = jsLanguageService.getSignatureHelpItems(FILE_NAME, currentTextDocument.offsetAt(position));
|
||||
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;
|
||||
},
|
||||
findDocumentHighlight(document: TextDocument, position: Position): DocumentHighlight[] {
|
||||
updateCurrentTextDocument(document);
|
||||
let occurrences = jsLanguageService.getOccurrencesAtPosition(FILE_NAME, currentTextDocument.offsetAt(position));
|
||||
if (occurrences) {
|
||||
return occurrences.map(entry => {
|
||||
return {
|
||||
range: convertRange(currentTextDocument, entry.textSpan),
|
||||
kind: <DocumentHighlightKind>(entry.isWriteAccess ? DocumentHighlightKind.Write : DocumentHighlightKind.Text)
|
||||
};
|
||||
});
|
||||
}
|
||||
return [];
|
||||
},
|
||||
findDocumentSymbols(document: TextDocument): SymbolInformation[] {
|
||||
updateCurrentTextDocument(document);
|
||||
let items = jsLanguageService.getNavigationBarItems(FILE_NAME);
|
||||
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(currentTextDocument, 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 [];
|
||||
},
|
||||
findDefinition(document: TextDocument, position: Position): Definition | null {
|
||||
updateCurrentTextDocument(document);
|
||||
let definition = jsLanguageService.getDefinitionAtPosition(FILE_NAME, currentTextDocument.offsetAt(position));
|
||||
if (definition) {
|
||||
return definition.filter(d => d.fileName === FILE_NAME).map(d => {
|
||||
return {
|
||||
uri: document.uri,
|
||||
range: convertRange(currentTextDocument, d.textSpan)
|
||||
};
|
||||
});
|
||||
}
|
||||
return null;
|
||||
},
|
||||
findReferences(document: TextDocument, position: Position): Location[] {
|
||||
updateCurrentTextDocument(document);
|
||||
let references = jsLanguageService.getReferencesAtPosition(FILE_NAME, currentTextDocument.offsetAt(position));
|
||||
if (references) {
|
||||
return references.filter(d => d.fileName === FILE_NAME).map(d => {
|
||||
return {
|
||||
uri: document.uri,
|
||||
range: convertRange(currentTextDocument, d.textSpan)
|
||||
};
|
||||
});
|
||||
}
|
||||
return [];
|
||||
},
|
||||
format(document: TextDocument, range: Range, formatParams: FormattingOptions, settings: Settings = globalSettings): TextEdit[] {
|
||||
currentTextDocument = documentRegions.get(document).getEmbeddedDocument('javascript', true);
|
||||
scriptFileVersion++;
|
||||
|
||||
let formatterSettings = settings && settings.javascript && settings.javascript.format;
|
||||
|
||||
let initialIndentLevel = computeInitialIndent(document, range, formatParams);
|
||||
let formatSettings = convertOptions(formatParams, formatterSettings, initialIndentLevel + 1);
|
||||
let start = currentTextDocument.offsetAt(range.start);
|
||||
let end = currentTextDocument.offsetAt(range.end);
|
||||
let lastLineRange = null;
|
||||
if (range.end.line > range.start.line && (range.end.character === 0 || isWhitespaceOnly(currentTextDocument.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(FILE_NAME, 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(currentTextDocument, edit.span),
|
||||
newText: edit.newText
|
||||
});
|
||||
}
|
||||
}
|
||||
if (lastLineRange) {
|
||||
result.push({
|
||||
range: lastLineRange,
|
||||
newText: generateIndent(initialIndentLevel, formatParams)
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
getFoldingRanges(document: TextDocument, range: Range): FoldingRange[] {
|
||||
updateCurrentTextDocument(document);
|
||||
let spans = jsLanguageService.getOutliningSpans(FILE_NAME);
|
||||
let rangeStartLine = range.start.line;
|
||||
let rangeEndLine = range.end.line;
|
||||
let ranges: FoldingRange[] = [];
|
||||
for (let span of spans) {
|
||||
let curr = convertRange(currentTextDocument, span.textSpan);
|
||||
let startLine = curr.start.line;
|
||||
let endLine = curr.end.line;
|
||||
if (startLine < endLine && startLine >= rangeStartLine && endLine < rangeEndLine) {
|
||||
let foldingRange: FoldingRange = { startLine, endLine };
|
||||
let match = document.getText(curr).match(/^\s*\/(\/\s*#(?:end)?region\b)|([\*\/])/);
|
||||
if (match) {
|
||||
foldingRange.type = match[1].length ? FoldingRangeType.Region : FoldingRangeType.Comment;
|
||||
}
|
||||
ranges.push(foldingRange);
|
||||
}
|
||||
}
|
||||
return ranges;
|
||||
},
|
||||
onDocumentRemoved(document: TextDocument) {
|
||||
jsDocuments.onDocumentRemoved(document);
|
||||
},
|
||||
dispose() {
|
||||
jsLanguageService.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.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { getLanguageService as getHTMLLanguageService, DocumentContext } from 'vscode-html-languageservice';
|
||||
import {
|
||||
CompletionItem, Location, SignatureHelp, Definition, TextEdit, TextDocument, Diagnostic, DocumentLink, Range,
|
||||
Hover, DocumentHighlight, CompletionList, Position, FormattingOptions, SymbolInformation
|
||||
} from 'vscode-languageserver-types';
|
||||
import { ColorInformation, ColorPresentation, Color } from 'vscode-languageserver';
|
||||
import { FoldingRange } from '../protocol/foldingProvider.proposed';
|
||||
|
||||
import { getLanguageModelCache, LanguageModelCache } from '../languageModelCache';
|
||||
import { getDocumentRegions, HTMLDocumentRegions } from './embeddedSupport';
|
||||
import { getCSSMode } from './cssMode';
|
||||
import { getJavascriptMode } from './javascriptMode';
|
||||
import { getHTMLMode } from './htmlMode';
|
||||
|
||||
export { ColorInformation, ColorPresentation, Color };
|
||||
|
||||
export interface Settings {
|
||||
css?: any;
|
||||
html?: any;
|
||||
javascript?: any;
|
||||
emmet?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface SettingProvider {
|
||||
getDocumentSettings(textDocument: TextDocument): Thenable<Settings>;
|
||||
}
|
||||
|
||||
export interface LanguageMode {
|
||||
getId(): string;
|
||||
configure?: (options: Settings) => void;
|
||||
doValidation?: (document: TextDocument, settings?: Settings) => Diagnostic[];
|
||||
doComplete?: (document: TextDocument, position: Position, settings?: Settings, registeredCompletionParticipants?: any[]) => CompletionList | null;
|
||||
setCompletionParticipants?: (registeredCompletionParticipants: any[]) => void;
|
||||
doResolve?: (document: TextDocument, item: CompletionItem) => CompletionItem;
|
||||
doHover?: (document: TextDocument, position: Position) => Hover | null;
|
||||
doSignatureHelp?: (document: TextDocument, position: Position) => SignatureHelp | null;
|
||||
findDocumentHighlight?: (document: TextDocument, position: Position) => DocumentHighlight[];
|
||||
findDocumentSymbols?: (document: TextDocument) => SymbolInformation[];
|
||||
findDocumentLinks?: (document: TextDocument, documentContext: DocumentContext) => DocumentLink[];
|
||||
findDefinition?: (document: TextDocument, position: Position) => Definition | null;
|
||||
findReferences?: (document: TextDocument, position: Position) => Location[];
|
||||
format?: (document: TextDocument, range: Range, options: FormattingOptions, settings?: Settings) => TextEdit[];
|
||||
findDocumentColors?: (document: TextDocument) => ColorInformation[];
|
||||
getColorPresentations?: (document: TextDocument, color: Color, range: Range) => ColorPresentation[];
|
||||
doAutoClose?: (document: TextDocument, position: Position) => string | null;
|
||||
getFoldingRanges?: (document: TextDocument, range: Range) => FoldingRange[];
|
||||
onDocumentRemoved(document: TextDocument): void;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface LanguageModes {
|
||||
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; }): LanguageModes {
|
||||
|
||||
var htmlLanguageService = getHTMLLanguageService();
|
||||
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);
|
||||
if (supportedLanguages['css']) {
|
||||
modes['css'] = getCSSMode(documentRegions);
|
||||
}
|
||||
if (supportedLanguages['javascript']) {
|
||||
modes['javascript'] = getJavascriptMode(documentRegions);
|
||||
}
|
||||
return {
|
||||
getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined {
|
||||
let languageId = documentRegions.get(document).getLanguageAtPosition(position);
|
||||
if (languageId) {
|
||||
return modes[languageId];
|
||||
}
|
||||
return void 0;
|
||||
},
|
||||
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,150 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { TextDocument, CompletionList, CompletionItemKind, CompletionItem, TextEdit, Range, Position } from 'vscode-languageserver-types';
|
||||
import { WorkspaceFolder } from 'vscode-languageserver';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import URI from 'vscode-uri';
|
||||
import { ICompletionParticipant } from 'vscode-html-languageservice';
|
||||
import { startsWith } from '../utils/strings';
|
||||
import { contains } from '../utils/arrays';
|
||||
|
||||
export function getPathCompletionParticipant(
|
||||
document: TextDocument,
|
||||
workspaceFolders: WorkspaceFolder[] | undefined,
|
||||
result: CompletionList
|
||||
): ICompletionParticipant {
|
||||
return {
|
||||
onHtmlAttributeValue: ({ tag, attribute, value, range }) => {
|
||||
|
||||
if (shouldDoPathCompletion(tag, attribute, value)) {
|
||||
if (!workspaceFolders || workspaceFolders.length === 0) {
|
||||
return;
|
||||
}
|
||||
const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders);
|
||||
|
||||
const suggestions = providePathSuggestions(value, range, URI.parse(document.uri).fsPath, workspaceRoot);
|
||||
result.items = [...suggestions, ...result.items];
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function shouldDoPathCompletion(tag: string, attr: string, value: string): boolean {
|
||||
if (startsWith(value, 'http') || startsWith(value, 'https') || startsWith(value, '//')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PATH_TAG_AND_ATTR[tag]) {
|
||||
if (typeof PATH_TAG_AND_ATTR[tag] === 'string') {
|
||||
return PATH_TAG_AND_ATTR[tag] === attr;
|
||||
} else {
|
||||
return contains(<string[]>PATH_TAG_AND_ATTR[tag], attr);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function providePathSuggestions(value: string, range: Range, activeDocFsPath: string, root?: string): CompletionItem[] {
|
||||
if (startsWith(value, '/') && !root) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let replaceRange: Range;
|
||||
const lastIndexOfSlash = value.lastIndexOf('/');
|
||||
if (lastIndexOfSlash === -1) {
|
||||
replaceRange = getFullReplaceRange(range);
|
||||
} else {
|
||||
const valueAfterLastSlash = value.slice(lastIndexOfSlash + 1);
|
||||
replaceRange = getReplaceRange(range, valueAfterLastSlash);
|
||||
}
|
||||
|
||||
let parentDir: string;
|
||||
if (lastIndexOfSlash === -1) {
|
||||
parentDir = path.resolve(root);
|
||||
} else {
|
||||
const valueBeforeLastSlash = value.slice(0, lastIndexOfSlash + 1);
|
||||
|
||||
parentDir = startsWith(value, '/')
|
||||
? path.resolve(root, '.' + valueBeforeLastSlash)
|
||||
: path.resolve(activeDocFsPath, '..', valueBeforeLastSlash);
|
||||
}
|
||||
|
||||
try {
|
||||
return fs.readdirSync(parentDir).map(f => {
|
||||
if (isDir(path.resolve(parentDir, f))) {
|
||||
return {
|
||||
label: f + '/',
|
||||
kind: CompletionItemKind.Folder,
|
||||
textEdit: TextEdit.replace(replaceRange, f + '/'),
|
||||
command: {
|
||||
title: 'Suggest',
|
||||
command: 'editor.action.triggerSuggest'
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
label: f,
|
||||
kind: CompletionItemKind.File,
|
||||
textEdit: TextEdit.replace(replaceRange, f)
|
||||
};
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const isDir = (p: string) => {
|
||||
return fs.statSync(p).isDirectory();
|
||||
};
|
||||
|
||||
function resolveWorkspaceRoot(activeDoc: TextDocument, workspaceFolders: WorkspaceFolder[]): string | undefined {
|
||||
for (let i = 0; i < workspaceFolders.length; i++) {
|
||||
if (startsWith(activeDoc.uri, workspaceFolders[i].uri)) {
|
||||
return path.resolve(URI.parse(workspaceFolders[i].uri).fsPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getFullReplaceRange(valueRange: Range) {
|
||||
const start = Position.create(valueRange.end.line, valueRange.start.character + 1);
|
||||
const end = Position.create(valueRange.end.line, valueRange.end.character - 1);
|
||||
return Range.create(start, end);
|
||||
}
|
||||
function getReplaceRange(valueRange: Range, valueAfterLastSlash: string) {
|
||||
const start = Position.create(valueRange.end.line, valueRange.end.character - 1 - valueAfterLastSlash.length);
|
||||
const end = Position.create(valueRange.end.line, valueRange.end.character - 1);
|
||||
return Range.create(start, end);
|
||||
}
|
||||
|
||||
// Selected from https://stackoverflow.com/a/2725168/1780148
|
||||
const PATH_TAG_AND_ATTR: { [tag: string]: string | string[] } = {
|
||||
// HTML 4
|
||||
a: 'href',
|
||||
body: 'background',
|
||||
del: 'cite',
|
||||
form: 'action',
|
||||
frame: ['src', 'longdesc'],
|
||||
img: ['src', 'longdesc'],
|
||||
ins: 'cite',
|
||||
link: 'href',
|
||||
object: 'data',
|
||||
q: 'cite',
|
||||
script: 'src',
|
||||
// HTML 5
|
||||
audio: 'src',
|
||||
button: 'formaction',
|
||||
command: 'icon',
|
||||
embed: 'src',
|
||||
html: 'manifest',
|
||||
input: 'formaction',
|
||||
source: 'src',
|
||||
track: 'src',
|
||||
video: ['src', 'poster']
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TextDocumentIdentifier } from 'vscode-languageserver-types';
|
||||
import { RequestType, TextDocumentRegistrationOptions, StaticRegistrationOptions } from 'vscode-languageserver-protocol';
|
||||
|
||||
// ---- capabilities
|
||||
|
||||
export interface FoldingProviderClientCapabilities {
|
||||
/**
|
||||
* The text document client capabilities
|
||||
*/
|
||||
textDocument?: {
|
||||
/**
|
||||
* Capabilities specific to the foldingProvider
|
||||
*/
|
||||
foldingProvider?: {
|
||||
/**
|
||||
* Whether implementation supports dynamic registration. If this is set to `true`
|
||||
* the client supports the new `(FoldingProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions)`
|
||||
* return value for the corresponding server capability as well.
|
||||
*/
|
||||
dynamicRegistration?: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface FoldingProviderOptions {
|
||||
}
|
||||
|
||||
export interface FoldingProviderServerCapabilities {
|
||||
/**
|
||||
* The server provides folding provider support.
|
||||
*/
|
||||
foldingProvider?: FoldingProviderOptions | (FoldingProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions);
|
||||
}
|
||||
|
||||
export interface FoldingRangeList {
|
||||
/**
|
||||
* The folding ranges.
|
||||
*/
|
||||
ranges: FoldingRange[];
|
||||
}
|
||||
|
||||
export enum FoldingRangeType {
|
||||
/**
|
||||
* Folding range for a comment
|
||||
*/
|
||||
Comment = 'comment',
|
||||
/**
|
||||
* Folding range for a imports or includes
|
||||
*/
|
||||
Imports = 'imports',
|
||||
/**
|
||||
* Folding range for a region (e.g. `#region`)
|
||||
*/
|
||||
Region = 'region'
|
||||
}
|
||||
|
||||
export interface FoldingRange {
|
||||
|
||||
/**
|
||||
* The start line number
|
||||
*/
|
||||
startLine: number;
|
||||
|
||||
/**
|
||||
* The end line number
|
||||
*/
|
||||
endLine: number;
|
||||
|
||||
/**
|
||||
* The actual color value for this folding range.
|
||||
*/
|
||||
type?: FoldingRangeType | string;
|
||||
}
|
||||
|
||||
export interface FoldingRangeRequestParam {
|
||||
/**
|
||||
* The text document.
|
||||
*/
|
||||
textDocument: TextDocumentIdentifier;
|
||||
|
||||
/**
|
||||
* The maximum number of ranges to provide
|
||||
*/
|
||||
maxRanges?: number;
|
||||
}
|
||||
|
||||
export namespace FoldingRangesRequest {
|
||||
export const type: RequestType<FoldingRangeRequestParam, FoldingRangeList | null, any, any> = new RequestType('textDocument/foldingRanges');
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import * as path from 'path';
|
||||
import Uri from 'vscode-uri';
|
||||
import { TextDocument, CompletionList, CompletionItemKind, } from 'vscode-languageserver-types';
|
||||
import { getLanguageModes } from '../modes/languageModes';
|
||||
import { getPathCompletionParticipant } from '../modes/pathCompletion';
|
||||
import { WorkspaceFolder } from 'vscode-languageserver';
|
||||
|
||||
export interface ItemDescription {
|
||||
label: string;
|
||||
documentation?: string;
|
||||
kind?: CompletionItemKind;
|
||||
resultText?: string;
|
||||
notAvailable?: boolean;
|
||||
}
|
||||
|
||||
|
||||
suite('Completions', () => {
|
||||
|
||||
let assertCompletion = function (completions: CompletionList, expected: ItemDescription, document: TextDocument, offset: number) {
|
||||
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) {
|
||||
assert.equal(TextDocument.applyEdits(document, [match.textEdit]), expected.resultText);
|
||||
}
|
||||
};
|
||||
|
||||
const testUri = 'test://test/test.html';
|
||||
|
||||
function assertCompletions(value: string, expected: { count?: number, items?: ItemDescription[] }, uri = testUri, workspaceFolders?: WorkspaceFolder[]): void {
|
||||
let offset = value.indexOf('|');
|
||||
value = value.substr(0, offset) + value.substr(offset + 1);
|
||||
|
||||
let document = TextDocument.create(uri, 'html', 0, value);
|
||||
let position = document.positionAt(offset);
|
||||
|
||||
var languageModes = getLanguageModes({ css: true, javascript: true });
|
||||
var mode = languageModes.getModeAtPosition(document, position)!;
|
||||
|
||||
if (!workspaceFolders) {
|
||||
workspaceFolders = [{ name: 'x', uri: path.dirname(uri) }];
|
||||
}
|
||||
|
||||
let participantResult = CompletionList.create([]);
|
||||
if (mode.setCompletionParticipants) {
|
||||
mode.setCompletionParticipants([getPathCompletionParticipant(document, workspaceFolders, participantResult)]);
|
||||
}
|
||||
|
||||
let list = mode.doComplete!(document, position)!;
|
||||
list.items = list.items.concat(participantResult.items);
|
||||
|
||||
if (expected.count) {
|
||||
assert.equal(list.items, expected.count);
|
||||
}
|
||||
if (expected.items) {
|
||||
for (let item of expected.items) {
|
||||
assertCompletion(list, item, document, offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test('HTML Javascript Completions', function (): any {
|
||||
assertCompletions('<html><script>window.|</script></html>', {
|
||||
items: [
|
||||
{ label: 'location', resultText: '<html><script>window.location</script></html>' },
|
||||
]
|
||||
});
|
||||
assertCompletions('<html><script>$.|</script></html>', {
|
||||
items: [
|
||||
{ label: 'getJSON', resultText: '<html><script>$.getJSON</script></html>' },
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
test('Path completion', function (): any {
|
||||
let testUri = Uri.file(path.resolve(__dirname, '../../test/pathCompletionFixtures/foo.html')).fsPath;
|
||||
|
||||
assertCompletions('<div><a href="about/|">', {
|
||||
items: [
|
||||
{ label: 'about.html', resultText: '<div><a href="about/about.html">' }
|
||||
]
|
||||
}, testUri);
|
||||
// Unquoted value is not supported in language service yet
|
||||
// assertCompletions(`<div><a href=about/|>`, {
|
||||
// items: [
|
||||
// { label: 'about.html', resultText: `<div><a href=about/about.html>` }
|
||||
// ]
|
||||
// }, testUri);
|
||||
assertCompletions(`<div><a href='about/|'>`, {
|
||||
items: [
|
||||
{ label: 'about.html', resultText: `<div><a href='about/about.html'>` }
|
||||
]
|
||||
}, testUri);
|
||||
// Don't think this is a common use case
|
||||
// assertCompletions('<div><a href="about/about|.xml">', {
|
||||
// items: [
|
||||
// { label: 'about.html', resultText: '<div><a href="about/about.html">' }
|
||||
// ]
|
||||
// }, testUri);
|
||||
assertCompletions('<div><a href="about/a|">', {
|
||||
items: [
|
||||
{ label: 'about.html', resultText: '<div><a href="about/about.html">' }
|
||||
]
|
||||
}, testUri);
|
||||
// We should not prompt suggestion before user enters any trigger character
|
||||
// assertCompletions('<div><a href="|">', {
|
||||
// items: [
|
||||
// { label: 'index.html', resultText: '<div><a href="index.html">' },
|
||||
// { label: 'about', resultText: '<div><a href="about/">' }
|
||||
// ]
|
||||
// }, testUri);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { getDocumentContext } from '../utils/documentContext';
|
||||
|
||||
suite('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,128 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import * as embeddedSupport from '../modes/embeddedSupport';
|
||||
import { TextDocument } from 'vscode-languageserver-types';
|
||||
import { getLanguageService } from 'vscode-html-languageservice';
|
||||
|
||||
suite('HTML Embedded Support', () => {
|
||||
|
||||
var htmlLanguageService = getLanguageService();
|
||||
|
||||
function assertLanguageId(value: string, expectedLanguageId: string | undefined): void {
|
||||
let offset = value.indexOf('|');
|
||||
value = value.substr(0, offset) + value.substr(offset + 1);
|
||||
|
||||
let document = TextDocument.create('test://test/test.html', 'html', 0, value);
|
||||
|
||||
let position = document.positionAt(offset);
|
||||
|
||||
let docRegions = embeddedSupport.getDocumentRegions(htmlLanguageService, document);
|
||||
let languageId = docRegions.getLanguageAtPosition(position);
|
||||
|
||||
assert.equal(languageId, expectedLanguageId);
|
||||
}
|
||||
|
||||
function assertEmbeddedLanguageContent(value: string, languageId: string, expectedContent: string): void {
|
||||
let document = TextDocument.create('test://test/test.html', 'html', 0, value);
|
||||
|
||||
let docRegions = embeddedSupport.getDocumentRegions(htmlLanguageService, document);
|
||||
let 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>', void 0);
|
||||
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,67 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import { getHTMLMode } from '../modes/htmlMode';
|
||||
import { TextDocument, CompletionList } from 'vscode-languageserver-types';
|
||||
import { getLanguageModelCache } from '../languageModelCache';
|
||||
|
||||
import { getLanguageService } from 'vscode-html-languageservice';
|
||||
import * as embeddedSupport from '../modes/embeddedSupport';
|
||||
import { getEmmetCompletionParticipants } from 'vscode-emmet-helper';
|
||||
import { getCSSMode } from '../modes/cssMode';
|
||||
|
||||
suite('Emmet Support', () => {
|
||||
|
||||
const htmlLanguageService = getLanguageService();
|
||||
|
||||
function assertCompletions(syntax: string, value: string, expectedProposal: string | null, expectedProposalDoc: string | null): void {
|
||||
const offset = value.indexOf('|');
|
||||
value = value.substr(0, offset) + value.substr(offset + 1);
|
||||
|
||||
const document = TextDocument.create('test://test/test.' + syntax, syntax, 0, value);
|
||||
const position = document.positionAt(offset);
|
||||
const documentRegions = getLanguageModelCache<embeddedSupport.HTMLDocumentRegions>(10, 60, document => embeddedSupport.getDocumentRegions(htmlLanguageService, document));
|
||||
const mode = syntax === 'html' ? getHTMLMode(htmlLanguageService) : getCSSMode(documentRegions);
|
||||
|
||||
const emmetCompletionList = CompletionList.create([], false);
|
||||
mode.setCompletionParticipants!([getEmmetCompletionParticipants(document, position, document.languageId, {}, emmetCompletionList)]);
|
||||
|
||||
const list = mode.doComplete!(document, position);
|
||||
assert.ok(list);
|
||||
assert.ok(emmetCompletionList);
|
||||
|
||||
if (expectedProposal && expectedProposalDoc) {
|
||||
let actualLabels = emmetCompletionList.items.map(c => c.label).sort();
|
||||
let actualDocs = emmetCompletionList.items.map(c => c.documentation).sort();
|
||||
assert.ok(actualLabels.indexOf(expectedProposal) !== -1, 'Not found:' + expectedProposal + ' is ' + actualLabels.join(', '));
|
||||
assert.ok(actualDocs.indexOf(expectedProposalDoc) !== -1, 'Not found:' + expectedProposalDoc + ' is ' + actualDocs.join(', '));
|
||||
} else {
|
||||
assert.ok(!emmetCompletionList.items.length && !emmetCompletionList.isIncomplete);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
test('Html Emmet Completions', function (): any {
|
||||
assertCompletions('html', 'ul|', 'ul', '<ul>|</ul>');
|
||||
assertCompletions('html', '<ul|', null, null);
|
||||
assertCompletions('html', '<html>ul|</html>', 'ul', '<ul>|</ul>');
|
||||
assertCompletions('html', '<img src=|', null, null);
|
||||
assertCompletions('html', '<div class=|/>', null, null);
|
||||
});
|
||||
|
||||
test('Css Emmet Completions', function (): any {
|
||||
assertCompletions('css', '<style>.foo { display: none; m10| }</style>', 'margin: 10px;', 'margin: 10px;');
|
||||
assertCompletions('css', '<style>foo { display: none; pos:f| }</style>', 'position: fixed;', 'position: fixed;');
|
||||
assertCompletions('css', '<style>foo { display: none; margin: a| }</style>', null, null);
|
||||
assertCompletions('css', '<style>foo| { display: none; }</style>', null, null);
|
||||
assertCompletions('css', '<style>foo {| display: none; }</style>', null, null);
|
||||
assertCompletions('css', '<style>foo { display: none;| }</style>', null, null);
|
||||
assertCompletions('css', '<style>foo { display: none|; }</style>', null, null);
|
||||
assertCompletions('css', '<style>.foo { display: none; -m-m10| }</style>', 'margin: 10px;', '-moz-margin: 10px;\nmargin: 10px;');
|
||||
});
|
||||
});
|
||||
22
extensions/html-language-features/server/src/test/fixtures/expected/19813-4spaces.html
vendored
Normal file
22
extensions/html-language-features/server/src/test/fixtures/expected/19813-4spaces.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
extensions/html-language-features/server/src/test/fixtures/expected/19813-tab.html
vendored
Normal file
22
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
extensions/html-language-features/server/src/test/fixtures/expected/19813.html
vendored
Normal file
22
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
extensions/html-language-features/server/src/test/fixtures/expected/21634.html
vendored
Normal file
6
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
extensions/html-language-features/server/src/test/fixtures/inputs/19813.html
vendored
Normal file
19
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
extensions/html-language-features/server/src/test/fixtures/inputs/21634.html
vendored
Normal file
6
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,259 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import { TextDocument } from 'vscode-languageserver';
|
||||
import { getFoldingRegions } from '../modes/htmlFolding';
|
||||
import { getLanguageModes } from '../modes/languageModes';
|
||||
|
||||
interface ExpectedIndentRange {
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
function assertRanges(lines: string[], expected: ExpectedIndentRange[], message?: string, nRanges?: number): void {
|
||||
let document = TextDocument.create('test://foo/bar.json', 'json', 1, lines.join('\n'));
|
||||
let languageModes = getLanguageModes({ css: true, javascript: true });
|
||||
let actual = getFoldingRegions(languageModes, document, nRanges, null)!.ranges;
|
||||
|
||||
let actualRanges = [];
|
||||
for (let i = 0; i < actual.length; i++) {
|
||||
actualRanges[i] = r(actual[i].startLine, actual[i].endLine, actual[i].type);
|
||||
}
|
||||
actualRanges = actualRanges.sort((r1, r2) => r1.startLine - r2.startLine);
|
||||
assert.deepEqual(actualRanges, expected, message);
|
||||
}
|
||||
|
||||
function r(startLine: number, endLine: number, type?: string): ExpectedIndentRange {
|
||||
return { startLine, endLine, type };
|
||||
}
|
||||
|
||||
suite('Object Folding', () => {
|
||||
test('Fold one level', () => {
|
||||
let input = [
|
||||
/*0*/'<html>',
|
||||
/*1*/'Hello',
|
||||
/*2*/'</html>'
|
||||
];
|
||||
assertRanges(input, [r(0, 1)]);
|
||||
});
|
||||
|
||||
test('Fold two level', () => {
|
||||
let input = [
|
||||
/*0*/'<html>',
|
||||
/*1*/'<head>',
|
||||
/*2*/'Hello',
|
||||
/*3*/'</head>',
|
||||
/*4*/'</html>'
|
||||
];
|
||||
assertRanges(input, [r(0, 3), r(1, 2)]);
|
||||
});
|
||||
|
||||
test('Fold siblings', () => {
|
||||
let input = [
|
||||
/*0*/'<html>',
|
||||
/*1*/'<head>',
|
||||
/*2*/'Head',
|
||||
/*3*/'</head>',
|
||||
/*4*/'<body class="f">',
|
||||
/*5*/'Body',
|
||||
/*6*/'</body>',
|
||||
/*7*/'</html>'
|
||||
];
|
||||
assertRanges(input, [r(0, 6), r(1, 2), r(4, 5)]);
|
||||
});
|
||||
|
||||
test('Fold self-closing tags', () => {
|
||||
let input = [
|
||||
/*0*/'<div>',
|
||||
/*1*/'<a href="top"/>',
|
||||
/*2*/'<img src="s">',
|
||||
/*3*/'<br/>',
|
||||
/*4*/'<br>',
|
||||
/*5*/'<img class="c"',
|
||||
/*6*/' src="top"',
|
||||
/*7*/'>',
|
||||
/*8*/'</div>'
|
||||
];
|
||||
assertRanges(input, [r(0, 7), r(5, 6)]);
|
||||
});
|
||||
|
||||
test('Fold commment', () => {
|
||||
let input = [
|
||||
/*0*/'<!--',
|
||||
/*1*/' multi line',
|
||||
/*2*/'-->',
|
||||
/*3*/'<!-- some stuff',
|
||||
/*4*/' some more stuff -->',
|
||||
];
|
||||
assertRanges(input, [r(0, 2, 'comment'), r(3, 4, 'comment')]);
|
||||
});
|
||||
|
||||
test('Fold regions', () => {
|
||||
let input = [
|
||||
/*0*/'<!-- #region -->',
|
||||
/*1*/'<!-- #region -->',
|
||||
/*2*/'<!-- #endregion -->',
|
||||
/*3*/'<!-- #endregion -->',
|
||||
];
|
||||
assertRanges(input, [r(0, 3, 'region'), r(1, 2, 'region')]);
|
||||
});
|
||||
|
||||
test('Embedded JavaScript', () => {
|
||||
let input = [
|
||||
/*0*/'<html>',
|
||||
/*1*/'<head>',
|
||||
/*2*/'<script>',
|
||||
/*3*/'function f() {',
|
||||
/*4*/'}',
|
||||
/*5*/'</script>',
|
||||
/*6*/'</head>',
|
||||
/*7*/'</html>',
|
||||
];
|
||||
assertRanges(input, [r(0, 6), r(1, 5), r(2, 4), r(3, 4)]);
|
||||
});
|
||||
|
||||
test('Embedded JavaScript - mutiple areas', () => {
|
||||
let input = [
|
||||
/* 0*/'<html>',
|
||||
/* 1*/'<head>',
|
||||
/* 2*/'<script>',
|
||||
/* 3*/' var x = {',
|
||||
/* 4*/' foo: true,',
|
||||
/* 5*/' bar: {}',
|
||||
/* 6*/' };',
|
||||
/* 7*/'</script>',
|
||||
/* 8*/'<script>',
|
||||
/* 9*/' test(() => {',
|
||||
/*10*/' f();',
|
||||
/*11*/' });',
|
||||
/*12*/'</script>',
|
||||
/*13*/'</head>',
|
||||
/*14*/'</html>',
|
||||
];
|
||||
assertRanges(input, [r(0, 13), r(1, 12), r(2, 6), r(3, 6), r(8, 11), r(9, 11)]);
|
||||
});
|
||||
|
||||
test('Embedded JavaScript - incomplete', () => {
|
||||
let input = [
|
||||
/* 0*/'<html>',
|
||||
/* 1*/'<head>',
|
||||
/* 2*/'<script>',
|
||||
/* 3*/' var x = {',
|
||||
/* 4*/'</script>',
|
||||
/* 5*/'<script>',
|
||||
/* 6*/' });',
|
||||
/* 7*/'</script>',
|
||||
/* 8*/'</head>',
|
||||
/* 9*/'</html>',
|
||||
];
|
||||
assertRanges(input, [r(0, 8), r(1, 7), r(2, 3), r(5, 6)]);
|
||||
});
|
||||
|
||||
test('Embedded JavaScript - regions', () => {
|
||||
let 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>',
|
||||
];
|
||||
assertRanges(input, [r(0, 9), r(1, 8), r(2, 7), r(3, 7, 'region'), r(4, 6, 'region')]);
|
||||
});
|
||||
|
||||
// test('Embedded JavaScript - mulit line comment', () => {
|
||||
// let input = [
|
||||
// /* 0*/'<html>',
|
||||
// /* 1*/'<head>',
|
||||
// /* 2*/'<script>',
|
||||
// /* 3*/' /*',
|
||||
// /* 4*/' * Hello',
|
||||
// /* 5*/' */',
|
||||
// /* 6*/'</script>',
|
||||
// /* 7*/'</head>',
|
||||
// /* 8*/'</html>',
|
||||
// ];
|
||||
// assertRanges(input, [r(0, 7), r(1, 6), r(2, 5), r(3, 5, 'comment')]);
|
||||
// });
|
||||
|
||||
test('Fold incomplete', () => {
|
||||
let input = [
|
||||
/*0*/'<body>',
|
||||
/*1*/'<div></div>',
|
||||
/*2*/'Hello',
|
||||
/*3*/'</div>',
|
||||
/*4*/'</body>',
|
||||
];
|
||||
assertRanges(input, [r(0, 3)]);
|
||||
});
|
||||
|
||||
test('Fold incomplete 2', () => {
|
||||
let input = [
|
||||
/*0*/'<be><div>',
|
||||
/*1*/'<!-- #endregion -->',
|
||||
/*2*/'</div>',
|
||||
];
|
||||
assertRanges(input, [r(0, 1)]);
|
||||
});
|
||||
|
||||
test('Fold intersecting region', () => {
|
||||
let input = [
|
||||
/*0*/'<body>',
|
||||
/*1*/'<!-- #region -->',
|
||||
/*2*/'Hello',
|
||||
/*3*/'<div></div>',
|
||||
/*4*/'</body>',
|
||||
/*5*/'<!-- #endregion -->',
|
||||
];
|
||||
assertRanges(input, [r(0, 3)]);
|
||||
});
|
||||
|
||||
|
||||
test('Test limit', () => {
|
||||
let 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>',
|
||||
];
|
||||
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', void 0);
|
||||
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);
|
||||
assertRanges(input, [r(0, 19), r(1, 18), r(2, 3), r(5, 11), r(13, 14), r(16, 17)], 'limit 7', 7);
|
||||
assertRanges(input, [r(0, 19), r(1, 18), r(2, 3), r(5, 11), r(13, 14), r(16, 17)], 'limit 6', 6);
|
||||
assertRanges(input, [r(0, 19), r(1, 18)], 'limit 5', 5);
|
||||
assertRanges(input, [r(0, 19), r(1, 18)], 'limit 4', 4);
|
||||
assertRanges(input, [r(0, 19), r(1, 18)], 'limit 3', 3);
|
||||
assertRanges(input, [r(0, 19), r(1, 18)], 'limit 2', 2);
|
||||
assertRanges(input, [r(0, 19)], 'limit 1', 1);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import 'mocha';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { getLanguageModes } from '../modes/languageModes';
|
||||
import { TextDocument, Range, FormattingOptions } from 'vscode-languageserver-types';
|
||||
|
||||
import { format } from '../modes/formatting';
|
||||
|
||||
suite('HTML Embedded Formatting', () => {
|
||||
|
||||
function assertFormat(value: string, expected: string, options?: any, formatOptions?: FormattingOptions, message?: string): void {
|
||||
var languageModes = getLanguageModes({ css: true, javascript: true });
|
||||
if (options) {
|
||||
languageModes.getAllModes().forEach(m => m.configure!(options));
|
||||
}
|
||||
|
||||
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 = format(languageModes, document, range, formatOptions, void 0, { css: true, javascript: true });
|
||||
|
||||
let actual = TextDocument.applyEdits(document, result);
|
||||
assert.equal(actual, expected, message);
|
||||
}
|
||||
|
||||
function assertFormatWithFixture(fixtureName: string, expectedPath: string, options?: any, formatOptions?: FormattingOptions): void {
|
||||
let input = fs.readFileSync(path.join(__dirname, 'fixtures', 'inputs', fixtureName)).toString().replace(/\r\n/mg, '\n');
|
||||
let expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'expected', expectedPath)).toString().replace(/\r\n/mg, '\n');
|
||||
assertFormat(input, expected, options, formatOptions, expectedPath);
|
||||
}
|
||||
|
||||
test('HTML only', function (): any {
|
||||
assertFormat('<html><body><p>Hello</p></body></html>', '<html>\n\n<body>\n <p>Hello</p>\n</body>\n\n</html>');
|
||||
assertFormat('|<html><body><p>Hello</p></body></html>|', '<html>\n\n<body>\n <p>Hello</p>\n</body>\n\n</html>');
|
||||
assertFormat('<html>|<body><p>Hello</p></body>|</html>', '<html><body>\n <p>Hello</p>\n</body></html>');
|
||||
});
|
||||
|
||||
test('HTML & Scripts', function (): any {
|
||||
assertFormat('<html><head><script></script></head></html>', '<html>\n\n<head>\n <script></script>\n</head>\n\n</html>');
|
||||
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>');
|
||||
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>');
|
||||
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>');
|
||||
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>');
|
||||
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', function () {
|
||||
assertFormatWithFixture('19813.html', '19813.html');
|
||||
assertFormatWithFixture('19813.html', '19813-4spaces.html', void 0, FormattingOptions.create(4, true));
|
||||
assertFormatWithFixture('19813.html', '19813-tab.html', void 0, FormattingOptions.create(1, false));
|
||||
assertFormatWithFixture('21634.html', '21634.html');
|
||||
});
|
||||
|
||||
test('Script end tag', function (): any {
|
||||
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', function (): any {
|
||||
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', function (): any {
|
||||
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', function (): any {
|
||||
let options = {
|
||||
html: {
|
||||
format: {
|
||||
endWithNewline: true
|
||||
}
|
||||
}
|
||||
};
|
||||
assertFormat('<html><body><p>Hello</p></body></html>', '<html>\n\n<body>\n <p>Hello</p>\n</body>\n\n</html>\n', options);
|
||||
assertFormat('<html>|<body><p>Hello</p></body>|</html>', '<html><body>\n <p>Hello</p>\n</body></html>', options);
|
||||
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', function (): any {
|
||||
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>');
|
||||
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', function (): any {
|
||||
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', function (): any {
|
||||
assertFormat('<script src="/js/main.js"> </script>', '<script src="/js/main.js"> </script>');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as path from 'path';
|
||||
import { providePathSuggestions } from '../../modes/pathCompletion';
|
||||
import { CompletionItemKind, Range, Position, CompletionItem, TextEdit, Command } from 'vscode-languageserver-types';
|
||||
|
||||
const fixtureRoot = path.resolve(__dirname, '../../../test/pathCompletionFixtures');
|
||||
|
||||
function toRange(line: number, startChar: number, endChar: number) {
|
||||
return Range.create(Position.create(line, startChar), Position.create(line, endChar));
|
||||
}
|
||||
function toTextEdit(line: number, startChar: number, endChar: number, newText: string) {
|
||||
const range = Range.create(Position.create(line, startChar), Position.create(line, endChar));
|
||||
return TextEdit.replace(range, newText);
|
||||
}
|
||||
|
||||
interface PathSuggestion {
|
||||
label?: string;
|
||||
kind?: CompletionItemKind;
|
||||
textEdit?: TextEdit;
|
||||
command?: Command;
|
||||
}
|
||||
|
||||
function assertSuggestions(actual: CompletionItem[], expected: PathSuggestion[]) {
|
||||
assert.equal(actual.length, expected.length, `Suggestions have length ${actual.length} but should have length ${expected.length}`);
|
||||
|
||||
for (let i = 0; i < expected.length; i++) {
|
||||
if (expected[i].label) {
|
||||
assert.equal(
|
||||
actual[i].label,
|
||||
expected[i].label,
|
||||
`Suggestion ${actual[i].label} should have label ${expected[i].label}`
|
||||
);
|
||||
}
|
||||
if (expected[i].kind) {
|
||||
assert.equal(actual[i].kind,
|
||||
expected[i].kind,
|
||||
`Suggestion ${actual[i].label} has kind ${actual[i].kind} but should have ${expected[i].kind}`
|
||||
);
|
||||
}
|
||||
if (expected[i].textEdit) {
|
||||
assert.equal(actual[i].textEdit!.newText, expected[i].textEdit!.newText);
|
||||
assert.deepEqual(actual[i].textEdit!.range, expected[i].textEdit!.range);
|
||||
}
|
||||
if (expected[i].command) {
|
||||
assert.equal(
|
||||
actual[i].command!.title,
|
||||
expected[i].command!.title,
|
||||
`Suggestion ${actual[i].label} has command title ${actual[i].command!.title} but should have command title ${expected[i].command!.title}`
|
||||
);
|
||||
assert.equal(
|
||||
actual[i].command!.command,
|
||||
expected[i].command!.command,
|
||||
`Suggestion ${actual[i].label} has command ${actual[i].command!.command} but should have command ${expected[i].command!.command}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suite('Path Completion - Relative Path:', () => {
|
||||
const mockRange = toRange(0, 3, 5);
|
||||
|
||||
test('Current Folder', () => {
|
||||
const value = './';
|
||||
const activeFileFsPath = path.resolve(fixtureRoot, 'index.html');
|
||||
const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath);
|
||||
|
||||
assertSuggestions(suggestions, [
|
||||
{ label: 'about/', kind: CompletionItemKind.Folder },
|
||||
{ label: 'index.html', kind: CompletionItemKind.File },
|
||||
{ label: 'src/', kind: CompletionItemKind.Folder }
|
||||
]);
|
||||
});
|
||||
|
||||
test('Parent Folder:', () => {
|
||||
const value = '../';
|
||||
const activeFileFsPath = path.resolve(fixtureRoot, 'about/about.html');
|
||||
const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath);
|
||||
|
||||
assertSuggestions(suggestions, [
|
||||
{ label: 'about/', kind: CompletionItemKind.Folder },
|
||||
{ label: 'index.html', kind: CompletionItemKind.File },
|
||||
{ label: 'src/', kind: CompletionItemKind.Folder }
|
||||
]);
|
||||
});
|
||||
|
||||
test('Adjacent Folder:', () => {
|
||||
const value = '../src/';
|
||||
const activeFileFsPath = path.resolve(fixtureRoot, 'about/about.html');
|
||||
const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath);
|
||||
|
||||
assertSuggestions(suggestions, [
|
||||
{ label: 'feature.js', kind: CompletionItemKind.File },
|
||||
{ label: 'test.js', kind: CompletionItemKind.File }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Path Completion - Absolute Path:', () => {
|
||||
const mockRange = toRange(0, 3, 5);
|
||||
|
||||
test('Root', () => {
|
||||
const value = '/';
|
||||
const activeFileFsPath1 = path.resolve(fixtureRoot, 'index.html');
|
||||
const activeFileFsPath2 = path.resolve(fixtureRoot, 'about/index.html');
|
||||
|
||||
const suggestions1 = providePathSuggestions(value, mockRange, activeFileFsPath1, fixtureRoot);
|
||||
const suggestions2 = providePathSuggestions(value, mockRange, activeFileFsPath2, fixtureRoot);
|
||||
|
||||
const verify = (suggestions: CompletionItem[]) => {
|
||||
assertSuggestions(suggestions, [
|
||||
{ label: 'about/', kind: CompletionItemKind.Folder },
|
||||
{ label: 'index.html', kind: CompletionItemKind.File },
|
||||
{ label: 'src/', kind: CompletionItemKind.Folder }
|
||||
]);
|
||||
};
|
||||
|
||||
verify(suggestions1);
|
||||
verify(suggestions2);
|
||||
});
|
||||
|
||||
test('Sub Folder', () => {
|
||||
const value = '/src/';
|
||||
const activeFileFsPath = path.resolve(fixtureRoot, 'about/about.html');
|
||||
const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath, fixtureRoot);
|
||||
|
||||
assertSuggestions(suggestions, [
|
||||
{ label: 'feature.js', kind: CompletionItemKind.File },
|
||||
{ label: 'test.js', kind: CompletionItemKind.File }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Path Completion - Folder Commands:', () => {
|
||||
const mockRange = toRange(0, 3, 5);
|
||||
|
||||
test('Folder should have command `editor.action.triggerSuggest', () => {
|
||||
const value = './';
|
||||
const activeFileFsPath = path.resolve(fixtureRoot, 'index.html');
|
||||
const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath);
|
||||
|
||||
assertSuggestions(suggestions, [
|
||||
{ label: 'about/', command: { title: 'Suggest', command: 'editor.action.triggerSuggest' } },
|
||||
{ label: 'index.html' },
|
||||
{ label: 'src/', command: { title: 'Suggest', command: 'editor.action.triggerSuggest' } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Path Completion - Incomplete Path at End:', () => {
|
||||
const mockRange = toRange(0, 3, 5);
|
||||
|
||||
test('Incomplete Path that starts with slash', () => {
|
||||
const value = '/src/f';
|
||||
const activeFileFsPath = path.resolve(fixtureRoot, 'about/about.html');
|
||||
const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath, fixtureRoot);
|
||||
|
||||
assertSuggestions(suggestions, [
|
||||
{ label: 'feature.js', kind: CompletionItemKind.File },
|
||||
{ label: 'test.js', kind: CompletionItemKind.File }
|
||||
]);
|
||||
});
|
||||
|
||||
test('Incomplete Path that does not start with slash', () => {
|
||||
const value = '../src/f';
|
||||
const activeFileFsPath = path.resolve(fixtureRoot, 'about/about.html');
|
||||
const suggestions = providePathSuggestions(value, mockRange, activeFileFsPath, fixtureRoot);
|
||||
|
||||
assertSuggestions(suggestions, [
|
||||
{ label: 'feature.js', kind: CompletionItemKind.File },
|
||||
{ label: 'test.js', kind: CompletionItemKind.File }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Path Completion - No leading dot or slash:', () => {
|
||||
|
||||
test('Top level completion', () => {
|
||||
const value = 's';
|
||||
const activeFileFsPath = path.resolve(fixtureRoot, 'index.html');
|
||||
const range = toRange(0, 3, 5);
|
||||
const suggestions = providePathSuggestions(value, range, activeFileFsPath, fixtureRoot);
|
||||
|
||||
assertSuggestions(suggestions, [
|
||||
{ label: 'about/', kind: CompletionItemKind.Folder, textEdit: toTextEdit(0, 4, 4, 'about/') },
|
||||
{ label: 'index.html', kind: CompletionItemKind.File, textEdit: toTextEdit(0, 4, 4, 'index.html') },
|
||||
{ label: 'src/', kind: CompletionItemKind.Folder, textEdit: toTextEdit(0, 4, 4, 'src/') }
|
||||
]);
|
||||
});
|
||||
|
||||
test('src/', () => {
|
||||
const value = 'src/';
|
||||
const activeFileFsPath = path.resolve(fixtureRoot, 'index.html');
|
||||
const range = toRange(0, 3, 8);
|
||||
const suggestions = providePathSuggestions(value, range, activeFileFsPath, fixtureRoot);
|
||||
|
||||
assertSuggestions(suggestions, [
|
||||
{ label: 'feature.js', kind: CompletionItemKind.File, textEdit: toTextEdit(0, 7, 7, 'feature.js') },
|
||||
{ label: 'test.js', kind: CompletionItemKind.File, textEdit: toTextEdit(0, 7, 7, 'test.js') }
|
||||
]);
|
||||
});
|
||||
|
||||
test('src/f', () => {
|
||||
const value = 'src/f';
|
||||
const activeFileFsPath = path.resolve(fixtureRoot, 'index.html');
|
||||
const range = toRange(0, 3, 9);
|
||||
const suggestions = providePathSuggestions(value, range, activeFileFsPath, fixtureRoot);
|
||||
|
||||
assertSuggestions(suggestions, [
|
||||
{ label: 'feature.js', kind: CompletionItemKind.File, textEdit: toTextEdit(0, 7, 8, 'feature.js') },
|
||||
{ label: 'test.js', kind: CompletionItemKind.File, textEdit: toTextEdit(0, 7, 8, 'test.js') }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Path Completion - TextEdit:', () => {
|
||||
|
||||
test('TextEdit has correct replace text and range', () => {
|
||||
const value = './';
|
||||
const activeFileFsPath = path.resolve(fixtureRoot, 'index.html');
|
||||
const range = toRange(0, 3, 5);
|
||||
const expectedReplaceRange = toRange(0, 4, 4);
|
||||
|
||||
const suggestions = providePathSuggestions(value, range, activeFileFsPath);
|
||||
|
||||
assertSuggestions(suggestions, [
|
||||
{ textEdit: TextEdit.replace(expectedReplaceRange, 'about/') },
|
||||
{ textEdit: TextEdit.replace(expectedReplaceRange, 'index.html') },
|
||||
{ textEdit: TextEdit.replace(expectedReplaceRange, 'src/') },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as words from '../utils/strings';
|
||||
|
||||
suite('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');
|
||||
});
|
||||
|
||||
});
|
||||
77
extensions/html-language-features/server/src/utils/arrays.ts
Normal file
77
extensions/html-language-features/server/src/utils/arrays.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
export function pushAll<T>(to: T[], from: T[]) {
|
||||
if (from) {
|
||||
for (var i = 0; i < from.length; i++) {
|
||||
to.push(from[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,40 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { DocumentContext } from 'vscode-html-languageservice';
|
||||
import { endsWith, startsWith } from '../utils/strings';
|
||||
import * as url from 'url';
|
||||
import { WorkspaceFolder } from 'vscode-languageserver';
|
||||
|
||||
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 void 0;
|
||||
}
|
||||
|
||||
return {
|
||||
resolveReference: (ref, base = documentUri) => {
|
||||
if (ref[0] === '/') { // resolve absolute path against the current workspace folder
|
||||
if (startsWith(base, 'file://')) {
|
||||
let folderUri = getRootFolder();
|
||||
if (folderUri) {
|
||||
return folderUri + ref.substr(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return url.resolve(base, ref);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
68
extensions/html-language-features/server/src/utils/runner.ts
Normal file
68
extensions/html-language-features/server/src/utils/runner.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
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 runSafeAsync<T>(func: () => Thenable<T>, errorVal: T, errorMessage: string, token: CancellationToken): Thenable<T | ResponseError<any>> {
|
||||
return new Promise<T | ResponseError<any>>((resolve, reject) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function runSafe<T, E>(func: () => T, errorVal: T, errorMessage: string, token: CancellationToken): Thenable<T | ResponseError<E>> {
|
||||
return new Promise<T | ResponseError<E>>((resolve, reject) => {
|
||||
setImmediate(() => {
|
||||
if (token.isCancellationRequested) {
|
||||
resolve(cancelValue());
|
||||
} else {
|
||||
try {
|
||||
let result = func();
|
||||
if (token.isCancellationRequested) {
|
||||
resolve(cancelValue());
|
||||
return;
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(formatError(errorMessage, e));
|
||||
resolve(errorVal);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function cancelValue<E>() {
|
||||
return new ResponseError<E>(ErrorCodes.RequestCancelled, 'Request cancelled');
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
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) {
|
||||
var 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