mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 12:19:20 +00:00
html headless
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createConnection, BrowserMessageReader, BrowserMessageWriter } from 'vscode-languageserver/browser';
|
||||
import { startServer } from '../htmlServer';
|
||||
|
||||
declare let self: any;
|
||||
|
||||
const messageReader = new BrowserMessageReader(self);
|
||||
const messageWriter = new BrowserMessageWriter(self);
|
||||
|
||||
const connection = createConnection(messageReader, messageWriter);
|
||||
|
||||
startServer(connection, {});
|
||||
@@ -4,26 +4,35 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IHTMLDataProvider, newHTMLDataProvider } from 'vscode-html-languageservice';
|
||||
import * as fs from 'fs';
|
||||
import { RequestService } from './requests';
|
||||
|
||||
export function getDataProviders(dataPaths?: string[]): IHTMLDataProvider[] {
|
||||
if (!dataPaths) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const providers: IHTMLDataProvider[] = [];
|
||||
|
||||
dataPaths.forEach((path, i) => {
|
||||
export function fetchHTMLDataProviders(dataPaths: string[], requestService: RequestService): Promise<IHTMLDataProvider[]> {
|
||||
const providers = dataPaths.map(async p => {
|
||||
try {
|
||||
if (fs.existsSync(path)) {
|
||||
const htmlData = JSON.parse(fs.readFileSync(path, 'utf-8'));
|
||||
|
||||
providers.push(newHTMLDataProvider(`customProvider${i}`, htmlData));
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(`Failed to load tag from ${path}`);
|
||||
const content = await requestService.getContent(p);
|
||||
return parseHTMLData(p, content);
|
||||
} catch (e) {
|
||||
return newHTMLDataProvider(p, { version: 1 });
|
||||
}
|
||||
});
|
||||
|
||||
return providers;
|
||||
}
|
||||
return Promise.all(providers);
|
||||
}
|
||||
|
||||
function parseHTMLData(id: string, source: string): IHTMLDataProvider {
|
||||
let rawData: any;
|
||||
|
||||
try {
|
||||
rawData = JSON.parse(source);
|
||||
} catch (err) {
|
||||
return newHTMLDataProvider(id, { version: 1 });
|
||||
}
|
||||
|
||||
return newHTMLDataProvider(id, {
|
||||
version: rawData.version || 1,
|
||||
tags: rawData.tags || [],
|
||||
globalAttributes: rawData.globalAttributes || [],
|
||||
valueSets: rawData.valueSets || []
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
559
extensions/html-language-features/server/src/htmlServer.ts
Normal file
559
extensions/html-language-features/server/src/htmlServer.ts
Normal file
@@ -0,0 +1,559 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import {
|
||||
Connection, TextDocuments, InitializeParams, InitializeResult, RequestType,
|
||||
DocumentRangeFormattingRequest, Disposable, DocumentSelector, TextDocumentPositionParams, ServerCapabilities,
|
||||
ConfigurationRequest, ConfigurationParams, DidChangeWorkspaceFoldersNotification,
|
||||
DocumentColorRequest, ColorPresentationRequest, TextDocumentSyncKind, NotificationType
|
||||
} from 'vscode-languageserver';
|
||||
import {
|
||||
getLanguageModes, LanguageModes, Settings, TextDocument, Position, Diagnostic, WorkspaceFolder, ColorInformation,
|
||||
Range, DocumentLink, SymbolInformation, TextDocumentIdentifier
|
||||
} from './modes/languageModes';
|
||||
|
||||
import { format } from './modes/formatting';
|
||||
import { pushAll } from './utils/arrays';
|
||||
import { getDocumentContext } from './utils/documentContext';
|
||||
import { URI } from 'vscode-uri';
|
||||
import { formatError, runSafe, runSafeAsync } from './utils/runner';
|
||||
|
||||
import { getFoldingRanges } from './modes/htmlFolding';
|
||||
import { fetchHTMLDataProviders } from './customData';
|
||||
import { getSelectionRanges } from './modes/selectionRanges';
|
||||
import { SemanticTokenProvider, newSemanticTokenProvider } from './modes/semanticTokens';
|
||||
import { RequestService, getRequestService } from './requests';
|
||||
|
||||
namespace CustomDataChangedNotification {
|
||||
export const type: NotificationType<string[]> = new NotificationType('html/customDataChanged');
|
||||
}
|
||||
|
||||
namespace TagCloseRequest {
|
||||
export const type: RequestType<TextDocumentPositionParams, string | null, any, any> = new RequestType('html/tag');
|
||||
}
|
||||
namespace OnTypeRenameRequest {
|
||||
export const type: RequestType<TextDocumentPositionParams, Range[] | null, any, any> = new RequestType('html/onTypeRename');
|
||||
}
|
||||
|
||||
// experimental: semantic tokens
|
||||
interface SemanticTokenParams {
|
||||
textDocument: TextDocumentIdentifier;
|
||||
ranges?: Range[];
|
||||
}
|
||||
namespace SemanticTokenRequest {
|
||||
export const type: RequestType<SemanticTokenParams, number[] | null, any, any> = new RequestType('html/semanticTokens');
|
||||
}
|
||||
namespace SemanticTokenLegendRequest {
|
||||
export const type: RequestType<void, { types: string[]; modifiers: string[] } | null, any, any> = new RequestType('html/semanticTokenLegend');
|
||||
}
|
||||
|
||||
export interface RuntimeEnvironment {
|
||||
file?: RequestService;
|
||||
http?: RequestService
|
||||
configureHttpRequests?(proxy: string, strictSSL: boolean): void;
|
||||
}
|
||||
|
||||
export function startServer(connection: Connection, runtime: RuntimeEnvironment) {
|
||||
|
||||
// Create a text document manager.
|
||||
const documents = new TextDocuments(TextDocument);
|
||||
// Make the text document manager listen on the connection
|
||||
// for open, change and close text document events
|
||||
documents.listen(connection);
|
||||
|
||||
let workspaceFolders: WorkspaceFolder[] = [];
|
||||
|
||||
let languageModes: LanguageModes;
|
||||
|
||||
let clientSnippetSupport = false;
|
||||
let dynamicFormatterRegistration = false;
|
||||
let scopedSettingsSupport = false;
|
||||
let workspaceFoldersSupport = false;
|
||||
let foldingRangeLimit = Number.MAX_VALUE;
|
||||
|
||||
const notReady = () => Promise.reject('Not Ready');
|
||||
let requestService: RequestService = { getContent: notReady, stat: notReady, readDirectory: notReady };
|
||||
|
||||
|
||||
|
||||
let globalSettings: Settings = {};
|
||||
let documentSettings: { [key: string]: Thenable<Settings> } = {};
|
||||
// remove document settings on close
|
||||
documents.onDidClose(e => {
|
||||
delete documentSettings[e.document.uri];
|
||||
});
|
||||
|
||||
function getDocumentSettings(textDocument: TextDocument, needsDocumentSettings: () => boolean): Thenable<Settings | undefined> {
|
||||
if (scopedSettingsSupport && needsDocumentSettings()) {
|
||||
let promise = documentSettings[textDocument.uri];
|
||||
if (!promise) {
|
||||
const scopeUri = textDocument.uri;
|
||||
const configRequestParam: ConfigurationParams = { items: [{ scopeUri, section: 'css' }, { scopeUri, section: 'html' }, { scopeUri, section: 'javascript' }] };
|
||||
promise = connection.sendRequest(ConfigurationRequest.type, configRequestParam).then(s => ({ css: s[0], html: s[1], javascript: s[2] }));
|
||||
documentSettings[textDocument.uri] = promise;
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// After the server has started the client sends an initialize request. The server receives
|
||||
// in the passed params the rootPath of the workspace plus the client capabilities
|
||||
connection.onInitialize((params: InitializeParams): InitializeResult => {
|
||||
const initializationOptions = params.initializationOptions;
|
||||
|
||||
workspaceFolders = (<any>params).workspaceFolders;
|
||||
if (!Array.isArray(workspaceFolders)) {
|
||||
workspaceFolders = [];
|
||||
if (params.rootPath) {
|
||||
workspaceFolders.push({ name: '', uri: URI.file(params.rootPath).toString() });
|
||||
}
|
||||
}
|
||||
|
||||
requestService = getRequestService(params.initializationOptions.handledSchemas || ['file'], connection, runtime);
|
||||
|
||||
const workspace = {
|
||||
get settings() { return globalSettings; },
|
||||
get folders() { return workspaceFolders; }
|
||||
};
|
||||
|
||||
languageModes = getLanguageModes(initializationOptions ? initializationOptions.embeddedLanguages : { css: true, javascript: true }, workspace, params.capabilities, requestService);
|
||||
|
||||
const dataPaths: string[] = params.initializationOptions.dataPaths || [];
|
||||
fetchHTMLDataProviders(dataPaths, requestService).then(dataProviders => {
|
||||
languageModes.updateDataProviders(dataProviders);
|
||||
});
|
||||
|
||||
documents.onDidClose(e => {
|
||||
languageModes.onDocumentRemoved(e.document);
|
||||
});
|
||||
connection.onShutdown(() => {
|
||||
languageModes.dispose();
|
||||
});
|
||||
|
||||
function getClientCapability<T>(name: string, def: T) {
|
||||
const keys = name.split('.');
|
||||
let c: any = params.capabilities;
|
||||
for (let i = 0; c && i < keys.length; i++) {
|
||||
if (!c.hasOwnProperty(keys[i])) {
|
||||
return def;
|
||||
}
|
||||
c = c[keys[i]];
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
clientSnippetSupport = getClientCapability('textDocument.completion.completionItem.snippetSupport', false);
|
||||
dynamicFormatterRegistration = getClientCapability('textDocument.rangeFormatting.dynamicRegistration', false) && (typeof params.initializationOptions.provideFormatter !== 'boolean');
|
||||
scopedSettingsSupport = getClientCapability('workspace.configuration', false);
|
||||
workspaceFoldersSupport = getClientCapability('workspace.workspaceFolders', false);
|
||||
foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE);
|
||||
const capabilities: ServerCapabilities = {
|
||||
textDocumentSync: TextDocumentSyncKind.Incremental,
|
||||
completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/'] } : undefined,
|
||||
hoverProvider: true,
|
||||
documentHighlightProvider: true,
|
||||
documentRangeFormattingProvider: params.initializationOptions.provideFormatter === true,
|
||||
documentLinkProvider: { resolveProvider: false },
|
||||
documentSymbolProvider: true,
|
||||
definitionProvider: true,
|
||||
signatureHelpProvider: { triggerCharacters: ['('] },
|
||||
referencesProvider: true,
|
||||
colorProvider: {},
|
||||
foldingRangeProvider: true,
|
||||
selectionRangeProvider: true,
|
||||
renameProvider: true
|
||||
};
|
||||
return { capabilities };
|
||||
});
|
||||
|
||||
connection.onInitialized(() => {
|
||||
if (workspaceFoldersSupport) {
|
||||
connection.client.register(DidChangeWorkspaceFoldersNotification.type);
|
||||
|
||||
connection.onNotification(DidChangeWorkspaceFoldersNotification.type, e => {
|
||||
const toAdd = e.event.added;
|
||||
const toRemove = e.event.removed;
|
||||
const updatedFolders = [];
|
||||
if (workspaceFolders) {
|
||||
for (const folder of workspaceFolders) {
|
||||
if (!toRemove.some(r => r.uri === folder.uri) && !toAdd.some(r => r.uri === folder.uri)) {
|
||||
updatedFolders.push(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
workspaceFolders = updatedFolders.concat(toAdd);
|
||||
documents.all().forEach(triggerValidation);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let formatterRegistration: Thenable<Disposable> | null = null;
|
||||
|
||||
// The settings have changed. Is send on server activation as well.
|
||||
connection.onDidChangeConfiguration((change) => {
|
||||
globalSettings = change.settings;
|
||||
documentSettings = {}; // reset all document settings
|
||||
documents.all().forEach(triggerValidation);
|
||||
|
||||
// dynamically enable & disable the formatter
|
||||
if (dynamicFormatterRegistration) {
|
||||
const enableFormatter = globalSettings && globalSettings.html && globalSettings.html.format && globalSettings.html.format.enable;
|
||||
if (enableFormatter) {
|
||||
if (!formatterRegistration) {
|
||||
const documentSelector: DocumentSelector = [{ language: 'html' }, { language: 'handlebars' }];
|
||||
formatterRegistration = connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector });
|
||||
}
|
||||
} else if (formatterRegistration) {
|
||||
formatterRegistration.then(r => r.dispose());
|
||||
formatterRegistration = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {};
|
||||
const validationDelayMs = 500;
|
||||
|
||||
// The content of a text document has changed. This event is emitted
|
||||
// when the text document first opened or when its content has changed.
|
||||
documents.onDidChangeContent(change => {
|
||||
triggerValidation(change.document);
|
||||
});
|
||||
|
||||
// a document has closed: clear all diagnostics
|
||||
documents.onDidClose(event => {
|
||||
cleanPendingValidation(event.document);
|
||||
connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
|
||||
});
|
||||
|
||||
function cleanPendingValidation(textDocument: TextDocument): void {
|
||||
const request = pendingValidationRequests[textDocument.uri];
|
||||
if (request) {
|
||||
clearTimeout(request);
|
||||
delete pendingValidationRequests[textDocument.uri];
|
||||
}
|
||||
}
|
||||
|
||||
function triggerValidation(textDocument: TextDocument): void {
|
||||
cleanPendingValidation(textDocument);
|
||||
pendingValidationRequests[textDocument.uri] = setTimeout(() => {
|
||||
delete pendingValidationRequests[textDocument.uri];
|
||||
validateTextDocument(textDocument);
|
||||
}, validationDelayMs);
|
||||
}
|
||||
|
||||
function isValidationEnabled(languageId: string, settings: Settings = globalSettings) {
|
||||
const validationSettings = settings && settings.html && settings.html.validate;
|
||||
if (validationSettings) {
|
||||
return languageId === 'css' && validationSettings.styles !== false || languageId === 'javascript' && validationSettings.scripts !== false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function validateTextDocument(textDocument: TextDocument) {
|
||||
try {
|
||||
const version = textDocument.version;
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
if (textDocument.languageId === 'html') {
|
||||
const modes = languageModes.getAllModesInDocument(textDocument);
|
||||
const settings = await getDocumentSettings(textDocument, () => modes.some(m => !!m.doValidation));
|
||||
const latestTextDocument = documents.get(textDocument.uri);
|
||||
if (latestTextDocument && latestTextDocument.version === version) { // check no new version has come in after in after the async op
|
||||
modes.forEach(mode => {
|
||||
if (mode.doValidation && isValidationEnabled(mode.getId(), settings)) {
|
||||
pushAll(diagnostics, mode.doValidation(latestTextDocument, settings));
|
||||
}
|
||||
});
|
||||
connection.sendDiagnostics({ uri: latestTextDocument.uri, diagnostics });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e));
|
||||
}
|
||||
}
|
||||
|
||||
connection.onCompletion(async (textDocumentPosition, token) => {
|
||||
return runSafeAsync(async () => {
|
||||
const document = documents.get(textDocumentPosition.textDocument.uri);
|
||||
if (!document) {
|
||||
return null;
|
||||
}
|
||||
const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
|
||||
if (!mode || !mode.doComplete) {
|
||||
return { isIncomplete: true, items: [] };
|
||||
}
|
||||
const doComplete = mode.doComplete!;
|
||||
|
||||
if (mode.getId() !== 'html') {
|
||||
/* __GDPR__
|
||||
"html.embbedded.complete" : {
|
||||
"languageId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
connection.telemetry.logEvent({ key: 'html.embbedded.complete', value: { languageId: mode.getId() } });
|
||||
}
|
||||
|
||||
const settings = await getDocumentSettings(document, () => doComplete.length > 2);
|
||||
const documentContext = getDocumentContext(document.uri, workspaceFolders);
|
||||
return doComplete(document, textDocumentPosition.position, documentContext, settings);
|
||||
|
||||
}, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onCompletionResolve((item, token) => {
|
||||
return runSafe(() => {
|
||||
const data = item.data;
|
||||
if (data && data.languageId && data.uri) {
|
||||
const mode = languageModes.getMode(data.languageId);
|
||||
const document = documents.get(data.uri);
|
||||
if (mode && mode.doResolve && document) {
|
||||
return mode.doResolve(document, item);
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}, item, `Error while resolving completion proposal`, token);
|
||||
});
|
||||
|
||||
connection.onHover((textDocumentPosition, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(textDocumentPosition.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
|
||||
if (mode && mode.doHover) {
|
||||
return mode.doHover(document, textDocumentPosition.position);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing hover for ${textDocumentPosition.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onDocumentHighlight((documentHighlightParams, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(documentHighlightParams.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, documentHighlightParams.position);
|
||||
if (mode && mode.findDocumentHighlight) {
|
||||
return mode.findDocumentHighlight(document, documentHighlightParams.position);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}, [], `Error while computing document highlights for ${documentHighlightParams.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onDefinition((definitionParams, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(definitionParams.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, definitionParams.position);
|
||||
if (mode && mode.findDefinition) {
|
||||
return mode.findDefinition(document, definitionParams.position);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}, null, `Error while computing definitions for ${definitionParams.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onReferences((referenceParams, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(referenceParams.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, referenceParams.position);
|
||||
if (mode && mode.findReferences) {
|
||||
return mode.findReferences(document, referenceParams.position);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}, [], `Error while computing references for ${referenceParams.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onSignatureHelp((signatureHelpParms, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(signatureHelpParms.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, signatureHelpParms.position);
|
||||
if (mode && mode.doSignatureHelp) {
|
||||
return mode.doSignatureHelp(document, signatureHelpParms.position);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing signature help for ${signatureHelpParms.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onDocumentRangeFormatting(async (formatParams, token) => {
|
||||
return runSafeAsync(async () => {
|
||||
const document = documents.get(formatParams.textDocument.uri);
|
||||
if (document) {
|
||||
let settings = await getDocumentSettings(document, () => true);
|
||||
if (!settings) {
|
||||
settings = globalSettings;
|
||||
}
|
||||
const unformattedTags: string = settings && settings.html && settings.html.format && settings.html.format.unformatted || '';
|
||||
const enabledModes = { css: !unformattedTags.match(/\bstyle\b/), javascript: !unformattedTags.match(/\bscript\b/) };
|
||||
|
||||
return format(languageModes, document, formatParams.range, formatParams.options, settings, enabledModes);
|
||||
}
|
||||
return [];
|
||||
}, [], `Error while formatting range for ${formatParams.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onDocumentLinks((documentLinkParam, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(documentLinkParam.textDocument.uri);
|
||||
const links: DocumentLink[] = [];
|
||||
if (document) {
|
||||
const 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(() => {
|
||||
const document = documents.get(documentSymbolParms.textDocument.uri);
|
||||
const symbols: SymbolInformation[] = [];
|
||||
if (document) {
|
||||
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(() => {
|
||||
const infos: ColorInformation[] = [];
|
||||
const 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(() => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, params.range.start);
|
||||
if (mode && mode.getColorPresentations) {
|
||||
return mode.getColorPresentations(document, params.color, params.range);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}, [], `Error while computing color presentations for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onRequest(TagCloseRequest.type, (params, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
const pos = params.position;
|
||||
if (pos.character > 0) {
|
||||
const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1));
|
||||
if (mode && mode.doAutoClose) {
|
||||
return mode.doAutoClose(document, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing tag close actions for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onFoldingRanges((params, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
return getFoldingRanges(languageModes, document, foldingRangeLimit, token);
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing folding regions for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onSelectionRanges((params, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
return getSelectionRanges(languageModes, document, params.positions);
|
||||
}
|
||||
return [];
|
||||
}, [], `Error while computing selection ranges for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onRenameRequest((params, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
const position: Position = params.position;
|
||||
|
||||
if (document) {
|
||||
const htmlMode = languageModes.getMode('html');
|
||||
if (htmlMode && htmlMode.doRename) {
|
||||
return htmlMode.doRename(document, position, params.newName);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing rename for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onRequest(OnTypeRenameRequest.type, (params, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
const pos = params.position;
|
||||
if (pos.character > 0) {
|
||||
const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1));
|
||||
if (mode && mode.doOnTypeRename) {
|
||||
return mode.doOnTypeRename(document, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing synced regions for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
let semanticTokensProvider: SemanticTokenProvider | undefined;
|
||||
function getSemanticTokenProvider() {
|
||||
if (!semanticTokensProvider) {
|
||||
semanticTokensProvider = newSemanticTokenProvider(languageModes);
|
||||
}
|
||||
return semanticTokensProvider;
|
||||
}
|
||||
|
||||
connection.onRequest(SemanticTokenRequest.type, (params, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
return getSemanticTokenProvider().getSemanticTokens(document, params.ranges);
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing semantic tokens for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onRequest(SemanticTokenLegendRequest.type, (_params, token) => {
|
||||
return runSafe(() => {
|
||||
return getSemanticTokenProvider().legend;
|
||||
}, null, `Error while computing semantic tokens legend`, token);
|
||||
});
|
||||
|
||||
connection.onNotification(CustomDataChangedNotification.type, dataPaths => {
|
||||
fetchHTMLDataProviders(dataPaths, requestService).then(dataProviders => {
|
||||
languageModes.updateDataProviders(dataProviders);
|
||||
});
|
||||
});
|
||||
|
||||
// Listen on the connection
|
||||
connection.listen();
|
||||
}
|
||||
@@ -1,544 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import {
|
||||
createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, RequestType,
|
||||
DocumentRangeFormattingRequest, Disposable, DocumentSelector, TextDocumentPositionParams, ServerCapabilities,
|
||||
ConfigurationRequest, ConfigurationParams, DidChangeWorkspaceFoldersNotification,
|
||||
DocumentColorRequest, ColorPresentationRequest, TextDocumentSyncKind
|
||||
} from 'vscode-languageserver';
|
||||
import {
|
||||
getLanguageModes, LanguageModes, Settings, TextDocument, Position, Diagnostic, WorkspaceFolder, ColorInformation,
|
||||
Range, DocumentLink, SymbolInformation, TextDocumentIdentifier
|
||||
} from './modes/languageModes';
|
||||
|
||||
import { format } from './modes/formatting';
|
||||
import { pushAll } from './utils/arrays';
|
||||
import { getDocumentContext } from './utils/documentContext';
|
||||
import { URI } from 'vscode-uri';
|
||||
import { formatError, runSafe, runSafeAsync } from './utils/runner';
|
||||
|
||||
import { getFoldingRanges } from './modes/htmlFolding';
|
||||
import { getDataProviders } from './customData';
|
||||
import { getSelectionRanges } from './modes/selectionRanges';
|
||||
import { SemanticTokenProvider, newSemanticTokenProvider } from './modes/semanticTokens';
|
||||
|
||||
namespace TagCloseRequest {
|
||||
export const type: RequestType<TextDocumentPositionParams, string | null, any, any> = new RequestType('html/tag');
|
||||
}
|
||||
namespace OnTypeRenameRequest {
|
||||
export const type: RequestType<TextDocumentPositionParams, Range[] | null, any, any> = new RequestType('html/onTypeRename');
|
||||
}
|
||||
|
||||
// experimental: semantic tokens
|
||||
interface SemanticTokenParams {
|
||||
textDocument: TextDocumentIdentifier;
|
||||
ranges?: Range[];
|
||||
}
|
||||
namespace SemanticTokenRequest {
|
||||
export const type: RequestType<SemanticTokenParams, number[] | null, any, any> = new RequestType('html/semanticTokens');
|
||||
}
|
||||
namespace SemanticTokenLegendRequest {
|
||||
export const type: RequestType<void, { types: string[]; modifiers: string[] } | null, any, any> = new RequestType('html/semanticTokenLegend');
|
||||
}
|
||||
|
||||
// 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 text document manager.
|
||||
const documents = new TextDocuments(TextDocument);
|
||||
// Make the text document manager listen on the connection
|
||||
// for open, change and close text document events
|
||||
documents.listen(connection);
|
||||
|
||||
let workspaceFolders: WorkspaceFolder[] = [];
|
||||
|
||||
let languageModes: LanguageModes;
|
||||
|
||||
let clientSnippetSupport = false;
|
||||
let dynamicFormatterRegistration = false;
|
||||
let scopedSettingsSupport = false;
|
||||
let workspaceFoldersSupport = false;
|
||||
let foldingRangeLimit = Number.MAX_VALUE;
|
||||
|
||||
let globalSettings: Settings = {};
|
||||
let documentSettings: { [key: string]: Thenable<Settings> } = {};
|
||||
// remove document settings on close
|
||||
documents.onDidClose(e => {
|
||||
delete documentSettings[e.document.uri];
|
||||
});
|
||||
|
||||
function getDocumentSettings(textDocument: TextDocument, needsDocumentSettings: () => boolean): Thenable<Settings | undefined> {
|
||||
if (scopedSettingsSupport && needsDocumentSettings()) {
|
||||
let promise = documentSettings[textDocument.uri];
|
||||
if (!promise) {
|
||||
const scopeUri = textDocument.uri;
|
||||
const configRequestParam: ConfigurationParams = { items: [{ scopeUri, section: 'css' }, { scopeUri, section: 'html' }, { scopeUri, section: 'javascript' }] };
|
||||
promise = connection.sendRequest(ConfigurationRequest.type, configRequestParam).then(s => ({ css: s[0], html: s[1], javascript: s[2] }));
|
||||
documentSettings[textDocument.uri] = promise;
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// After the server has started the client sends an initialize request. The server receives
|
||||
// in the passed params the rootPath of the workspace plus the client capabilities
|
||||
connection.onInitialize((params: InitializeParams): InitializeResult => {
|
||||
const initializationOptions = params.initializationOptions;
|
||||
|
||||
workspaceFolders = (<any>params).workspaceFolders;
|
||||
if (!Array.isArray(workspaceFolders)) {
|
||||
workspaceFolders = [];
|
||||
if (params.rootPath) {
|
||||
workspaceFolders.push({ name: '', uri: URI.file(params.rootPath).toString() });
|
||||
}
|
||||
}
|
||||
|
||||
const dataPaths: string[] = params.initializationOptions.dataPaths;
|
||||
const providers = getDataProviders(dataPaths);
|
||||
|
||||
const workspace = {
|
||||
get settings() { return globalSettings; },
|
||||
get folders() { return workspaceFolders; }
|
||||
};
|
||||
|
||||
languageModes = getLanguageModes(initializationOptions ? initializationOptions.embeddedLanguages : { css: true, javascript: true }, workspace, params.capabilities, providers);
|
||||
|
||||
documents.onDidClose(e => {
|
||||
languageModes.onDocumentRemoved(e.document);
|
||||
});
|
||||
connection.onShutdown(() => {
|
||||
languageModes.dispose();
|
||||
});
|
||||
|
||||
function getClientCapability<T>(name: string, def: T) {
|
||||
const keys = name.split('.');
|
||||
let c: any = params.capabilities;
|
||||
for (let i = 0; c && i < keys.length; i++) {
|
||||
if (!c.hasOwnProperty(keys[i])) {
|
||||
return def;
|
||||
}
|
||||
c = c[keys[i]];
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
clientSnippetSupport = getClientCapability('textDocument.completion.completionItem.snippetSupport', false);
|
||||
dynamicFormatterRegistration = getClientCapability('textDocument.rangeFormatting.dynamicRegistration', false) && (typeof params.initializationOptions.provideFormatter !== 'boolean');
|
||||
scopedSettingsSupport = getClientCapability('workspace.configuration', false);
|
||||
workspaceFoldersSupport = getClientCapability('workspace.workspaceFolders', false);
|
||||
foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE);
|
||||
const capabilities: ServerCapabilities = {
|
||||
textDocumentSync: TextDocumentSyncKind.Incremental,
|
||||
completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/'] } : undefined,
|
||||
hoverProvider: true,
|
||||
documentHighlightProvider: true,
|
||||
documentRangeFormattingProvider: params.initializationOptions.provideFormatter === true,
|
||||
documentLinkProvider: { resolveProvider: false },
|
||||
documentSymbolProvider: true,
|
||||
definitionProvider: true,
|
||||
signatureHelpProvider: { triggerCharacters: ['('] },
|
||||
referencesProvider: true,
|
||||
colorProvider: {},
|
||||
foldingRangeProvider: true,
|
||||
selectionRangeProvider: true,
|
||||
renameProvider: true
|
||||
};
|
||||
return { capabilities };
|
||||
});
|
||||
|
||||
connection.onInitialized(() => {
|
||||
if (workspaceFoldersSupport) {
|
||||
connection.client.register(DidChangeWorkspaceFoldersNotification.type);
|
||||
|
||||
connection.onNotification(DidChangeWorkspaceFoldersNotification.type, e => {
|
||||
const toAdd = e.event.added;
|
||||
const toRemove = e.event.removed;
|
||||
const updatedFolders = [];
|
||||
if (workspaceFolders) {
|
||||
for (const folder of workspaceFolders) {
|
||||
if (!toRemove.some(r => r.uri === folder.uri) && !toAdd.some(r => r.uri === folder.uri)) {
|
||||
updatedFolders.push(folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
workspaceFolders = updatedFolders.concat(toAdd);
|
||||
documents.all().forEach(triggerValidation);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let formatterRegistration: Thenable<Disposable> | null = null;
|
||||
|
||||
// The settings have changed. Is send on server activation as well.
|
||||
connection.onDidChangeConfiguration((change) => {
|
||||
globalSettings = change.settings;
|
||||
documentSettings = {}; // reset all document settings
|
||||
documents.all().forEach(triggerValidation);
|
||||
|
||||
// dynamically enable & disable the formatter
|
||||
if (dynamicFormatterRegistration) {
|
||||
const enableFormatter = globalSettings && globalSettings.html && globalSettings.html.format && globalSettings.html.format.enable;
|
||||
if (enableFormatter) {
|
||||
if (!formatterRegistration) {
|
||||
const documentSelector: DocumentSelector = [{ language: 'html' }, { language: 'handlebars' }];
|
||||
formatterRegistration = connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector });
|
||||
}
|
||||
} else if (formatterRegistration) {
|
||||
formatterRegistration.then(r => r.dispose());
|
||||
formatterRegistration = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {};
|
||||
const validationDelayMs = 500;
|
||||
|
||||
// The content of a text document has changed. This event is emitted
|
||||
// when the text document first opened or when its content has changed.
|
||||
documents.onDidChangeContent(change => {
|
||||
triggerValidation(change.document);
|
||||
});
|
||||
|
||||
// a document has closed: clear all diagnostics
|
||||
documents.onDidClose(event => {
|
||||
cleanPendingValidation(event.document);
|
||||
connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
|
||||
});
|
||||
|
||||
function cleanPendingValidation(textDocument: TextDocument): void {
|
||||
const request = pendingValidationRequests[textDocument.uri];
|
||||
if (request) {
|
||||
clearTimeout(request);
|
||||
delete pendingValidationRequests[textDocument.uri];
|
||||
}
|
||||
}
|
||||
|
||||
function triggerValidation(textDocument: TextDocument): void {
|
||||
cleanPendingValidation(textDocument);
|
||||
pendingValidationRequests[textDocument.uri] = setTimeout(() => {
|
||||
delete pendingValidationRequests[textDocument.uri];
|
||||
validateTextDocument(textDocument);
|
||||
}, validationDelayMs);
|
||||
}
|
||||
|
||||
function isValidationEnabled(languageId: string, settings: Settings = globalSettings) {
|
||||
const validationSettings = settings && settings.html && settings.html.validate;
|
||||
if (validationSettings) {
|
||||
return languageId === 'css' && validationSettings.styles !== false || languageId === 'javascript' && validationSettings.scripts !== false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function validateTextDocument(textDocument: TextDocument) {
|
||||
try {
|
||||
const version = textDocument.version;
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
if (textDocument.languageId === 'html') {
|
||||
const modes = languageModes.getAllModesInDocument(textDocument);
|
||||
const settings = await getDocumentSettings(textDocument, () => modes.some(m => !!m.doValidation));
|
||||
const latestTextDocument = documents.get(textDocument.uri);
|
||||
if (latestTextDocument && latestTextDocument.version === version) { // check no new version has come in after in after the async op
|
||||
modes.forEach(mode => {
|
||||
if (mode.doValidation && isValidationEnabled(mode.getId(), settings)) {
|
||||
pushAll(diagnostics, mode.doValidation(latestTextDocument, settings));
|
||||
}
|
||||
});
|
||||
connection.sendDiagnostics({ uri: latestTextDocument.uri, diagnostics });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e));
|
||||
}
|
||||
}
|
||||
|
||||
connection.onCompletion(async (textDocumentPosition, token) => {
|
||||
return runSafeAsync(async () => {
|
||||
const document = documents.get(textDocumentPosition.textDocument.uri);
|
||||
if (!document) {
|
||||
return null;
|
||||
}
|
||||
const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
|
||||
if (!mode || !mode.doComplete) {
|
||||
return { isIncomplete: true, items: [] };
|
||||
}
|
||||
const doComplete = mode.doComplete!;
|
||||
|
||||
if (mode.getId() !== 'html') {
|
||||
/* __GDPR__
|
||||
"html.embbedded.complete" : {
|
||||
"languageId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
connection.telemetry.logEvent({ key: 'html.embbedded.complete', value: { languageId: mode.getId() } });
|
||||
}
|
||||
|
||||
const settings = await getDocumentSettings(document, () => doComplete.length > 2);
|
||||
const result = doComplete(document, textDocumentPosition.position, settings);
|
||||
return result;
|
||||
|
||||
}, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onCompletionResolve((item, token) => {
|
||||
return runSafe(() => {
|
||||
const data = item.data;
|
||||
if (data && data.languageId && data.uri) {
|
||||
const mode = languageModes.getMode(data.languageId);
|
||||
const document = documents.get(data.uri);
|
||||
if (mode && mode.doResolve && document) {
|
||||
return mode.doResolve(document, item);
|
||||
}
|
||||
}
|
||||
return item;
|
||||
}, item, `Error while resolving completion proposal`, token);
|
||||
});
|
||||
|
||||
connection.onHover((textDocumentPosition, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(textDocumentPosition.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
|
||||
if (mode && mode.doHover) {
|
||||
return mode.doHover(document, textDocumentPosition.position);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing hover for ${textDocumentPosition.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onDocumentHighlight((documentHighlightParams, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(documentHighlightParams.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, documentHighlightParams.position);
|
||||
if (mode && mode.findDocumentHighlight) {
|
||||
return mode.findDocumentHighlight(document, documentHighlightParams.position);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}, [], `Error while computing document highlights for ${documentHighlightParams.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onDefinition((definitionParams, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(definitionParams.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, definitionParams.position);
|
||||
if (mode && mode.findDefinition) {
|
||||
return mode.findDefinition(document, definitionParams.position);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}, null, `Error while computing definitions for ${definitionParams.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onReferences((referenceParams, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(referenceParams.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, referenceParams.position);
|
||||
if (mode && mode.findReferences) {
|
||||
return mode.findReferences(document, referenceParams.position);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}, [], `Error while computing references for ${referenceParams.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onSignatureHelp((signatureHelpParms, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(signatureHelpParms.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, signatureHelpParms.position);
|
||||
if (mode && mode.doSignatureHelp) {
|
||||
return mode.doSignatureHelp(document, signatureHelpParms.position);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing signature help for ${signatureHelpParms.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onDocumentRangeFormatting(async (formatParams, token) => {
|
||||
return runSafeAsync(async () => {
|
||||
const document = documents.get(formatParams.textDocument.uri);
|
||||
if (document) {
|
||||
let settings = await getDocumentSettings(document, () => true);
|
||||
if (!settings) {
|
||||
settings = globalSettings;
|
||||
}
|
||||
const unformattedTags: string = settings && settings.html && settings.html.format && settings.html.format.unformatted || '';
|
||||
const enabledModes = { css: !unformattedTags.match(/\bstyle\b/), javascript: !unformattedTags.match(/\bscript\b/) };
|
||||
|
||||
return format(languageModes, document, formatParams.range, formatParams.options, settings, enabledModes);
|
||||
}
|
||||
return [];
|
||||
}, [], `Error while formatting range for ${formatParams.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onDocumentLinks((documentLinkParam, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(documentLinkParam.textDocument.uri);
|
||||
const links: DocumentLink[] = [];
|
||||
if (document) {
|
||||
const 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(() => {
|
||||
const document = documents.get(documentSymbolParms.textDocument.uri);
|
||||
const symbols: SymbolInformation[] = [];
|
||||
if (document) {
|
||||
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(() => {
|
||||
const infos: ColorInformation[] = [];
|
||||
const 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(() => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
const mode = languageModes.getModeAtPosition(document, params.range.start);
|
||||
if (mode && mode.getColorPresentations) {
|
||||
return mode.getColorPresentations(document, params.color, params.range);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}, [], `Error while computing color presentations for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onRequest(TagCloseRequest.type, (params, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
const pos = params.position;
|
||||
if (pos.character > 0) {
|
||||
const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1));
|
||||
if (mode && mode.doAutoClose) {
|
||||
return mode.doAutoClose(document, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing tag close actions for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onFoldingRanges((params, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
return getFoldingRanges(languageModes, document, foldingRangeLimit, token);
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing folding regions for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onSelectionRanges((params, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
return getSelectionRanges(languageModes, document, params.positions);
|
||||
}
|
||||
return [];
|
||||
}, [], `Error while computing selection ranges for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onRenameRequest((params, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
const position: Position = params.position;
|
||||
|
||||
if (document) {
|
||||
const htmlMode = languageModes.getMode('html');
|
||||
if (htmlMode && htmlMode.doRename) {
|
||||
return htmlMode.doRename(document, position, params.newName);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing rename for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onRequest(OnTypeRenameRequest.type, (params, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
const pos = params.position;
|
||||
if (pos.character > 0) {
|
||||
const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1));
|
||||
if (mode && mode.doOnTypeRename) {
|
||||
return mode.doOnTypeRename(document, pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing synced regions for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
let semanticTokensProvider: SemanticTokenProvider | undefined;
|
||||
function getSemanticTokenProvider() {
|
||||
if (!semanticTokensProvider) {
|
||||
semanticTokensProvider = newSemanticTokenProvider(languageModes);
|
||||
}
|
||||
return semanticTokensProvider;
|
||||
}
|
||||
|
||||
connection.onRequest(SemanticTokenRequest.type, (params, token) => {
|
||||
return runSafe(() => {
|
||||
const document = documents.get(params.textDocument.uri);
|
||||
if (document) {
|
||||
return getSemanticTokenProvider().getSemanticTokens(document, params.ranges);
|
||||
}
|
||||
return null;
|
||||
}, null, `Error while computing semantic tokens for ${params.textDocument.uri}`, token);
|
||||
});
|
||||
|
||||
connection.onRequest(SemanticTokenLegendRequest.type, (_params, token) => {
|
||||
return runSafe(() => {
|
||||
return getSemanticTokenProvider().legend;
|
||||
}, null, `Error while computing semantic tokens legend`, token);
|
||||
});
|
||||
|
||||
|
||||
// Listen on the connection
|
||||
connection.listen();
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache';
|
||||
import { Stylesheet, LanguageService as CSSLanguageService } from 'vscode-css-languageservice';
|
||||
import { FoldingRange, LanguageMode, Workspace, Color, TextDocument, Position, Range, CompletionList } from './languageModes';
|
||||
import { FoldingRange, LanguageMode, Workspace, Color, TextDocument, Position, Range, CompletionList, DocumentContext } from './languageModes';
|
||||
import { HTMLDocumentRegions, CSS_STYLE_RULE } from './embeddedSupport';
|
||||
|
||||
export function getCSSMode(cssLanguageService: CSSLanguageService, documentRegions: LanguageModelCache<HTMLDocumentRegions>, workspace: Workspace): LanguageMode {
|
||||
@@ -20,10 +20,10 @@ export function getCSSMode(cssLanguageService: CSSLanguageService, documentRegio
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
return cssLanguageService.doValidation(embedded, cssStylesheets.get(embedded), settings && settings.css);
|
||||
},
|
||||
doComplete(document: TextDocument, position: Position, _settings = workspace.settings) {
|
||||
doComplete(document: TextDocument, position: Position, documentContext: DocumentContext, _settings = workspace.settings) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
const stylesheet = cssStylesheets.get(embedded);
|
||||
return cssLanguageService.doComplete(embedded, position, stylesheet) || CompletionList.create();
|
||||
return cssLanguageService.doComplete2(embedded, position, stylesheet, documentContext) || CompletionList.create();
|
||||
},
|
||||
doHover(document: TextDocument, position: Position) {
|
||||
let embedded = embeddedCSSDocuments.get(document);
|
||||
|
||||
@@ -7,10 +7,9 @@ import { getLanguageModelCache } from '../languageModelCache';
|
||||
import {
|
||||
LanguageService as HTMLLanguageService, HTMLDocument, DocumentContext, FormattingOptions,
|
||||
HTMLFormatConfiguration, SelectionRange,
|
||||
TextDocument, Position, Range, CompletionItem, FoldingRange,
|
||||
TextDocument, Position, Range, FoldingRange,
|
||||
LanguageMode, Workspace
|
||||
} from './languageModes';
|
||||
import { getPathCompletionParticipant } from './pathCompletion';
|
||||
|
||||
export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace: Workspace): LanguageMode {
|
||||
let htmlDocuments = getLanguageModelCache<HTMLDocument>(10, 60, document => htmlLanguageService.parseHTMLDocument(document));
|
||||
@@ -21,19 +20,15 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace:
|
||||
getSelectionRange(document: TextDocument, position: Position): SelectionRange {
|
||||
return htmlLanguageService.getSelectionRanges(document, [position])[0];
|
||||
},
|
||||
doComplete(document: TextDocument, position: Position, settings = workspace.settings) {
|
||||
doComplete(document: TextDocument, position: Position, documentContext: DocumentContext, settings = workspace.settings) {
|
||||
let options = settings && settings.html && settings.html.suggest;
|
||||
let doAutoComplete = settings && settings.html && settings.html.autoClosingTags;
|
||||
if (doAutoComplete) {
|
||||
options.hideAutoCompleteProposals = true;
|
||||
}
|
||||
let pathCompletionProposals: CompletionItem[] = [];
|
||||
let participants = [getPathCompletionParticipant(document, workspace.folders, pathCompletionProposals)];
|
||||
htmlLanguageService.setCompletionParticipants(participants);
|
||||
|
||||
const htmlDocument = htmlDocuments.get(document);
|
||||
let completionList = htmlLanguageService.doComplete(document, position, htmlDocument, options);
|
||||
completionList.items.push(...pathCompletionProposals);
|
||||
let completionList = htmlLanguageService.doComplete2(document, position, htmlDocument, documentContext, options);
|
||||
return completionList;
|
||||
},
|
||||
doHover(document: TextDocument, position: Position) {
|
||||
|
||||
@@ -8,20 +8,21 @@ import {
|
||||
SymbolInformation, SymbolKind, CompletionItem, Location, SignatureHelp, SignatureInformation, ParameterInformation,
|
||||
Definition, TextEdit, TextDocument, Diagnostic, DiagnosticSeverity, Range, CompletionItemKind, Hover, MarkedString,
|
||||
DocumentHighlight, DocumentHighlightKind, CompletionList, Position, FormattingOptions, FoldingRange, FoldingRangeKind, SelectionRange,
|
||||
LanguageMode, Settings, SemanticTokenData, Workspace
|
||||
LanguageMode, Settings, SemanticTokenData, Workspace, DocumentContext
|
||||
} from './languageModes';
|
||||
import { getWordAtText, startsWith, isWhitespaceOnly, repeat } from '../utils/strings';
|
||||
import { HTMLDocumentRegions } from './embeddedSupport';
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import { join } from 'path';
|
||||
import { getSemanticTokens, getSemanticTokenLegend } from './javascriptSemanticTokens';
|
||||
import { joinPath } from '../requests';
|
||||
|
||||
|
||||
const JS_WORD_REGEX = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g;
|
||||
|
||||
let jquery_d_ts = join(__dirname, '../lib/jquery.d.ts'); // when packaged
|
||||
let jquery_d_ts = joinPath(__dirname, '../lib/jquery.d.ts'); // when packaged
|
||||
if (!ts.sys.fileExists(jquery_d_ts)) {
|
||||
jquery_d_ts = join(__dirname, '../../lib/jquery.d.ts'); // from source
|
||||
jquery_d_ts = joinPath(__dirname, '../../lib/jquery.d.ts'); // from source
|
||||
}
|
||||
|
||||
export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocumentRegions>, languageId: 'javascript' | 'typescript', workspace: Workspace): LanguageMode {
|
||||
@@ -64,7 +65,8 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
|
||||
};
|
||||
},
|
||||
getCurrentDirectory: () => '',
|
||||
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options)
|
||||
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
|
||||
|
||||
};
|
||||
let jsLanguageService = ts.createLanguageService(host);
|
||||
|
||||
@@ -88,7 +90,7 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
|
||||
};
|
||||
});
|
||||
},
|
||||
doComplete(document: TextDocument, position: Position): CompletionList {
|
||||
async doComplete(document: TextDocument, position: Position, _documentContext: DocumentContext): Promise<CompletionList> {
|
||||
updateCurrentTextDocument(document);
|
||||
let offset = currentTextDocument.offsetAt(position);
|
||||
let completions = jsLanguageService.getCompletionsAtPosition(workingFile, offset, { includeExternalModuleExports: false, includeInsertTextCompletions: false });
|
||||
|
||||
@@ -16,6 +16,7 @@ import { getCSSMode } from './cssMode';
|
||||
import { getDocumentRegions, HTMLDocumentRegions } from './embeddedSupport';
|
||||
import { getHTMLMode } from './htmlMode';
|
||||
import { getJavaScriptMode } from './javascriptMode';
|
||||
import { RequestService } from '../requests';
|
||||
|
||||
export * from 'vscode-html-languageservice';
|
||||
export { WorkspaceFolder } from 'vscode-languageserver';
|
||||
@@ -42,7 +43,7 @@ export interface LanguageMode {
|
||||
getId(): string;
|
||||
getSelectionRange?: (document: TextDocument, position: Position) => SelectionRange;
|
||||
doValidation?: (document: TextDocument, settings?: Settings) => Diagnostic[];
|
||||
doComplete?: (document: TextDocument, position: Position, settings?: Settings) => CompletionList;
|
||||
doComplete?: (document: TextDocument, position: Position, documentContext: DocumentContext, settings?: Settings) => Promise<CompletionList>;
|
||||
doResolve?: (document: TextDocument, item: CompletionItem) => CompletionItem;
|
||||
doHover?: (document: TextDocument, position: Position) => Hover | null;
|
||||
doSignatureHelp?: (document: TextDocument, position: Position) => SignatureHelp | null;
|
||||
@@ -66,6 +67,7 @@ export interface LanguageMode {
|
||||
}
|
||||
|
||||
export interface LanguageModes {
|
||||
updateDataProviders(dataProviders: IHTMLDataProvider[]): void;
|
||||
getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined;
|
||||
getModesInRange(document: TextDocument, range: Range): LanguageModeRange[];
|
||||
getAllModes(): LanguageMode[];
|
||||
@@ -80,9 +82,9 @@ export interface LanguageModeRange extends Range {
|
||||
attributeValue?: boolean;
|
||||
}
|
||||
|
||||
export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }, workspace: Workspace, clientCapabilities: ClientCapabilities, customDataProviders?: IHTMLDataProvider[]): LanguageModes {
|
||||
const htmlLanguageService = getHTMLLanguageService({ customDataProviders, clientCapabilities });
|
||||
const cssLanguageService = getCSSLanguageService({ clientCapabilities });
|
||||
export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }, workspace: Workspace, clientCapabilities: ClientCapabilities, requestService: RequestService): LanguageModes {
|
||||
const htmlLanguageService = getHTMLLanguageService({ clientCapabilities, fileSystemProvider: requestService });
|
||||
const cssLanguageService = getCSSLanguageService({ clientCapabilities, fileSystemProvider: requestService });
|
||||
|
||||
let documentRegions = getLanguageModelCache<HTMLDocumentRegions>(10, 60, document => getDocumentRegions(htmlLanguageService, document));
|
||||
|
||||
@@ -99,6 +101,9 @@ export function getLanguageModes(supportedLanguages: { [languageId: string]: boo
|
||||
modes['typescript'] = getJavaScriptMode(documentRegions, 'typescript', workspace);
|
||||
}
|
||||
return {
|
||||
async updateDataProviders(dataProviders: IHTMLDataProvider[]): Promise<void> {
|
||||
htmlLanguageService.setDataProviders(true, dataProviders);
|
||||
},
|
||||
getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined {
|
||||
let languageId = documentRegions.get(document).getLanguageAtPosition(position);
|
||||
if (languageId) {
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { URI } from 'vscode-uri';
|
||||
import { ICompletionParticipant, TextDocument, CompletionItemKind, CompletionItem, TextEdit, Range, Position, WorkspaceFolder } from './languageModes';
|
||||
import { startsWith } from '../utils/strings';
|
||||
import { contains } from '../utils/arrays';
|
||||
|
||||
export function getPathCompletionParticipant(
|
||||
document: TextDocument,
|
||||
workspaceFolders: WorkspaceFolder[],
|
||||
result: CompletionItem[]
|
||||
): ICompletionParticipant {
|
||||
return {
|
||||
onHtmlAttributeValue: ({ tag, attribute, value: valueBeforeCursor, range }) => {
|
||||
const fullValue = stripQuotes(document.getText(range));
|
||||
|
||||
if (shouldDoPathCompletion(tag, attribute, fullValue)) {
|
||||
if (workspaceFolders.length === 0) {
|
||||
return;
|
||||
}
|
||||
const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders);
|
||||
|
||||
const paths = providePaths(valueBeforeCursor, URI.parse(document.uri).fsPath, workspaceRoot);
|
||||
result.push(...paths.map(p => pathToSuggestion(p, valueBeforeCursor, fullValue, range)));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function stripQuotes(fullValue: string) {
|
||||
if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) {
|
||||
return fullValue.slice(1, -1);
|
||||
} else {
|
||||
return fullValue;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldDoPathCompletion(tag: string, attr: string, value: string) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of path suggestions. Folder suggestions are suffixed with a slash.
|
||||
*/
|
||||
function providePaths(valueBeforeCursor: string, activeDocFsPath: string, root?: string): string[] {
|
||||
const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/');
|
||||
const valueBeforeLastSlash = valueBeforeCursor.slice(0, lastIndexOfSlash + 1);
|
||||
|
||||
const startsWithSlash = startsWith(valueBeforeCursor, '/');
|
||||
let parentDir: string;
|
||||
if (startsWithSlash) {
|
||||
if (!root) {
|
||||
return [];
|
||||
}
|
||||
parentDir = path.resolve(root, '.' + valueBeforeLastSlash);
|
||||
} else {
|
||||
parentDir = path.resolve(activeDocFsPath, '..', valueBeforeLastSlash);
|
||||
}
|
||||
|
||||
try {
|
||||
const paths = fs.readdirSync(parentDir).map(f => {
|
||||
return isDir(path.resolve(parentDir, f))
|
||||
? f + '/'
|
||||
: f;
|
||||
});
|
||||
return paths.filter(p => p[0] !== '.');
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function isDir(p: string) {
|
||||
try {
|
||||
return fs.statSync(p).isDirectory();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function pathToSuggestion(p: string, valueBeforeCursor: string, fullValue: string, range: Range): CompletionItem {
|
||||
const isDir = p[p.length - 1] === '/';
|
||||
|
||||
let replaceRange: Range;
|
||||
const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/');
|
||||
if (lastIndexOfSlash === -1) {
|
||||
replaceRange = shiftRange(range, 1, -1);
|
||||
} else {
|
||||
// For cases where cursor is in the middle of attribute value, like <script src="./s|rc/test.js">
|
||||
// Find the last slash before cursor, and calculate the start of replace range from there
|
||||
const valueAfterLastSlash = fullValue.slice(lastIndexOfSlash + 1);
|
||||
const startPos = shiftPosition(range.end, -1 - valueAfterLastSlash.length);
|
||||
|
||||
// If whitespace exists, replace until there is no more
|
||||
const whitespaceIndex = valueAfterLastSlash.indexOf(' ');
|
||||
let endPos;
|
||||
if (whitespaceIndex !== -1) {
|
||||
endPos = shiftPosition(startPos, whitespaceIndex);
|
||||
} else {
|
||||
endPos = shiftPosition(range.end, -1);
|
||||
}
|
||||
replaceRange = Range.create(startPos, endPos);
|
||||
}
|
||||
|
||||
if (isDir) {
|
||||
return {
|
||||
label: p,
|
||||
kind: CompletionItemKind.Folder,
|
||||
textEdit: TextEdit.replace(replaceRange, p),
|
||||
command: {
|
||||
title: 'Suggest',
|
||||
command: 'editor.action.triggerSuggest'
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
label: p,
|
||||
kind: CompletionItemKind.File,
|
||||
textEdit: TextEdit.replace(replaceRange, p)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function resolveWorkspaceRoot(activeDoc: TextDocument, workspaceFolders: WorkspaceFolder[]): string | undefined {
|
||||
for (const folder of workspaceFolders) {
|
||||
if (startsWith(activeDoc.uri, folder.uri)) {
|
||||
return path.resolve(URI.parse(folder.uri).fsPath);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function shiftPosition(pos: Position, offset: number): Position {
|
||||
return Position.create(pos.line, pos.character + offset);
|
||||
}
|
||||
function shiftRange(range: Range, startOffset: number, endOffset: number): Range {
|
||||
const start = shiftPosition(range.start, startOffset);
|
||||
const end = shiftPosition(range.end, endOffset);
|
||||
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',
|
||||
area: '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: ['src', 'formaction'],
|
||||
source: 'src',
|
||||
track: 'src',
|
||||
video: ['src', 'poster']
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createConnection, Connection } from 'vscode-languageserver/node';
|
||||
import { formatError } from '../utils/runner';
|
||||
import { startServer } from '../htmlServer';
|
||||
import { getNodeFSRequestService } from './nodeFs';
|
||||
|
||||
|
||||
// Create a connection for the server.
|
||||
const connection: Connection = createConnection();
|
||||
|
||||
console.log = connection.console.log.bind(connection.console);
|
||||
console.error = connection.console.error.bind(connection.console);
|
||||
|
||||
process.on('unhandledRejection', (e: any) => {
|
||||
connection.console.error(formatError(`Unhandled exception`, e));
|
||||
});
|
||||
|
||||
|
||||
startServer(connection, { file: getNodeFSRequestService() });
|
||||
87
extensions/html-language-features/server/src/node/nodeFs.ts
Normal file
87
extensions/html-language-features/server/src/node/nodeFs.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { RequestService, getScheme } from '../requests';
|
||||
import { URI as Uri } from 'vscode-uri';
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { FileType } from 'vscode-css-languageservice';
|
||||
|
||||
export function getNodeFSRequestService(): RequestService {
|
||||
function ensureFileUri(location: string) {
|
||||
if (getScheme(location) !== 'file') {
|
||||
throw new Error('fileRequestService can only handle file URLs');
|
||||
}
|
||||
}
|
||||
return {
|
||||
getContent(location: string, encoding?: string) {
|
||||
ensureFileUri(location);
|
||||
return new Promise((c, e) => {
|
||||
const uri = Uri.parse(location);
|
||||
fs.readFile(uri.fsPath, encoding, (err, buf) => {
|
||||
if (err) {
|
||||
return e(err);
|
||||
}
|
||||
c(buf.toString());
|
||||
|
||||
});
|
||||
});
|
||||
},
|
||||
stat(location: string) {
|
||||
ensureFileUri(location);
|
||||
return new Promise((c, e) => {
|
||||
const uri = Uri.parse(location);
|
||||
fs.stat(uri.fsPath, (err, stats) => {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return c({ type: FileType.Unknown, ctime: -1, mtime: -1, size: -1 });
|
||||
} else {
|
||||
return e(err);
|
||||
}
|
||||
}
|
||||
|
||||
let type = FileType.Unknown;
|
||||
if (stats.isFile()) {
|
||||
type = FileType.File;
|
||||
} else if (stats.isDirectory()) {
|
||||
type = FileType.Directory;
|
||||
} else if (stats.isSymbolicLink()) {
|
||||
type = FileType.SymbolicLink;
|
||||
}
|
||||
|
||||
c({
|
||||
type,
|
||||
ctime: stats.ctime.getTime(),
|
||||
mtime: stats.mtime.getTime(),
|
||||
size: stats.size
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
readDirectory(location: string) {
|
||||
ensureFileUri(location);
|
||||
return new Promise((c, e) => {
|
||||
const path = Uri.parse(location).fsPath;
|
||||
|
||||
fs.readdir(path, { withFileTypes: true }, (err, children) => {
|
||||
if (err) {
|
||||
return e(err);
|
||||
}
|
||||
c(children.map(stat => {
|
||||
if (stat.isSymbolicLink()) {
|
||||
return [stat.name, FileType.SymbolicLink];
|
||||
} else if (stat.isDirectory()) {
|
||||
return [stat.name, FileType.Directory];
|
||||
} else if (stat.isFile()) {
|
||||
return [stat.name, FileType.File];
|
||||
} else {
|
||||
return [stat.name, FileType.Unknown];
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
177
extensions/html-language-features/server/src/requests.ts
Normal file
177
extensions/html-language-features/server/src/requests.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vscode-uri';
|
||||
import { RequestType, Connection } from 'vscode-languageserver';
|
||||
import { RuntimeEnvironment } from './htmlServer';
|
||||
|
||||
export namespace FsContentRequest {
|
||||
export const type: RequestType<{ uri: string; encoding?: string; }, string, any, any> = new RequestType('fs/content');
|
||||
}
|
||||
export namespace FsStatRequest {
|
||||
export const type: RequestType<string, FileStat, any, any> = new RequestType('fs/stat');
|
||||
}
|
||||
|
||||
export namespace FsReadDirRequest {
|
||||
export const type: RequestType<string, [string, FileType][], any, any> = new RequestType('fs/readDir');
|
||||
}
|
||||
|
||||
export enum FileType {
|
||||
/**
|
||||
* The file type is unknown.
|
||||
*/
|
||||
Unknown = 0,
|
||||
/**
|
||||
* A regular file.
|
||||
*/
|
||||
File = 1,
|
||||
/**
|
||||
* A directory.
|
||||
*/
|
||||
Directory = 2,
|
||||
/**
|
||||
* A symbolic link to a file.
|
||||
*/
|
||||
SymbolicLink = 64
|
||||
}
|
||||
export interface FileStat {
|
||||
/**
|
||||
* The type of the file, e.g. is a regular file, a directory, or symbolic link
|
||||
* to a file.
|
||||
*/
|
||||
type: FileType;
|
||||
/**
|
||||
* The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
|
||||
*/
|
||||
ctime: number;
|
||||
/**
|
||||
* The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
|
||||
*/
|
||||
mtime: number;
|
||||
/**
|
||||
* The size in bytes.
|
||||
*/
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface RequestService {
|
||||
getContent(uri: string, encoding?: string): Promise<string>;
|
||||
|
||||
stat(uri: string): Promise<FileStat>;
|
||||
readDirectory(uri: string): Promise<[string, FileType][]>;
|
||||
}
|
||||
|
||||
|
||||
export function getRequestService(handledSchemas: string[], connection: Connection, runtime: RuntimeEnvironment): RequestService {
|
||||
const builtInHandlers: { [protocol: string]: RequestService | undefined } = {};
|
||||
for (let protocol of handledSchemas) {
|
||||
if (protocol === 'file') {
|
||||
builtInHandlers[protocol] = runtime.file;
|
||||
} else if (protocol === 'http' || protocol === 'https') {
|
||||
builtInHandlers[protocol] = runtime.http;
|
||||
}
|
||||
}
|
||||
return {
|
||||
async stat(uri: string): Promise<FileStat> {
|
||||
const handler = builtInHandlers[getScheme(uri)];
|
||||
if (handler) {
|
||||
return handler.stat(uri);
|
||||
}
|
||||
const res = await connection.sendRequest(FsStatRequest.type, uri.toString());
|
||||
return res;
|
||||
},
|
||||
readDirectory(uri: string): Promise<[string, FileType][]> {
|
||||
const handler = builtInHandlers[getScheme(uri)];
|
||||
if (handler) {
|
||||
return handler.readDirectory(uri);
|
||||
}
|
||||
return connection.sendRequest(FsReadDirRequest.type, uri.toString());
|
||||
},
|
||||
getContent(uri: string, encoding?: string): Promise<string> {
|
||||
const handler = builtInHandlers[getScheme(uri)];
|
||||
if (handler) {
|
||||
return handler.getContent(uri, encoding);
|
||||
}
|
||||
return connection.sendRequest(FsContentRequest.type, { uri: uri.toString(), encoding });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getScheme(uri: string) {
|
||||
return uri.substr(0, uri.indexOf(':'));
|
||||
}
|
||||
|
||||
export function dirname(uri: string) {
|
||||
const lastIndexOfSlash = uri.lastIndexOf('/');
|
||||
return lastIndexOfSlash !== -1 ? uri.substr(0, lastIndexOfSlash) : '';
|
||||
}
|
||||
|
||||
export function basename(uri: string) {
|
||||
const lastIndexOfSlash = uri.lastIndexOf('/');
|
||||
return uri.substr(lastIndexOfSlash + 1);
|
||||
}
|
||||
|
||||
|
||||
const Slash = '/'.charCodeAt(0);
|
||||
const Dot = '.'.charCodeAt(0);
|
||||
|
||||
export function extname(uri: string) {
|
||||
for (let i = uri.length - 1; i >= 0; i--) {
|
||||
const ch = uri.charCodeAt(i);
|
||||
if (ch === Dot) {
|
||||
if (i > 0 && uri.charCodeAt(i - 1) !== Slash) {
|
||||
return uri.substr(i);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else if (ch === Slash) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function isAbsolutePath(path: string) {
|
||||
return path.charCodeAt(0) === Slash;
|
||||
}
|
||||
|
||||
export function resolvePath(uriString: string, path: string): string {
|
||||
if (isAbsolutePath(path)) {
|
||||
const uri = URI.parse(uriString);
|
||||
const parts = path.split('/');
|
||||
return uri.with({ path: normalizePath(parts) }).toString();
|
||||
}
|
||||
return joinPath(uriString, path);
|
||||
}
|
||||
|
||||
export function normalizePath(parts: string[]): string {
|
||||
const newParts: string[] = [];
|
||||
for (const part of parts) {
|
||||
if (part.length === 0 || part.length === 1 && part.charCodeAt(0) === Dot) {
|
||||
// ignore
|
||||
} else if (part.length === 2 && part.charCodeAt(0) === Dot && part.charCodeAt(1) === Dot) {
|
||||
newParts.pop();
|
||||
} else {
|
||||
newParts.push(part);
|
||||
}
|
||||
}
|
||||
if (parts.length > 1 && parts[parts.length - 1].length === 0) {
|
||||
newParts.push('');
|
||||
}
|
||||
let res = newParts.join('/');
|
||||
if (parts[0].length === 0) {
|
||||
res = '/' + res;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function joinPath(uriString: string, ...paths: string[]): string {
|
||||
const uri = URI.parse(uriString);
|
||||
const parts = uri.path.split('/');
|
||||
for (let path of paths) {
|
||||
parts.push(...path.split('/'));
|
||||
}
|
||||
return uri.with({ path: normalizePath(parts) }).toString();
|
||||
}
|
||||
@@ -6,7 +6,9 @@ import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import * as path from 'path';
|
||||
import { URI } from 'vscode-uri';
|
||||
import { getLanguageModes, WorkspaceFolder, TextDocument, CompletionList, CompletionItemKind, ClientCapabilities} from '../modes/languageModes';
|
||||
import { getLanguageModes, WorkspaceFolder, TextDocument, CompletionList, CompletionItemKind, ClientCapabilities, TextEdit } from '../modes/languageModes';
|
||||
import { getNodeFSRequestService } from '../node/nodeFs';
|
||||
import { getDocumentContext } from '../utils/documentContext';
|
||||
export interface ItemDescription {
|
||||
label: string;
|
||||
documentation?: string;
|
||||
@@ -34,7 +36,8 @@ export function assertCompletion(completions: CompletionList, expected: ItemDesc
|
||||
assert.equal(match.kind, expected.kind);
|
||||
}
|
||||
if (expected.resultText && match.textEdit) {
|
||||
assert.equal(TextDocument.applyEdits(document, [match.textEdit]), expected.resultText);
|
||||
const edit = TextEdit.is(match.textEdit) ? match.textEdit : TextEdit.replace(match.textEdit.replace, match.textEdit.newText);
|
||||
assert.equal(TextDocument.applyEdits(document, [edit]), expected.resultText);
|
||||
}
|
||||
if (expected.command) {
|
||||
assert.deepEqual(match.command, expected.command);
|
||||
@@ -43,7 +46,7 @@ export function assertCompletion(completions: CompletionList, expected: ItemDesc
|
||||
|
||||
const testUri = 'test://test/test.html';
|
||||
|
||||
export function testCompletionFor(value: string, expected: { count?: number, items?: ItemDescription[] }, uri = testUri, workspaceFolders?: WorkspaceFolder[]): void {
|
||||
export async function testCompletionFor(value: string, expected: { count?: number, items?: ItemDescription[] }, uri = testUri, workspaceFolders?: WorkspaceFolder[]): Promise<void> {
|
||||
let offset = value.indexOf('|');
|
||||
value = value.substr(0, offset) + value.substr(offset + 1);
|
||||
|
||||
@@ -54,11 +57,12 @@ export function testCompletionFor(value: string, expected: { count?: number, ite
|
||||
|
||||
let document = TextDocument.create(uri, 'html', 0, value);
|
||||
let position = document.positionAt(offset);
|
||||
const context = getDocumentContext(uri, workspace.folders)
|
||||
|
||||
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST);
|
||||
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
|
||||
const mode = languageModes.getModeAtPosition(document, position)!;
|
||||
|
||||
let list = mode.doComplete!(document, position);
|
||||
let list = await mode.doComplete!(document, position, context);
|
||||
|
||||
if (expected.count) {
|
||||
assert.equal(list.items.length, expected.count);
|
||||
@@ -71,13 +75,13 @@ export function testCompletionFor(value: string, expected: { count?: number, ite
|
||||
}
|
||||
|
||||
suite('HTML Completion', () => {
|
||||
test('HTML JavaScript Completions', function (): any {
|
||||
testCompletionFor('<html><script>window.|</script></html>', {
|
||||
test('HTML JavaScript Completions', async () => {
|
||||
await testCompletionFor('<html><script>window.|</script></html>', {
|
||||
items: [
|
||||
{ label: 'location', resultText: '<html><script>window.location</script></html>' },
|
||||
]
|
||||
});
|
||||
testCompletionFor('<html><script>$.|</script></html>', {
|
||||
await testCompletionFor('<html><script>$.|</script></html>', {
|
||||
items: [
|
||||
{ label: 'getJSON', resultText: '<html><script>$.getJSON</script></html>' },
|
||||
]
|
||||
@@ -96,8 +100,8 @@ suite('HTML Path Completion', () => {
|
||||
const indexHtmlUri = URI.file(path.resolve(fixtureRoot, 'index.html')).toString();
|
||||
const aboutHtmlUri = URI.file(path.resolve(fixtureRoot, 'about/about.html')).toString();
|
||||
|
||||
test('Basics - Correct label/kind/result/command', () => {
|
||||
testCompletionFor('<script src="./|">', {
|
||||
test('Basics - Correct label/kind/result/command', async () => {
|
||||
await testCompletionFor('<script src="./|">', {
|
||||
items: [
|
||||
{ label: 'about/', kind: CompletionItemKind.Folder, resultText: '<script src="./about/">', command: triggerSuggestCommand },
|
||||
{ label: 'index.html', kind: CompletionItemKind.File, resultText: '<script src="./index.html">' },
|
||||
@@ -106,8 +110,8 @@ suite('HTML Path Completion', () => {
|
||||
}, indexHtmlUri);
|
||||
});
|
||||
|
||||
test('Basics - Single Quote', () => {
|
||||
testCompletionFor(`<script src='./|'>`, {
|
||||
test('Basics - Single Quote', async () => {
|
||||
await testCompletionFor(`<script src='./|'>`, {
|
||||
items: [
|
||||
{ label: 'about/', kind: CompletionItemKind.Folder, resultText: `<script src='./about/'>`, command: triggerSuggestCommand },
|
||||
{ label: 'index.html', kind: CompletionItemKind.File, resultText: `<script src='./index.html'>` },
|
||||
@@ -116,18 +120,18 @@ suite('HTML Path Completion', () => {
|
||||
}, indexHtmlUri);
|
||||
});
|
||||
|
||||
test('No completion for remote paths', () => {
|
||||
testCompletionFor('<script src="http:">', { items: [] });
|
||||
testCompletionFor('<script src="http:/|">', { items: [] });
|
||||
testCompletionFor('<script src="http://|">', { items: [] });
|
||||
testCompletionFor('<script src="https:|">', { items: [] });
|
||||
testCompletionFor('<script src="https:/|">', { items: [] });
|
||||
testCompletionFor('<script src="https://|">', { items: [] });
|
||||
testCompletionFor('<script src="//|">', { items: [] });
|
||||
test('No completion for remote paths', async () => {
|
||||
await testCompletionFor('<script src="http:">', { items: [] });
|
||||
await testCompletionFor('<script src="http:/|">', { items: [] });
|
||||
await testCompletionFor('<script src="http://|">', { items: [] });
|
||||
await testCompletionFor('<script src="https:|">', { items: [] });
|
||||
await testCompletionFor('<script src="https:/|">', { items: [] });
|
||||
await testCompletionFor('<script src="https://|">', { items: [] });
|
||||
await testCompletionFor('<script src="//|">', { items: [] });
|
||||
});
|
||||
|
||||
test('Relative Path', () => {
|
||||
testCompletionFor('<script src="../|">', {
|
||||
test('Relative Path', async () => {
|
||||
await testCompletionFor('<script src="../|">', {
|
||||
items: [
|
||||
{ label: 'about/', resultText: '<script src="../about/">' },
|
||||
{ label: 'index.html', resultText: '<script src="../index.html">' },
|
||||
@@ -135,7 +139,7 @@ suite('HTML Path Completion', () => {
|
||||
]
|
||||
}, aboutHtmlUri);
|
||||
|
||||
testCompletionFor('<script src="../src/|">', {
|
||||
await testCompletionFor('<script src="../src/|">', {
|
||||
items: [
|
||||
{ label: 'feature.js', resultText: '<script src="../src/feature.js">' },
|
||||
{ label: 'test.js', resultText: '<script src="../src/test.js">' },
|
||||
@@ -143,8 +147,8 @@ suite('HTML Path Completion', () => {
|
||||
}, aboutHtmlUri);
|
||||
});
|
||||
|
||||
test('Absolute Path', () => {
|
||||
testCompletionFor('<script src="/|">', {
|
||||
test('Absolute Path', async () => {
|
||||
await testCompletionFor('<script src="/|">', {
|
||||
items: [
|
||||
{ label: 'about/', resultText: '<script src="/about/">' },
|
||||
{ label: 'index.html', resultText: '<script src="/index.html">' },
|
||||
@@ -152,7 +156,7 @@ suite('HTML Path Completion', () => {
|
||||
]
|
||||
}, indexHtmlUri);
|
||||
|
||||
testCompletionFor('<script src="/src/|">', {
|
||||
await testCompletionFor('<script src="/src/|">', {
|
||||
items: [
|
||||
{ label: 'feature.js', resultText: '<script src="/src/feature.js">' },
|
||||
{ label: 'test.js', resultText: '<script src="/src/test.js">' },
|
||||
@@ -160,9 +164,9 @@ suite('HTML Path Completion', () => {
|
||||
}, aboutHtmlUri, [fixtureWorkspace]);
|
||||
});
|
||||
|
||||
test('Empty Path Value', () => {
|
||||
test('Empty Path Value', async () => {
|
||||
// document: index.html
|
||||
testCompletionFor('<script src="|">', {
|
||||
await testCompletionFor('<script src="|">', {
|
||||
items: [
|
||||
{ label: 'about/', resultText: '<script src="about/">' },
|
||||
{ label: 'index.html', resultText: '<script src="index.html">' },
|
||||
@@ -170,7 +174,7 @@ suite('HTML Path Completion', () => {
|
||||
]
|
||||
}, indexHtmlUri);
|
||||
// document: about.html
|
||||
testCompletionFor('<script src="|">', {
|
||||
await testCompletionFor('<script src="|">', {
|
||||
items: [
|
||||
{ label: 'about.css', resultText: '<script src="about.css">' },
|
||||
{ label: 'about.html', resultText: '<script src="about.html">' },
|
||||
@@ -178,15 +182,15 @@ suite('HTML Path Completion', () => {
|
||||
]
|
||||
}, aboutHtmlUri);
|
||||
});
|
||||
test('Incomplete Path', () => {
|
||||
testCompletionFor('<script src="/src/f|">', {
|
||||
test('Incomplete Path', async () => {
|
||||
await testCompletionFor('<script src="/src/f|">', {
|
||||
items: [
|
||||
{ label: 'feature.js', resultText: '<script src="/src/feature.js">' },
|
||||
{ label: 'test.js', resultText: '<script src="/src/test.js">' },
|
||||
]
|
||||
}, aboutHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
testCompletionFor('<script src="../src/f|">', {
|
||||
await testCompletionFor('<script src="../src/f|">', {
|
||||
items: [
|
||||
{ label: 'feature.js', resultText: '<script src="../src/feature.js">' },
|
||||
{ label: 'test.js', resultText: '<script src="../src/test.js">' },
|
||||
@@ -194,9 +198,9 @@ suite('HTML Path Completion', () => {
|
||||
}, aboutHtmlUri, [fixtureWorkspace]);
|
||||
});
|
||||
|
||||
test('No leading dot or slash', () => {
|
||||
test('No leading dot or slash', async () => {
|
||||
// document: index.html
|
||||
testCompletionFor('<script src="s|">', {
|
||||
await testCompletionFor('<script src="s|">', {
|
||||
items: [
|
||||
{ label: 'about/', resultText: '<script src="about/">' },
|
||||
{ label: 'index.html', resultText: '<script src="index.html">' },
|
||||
@@ -204,14 +208,14 @@ suite('HTML Path Completion', () => {
|
||||
]
|
||||
}, indexHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
testCompletionFor('<script src="src/|">', {
|
||||
await testCompletionFor('<script src="src/|">', {
|
||||
items: [
|
||||
{ label: 'feature.js', resultText: '<script src="src/feature.js">' },
|
||||
{ label: 'test.js', resultText: '<script src="src/test.js">' },
|
||||
]
|
||||
}, indexHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
testCompletionFor('<script src="src/f|">', {
|
||||
await testCompletionFor('<script src="src/f|">', {
|
||||
items: [
|
||||
{ label: 'feature.js', resultText: '<script src="src/feature.js">' },
|
||||
{ label: 'test.js', resultText: '<script src="src/test.js">' },
|
||||
@@ -219,7 +223,7 @@ suite('HTML Path Completion', () => {
|
||||
}, indexHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
// document: about.html
|
||||
testCompletionFor('<script src="s|">', {
|
||||
await testCompletionFor('<script src="s|">', {
|
||||
items: [
|
||||
{ label: 'about.css', resultText: '<script src="about.css">' },
|
||||
{ label: 'about.html', resultText: '<script src="about.html">' },
|
||||
@@ -227,29 +231,29 @@ suite('HTML Path Completion', () => {
|
||||
]
|
||||
}, aboutHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
testCompletionFor('<script src="media/|">', {
|
||||
await testCompletionFor('<script src="media/|">', {
|
||||
items: [
|
||||
{ label: 'icon.pic', resultText: '<script src="media/icon.pic">' }
|
||||
]
|
||||
}, aboutHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
testCompletionFor('<script src="media/f|">', {
|
||||
await testCompletionFor('<script src="media/f|">', {
|
||||
items: [
|
||||
{ label: 'icon.pic', resultText: '<script src="media/icon.pic">' }
|
||||
]
|
||||
}, aboutHtmlUri, [fixtureWorkspace]);
|
||||
});
|
||||
|
||||
test('Trigger completion in middle of path', () => {
|
||||
test('Trigger completion in middle of path', async () => {
|
||||
// document: index.html
|
||||
testCompletionFor('<script src="src/f|eature.js">', {
|
||||
await testCompletionFor('<script src="src/f|eature.js">', {
|
||||
items: [
|
||||
{ label: 'feature.js', resultText: '<script src="src/feature.js">' },
|
||||
{ label: 'test.js', resultText: '<script src="src/test.js">' },
|
||||
]
|
||||
}, indexHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
testCompletionFor('<script src="s|rc/feature.js">', {
|
||||
await testCompletionFor('<script src="s|rc/feature.js">', {
|
||||
items: [
|
||||
{ label: 'about/', resultText: '<script src="about/">' },
|
||||
{ label: 'index.html', resultText: '<script src="index.html">' },
|
||||
@@ -258,13 +262,13 @@ suite('HTML Path Completion', () => {
|
||||
}, indexHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
// document: about.html
|
||||
testCompletionFor('<script src="media/f|eature.js">', {
|
||||
await testCompletionFor('<script src="media/f|eature.js">', {
|
||||
items: [
|
||||
{ label: 'icon.pic', resultText: '<script src="media/icon.pic">' }
|
||||
]
|
||||
}, aboutHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
testCompletionFor('<script src="m|edia/feature.js">', {
|
||||
await testCompletionFor('<script src="m|edia/feature.js">', {
|
||||
items: [
|
||||
{ label: 'about.css', resultText: '<script src="about.css">' },
|
||||
{ label: 'about.html', resultText: '<script src="about.html">' },
|
||||
@@ -274,8 +278,8 @@ suite('HTML Path Completion', () => {
|
||||
});
|
||||
|
||||
|
||||
test('Trigger completion in middle of path and with whitespaces', () => {
|
||||
testCompletionFor('<script src="./| about/about.html>', {
|
||||
test('Trigger completion in middle of path and with whitespaces', async () => {
|
||||
await testCompletionFor('<script src="./| about/about.html>', {
|
||||
items: [
|
||||
{ label: 'about/', resultText: '<script src="./about/ about/about.html>' },
|
||||
{ label: 'index.html', resultText: '<script src="./index.html about/about.html>' },
|
||||
@@ -283,7 +287,7 @@ suite('HTML Path Completion', () => {
|
||||
]
|
||||
}, indexHtmlUri, [fixtureWorkspace]);
|
||||
|
||||
testCompletionFor('<script src="./a|bout /about.html>', {
|
||||
await testCompletionFor('<script src="./a|bout /about.html>', {
|
||||
items: [
|
||||
{ label: 'about/', resultText: '<script src="./about/ /about.html>' },
|
||||
{ label: 'index.html', resultText: '<script src="./index.html /about.html>' },
|
||||
@@ -292,13 +296,13 @@ suite('HTML Path Completion', () => {
|
||||
}, indexHtmlUri, [fixtureWorkspace]);
|
||||
});
|
||||
|
||||
test('Completion should ignore files/folders starting with dot', () => {
|
||||
testCompletionFor('<script src="./|"', {
|
||||
test('Completion should ignore files/folders starting with dot', async () => {
|
||||
await testCompletionFor('<script src="./|"', {
|
||||
count: 3
|
||||
}, indexHtmlUri, [fixtureWorkspace]);
|
||||
});
|
||||
|
||||
test('Unquoted Path', () => {
|
||||
test('Unquoted Path', async () => {
|
||||
/* Unquoted value is not supported in html language service yet
|
||||
testCompletionFor(`<div><a href=about/|>`, {
|
||||
items: [
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as assert from 'assert';
|
||||
import { getFoldingRanges } from '../modes/htmlFolding';
|
||||
import { TextDocument, getLanguageModes } from '../modes/languageModes';
|
||||
import { ClientCapabilities } from 'vscode-css-languageservice';
|
||||
import { getNodeFSRequestService } from '../node/nodeFs';
|
||||
|
||||
interface ExpectedIndentRange {
|
||||
startLine: number;
|
||||
@@ -21,7 +22,7 @@ function assertRanges(lines: string[], expected: ExpectedIndentRange[], message?
|
||||
settings: {},
|
||||
folders: [{ name: 'foo', uri: 'test://foo' }]
|
||||
};
|
||||
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST);
|
||||
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
|
||||
const actual = getFoldingRanges(languageModes, document, nRanges, null);
|
||||
|
||||
let actualRanges = [];
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as assert from 'assert';
|
||||
import { getLanguageModes, TextDocument, Range, FormattingOptions, ClientCapabilities } from '../modes/languageModes';
|
||||
|
||||
import { format } from '../modes/formatting';
|
||||
import { getNodeFSRequestService } from '../node/nodeFs';
|
||||
|
||||
suite('HTML Embedded Formatting', () => {
|
||||
|
||||
@@ -18,7 +19,7 @@ suite('HTML Embedded Formatting', () => {
|
||||
settings: options,
|
||||
folders: [{ name: 'foo', uri: 'test://foo' }]
|
||||
};
|
||||
let languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST);
|
||||
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
|
||||
|
||||
let rangeStartOffset = value.indexOf('|');
|
||||
let rangeEndOffset;
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import { getLanguageModes, ClientCapabilities, TextDocument, SelectionRange} from '../modes/languageModes';
|
||||
import { getSelectionRanges } from '../modes/selectionRanges';
|
||||
import { getNodeFSRequestService } from '../node/nodeFs';
|
||||
|
||||
function assertRanges(content: string, expected: (number | string)[][]): void {
|
||||
let message = `${content} gives selection range:\n`;
|
||||
@@ -18,7 +19,7 @@ function assertRanges(content: string, expected: (number | string)[][]): void {
|
||||
settings: {},
|
||||
folders: [{ name: 'foo', uri: 'test://foo' }]
|
||||
};
|
||||
let languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST);
|
||||
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
|
||||
|
||||
const document = TextDocument.create('test://foo.html', 'html', 1, content);
|
||||
const actualRanges = getSelectionRanges(languageModes, document, [document.positionAt(offset)]);
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'mocha';
|
||||
import * as assert from 'assert';
|
||||
import { TextDocument, getLanguageModes, ClientCapabilities, Range, Position } from '../modes/languageModes';
|
||||
import { newSemanticTokenProvider } from '../modes/semanticTokens';
|
||||
import { getNodeFSRequestService } from '../node/nodeFs';
|
||||
|
||||
interface ExpectedToken {
|
||||
startLine: number;
|
||||
@@ -21,7 +22,7 @@ function assertTokens(lines: string[], expected: ExpectedToken[], ranges?: Range
|
||||
settings: {},
|
||||
folders: [{ name: 'foo', uri: 'test://foo' }]
|
||||
};
|
||||
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST);
|
||||
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
|
||||
const semanticTokensProvider = newSemanticTokenProvider(languageModes);
|
||||
|
||||
const legend = semanticTokensProvider.legend;
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DocumentContext, WorkspaceFolder } from '../modes/languageModes';
|
||||
import { DocumentContext } from 'vscode-css-languageservice';
|
||||
import { endsWith, startsWith } from '../utils/strings';
|
||||
import * as url from 'url';
|
||||
import { WorkspaceFolder } from 'vscode-languageserver';
|
||||
import { resolvePath } from '../requests';
|
||||
|
||||
export function getDocumentContext(documentUri: string, workspaceFolders: WorkspaceFolder[]): DocumentContext {
|
||||
function getRootFolder(): string | undefined {
|
||||
@@ -22,20 +23,15 @@ export function getDocumentContext(documentUri: string, workspaceFolders: Worksp
|
||||
}
|
||||
|
||||
return {
|
||||
resolveReference: (ref, base = documentUri) => {
|
||||
resolveReference: (ref: string, 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);
|
||||
}
|
||||
let folderUri = getRootFolder();
|
||||
if (folderUri) {
|
||||
return folderUri + ref.substr(1);
|
||||
}
|
||||
}
|
||||
try {
|
||||
return url.resolve(base, ref);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
base = base.substr(0, base.lastIndexOf('/') + 1);
|
||||
return resolvePath(base, ref);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user