chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { window, workspace, Disposable, TextDocument, Position, SnippetString, TextDocumentChangeEvent, TextDocumentChangeReason, TextDocumentContentChangeEvent } from 'vscode';
|
||||
import { Runtime } from './htmlClient';
|
||||
import { LanguageParticipants } from './languageParticipants';
|
||||
|
||||
export function activateAutoInsertion(provider: (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position) => Thenable<string>, languageParticipants: LanguageParticipants, runtime: Runtime): Disposable {
|
||||
const disposables: Disposable[] = [];
|
||||
workspace.onDidChangeTextDocument(onDidChangeTextDocument, null, disposables);
|
||||
|
||||
let anyIsEnabled = false;
|
||||
const isEnabled = {
|
||||
'autoQuote': false,
|
||||
'autoClose': false
|
||||
};
|
||||
updateEnabledState();
|
||||
window.onDidChangeActiveTextEditor(updateEnabledState, null, disposables);
|
||||
|
||||
let timeout: Disposable | undefined = undefined;
|
||||
|
||||
disposables.push({
|
||||
dispose: () => {
|
||||
timeout?.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
function updateEnabledState() {
|
||||
anyIsEnabled = false;
|
||||
const editor = window.activeTextEditor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const document = editor.document;
|
||||
if (!languageParticipants.useAutoInsert(document.languageId)) {
|
||||
return;
|
||||
}
|
||||
const configurations = workspace.getConfiguration(undefined, document.uri);
|
||||
isEnabled['autoQuote'] = configurations.get<boolean>('html.autoCreateQuotes') ?? false;
|
||||
isEnabled['autoClose'] = configurations.get<boolean>('html.autoClosingTags') ?? false;
|
||||
anyIsEnabled = isEnabled['autoQuote'] || isEnabled['autoClose'];
|
||||
}
|
||||
|
||||
function onDidChangeTextDocument({ document, contentChanges, reason }: TextDocumentChangeEvent) {
|
||||
if (!anyIsEnabled || contentChanges.length === 0 || reason === TextDocumentChangeReason.Undo || reason === TextDocumentChangeReason.Redo) {
|
||||
return;
|
||||
}
|
||||
const activeDocument = window.activeTextEditor && window.activeTextEditor.document;
|
||||
if (document !== activeDocument) {
|
||||
return;
|
||||
}
|
||||
if (timeout) {
|
||||
timeout.dispose();
|
||||
}
|
||||
|
||||
const lastChange = contentChanges[contentChanges.length - 1];
|
||||
if (lastChange.rangeLength === 0 && isSingleLine(lastChange.text)) {
|
||||
const lastCharacter = lastChange.text[lastChange.text.length - 1];
|
||||
if (isEnabled['autoQuote'] && lastCharacter === '=') {
|
||||
doAutoInsert('autoQuote', document, lastChange);
|
||||
} else if (isEnabled['autoClose'] && (lastCharacter === '>' || lastCharacter === '/')) {
|
||||
doAutoInsert('autoClose', document, lastChange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isSingleLine(text: string): boolean {
|
||||
return !/\n/.test(text);
|
||||
}
|
||||
|
||||
function doAutoInsert(kind: 'autoQuote' | 'autoClose', document: TextDocument, lastChange: TextDocumentContentChangeEvent) {
|
||||
const rangeStart = lastChange.range.start;
|
||||
const version = document.version;
|
||||
timeout = runtime.timer.setTimeout(() => {
|
||||
const position = new Position(rangeStart.line, rangeStart.character + lastChange.text.length);
|
||||
provider(kind, document, position).then(text => {
|
||||
if (text && isEnabled[kind]) {
|
||||
const activeEditor = window.activeTextEditor;
|
||||
if (activeEditor) {
|
||||
const activeDocument = activeEditor.document;
|
||||
if (document === activeDocument && activeDocument.version === version) {
|
||||
const selections = activeEditor.selections;
|
||||
if (selections.length && selections.some(s => s.active.isEqual(position))) {
|
||||
activeEditor.insertSnippet(new SnippetString(text), selections.map(s => s.active));
|
||||
} else {
|
||||
activeEditor.insertSnippet(new SnippetString(text), position);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
timeout = undefined;
|
||||
}, 100);
|
||||
}
|
||||
return Disposable.from(...disposables);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, ExtensionContext, Uri, l10n } from 'vscode';
|
||||
import { LanguageClientOptions } from 'vscode-languageclient';
|
||||
import { startClient, LanguageClientConstructor, AsyncDisposable } from '../htmlClient';
|
||||
import { LanguageClient } from 'vscode-languageclient/browser';
|
||||
|
||||
let client: AsyncDisposable | undefined;
|
||||
|
||||
// this method is called when vs code is activated
|
||||
export async function activate(context: ExtensionContext) {
|
||||
const serverMain = Uri.joinPath(context.extensionUri, 'server/dist/browser/htmlServerMain.js');
|
||||
try {
|
||||
const worker = new Worker(serverMain.toString());
|
||||
worker.postMessage({ i10lLocation: l10n.uri?.toString(false) ?? '' });
|
||||
|
||||
const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => {
|
||||
return new LanguageClient(id, name, worker, clientOptions);
|
||||
};
|
||||
|
||||
const timer = {
|
||||
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable {
|
||||
const handle = setTimeout(callback, ms, ...args);
|
||||
return { dispose: () => clearTimeout(handle) };
|
||||
}
|
||||
};
|
||||
|
||||
client = await startClient(context, newLanguageClient, { TextDecoder, timer });
|
||||
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deactivate(): Promise<void> {
|
||||
if (client) {
|
||||
await client.dispose();
|
||||
client = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { workspace, extensions, Uri, EventEmitter, Disposable } from 'vscode';
|
||||
import { Runtime } from './htmlClient';
|
||||
import { Utils } from 'vscode-uri';
|
||||
|
||||
|
||||
export function getCustomDataSource(runtime: Runtime, toDispose: Disposable[]) {
|
||||
let localExtensionUris = new Set<string>();
|
||||
let externalExtensionUris = new Set<string>();
|
||||
const workspaceUris = new Set<string>();
|
||||
|
||||
collectInWorkspaces(workspaceUris);
|
||||
collectInExtensions(localExtensionUris, externalExtensionUris);
|
||||
|
||||
const onChange = new EventEmitter<void>();
|
||||
|
||||
toDispose.push(extensions.onDidChange(_ => {
|
||||
const newLocalExtensionUris = new Set<string>();
|
||||
const newExternalExtensionUris = new Set<string>();
|
||||
collectInExtensions(newLocalExtensionUris, newExternalExtensionUris);
|
||||
if (hasChanges(newLocalExtensionUris, localExtensionUris) || hasChanges(newExternalExtensionUris, externalExtensionUris)) {
|
||||
localExtensionUris = newLocalExtensionUris;
|
||||
externalExtensionUris = newExternalExtensionUris;
|
||||
onChange.fire();
|
||||
}
|
||||
}));
|
||||
toDispose.push(workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('html.customData')) {
|
||||
workspaceUris.clear();
|
||||
collectInWorkspaces(workspaceUris);
|
||||
onChange.fire();
|
||||
}
|
||||
}));
|
||||
|
||||
toDispose.push(workspace.onDidChangeTextDocument(e => {
|
||||
const path = e.document.uri.toString();
|
||||
if (externalExtensionUris.has(path) || workspaceUris.has(path)) {
|
||||
onChange.fire();
|
||||
}
|
||||
}));
|
||||
|
||||
return {
|
||||
get uris() {
|
||||
return [...localExtensionUris].concat([...externalExtensionUris], [...workspaceUris]);
|
||||
},
|
||||
get onDidChange() {
|
||||
return onChange.event;
|
||||
},
|
||||
getContent(uriString: string): Thenable<string> {
|
||||
const uri = Uri.parse(uriString);
|
||||
if (localExtensionUris.has(uriString)) {
|
||||
return workspace.fs.readFile(uri).then(buffer => {
|
||||
return new runtime.TextDecoder().decode(buffer);
|
||||
});
|
||||
}
|
||||
return workspace.openTextDocument(uri).then(doc => {
|
||||
return doc.getText();
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function hasChanges(s1: Set<string>, s2: Set<string>) {
|
||||
if (s1.size !== s2.size) {
|
||||
return true;
|
||||
}
|
||||
for (const uri of s1) {
|
||||
if (!s2.has(uri)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isURI(uriOrPath: string) {
|
||||
return /^(?<scheme>\w[\w\d+.-]*):/.test(uriOrPath);
|
||||
}
|
||||
|
||||
|
||||
function collectInWorkspaces(workspaceUris: Set<string>): Set<string> {
|
||||
const workspaceFolders = workspace.workspaceFolders;
|
||||
|
||||
const dataPaths = new Set<string>();
|
||||
|
||||
if (!workspaceFolders) {
|
||||
return dataPaths;
|
||||
}
|
||||
|
||||
const collect = (uriOrPaths: string[] | undefined, rootFolder: Uri) => {
|
||||
if (Array.isArray(uriOrPaths)) {
|
||||
for (const uriOrPath of uriOrPaths) {
|
||||
if (typeof uriOrPath === 'string') {
|
||||
if (!isURI(uriOrPath)) {
|
||||
// path in the workspace
|
||||
workspaceUris.add(Utils.resolvePath(rootFolder, uriOrPath).toString());
|
||||
} else {
|
||||
// external uri
|
||||
workspaceUris.add(uriOrPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < workspaceFolders.length; i++) {
|
||||
const folderUri = workspaceFolders[i].uri;
|
||||
const allHtmlConfig = workspace.getConfiguration('html', folderUri);
|
||||
const customDataInspect = allHtmlConfig.inspect<string[]>('customData');
|
||||
if (customDataInspect) {
|
||||
collect(customDataInspect.workspaceFolderValue, folderUri);
|
||||
if (i === 0) {
|
||||
if (workspace.workspaceFile) {
|
||||
collect(customDataInspect.workspaceValue, workspace.workspaceFile);
|
||||
}
|
||||
collect(customDataInspect.globalValue, folderUri);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return dataPaths;
|
||||
}
|
||||
|
||||
function collectInExtensions(localExtensionUris: Set<string>, externalUris: Set<string>): void {
|
||||
for (const extension of extensions.allAcrossExtensionHosts) {
|
||||
const customData = extension.packageJSON?.contributes?.html?.customData;
|
||||
if (Array.isArray(customData)) {
|
||||
for (const uriOrPath of customData) {
|
||||
if (!isURI(uriOrPath)) {
|
||||
// relative path in an extension
|
||||
localExtensionUris.add(Uri.joinPath(extension.extensionUri, uriOrPath).toString());
|
||||
} else {
|
||||
// external uri
|
||||
externalUris.add(uriOrPath);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import {
|
||||
languages, ExtensionContext, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, workspace, extensions,
|
||||
Disposable, FormattingOptions, CancellationToken, ProviderResult, TextEdit, CompletionContext, CompletionList, SemanticTokensLegend,
|
||||
DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider, SemanticTokens, window, commands, OutputChannel, l10n
|
||||
} from 'vscode';
|
||||
import {
|
||||
LanguageClientOptions, RequestType, DocumentRangeFormattingParams,
|
||||
DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, TextDocumentIdentifier, RequestType0, Range as LspRange, Position as LspPosition, NotificationType, BaseLanguageClient
|
||||
} from 'vscode-languageclient';
|
||||
import { FileSystemProvider, serveFileSystemRequests } from './requests';
|
||||
import { getCustomDataSource } from './customData';
|
||||
import { activateAutoInsertion } from './autoInsertion';
|
||||
import { getLanguageParticipants, LanguageParticipants } from './languageParticipants';
|
||||
|
||||
namespace CustomDataChangedNotification {
|
||||
export const type: NotificationType<string[]> = new NotificationType('html/customDataChanged');
|
||||
}
|
||||
|
||||
namespace CustomDataContent {
|
||||
export const type: RequestType<string, string, any> = new RequestType('html/customDataContent');
|
||||
}
|
||||
|
||||
interface AutoInsertParams {
|
||||
/**
|
||||
* The auto insert kind
|
||||
*/
|
||||
kind: 'autoQuote' | 'autoClose';
|
||||
/**
|
||||
* The text document.
|
||||
*/
|
||||
textDocument: TextDocumentIdentifier;
|
||||
/**
|
||||
* The position inside the text document.
|
||||
*/
|
||||
position: LspPosition;
|
||||
}
|
||||
|
||||
namespace AutoInsertRequest {
|
||||
export const type: RequestType<AutoInsertParams, string, any> = new RequestType('html/autoInsert');
|
||||
}
|
||||
|
||||
// experimental: semantic tokens
|
||||
interface SemanticTokenParams {
|
||||
textDocument: TextDocumentIdentifier;
|
||||
ranges?: LspRange[];
|
||||
}
|
||||
namespace SemanticTokenRequest {
|
||||
export const type: RequestType<SemanticTokenParams, number[] | null, any> = new RequestType('html/semanticTokens');
|
||||
}
|
||||
namespace SemanticTokenLegendRequest {
|
||||
export const type: RequestType0<{ types: string[]; modifiers: string[] } | null, any> = new RequestType0('html/semanticTokenLegend');
|
||||
}
|
||||
|
||||
namespace SettingIds {
|
||||
export const linkedEditing = 'editor.linkedEditing';
|
||||
export const formatEnable = 'html.format.enable';
|
||||
|
||||
}
|
||||
|
||||
export interface TelemetryReporter {
|
||||
sendTelemetryEvent(eventName: string, properties?: {
|
||||
[key: string]: string;
|
||||
}, measurements?: {
|
||||
[key: string]: number;
|
||||
}): void;
|
||||
}
|
||||
|
||||
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;
|
||||
|
||||
export const languageServerDescription = l10n.t('HTML Language Server');
|
||||
|
||||
export interface Runtime {
|
||||
TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string } };
|
||||
fileFs?: FileSystemProvider;
|
||||
telemetry?: TelemetryReporter;
|
||||
readonly timer: {
|
||||
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AsyncDisposable {
|
||||
dispose(): Promise<void>;
|
||||
}
|
||||
|
||||
export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise<AsyncDisposable> {
|
||||
|
||||
const outputChannel = window.createOutputChannel(languageServerDescription);
|
||||
|
||||
const languageParticipants = getLanguageParticipants();
|
||||
context.subscriptions.push(languageParticipants);
|
||||
|
||||
let client: Disposable | undefined = await startClientWithParticipants(languageParticipants, newLanguageClient, outputChannel, runtime);
|
||||
|
||||
const promptForLinkedEditingKey = 'html.promptForLinkedEditing';
|
||||
if (extensions.getExtension('formulahendry.auto-rename-tag') !== undefined && (context.globalState.get(promptForLinkedEditingKey) !== false)) {
|
||||
const config = workspace.getConfiguration('editor', { languageId: 'html' });
|
||||
if (!config.get('linkedEditing') && !config.get('renameOnType')) {
|
||||
const activeEditorListener = window.onDidChangeActiveTextEditor(async e => {
|
||||
if (e && languageParticipants.hasLanguage(e.document.languageId)) {
|
||||
context.globalState.update(promptForLinkedEditingKey, false);
|
||||
activeEditorListener.dispose();
|
||||
const configure = l10n.t('Configure');
|
||||
const res = await window.showInformationMessage(l10n.t('VS Code now has built-in support for auto-renaming tags. Do you want to enable it?'), configure);
|
||||
if (res === configure) {
|
||||
commands.executeCommand('workbench.action.openSettings', SettingIds.linkedEditing);
|
||||
}
|
||||
}
|
||||
});
|
||||
context.subscriptions.push(activeEditorListener);
|
||||
}
|
||||
}
|
||||
|
||||
let restartTrigger: Disposable | undefined;
|
||||
languageParticipants.onDidChange(() => {
|
||||
if (restartTrigger) {
|
||||
restartTrigger.dispose();
|
||||
}
|
||||
restartTrigger = runtime.timer.setTimeout(async () => {
|
||||
if (client) {
|
||||
outputChannel.appendLine('Extensions have changed, restarting HTML server...');
|
||||
outputChannel.appendLine('');
|
||||
const oldClient = client;
|
||||
client = undefined;
|
||||
await oldClient.dispose();
|
||||
client = await startClientWithParticipants(languageParticipants, newLanguageClient, outputChannel, runtime);
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
return {
|
||||
dispose: async () => {
|
||||
restartTrigger?.dispose();
|
||||
await client?.dispose();
|
||||
outputChannel.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function startClientWithParticipants(languageParticipants: LanguageParticipants, newLanguageClient: LanguageClientConstructor, outputChannel: OutputChannel, runtime: Runtime): Promise<AsyncDisposable> {
|
||||
|
||||
const toDispose: Disposable[] = [];
|
||||
|
||||
const documentSelector = languageParticipants.documentSelector;
|
||||
const embeddedLanguages = { css: true, javascript: true };
|
||||
|
||||
let rangeFormatting: Disposable | undefined = undefined;
|
||||
|
||||
// Options to control the language client
|
||||
const clientOptions: LanguageClientOptions = {
|
||||
documentSelector,
|
||||
synchronize: {
|
||||
configurationSection: ['html', 'css', 'javascript', 'js/ts'], // the settings to synchronize
|
||||
},
|
||||
initializationOptions: {
|
||||
embeddedLanguages,
|
||||
handledSchemas: ['file'],
|
||||
provideFormatter: false, // tell the server to not provide formatting capability and ignore the `html.format.enable` setting.
|
||||
customCapabilities: { rangeFormatting: { editLimit: 10000 } }
|
||||
},
|
||||
middleware: {
|
||||
// testing the replace / insert mode
|
||||
provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult<CompletionItem[] | CompletionList> {
|
||||
function updateRanges(item: CompletionItem) {
|
||||
const range = item.range;
|
||||
if (range instanceof Range && range.end.isAfter(position) && range.start.isBeforeOrEqual(position)) {
|
||||
item.range = { inserting: new Range(range.start, position), replacing: range };
|
||||
}
|
||||
}
|
||||
function updateProposals(r: CompletionItem[] | CompletionList | null | undefined): CompletionItem[] | CompletionList | null | undefined {
|
||||
if (r) {
|
||||
(Array.isArray(r) ? r : r.items).forEach(updateRanges);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
const isThenable = <T>(obj: ProviderResult<T>): obj is Thenable<T> => obj && (<any>obj)['then'];
|
||||
|
||||
const r = next(document, position, context, token);
|
||||
if (isThenable<CompletionItem[] | CompletionList | null | undefined>(r)) {
|
||||
return r.then(updateProposals);
|
||||
}
|
||||
return updateProposals(r);
|
||||
}
|
||||
}
|
||||
};
|
||||
clientOptions.outputChannel = outputChannel;
|
||||
|
||||
// Create the language client and start the client.
|
||||
const client = newLanguageClient('html', languageServerDescription, clientOptions);
|
||||
client.registerProposedFeatures();
|
||||
|
||||
await client.start();
|
||||
|
||||
toDispose.push(serveFileSystemRequests(client, runtime));
|
||||
|
||||
const customDataSource = getCustomDataSource(runtime, toDispose);
|
||||
|
||||
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
|
||||
customDataSource.onDidChange(() => {
|
||||
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
|
||||
}, undefined, toDispose);
|
||||
toDispose.push(client.onRequest(CustomDataContent.type, customDataSource.getContent));
|
||||
|
||||
|
||||
const insertRequestor = (kind: 'autoQuote' | 'autoClose', document: TextDocument, position: Position): Promise<string> => {
|
||||
const param: AutoInsertParams = {
|
||||
kind,
|
||||
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
|
||||
position: client.code2ProtocolConverter.asPosition(position)
|
||||
};
|
||||
return client.sendRequest(AutoInsertRequest.type, param);
|
||||
};
|
||||
|
||||
const disposable = activateAutoInsertion(insertRequestor, languageParticipants, runtime);
|
||||
toDispose.push(disposable);
|
||||
|
||||
const disposable2 = client.onTelemetry(e => {
|
||||
runtime.telemetry?.sendTelemetryEvent(e.key, e.data);
|
||||
});
|
||||
toDispose.push(disposable2);
|
||||
|
||||
// manually register / deregister format provider based on the `html.format.enable` setting avoiding issues with late registration. See #71652.
|
||||
updateFormatterRegistration();
|
||||
toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() });
|
||||
toDispose.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration(SettingIds.formatEnable) && updateFormatterRegistration()));
|
||||
|
||||
client.sendRequest(SemanticTokenLegendRequest.type).then(legend => {
|
||||
if (legend) {
|
||||
const provider: DocumentSemanticTokensProvider & DocumentRangeSemanticTokensProvider = {
|
||||
provideDocumentSemanticTokens(doc) {
|
||||
const params: SemanticTokenParams = {
|
||||
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc),
|
||||
};
|
||||
return client.sendRequest(SemanticTokenRequest.type, params).then(data => {
|
||||
return data && new SemanticTokens(new Uint32Array(data));
|
||||
});
|
||||
},
|
||||
provideDocumentRangeSemanticTokens(doc, range) {
|
||||
const params: SemanticTokenParams = {
|
||||
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc),
|
||||
ranges: [client.code2ProtocolConverter.asRange(range)]
|
||||
};
|
||||
return client.sendRequest(SemanticTokenRequest.type, params).then(data => {
|
||||
return data && new SemanticTokens(new Uint32Array(data));
|
||||
});
|
||||
}
|
||||
};
|
||||
toDispose.push(languages.registerDocumentSemanticTokensProvider(documentSelector, provider, new SemanticTokensLegend(legend.types, legend.modifiers)));
|
||||
}
|
||||
});
|
||||
|
||||
function updateFormatterRegistration() {
|
||||
const formatEnabled = workspace.getConfiguration().get(SettingIds.formatEnable);
|
||||
if (!formatEnabled && rangeFormatting) {
|
||||
rangeFormatting.dispose();
|
||||
rangeFormatting = undefined;
|
||||
} else if (formatEnabled && !rangeFormatting) {
|
||||
rangeFormatting = languages.registerDocumentRangeFormattingEditProvider(documentSelector, {
|
||||
provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult<TextEdit[]> {
|
||||
const filesConfig = workspace.getConfiguration('files', document);
|
||||
const fileFormattingOptions = {
|
||||
trimTrailingWhitespace: filesConfig.get<boolean>('trimTrailingWhitespace'),
|
||||
trimFinalNewlines: filesConfig.get<boolean>('trimFinalNewlines'),
|
||||
insertFinalNewline: filesConfig.get<boolean>('insertFinalNewline'),
|
||||
};
|
||||
const params: DocumentRangeFormattingParams = {
|
||||
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
|
||||
range: client.code2ProtocolConverter.asRange(range),
|
||||
options: client.code2ProtocolConverter.asFormattingOptions(options, fileFormattingOptions)
|
||||
};
|
||||
return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then(
|
||||
client.protocol2CodeConverter.asTextEdits,
|
||||
(error) => {
|
||||
client.handleFailedRequest(DocumentRangeFormattingRequest.type, undefined, error, []);
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const regionCompletionRegExpr = /^(\s*)(<(!(-(-\s*(#\w*)?)?)?)?)?$/;
|
||||
const htmlSnippetCompletionRegExpr = /^(\s*)(<(h(t(m(l)?)?)?)?)?$/;
|
||||
toDispose.push(languages.registerCompletionItemProvider(documentSelector, {
|
||||
provideCompletionItems(doc, pos) {
|
||||
const results: CompletionItem[] = [];
|
||||
const lineUntilPos = doc.getText(new Range(new Position(pos.line, 0), pos));
|
||||
const match = lineUntilPos.match(regionCompletionRegExpr);
|
||||
if (match) {
|
||||
const range = new Range(new Position(pos.line, match[1].length), pos);
|
||||
const beginProposal = new CompletionItem('#region', CompletionItemKind.Snippet);
|
||||
beginProposal.range = range;
|
||||
beginProposal.insertText = new SnippetString('<!-- #region $1-->');
|
||||
beginProposal.documentation = l10n.t('Folding Region Start');
|
||||
beginProposal.filterText = match[2];
|
||||
beginProposal.sortText = 'za';
|
||||
results.push(beginProposal);
|
||||
const endProposal = new CompletionItem('#endregion', CompletionItemKind.Snippet);
|
||||
endProposal.range = range;
|
||||
endProposal.insertText = new SnippetString('<!-- #endregion -->');
|
||||
endProposal.documentation = l10n.t('Folding Region End');
|
||||
endProposal.filterText = match[2];
|
||||
endProposal.sortText = 'zb';
|
||||
results.push(endProposal);
|
||||
}
|
||||
const match2 = lineUntilPos.match(htmlSnippetCompletionRegExpr);
|
||||
if (match2 && doc.getText(new Range(new Position(0, 0), pos)).match(htmlSnippetCompletionRegExpr)) {
|
||||
const range = new Range(new Position(pos.line, match2[1].length), pos);
|
||||
const snippetProposal = new CompletionItem('HTML sample', CompletionItemKind.Snippet);
|
||||
snippetProposal.range = range;
|
||||
const content = ['<!DOCTYPE html>',
|
||||
'<html>',
|
||||
'<head>',
|
||||
'\t<meta charset=\'utf-8\'>',
|
||||
'\t<meta http-equiv=\'X-UA-Compatible\' content=\'IE=edge\'>',
|
||||
'\t<title>${1:Page Title}</title>',
|
||||
'\t<meta name=\'viewport\' content=\'width=device-width, initial-scale=1\'>',
|
||||
'\t<link rel=\'stylesheet\' type=\'text/css\' media=\'screen\' href=\'${2:main.css}\'>',
|
||||
'\t<script src=\'${3:main.js}\'></script>',
|
||||
'</head>',
|
||||
'<body>',
|
||||
'\t$0',
|
||||
'</body>',
|
||||
'</html>'].join('\n');
|
||||
snippetProposal.insertText = new SnippetString(content);
|
||||
snippetProposal.documentation = l10n.t('Simple HTML5 starting point');
|
||||
snippetProposal.filterText = match2[2];
|
||||
snippetProposal.sortText = 'za';
|
||||
results.push(snippetProposal);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}));
|
||||
|
||||
return {
|
||||
dispose: async () => {
|
||||
await client.stop();
|
||||
toDispose.forEach(d => d.dispose());
|
||||
rangeFormatting?.dispose();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, EventEmitter, extensions } from 'vscode';
|
||||
|
||||
/**
|
||||
* HTML language participant contribution.
|
||||
*/
|
||||
interface LanguageParticipantContribution {
|
||||
/**
|
||||
* The id of the language which participates with the HTML language server.
|
||||
*/
|
||||
languageId: string;
|
||||
/**
|
||||
* true if the language activates the auto insertion and false otherwise.
|
||||
*/
|
||||
autoInsert?: boolean;
|
||||
}
|
||||
|
||||
export interface LanguageParticipants {
|
||||
readonly onDidChange: Event<void>;
|
||||
readonly documentSelector: string[];
|
||||
hasLanguage(languageId: string): boolean;
|
||||
useAutoInsert(languageId: string): boolean;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export function getLanguageParticipants(): LanguageParticipants {
|
||||
const onDidChangeEmmiter = new EventEmitter<void>();
|
||||
let languages = new Set<string>();
|
||||
let autoInsert = new Set<string>();
|
||||
|
||||
function update() {
|
||||
const oldLanguages = languages, oldAutoInsert = autoInsert;
|
||||
|
||||
languages = new Set();
|
||||
languages.add('html');
|
||||
autoInsert = new Set();
|
||||
autoInsert.add('html');
|
||||
|
||||
for (const extension of extensions.allAcrossExtensionHosts) {
|
||||
const htmlLanguageParticipants = extension.packageJSON?.contributes?.htmlLanguageParticipants as LanguageParticipantContribution[];
|
||||
if (Array.isArray(htmlLanguageParticipants)) {
|
||||
for (const htmlLanguageParticipant of htmlLanguageParticipants) {
|
||||
const languageId = htmlLanguageParticipant.languageId;
|
||||
if (typeof languageId === 'string') {
|
||||
languages.add(languageId);
|
||||
if (htmlLanguageParticipant.autoInsert !== false) {
|
||||
autoInsert.add(languageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return !isEqualSet(languages, oldLanguages) || !isEqualSet(autoInsert, oldAutoInsert);
|
||||
}
|
||||
update();
|
||||
|
||||
const changeListener = extensions.onDidChange(_ => {
|
||||
if (update()) {
|
||||
onDidChangeEmmiter.fire();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
onDidChange: onDidChangeEmmiter.event,
|
||||
get documentSelector() { return Array.from(languages); },
|
||||
hasLanguage(languageId: string) { return languages.has(languageId); },
|
||||
useAutoInsert(languageId: string) { return autoInsert.has(languageId); },
|
||||
dispose: () => changeListener.dispose()
|
||||
};
|
||||
}
|
||||
|
||||
function isEqualSet<T>(s1: Set<T>, s2: Set<T>) {
|
||||
if (s1.size !== s2.size) {
|
||||
return false;
|
||||
}
|
||||
for (const e of s1) {
|
||||
if (!s2.has(e)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { getNodeFileFS } from './nodeFs';
|
||||
import { Disposable, ExtensionContext, l10n } from 'vscode';
|
||||
import { startClient, LanguageClientConstructor, AsyncDisposable } from '../htmlClient';
|
||||
import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node';
|
||||
import { TextDecoder } from 'util';
|
||||
import * as fs from 'fs';
|
||||
import TelemetryReporter from '@vscode/extension-telemetry';
|
||||
|
||||
|
||||
let telemetry: TelemetryReporter | undefined;
|
||||
let client: AsyncDisposable | undefined;
|
||||
|
||||
// this method is called when vs code is activated
|
||||
export async function activate(context: ExtensionContext) {
|
||||
|
||||
const clientPackageJSON = getPackageInfo(context);
|
||||
telemetry = new TelemetryReporter(clientPackageJSON.aiKey);
|
||||
|
||||
const serverMain = `./server/${clientPackageJSON.main.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/htmlServerMain`;
|
||||
const serverModule = context.asAbsolutePath(serverMain);
|
||||
|
||||
// The debug options for the server
|
||||
const debugOptions = { execArgv: ['--nolazy', '--inspect=' + (8000 + Math.round(Math.random() * 999))] };
|
||||
|
||||
// If the extension is launch in debug mode the debug server options are use
|
||||
// Otherwise the run options are used
|
||||
const serverOptions: ServerOptions = {
|
||||
run: { module: serverModule, transport: TransportKind.ipc },
|
||||
debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }
|
||||
};
|
||||
|
||||
const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => {
|
||||
return new LanguageClient(id, name, serverOptions, clientOptions);
|
||||
};
|
||||
|
||||
const timer = {
|
||||
setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable {
|
||||
const handle = setTimeout(callback, ms, ...args);
|
||||
return { dispose: () => clearTimeout(handle) };
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// pass the location of the localization bundle to the server
|
||||
process.env['VSCODE_L10N_BUNDLE_LOCATION'] = l10n.uri?.toString() ?? '';
|
||||
|
||||
client = await startClient(context, newLanguageClient, { fileFs: getNodeFileFS(), TextDecoder, telemetry, timer });
|
||||
}
|
||||
|
||||
export async function deactivate(): Promise<void> {
|
||||
if (client) {
|
||||
await client.dispose();
|
||||
client = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
interface IPackageInfo {
|
||||
name: string;
|
||||
version: string;
|
||||
aiKey: string;
|
||||
main: string;
|
||||
}
|
||||
|
||||
function getPackageInfo(context: ExtensionContext): IPackageInfo {
|
||||
const location = context.asAbsolutePath('./package.json');
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(location).toString());
|
||||
} catch (e) {
|
||||
console.log(`Problems reading ${location}: ${e}`);
|
||||
return { name: '', version: '', aiKey: '', main: '' };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import { Uri } from 'vscode';
|
||||
import { FileSystemProvider, FileType } from '../requests';
|
||||
|
||||
export function getNodeFileFS(): FileSystemProvider {
|
||||
function ensureFileUri(location: string) {
|
||||
if (!location.startsWith('file:')) {
|
||||
throw new Error('fileRequestService can only handle file URLs');
|
||||
}
|
||||
}
|
||||
return {
|
||||
stat(location: string) {
|
||||
ensureFileUri(location);
|
||||
return new Promise((c, e) => {
|
||||
const uri = Uri.parse(location);
|
||||
fs.stat(uri.fsPath, (err, stats) => {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return c({ type: FileType.Unknown, ctime: -1, mtime: -1, size: -1 });
|
||||
} else {
|
||||
return e(err);
|
||||
}
|
||||
}
|
||||
|
||||
let type = FileType.Unknown;
|
||||
if (stats.isFile()) {
|
||||
type = FileType.File;
|
||||
} else if (stats.isDirectory()) {
|
||||
type = FileType.Directory;
|
||||
} else if (stats.isSymbolicLink()) {
|
||||
type = FileType.SymbolicLink;
|
||||
}
|
||||
|
||||
c({
|
||||
type,
|
||||
ctime: stats.ctime.getTime(),
|
||||
mtime: stats.mtime.getTime(),
|
||||
size: stats.size
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
readDirectory(location: string) {
|
||||
ensureFileUri(location);
|
||||
return new Promise((c, e) => {
|
||||
const path = Uri.parse(location).fsPath;
|
||||
|
||||
fs.readdir(path, { withFileTypes: true }, (err, children) => {
|
||||
if (err) {
|
||||
return e(err);
|
||||
}
|
||||
c(children.map(stat => {
|
||||
if (stat.isSymbolicLink()) {
|
||||
return [stat.name, FileType.SymbolicLink];
|
||||
} else if (stat.isDirectory()) {
|
||||
return [stat.name, FileType.Directory];
|
||||
} else if (stat.isFile()) {
|
||||
return [stat.name, FileType.File];
|
||||
} else {
|
||||
return [stat.name, FileType.Unknown];
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Uri, workspace, Disposable } from 'vscode';
|
||||
import { RequestType, BaseLanguageClient } from 'vscode-languageclient';
|
||||
import { Runtime } from './htmlClient';
|
||||
|
||||
export namespace FsStatRequest {
|
||||
export const type: RequestType<string, FileStat, any> = new RequestType('fs/stat');
|
||||
}
|
||||
|
||||
export namespace FsReadDirRequest {
|
||||
export const type: RequestType<string, [string, FileType][], any> = new RequestType('fs/readDir');
|
||||
}
|
||||
|
||||
export function serveFileSystemRequests(client: BaseLanguageClient, runtime: Runtime): Disposable {
|
||||
const disposables = [];
|
||||
disposables.push(client.onRequest(FsReadDirRequest.type, (uriString: string) => {
|
||||
const uri = Uri.parse(uriString);
|
||||
if (uri.scheme === 'file' && runtime.fileFs) {
|
||||
return runtime.fileFs.readDirectory(uriString);
|
||||
}
|
||||
return workspace.fs.readDirectory(uri);
|
||||
}));
|
||||
disposables.push(client.onRequest(FsStatRequest.type, (uriString: string) => {
|
||||
const uri = Uri.parse(uriString);
|
||||
if (uri.scheme === 'file' && runtime.fileFs) {
|
||||
return runtime.fileFs.stat(uriString);
|
||||
}
|
||||
return workspace.fs.stat(uri);
|
||||
}));
|
||||
return Disposable.from(...disposables);
|
||||
}
|
||||
|
||||
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 FileSystemProvider {
|
||||
stat(uri: string): Promise<FileStat>;
|
||||
readDirectory(uri: string): Promise<[string, FileType][]>;
|
||||
}
|
||||
Reference in New Issue
Block a user