From 4a8997afb556fa7f718c948e1d0d8c90496e58ec Mon Sep 17 00:00:00 2001 From: Johnson Chu Date: Fri, 9 Feb 2024 08:31:54 +0800 Subject: [PATCH 01/77] Embedded support by Volar --- .../client/src/autoInsertion.ts | 92 --- .../client/src/browser/htmlClientMain.ts | 51 -- .../client/src/customData.ts | 143 ----- .../client/src/htmlClient.ts | 349 ---------- .../client/src/languageParticipants.ts | 87 --- .../client/src/node/htmlClientMain.ts | 104 ++- .../client/src/node/nodeFs.ts | 72 --- .../client/src/requests.ts | 78 --- .../html-language-features/package.json | 6 +- .../server/package.json | 9 +- .../server/src/browser/htmlServerMain.ts | 42 +- .../server/src/customData.ts | 38 -- .../server/src/htmlServer.ts | 593 ----------------- .../server/src/languageModelCache.ts | 82 --- .../server/src/languagePlugin.ts | 86 +++ .../server/src/modes/cssMode.ts | 73 --- .../server/src/modes/embeddedSupport.ts | 166 +---- .../server/src/modes/formatting.ts | 96 --- .../server/src/modes/htmlFolding.ts | 115 ---- .../server/src/modes/htmlMode.ts | 106 --- .../server/src/modes/javascriptLibs.ts | 33 - .../server/src/modes/javascriptMode.ts | 603 ------------------ .../src/modes/javascriptSemanticTokens.ts | 109 ---- .../server/src/modes/languageModes.ts | 188 ------ .../server/src/modes/selectionRanges.ts | 26 - .../server/src/modes/semanticTokens.ts | 137 ---- .../server/src/node/htmlServerMain.ts | 48 +- .../server/src/node/nodeFs.ts | 74 --- .../server/src/requests.ts | 78 --- .../server/src/test/completions.test.ts | 319 --------- .../server/src/test/documentContext.test.ts | 20 - .../server/src/test/embedded.test.ts | 131 ---- .../test/fixtures/expected/19813-4spaces.html | 22 - .../src/test/fixtures/expected/19813-tab.html | 22 - .../src/test/fixtures/expected/19813.html | 22 - .../src/test/fixtures/expected/21634.html | 6 - .../src/test/fixtures/inputs/19813.html | 19 - .../src/test/fixtures/inputs/21634.html | 6 - .../server/src/test/folding.test.ts | 215 ------- .../server/src/test/formatting.test.ts | 207 ------ .../src/test/pathCompletionFixtures/.foo.js | 4 - .../pathCompletionFixtures/about/about.css | 4 - .../pathCompletionFixtures/about/about.html | 0 .../about/media/icon.pic | 4 - .../test/pathCompletionFixtures/index.html | 0 .../pathCompletionFixtures/src/feature.js | 4 - .../test/pathCompletionFixtures/src/test.js | 4 - .../server/src/test/rename.test.ts | 205 ------ .../server/src/test/selectionRanges.test.ts | 81 --- .../server/src/test/semanticTokens.test.ts | 228 ------- .../server/src/test/words.test.ts | 71 --- .../server/src/utils/arrays.ts | 76 --- .../server/src/utils/documentContext.ts | 43 -- .../server/src/utils/positions.ts | 16 - .../server/src/utils/runner.ts | 47 -- .../server/src/utils/strings.ts | 78 --- .../server/src/utils/validation.ts | 108 ---- .../html-language-features/server/yarn.lock | 132 +++- extensions/html-language-features/yarn.lock | 108 +++- 59 files changed, 426 insertions(+), 5460 deletions(-) delete mode 100644 extensions/html-language-features/client/src/autoInsertion.ts delete mode 100644 extensions/html-language-features/client/src/browser/htmlClientMain.ts delete mode 100644 extensions/html-language-features/client/src/customData.ts delete mode 100644 extensions/html-language-features/client/src/htmlClient.ts delete mode 100644 extensions/html-language-features/client/src/languageParticipants.ts delete mode 100644 extensions/html-language-features/client/src/node/nodeFs.ts delete mode 100644 extensions/html-language-features/client/src/requests.ts delete mode 100644 extensions/html-language-features/server/src/customData.ts delete mode 100644 extensions/html-language-features/server/src/htmlServer.ts delete mode 100644 extensions/html-language-features/server/src/languageModelCache.ts create mode 100644 extensions/html-language-features/server/src/languagePlugin.ts delete mode 100644 extensions/html-language-features/server/src/modes/cssMode.ts delete mode 100644 extensions/html-language-features/server/src/modes/formatting.ts delete mode 100644 extensions/html-language-features/server/src/modes/htmlFolding.ts delete mode 100644 extensions/html-language-features/server/src/modes/htmlMode.ts delete mode 100644 extensions/html-language-features/server/src/modes/javascriptLibs.ts delete mode 100644 extensions/html-language-features/server/src/modes/javascriptMode.ts delete mode 100644 extensions/html-language-features/server/src/modes/javascriptSemanticTokens.ts delete mode 100644 extensions/html-language-features/server/src/modes/languageModes.ts delete mode 100644 extensions/html-language-features/server/src/modes/selectionRanges.ts delete mode 100644 extensions/html-language-features/server/src/modes/semanticTokens.ts delete mode 100644 extensions/html-language-features/server/src/node/nodeFs.ts delete mode 100644 extensions/html-language-features/server/src/requests.ts delete mode 100644 extensions/html-language-features/server/src/test/completions.test.ts delete mode 100644 extensions/html-language-features/server/src/test/documentContext.test.ts delete mode 100644 extensions/html-language-features/server/src/test/embedded.test.ts delete mode 100644 extensions/html-language-features/server/src/test/fixtures/expected/19813-4spaces.html delete mode 100644 extensions/html-language-features/server/src/test/fixtures/expected/19813-tab.html delete mode 100644 extensions/html-language-features/server/src/test/fixtures/expected/19813.html delete mode 100644 extensions/html-language-features/server/src/test/fixtures/expected/21634.html delete mode 100644 extensions/html-language-features/server/src/test/fixtures/inputs/19813.html delete mode 100644 extensions/html-language-features/server/src/test/fixtures/inputs/21634.html delete mode 100644 extensions/html-language-features/server/src/test/folding.test.ts delete mode 100644 extensions/html-language-features/server/src/test/formatting.test.ts delete mode 100644 extensions/html-language-features/server/src/test/pathCompletionFixtures/.foo.js delete mode 100644 extensions/html-language-features/server/src/test/pathCompletionFixtures/about/about.css delete mode 100644 extensions/html-language-features/server/src/test/pathCompletionFixtures/about/about.html delete mode 100644 extensions/html-language-features/server/src/test/pathCompletionFixtures/about/media/icon.pic delete mode 100644 extensions/html-language-features/server/src/test/pathCompletionFixtures/index.html delete mode 100644 extensions/html-language-features/server/src/test/pathCompletionFixtures/src/feature.js delete mode 100644 extensions/html-language-features/server/src/test/pathCompletionFixtures/src/test.js delete mode 100644 extensions/html-language-features/server/src/test/rename.test.ts delete mode 100644 extensions/html-language-features/server/src/test/selectionRanges.test.ts delete mode 100644 extensions/html-language-features/server/src/test/semanticTokens.test.ts delete mode 100644 extensions/html-language-features/server/src/test/words.test.ts delete mode 100644 extensions/html-language-features/server/src/utils/arrays.ts delete mode 100644 extensions/html-language-features/server/src/utils/documentContext.ts delete mode 100644 extensions/html-language-features/server/src/utils/positions.ts delete mode 100644 extensions/html-language-features/server/src/utils/runner.ts delete mode 100644 extensions/html-language-features/server/src/utils/strings.ts delete mode 100644 extensions/html-language-features/server/src/utils/validation.ts diff --git a/extensions/html-language-features/client/src/autoInsertion.ts b/extensions/html-language-features/client/src/autoInsertion.ts deleted file mode 100644 index e95e6a64a09db..0000000000000 --- a/extensions/html-language-features/client/src/autoInsertion.ts +++ /dev/null @@ -1,92 +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 { 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, 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('html.autoCreateQuotes') ?? false; - isEnabled['autoClose'] = configurations.get('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]; - const lastCharacter = lastChange.text[lastChange.text.length - 1]; - if (isEnabled['autoQuote'] && lastChange.rangeLength === 0 && lastCharacter === '=') { - doAutoInsert('autoQuote', document, lastChange); - } else if (isEnabled['autoClose'] && lastChange.rangeLength === 0 && (lastCharacter === '>' || lastCharacter === '/')) { - doAutoInsert('autoClose', document, lastChange); - } - } - - 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); -} diff --git a/extensions/html-language-features/client/src/browser/htmlClientMain.ts b/extensions/html-language-features/client/src/browser/htmlClientMain.ts deleted file mode 100644 index 3f10e6d131fa7..0000000000000 --- a/extensions/html-language-features/client/src/browser/htmlClientMain.ts +++ /dev/null @@ -1,51 +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 { Disposable, ExtensionContext, Uri, l10n } from 'vscode'; -import { LanguageClientOptions } from 'vscode-languageclient'; -import { startClient, LanguageClientConstructor, AsyncDisposable } from '../htmlClient'; -import { LanguageClient } from 'vscode-languageclient/browser'; - -declare const Worker: { - new(stringUrl: string): any; -}; -declare const TextDecoder: { - new(encoding?: string): { decode(buffer: ArrayBuffer): string }; -}; - -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, clientOptions, worker); - }; - - 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 { - if (client) { - await client.dispose(); - client = undefined; - } -} - diff --git a/extensions/html-language-features/client/src/customData.ts b/extensions/html-language-features/client/src/customData.ts deleted file mode 100644 index 71d847a4f391a..0000000000000 --- a/extensions/html-language-features/client/src/customData.ts +++ /dev/null @@ -1,143 +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 { 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(); - let externalExtensionUris = new Set(); - const workspaceUris = new Set(); - - collectInWorkspaces(workspaceUris); - collectInExtensions(localExtensionUris, externalExtensionUris); - - const onChange = new EventEmitter(); - - toDispose.push(extensions.onDidChange(_ => { - const newLocalExtensionUris = new Set(); - const newExternalExtensionUris = new Set(); - 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 { - 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, s2: Set) { - 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 /^(?\w[\w\d+.-]*):/.test(uriOrPath); -} - - -function collectInWorkspaces(workspaceUris: Set): Set { - const workspaceFolders = workspace.workspaceFolders; - - const dataPaths = new Set(); - - 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('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, externalUris: Set): 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); - } - - } - } - } -} diff --git a/extensions/html-language-features/client/src/htmlClient.ts b/extensions/html-language-features/client/src/htmlClient.ts deleted file mode 100644 index 7b69c795f9043..0000000000000 --- a/extensions/html-language-features/client/src/htmlClient.ts +++ /dev/null @@ -1,349 +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 { - 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 = new NotificationType('html/customDataChanged'); -} - -namespace CustomDataContent { - export const type: RequestType = 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 = new RequestType('html/autoInsert'); -} - -// experimental: semantic tokens -interface SemanticTokenParams { - textDocument: TextDocumentIdentifier; - ranges?: LspRange[]; -} -namespace SemanticTokenRequest { - export const type: RequestType = 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; -} - -export async function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime): Promise { - - 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 { - - 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 { - 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 = (obj: ProviderResult): obj is Thenable => obj && (obj)['then']; - - const r = next(document, position, context, token); - if (isThenable(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 => { - 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 { - const filesConfig = workspace.getConfiguration('files', document); - const fileFormattingOptions = { - trimTrailingWhitespace: filesConfig.get('trimTrailingWhitespace'), - trimFinalNewlines: filesConfig.get('trimFinalNewlines'), - insertFinalNewline: filesConfig.get('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(''); - 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(''); - 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 = ['', - '', - '', - '\t', - '\t', - '\t${1:Page Title}', - '\t', - '\t', - '\t', - '', - '', - '\t$0', - '', - ''].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(); - } - }; - -} diff --git a/extensions/html-language-features/client/src/languageParticipants.ts b/extensions/html-language-features/client/src/languageParticipants.ts deleted file mode 100644 index e3d5612b9b32d..0000000000000 --- a/extensions/html-language-features/client/src/languageParticipants.ts +++ /dev/null @@ -1,87 +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 { DocumentSelector } from 'vscode-languageclient'; -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; - readonly documentSelector: DocumentSelector; - hasLanguage(languageId: string): boolean; - useAutoInsert(languageId: string): boolean; - dispose(): void; -} - -export function getLanguageParticipants(): LanguageParticipants { - const onDidChangeEmmiter = new EventEmitter(); - let languages = new Set(); - let autoInsert = new Set(); - - 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(s1: Set, s2: Set) { - if (s1.size !== s2.size) { - return false; - } - for (const e of s1) { - if (!s2.has(e)) { - return false; - } - } - return true; -} diff --git a/extensions/html-language-features/client/src/node/htmlClientMain.ts b/extensions/html-language-features/client/src/node/htmlClientMain.ts index cdb995b3286db..8a8dcd2e10364 100644 --- a/extensions/html-language-features/client/src/node/htmlClientMain.ts +++ b/extensions/html-language-features/client/src/node/htmlClientMain.ts @@ -3,75 +3,55 @@ * 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'; +import type { InitializationOptions } from '@volar/language-server'; +import * as serverProtocol from '@volar/language-server/protocol'; +import { activateAutoInsertion, createLabsInfo, getTsdk } from '@volar/vscode'; +import * as vscode from 'vscode'; +import * as lsp from 'vscode-languageclient/node'; +let client: lsp.BaseLanguageClient; -let telemetry: TelemetryReporter | undefined; -let client: AsyncDisposable | undefined; +export async function activate(context: vscode.ExtensionContext) { -// 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 serverModule = context.asAbsolutePath('./server/out/node/htmlServerMain'); + const runOptions = { execArgv: [] }; 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 serverOptions: lsp.ServerOptions = { + run: { + module: serverModule, + transport: lsp.TransportKind.ipc, + options: runOptions + }, + debug: { + module: serverModule, + transport: lsp.TransportKind.ipc, + options: debugOptions + }, }; - - const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - return new LanguageClient(id, name, serverOptions, clientOptions); + const initializationOptions: InitializationOptions = { + typescript: { + tsdk: (await getTsdk(context)).tsdk, + }, }; - - const timer = { - setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable { - const handle = setTimeout(callback, ms, ...args); - return { dispose: () => clearTimeout(handle) }; - } + const clientOptions: lsp.LanguageClientOptions = { + documentSelector: [{ language: 'html' }], + initializationOptions, }; - - - // 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 { - if (client) { - await client.dispose(); - client = undefined; - } -} - -interface IPackageInfo { - name: string; - version: string; - aiKey: string; - main: string; + client = new lsp.LanguageClient( + 'html', + 'HTML', + serverOptions, + clientOptions, + ); + await client.start(); + + activateAutoInsertion('html', client); + + const labsInfo = createLabsInfo(serverProtocol); + labsInfo.addLanguageClient(client); + return labsInfo.extensionExports; } -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: '' }; - } +export function deactivate(): Thenable | undefined { + return client?.stop(); } diff --git a/extensions/html-language-features/client/src/node/nodeFs.ts b/extensions/html-language-features/client/src/node/nodeFs.ts deleted file mode 100644 index 46a3aeb9de459..0000000000000 --- a/extensions/html-language-features/client/src/node/nodeFs.ts +++ /dev/null @@ -1,72 +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 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]; - } - })); - }); - }); - } - }; -} diff --git a/extensions/html-language-features/client/src/requests.ts b/extensions/html-language-features/client/src/requests.ts deleted file mode 100644 index 8106f0442280f..0000000000000 --- a/extensions/html-language-features/client/src/requests.ts +++ /dev/null @@ -1,78 +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 { Uri, workspace, Disposable } from 'vscode'; -import { RequestType, BaseLanguageClient } from 'vscode-languageclient'; -import { Runtime } from './htmlClient'; - -export namespace FsStatRequest { - export const type: RequestType = new RequestType('fs/stat'); -} - -export namespace FsReadDirRequest { - export const type: RequestType = 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; - readDirectory(uri: string): Promise<[string, FileType][]>; -} diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index 347af8182737c..8bae4c2df5bb9 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -258,9 +258,9 @@ ] }, "dependencies": { - "@vscode/extension-telemetry": "^0.9.0", - "vscode-languageclient": "9.0.1", - "vscode-uri": "^3.0.8" + "@volar/language-server": "~2.0.2", + "@volar/vscode": "~2.0.2", + "@vscode/extension-telemetry": "^0.9.0" }, "devDependencies": { "@types/node": "18.x" diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 13727939f3c8b..cc42f1feca34b 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -9,12 +9,11 @@ }, "main": "./out/node/htmlServerMain", "dependencies": { + "@volar/language-server": "~2.0.2", "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.2.12", - "vscode-html-languageservice": "^5.1.2", - "vscode-languageserver": "^9.0.2-next.1", - "vscode-languageserver-textdocument": "^1.0.11", - "vscode-uri": "^3.0.8" + "volar-service-css": "0.0.28", + "volar-service-html": "0.0.28", + "vscode-html-languageservice": "^5.1.2" }, "devDependencies": { "@types/mocha": "^9.1.1", diff --git a/extensions/html-language-features/server/src/browser/htmlServerMain.ts b/extensions/html-language-features/server/src/browser/htmlServerMain.ts index 3264513e0ff84..9a19dbeee874b 100644 --- a/extensions/html-language-features/server/src/browser/htmlServerMain.ts +++ b/extensions/html-language-features/server/src/browser/htmlServerMain.ts @@ -3,28 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createConnection, BrowserMessageReader, BrowserMessageWriter, Disposable } from 'vscode-languageserver/browser'; -import { RuntimeEnvironment, startServer } from '../htmlServer'; +import { createServer, createConnection, createSimpleProjectProvider } from '@volar/language-server/node'; +import { create as createCssServicePlugin } from 'volar-service-css'; +import { create as createHtmlServicePlugin } from 'volar-service-html'; +import { htmlLanguagePlugin } from '../languagePlugin'; -const messageReader = new BrowserMessageReader(self); -const messageWriter = new BrowserMessageWriter(self); +const connection = createConnection(); +const server = createServer(connection); -const connection = createConnection(messageReader, messageWriter); +connection.onInitialize(params => { + return server.initialize(params, createSimpleProjectProvider, { + getLanguagePlugins() { + return [htmlLanguagePlugin]; + }, + getServicePlugins() { + return [ + createCssServicePlugin(), + createHtmlServicePlugin(), + ]; + }, + }); +}); -console.log = connection.console.log.bind(connection.console); -console.error = connection.console.error.bind(connection.console); +connection.onInitialized(server.initialized); -const runtime: RuntimeEnvironment = { - timer: { - setImmediate(callback: (...args: any[]) => void, ...args: any[]): Disposable { - const handle = setTimeout(callback, 0, ...args); - return { dispose: () => clearTimeout(handle) }; - }, - setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable { - const handle = setTimeout(callback, ms, ...args); - return { dispose: () => clearTimeout(handle) }; - } - } -}; +connection.onShutdown(server.shutdown); -startServer(connection, runtime); +connection.listen(); diff --git a/extensions/html-language-features/server/src/customData.ts b/extensions/html-language-features/server/src/customData.ts deleted file mode 100644 index 08e40bf2db3ee..0000000000000 --- a/extensions/html-language-features/server/src/customData.ts +++ /dev/null @@ -1,38 +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 { IHTMLDataProvider, newHTMLDataProvider } from 'vscode-html-languageservice'; -import { CustomDataRequestService } from './htmlServer'; - -export function fetchHTMLDataProviders(dataPaths: string[], requestService: CustomDataRequestService): Promise { - const providers = dataPaths.map(async p => { - try { - const content = await requestService.getContent(p); - return parseHTMLData(p, content); - } catch (e) { - return newHTMLDataProvider(p, { version: 1 }); - } - }); - - return Promise.all(providers); -} - -function parseHTMLData(id: string, source: string): IHTMLDataProvider { - let rawData: any; - - try { - rawData = JSON.parse(source); - } catch (err) { - return newHTMLDataProvider(id, { version: 1 }); - } - - return newHTMLDataProvider(id, { - version: rawData.version || 1, - tags: rawData.tags || [], - globalAttributes: rawData.globalAttributes || [], - valueSets: rawData.valueSets || [] - }); -} - diff --git a/extensions/html-language-features/server/src/htmlServer.ts b/extensions/html-language-features/server/src/htmlServer.ts deleted file mode 100644 index 29aa041746cc5..0000000000000 --- a/extensions/html-language-features/server/src/htmlServer.ts +++ /dev/null @@ -1,593 +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 { - Connection, TextDocuments, InitializeParams, InitializeResult, RequestType, - DocumentRangeFormattingRequest, Disposable, ServerCapabilities, - ConfigurationRequest, ConfigurationParams, DidChangeWorkspaceFoldersNotification, - DocumentColorRequest, ColorPresentationRequest, TextDocumentSyncKind, NotificationType, RequestType0, DocumentFormattingRequest, FormattingOptions, TextEdit -} from 'vscode-languageserver'; -import { - getLanguageModes, LanguageModes, Settings, TextDocument, Position, Diagnostic, WorkspaceFolder, ColorInformation, - Range, DocumentLink, SymbolInformation, TextDocumentIdentifier, isCompletionItemData -} from './modes/languageModes'; - -import { format } from './modes/formatting'; -import { pushAll } from './utils/arrays'; -import { getDocumentContext } from './utils/documentContext'; -import { URI } from 'vscode-uri'; -import { formatError, runSafe } from './utils/runner'; -import { DiagnosticsSupport, registerDiagnosticsPullSupport, registerDiagnosticsPushSupport } from './utils/validation'; - -import { getFoldingRanges } from './modes/htmlFolding'; -import { fetchHTMLDataProviders } from './customData'; -import { getSelectionRanges } from './modes/selectionRanges'; -import { SemanticTokenProvider, newSemanticTokenProvider } from './modes/semanticTokens'; -import { FileSystemProvider, getFileSystemProvider } from './requests'; - -namespace CustomDataChangedNotification { - export const type: NotificationType = new NotificationType('html/customDataChanged'); -} - -namespace CustomDataContent { - export const type: RequestType = 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: Position; -} - -namespace AutoInsertRequest { - export const type: RequestType = new RequestType('html/autoInsert'); -} - -// experimental: semantic tokens -interface SemanticTokenParams { - textDocument: TextDocumentIdentifier; - ranges?: Range[]; -} -namespace SemanticTokenRequest { - export const type: RequestType = new RequestType('html/semanticTokens'); -} -namespace SemanticTokenLegendRequest { - export const type: RequestType0<{ types: string[]; modifiers: string[] } | null, any> = new RequestType0('html/semanticTokenLegend'); -} - -export interface RuntimeEnvironment { - fileFs?: FileSystemProvider; - configureHttpRequests?(proxy: string | undefined, strictSSL: boolean): void; - readonly timer: { - setImmediate(callback: (...args: any[]) => void, ...args: any[]): Disposable; - setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable; - }; -} - - -export interface CustomDataRequestService { - getContent(uri: string): Promise; -} - - -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 diagnosticsSupport: DiagnosticsSupport | undefined; - - let clientSnippetSupport = false; - let dynamicFormatterRegistration = false; - let scopedSettingsSupport = false; - let workspaceFoldersSupport = false; - let foldingRangeLimit = Number.MAX_VALUE; - let formatterMaxNumberOfEdits = Number.MAX_VALUE; - - const customDataRequestService: CustomDataRequestService = { - getContent(uri: string) { - return connection.sendRequest(CustomDataContent.type, uri); - } - }; - - let globalSettings: Settings = {}; - let documentSettings: { [key: string]: Thenable } = {}; - // remove document settings on close - documents.onDidClose(e => { - delete documentSettings[e.document.uri]; - }); - - function getDocumentSettings(textDocument: TextDocument, needsDocumentSettings: () => boolean): Thenable { - if (scopedSettingsSupport && needsDocumentSettings()) { - let promise = documentSettings[textDocument.uri]; - if (!promise) { - const scopeUri = textDocument.uri; - const sections = ['css', 'html', 'javascript', 'js/ts']; - const configRequestParam: ConfigurationParams = { items: sections.map(section => ({ scopeUri, section })) }; - promise = connection.sendRequest(ConfigurationRequest.type, configRequestParam).then(s => ({ css: s[0], html: s[1], javascript: s[2], 'js/ts': s[3] })); - 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 as any || {}; - - workspaceFolders = (params).workspaceFolders; - if (!Array.isArray(workspaceFolders)) { - workspaceFolders = []; - if (params.rootPath) { - workspaceFolders.push({ name: '', uri: URI.file(params.rootPath).toString() }); - } - } - - const handledSchemas = initializationOptions?.handledSchemas as string[] ?? ['file']; - - const fileSystemProvider = getFileSystemProvider(handledSchemas, connection, runtime); - - const workspace = { - get settings() { return globalSettings; }, - get folders() { return workspaceFolders; } - }; - - languageModes = getLanguageModes(initializationOptions?.embeddedLanguages || { css: true, javascript: true }, workspace, params.capabilities, fileSystemProvider); - - const dataPaths: string[] = initializationOptions?.dataPaths || []; - fetchHTMLDataProviders(dataPaths, customDataRequestService).then(dataProviders => { - languageModes.updateDataProviders(dataProviders); - }); - - documents.onDidClose(e => { - languageModes.onDocumentRemoved(e.document); - }); - connection.onShutdown(() => { - languageModes.dispose(); - }); - - function getClientCapability(name: string, def: T) { - const keys = name.split('.'); - let c: any = params.capabilities; - for (let i = 0; c && i < keys.length; i++) { - if (!c.hasOwnProperty(keys[i])) { - return def; - } - c = c[keys[i]]; - } - return c; - } - - clientSnippetSupport = getClientCapability('textDocument.completion.completionItem.snippetSupport', false); - dynamicFormatterRegistration = getClientCapability('textDocument.rangeFormatting.dynamicRegistration', false) && (typeof initializationOptions?.provideFormatter !== 'boolean'); - scopedSettingsSupport = getClientCapability('workspace.configuration', false); - workspaceFoldersSupport = getClientCapability('workspace.workspaceFolders', false); - foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE); - formatterMaxNumberOfEdits = initializationOptions?.customCapabilities?.rangeFormatting?.editLimit || Number.MAX_VALUE; - - const supportsDiagnosticPull = getClientCapability('textDocument.diagnostic', undefined); - if (supportsDiagnosticPull === undefined) { - diagnosticsSupport = registerDiagnosticsPushSupport(documents, connection, runtime, validateTextDocument); - } else { - diagnosticsSupport = registerDiagnosticsPullSupport(documents, connection, runtime, validateTextDocument); - } - - const capabilities: ServerCapabilities = { - textDocumentSync: TextDocumentSyncKind.Incremental, - completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/'] } : undefined, - hoverProvider: true, - documentHighlightProvider: true, - documentRangeFormattingProvider: initializationOptions?.provideFormatter === true, - documentFormattingProvider: initializationOptions?.provideFormatter === true, - documentLinkProvider: { resolveProvider: false }, - documentSymbolProvider: true, - definitionProvider: true, - signatureHelpProvider: { triggerCharacters: ['('] }, - referencesProvider: true, - colorProvider: {}, - foldingRangeProvider: true, - selectionRangeProvider: true, - renameProvider: true, - linkedEditingRangeProvider: true, - diagnosticProvider: { - documentSelector: null, - interFileDependencies: false, - workspaceDiagnostics: false - } - }; - 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); - diagnosticsSupport?.requestRefresh(); - }); - } - }); - - let formatterRegistrations: Thenable[] | null = null; - - // The settings have changed. Is send on server activation as well. - connection.onDidChangeConfiguration((change) => { - globalSettings = change.settings as Settings; - documentSettings = {}; // reset all document settings - diagnosticsSupport?.requestRefresh(); - - // dynamically enable & disable the formatter - if (dynamicFormatterRegistration) { - const enableFormatter = globalSettings && globalSettings.html && globalSettings.html.format && globalSettings.html.format.enable; - if (enableFormatter) { - if (!formatterRegistrations) { - const documentSelector = [{ language: 'html' }, { language: 'handlebars' }]; - formatterRegistrations = [ - connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector }), - connection.client.register(DocumentFormattingRequest.type, { documentSelector }) - ]; - } - } else if (formatterRegistrations) { - formatterRegistrations.forEach(p => p.then(r => r.dispose())); - formatterRegistrations = null; - } - } - }); - - 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): Promise { - try { - const version = textDocument.version; - const diagnostics: Diagnostic[] = []; - if (textDocument.languageId === 'html') { - const modes = languageModes.getAllModesInDocument(textDocument); - const settings = await getDocumentSettings(textDocument, () => modes.some(m => !!m.doValidation)); - const latestTextDocument = documents.get(textDocument.uri); - if (latestTextDocument && latestTextDocument.version === version) { // check no new version has come in after in after the async op - for (const mode of modes) { - if (mode.doValidation && isValidationEnabled(mode.getId(), settings)) { - pushAll(diagnostics, await mode.doValidation(latestTextDocument, settings)); - } - } - return diagnostics; - } - } - } catch (e) { - connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); - } - return []; - } - - connection.onCompletion(async (textDocumentPosition, token) => { - return runSafe(runtime, 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; - - 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(runtime, async () => { - const data = item.data; - if (isCompletionItemData(data)) { - 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(runtime, async () => { - const document = documents.get(textDocumentPosition.textDocument.uri); - if (document) { - const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position); - const doHover = mode?.doHover; - if (doHover) { - const settings = await getDocumentSettings(document, () => doHover.length > 2); - return doHover(document, textDocumentPosition.position, settings); - } - } - return null; - }, null, `Error while computing hover for ${textDocumentPosition.textDocument.uri}`, token); - }); - - connection.onDocumentHighlight((documentHighlightParams, token) => { - return runSafe(runtime, async () => { - const document = documents.get(documentHighlightParams.textDocument.uri); - if (document) { - const mode = languageModes.getModeAtPosition(document, documentHighlightParams.position); - if (mode && mode.findDocumentHighlight) { - return mode.findDocumentHighlight(document, documentHighlightParams.position); - } - } - return []; - }, [], `Error while computing document highlights for ${documentHighlightParams.textDocument.uri}`, token); - }); - - connection.onDefinition((definitionParams, token) => { - return runSafe(runtime, async () => { - const document = documents.get(definitionParams.textDocument.uri); - if (document) { - const mode = languageModes.getModeAtPosition(document, definitionParams.position); - if (mode && mode.findDefinition) { - return mode.findDefinition(document, definitionParams.position); - } - } - return []; - }, null, `Error while computing definitions for ${definitionParams.textDocument.uri}`, token); - }); - - connection.onReferences((referenceParams, token) => { - return runSafe(runtime, async () => { - const document = documents.get(referenceParams.textDocument.uri); - if (document) { - const mode = languageModes.getModeAtPosition(document, referenceParams.position); - if (mode && mode.findReferences) { - return mode.findReferences(document, referenceParams.position); - } - } - return []; - }, [], `Error while computing references for ${referenceParams.textDocument.uri}`, token); - }); - - connection.onSignatureHelp((signatureHelpParms, token) => { - return runSafe(runtime, async () => { - const document = documents.get(signatureHelpParms.textDocument.uri); - if (document) { - const mode = languageModes.getModeAtPosition(document, signatureHelpParms.position); - if (mode && mode.doSignatureHelp) { - return mode.doSignatureHelp(document, signatureHelpParms.position); - } - } - return null; - }, null, `Error while computing signature help for ${signatureHelpParms.textDocument.uri}`, token); - }); - - async function onFormat(textDocument: TextDocumentIdentifier, range: Range | undefined, options: FormattingOptions): Promise { - const document = documents.get(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/) }; - - const edits = await format(languageModes, document, range ?? getFullRange(document), options, settings, enabledModes); - if (edits.length > formatterMaxNumberOfEdits) { - const newText = TextDocument.applyEdits(document, edits); - return [TextEdit.replace(getFullRange(document), newText)]; - } - return edits; - } - return []; - } - - connection.onDocumentRangeFormatting((formatParams, token) => { - return runSafe(runtime, () => onFormat(formatParams.textDocument, formatParams.range, formatParams.options), [], `Error while formatting range for ${formatParams.textDocument.uri}`, token); - }); - - connection.onDocumentFormatting((formatParams, token) => { - return runSafe(runtime, () => onFormat(formatParams.textDocument, undefined, formatParams.options), [], `Error while formatting ${formatParams.textDocument.uri}`, token); - }); - - connection.onDocumentLinks((documentLinkParam, token) => { - return runSafe(runtime, async () => { - const document = documents.get(documentLinkParam.textDocument.uri); - const links: DocumentLink[] = []; - if (document) { - const documentContext = getDocumentContext(document.uri, workspaceFolders); - for (const m of languageModes.getAllModesInDocument(document)) { - if (m.findDocumentLinks) { - pushAll(links, await m.findDocumentLinks(document, documentContext)); - } - } - } - return links; - }, [], `Error while document links for ${documentLinkParam.textDocument.uri}`, token); - }); - - connection.onDocumentSymbol((documentSymbolParms, token) => { - return runSafe(runtime, async () => { - const document = documents.get(documentSymbolParms.textDocument.uri); - const symbols: SymbolInformation[] = []; - if (document) { - for (const m of languageModes.getAllModesInDocument(document)) { - if (m.findDocumentSymbols) { - pushAll(symbols, await m.findDocumentSymbols(document)); - } - } - } - return symbols; - }, [], `Error while computing document symbols for ${documentSymbolParms.textDocument.uri}`, token); - }); - - connection.onRequest(DocumentColorRequest.type, (params, token) => { - return runSafe(runtime, async () => { - const infos: ColorInformation[] = []; - const document = documents.get(params.textDocument.uri); - if (document) { - for (const m of languageModes.getAllModesInDocument(document)) { - if (m.findDocumentColors) { - pushAll(infos, await m.findDocumentColors(document)); - } - } - } - return infos; - }, [], `Error while computing document colors for ${params.textDocument.uri}`, token); - }); - - connection.onRequest(ColorPresentationRequest.type, (params, token) => { - return runSafe(runtime, async () => { - const document = documents.get(params.textDocument.uri); - if (document) { - const mode = languageModes.getModeAtPosition(document, params.range.start); - if (mode && mode.getColorPresentations) { - return mode.getColorPresentations(document, params.color, params.range); - } - } - return []; - }, [], `Error while computing color presentations for ${params.textDocument.uri}`, token); - }); - - connection.onRequest(AutoInsertRequest.type, (params, token) => { - return runSafe(runtime, async () => { - const document = documents.get(params.textDocument.uri); - if (document) { - const pos = params.position; - if (pos.character > 0) { - const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1)); - if (mode && mode.doAutoInsert) { - return mode.doAutoInsert(document, pos, params.kind); - } - } - } - return null; - }, null, `Error while computing auto insert actions for ${params.textDocument.uri}`, token); - }); - - connection.onFoldingRanges((params, token) => { - return runSafe(runtime, async () => { - const document = documents.get(params.textDocument.uri); - if (document) { - return getFoldingRanges(languageModes, document, foldingRangeLimit, token); - } - return null; - }, null, `Error while computing folding regions for ${params.textDocument.uri}`, token); - }); - - connection.onSelectionRanges((params, token) => { - return runSafe(runtime, async () => { - const document = documents.get(params.textDocument.uri); - if (document) { - return getSelectionRanges(languageModes, document, params.positions); - } - return []; - }, [], `Error while computing selection ranges for ${params.textDocument.uri}`, token); - }); - - connection.onRenameRequest((params, token) => { - return runSafe(runtime, async () => { - const document = documents.get(params.textDocument.uri); - const position: Position = params.position; - - if (document) { - const mode = languageModes.getModeAtPosition(document, params.position); - - if (mode && mode.doRename) { - return mode.doRename(document, position, params.newName); - } - } - return null; - }, null, `Error while computing rename for ${params.textDocument.uri}`, token); - }); - - connection.languages.onLinkedEditingRange((params, token) => { - return /* todo remove when microsoft/vscode-languageserver-node#700 fixed */ runSafe(runtime, async () => { - const document = documents.get(params.textDocument.uri); - if (document) { - const pos = params.position; - if (pos.character > 0) { - const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1)); - if (mode && mode.doLinkedEditing) { - const ranges = await mode.doLinkedEditing(document, pos); - if (ranges) { - return { ranges }; - } - } - } - } - 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(runtime, async () => { - const document = documents.get(params.textDocument.uri); - if (document) { - return getSemanticTokenProvider().getSemanticTokens(document, params.ranges); - } - return null; - }, null, `Error while computing semantic tokens for ${params.textDocument.uri}`, token); - }); - - connection.onRequest(SemanticTokenLegendRequest.type, token => { - return runSafe(runtime, async () => { - return getSemanticTokenProvider().legend; - }, null, `Error while computing semantic tokens legend`, token); - }); - - connection.onNotification(CustomDataChangedNotification.type, dataPaths => { - fetchHTMLDataProviders(dataPaths, customDataRequestService).then(dataProviders => { - languageModes.updateDataProviders(dataProviders); - }); - }); - - // Listen on the connection - connection.listen(); -} - -function getFullRange(document: TextDocument): Range { - return Range.create(Position.create(0, 0), document.positionAt(document.getText().length)); -} diff --git a/extensions/html-language-features/server/src/languageModelCache.ts b/extensions/html-language-features/server/src/languageModelCache.ts deleted file mode 100644 index 048d84d37cd3d..0000000000000 --- a/extensions/html-language-features/server/src/languageModelCache.ts +++ /dev/null @@ -1,82 +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 { TextDocument } from 'vscode-html-languageservice'; - -export interface LanguageModelCache { - get(document: TextDocument): T; - onDocumentRemoved(document: TextDocument): void; - dispose(): void; -} - -export function getLanguageModelCache(maxEntries: number, cleanupIntervalTimeInSec: number, parse: (document: TextDocument) => T): LanguageModelCache { - let languageModels: { [uri: string]: { version: number; languageId: string; cTime: number; languageModel: T } } = {}; - let nModels = 0; - - let cleanupInterval: NodeJS.Timer | undefined = undefined; - if (cleanupIntervalTimeInSec > 0) { - cleanupInterval = setInterval(() => { - const cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000; - const uris = Object.keys(languageModels); - for (const uri of uris) { - const languageModelInfo = languageModels[uri]; - if (languageModelInfo.cTime < cutoffTime) { - delete languageModels[uri]; - nModels--; - } - } - }, cleanupIntervalTimeInSec * 1000); - } - - return { - get(document: TextDocument): T { - const version = document.version; - const languageId = document.languageId; - const languageModelInfo = languageModels[document.uri]; - if (languageModelInfo && languageModelInfo.version === version && languageModelInfo.languageId === languageId) { - languageModelInfo.cTime = Date.now(); - return languageModelInfo.languageModel; - } - const languageModel = parse(document); - languageModels[document.uri] = { languageModel, version, languageId, cTime: Date.now() }; - if (!languageModelInfo) { - nModels++; - } - - if (nModels === maxEntries) { - let oldestTime = Number.MAX_VALUE; - let oldestUri = null; - for (const uri in languageModels) { - const languageModelInfo = languageModels[uri]; - if (languageModelInfo.cTime < oldestTime) { - oldestUri = uri; - oldestTime = languageModelInfo.cTime; - } - } - if (oldestUri) { - delete languageModels[oldestUri]; - nModels--; - } - } - return languageModel; - - }, - onDocumentRemoved(document: TextDocument) { - const uri = document.uri; - if (languageModels[uri]) { - delete languageModels[uri]; - nModels--; - } - }, - dispose() { - if (typeof cleanupInterval !== 'undefined') { - clearInterval(cleanupInterval); - cleanupInterval = undefined; - languageModels = {}; - nModels = 0; - } - } - }; -} diff --git a/extensions/html-language-features/server/src/languagePlugin.ts b/extensions/html-language-features/server/src/languagePlugin.ts new file mode 100644 index 0000000000000..7afa91f2f4294 --- /dev/null +++ b/extensions/html-language-features/server/src/languagePlugin.ts @@ -0,0 +1,86 @@ +import type { LanguagePlugin, VirtualCode } from '@volar/language-server'; +import type * as ts from 'typescript'; +import { getLanguageService } from 'vscode-html-languageservice'; +import { getDocumentRegions } from './modes/embeddedSupport'; + +const htmlLanguageService = getLanguageService(); + +export const htmlLanguagePlugin: LanguagePlugin = { + createVirtualCode(_fileId, languageId, snapshot) { + if (languageId === 'html') { + return createHtmlVirtualCode(snapshot); + } + return undefined; + }, + updateVirtualCode(_fileId, _virtualCode, newSnapshot) { + return createHtmlVirtualCode(newSnapshot); + }, +} + +function createHtmlVirtualCode(snapshot: ts.IScriptSnapshot): VirtualCode { + const root: VirtualCode = { + id: 'root', + languageId: 'html', + snapshot, + mappings: [{ + sourceOffsets: [0], + generatedOffsets: [0], + lengths: [snapshot.getLength()], + data: { + verification: true, + completion: true, + semantic: true, + navigation: true, + structure: true, + format: true, + }, + }], + embeddedCodes: [], + }; + const documentRegions = getDocumentRegions(htmlLanguageService, snapshot.getText(0, snapshot.getLength())); + const languageIdIndexes: Record = {}; + for (const documentRegion of documentRegions.getEmbeddedRegions()) { + if (!documentRegion.languageId) { + continue; + } + languageIdIndexes[documentRegion.languageId] ??= 0; + root.embeddedCodes.push({ + languageId: documentRegion.languageId, + id: documentRegion.languageId + '_' + languageIdIndexes[documentRegion.languageId], + snapshot: { + getText(start, end) { + return documentRegion.content.substring(start, end); + }, + getLength() { + return documentRegion.content.length; + }, + getChangeRange() { + return undefined; + }, + }, + mappings: [{ + sourceOffsets: [documentRegion.start], + generatedOffsets: [documentRegion.generatedStart], + lengths: [documentRegion.length], + data: documentRegion.attributeValue ? { + verification: false, + completion: true, + semantic: true, + navigation: true, + structure: false, + format: false, + } : { + verification: true, + completion: true, + semantic: true, + navigation: true, + structure: true, + format: false, + }, + }], + embeddedCodes: [], + }); + languageIdIndexes[documentRegion.languageId]++; + } + return root; +} diff --git a/extensions/html-language-features/server/src/modes/cssMode.ts b/extensions/html-language-features/server/src/modes/cssMode.ts deleted file mode 100644 index 789ac5c287ce3..0000000000000 --- a/extensions/html-language-features/server/src/modes/cssMode.ts +++ /dev/null @@ -1,73 +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 { LanguageModelCache, getLanguageModelCache } from '../languageModelCache'; -import { Stylesheet, LanguageService as CSSLanguageService } from 'vscode-css-languageservice'; -import { LanguageMode, Workspace, Color, TextDocument, Position, Range, CompletionList, DocumentContext, Diagnostic } from './languageModes'; -import { HTMLDocumentRegions, CSS_STYLE_RULE } from './embeddedSupport'; - -export function getCSSMode(cssLanguageService: CSSLanguageService, documentRegions: LanguageModelCache, workspace: Workspace): LanguageMode { - const embeddedCSSDocuments = getLanguageModelCache(10, 60, document => documentRegions.get(document).getEmbeddedDocument('css')); - const cssStylesheets = getLanguageModelCache(10, 60, document => cssLanguageService.parseStylesheet(document)); - - return { - getId() { - return 'css'; - }, - async doValidation(document: TextDocument, settings = workspace.settings) { - const embedded = embeddedCSSDocuments.get(document); - return (cssLanguageService.doValidation(embedded, cssStylesheets.get(embedded), settings && settings.css) as Diagnostic[]); - }, - async doComplete(document: TextDocument, position: Position, documentContext: DocumentContext, _settings = workspace.settings) { - const embedded = embeddedCSSDocuments.get(document); - const stylesheet = cssStylesheets.get(embedded); - return cssLanguageService.doComplete2(embedded, position, stylesheet, documentContext, _settings?.css?.completion) || CompletionList.create(); - }, - async doHover(document: TextDocument, position: Position, settings = workspace.settings) { - const embedded = embeddedCSSDocuments.get(document); - return cssLanguageService.doHover(embedded, position, cssStylesheets.get(embedded), settings?.css?.hover); - }, - async findDocumentHighlight(document: TextDocument, position: Position) { - const embedded = embeddedCSSDocuments.get(document); - return cssLanguageService.findDocumentHighlights(embedded, position, cssStylesheets.get(embedded)); - }, - async findDocumentSymbols(document: TextDocument) { - const embedded = embeddedCSSDocuments.get(document); - return cssLanguageService.findDocumentSymbols(embedded, cssStylesheets.get(embedded)).filter(s => s.name !== CSS_STYLE_RULE); - }, - async findDefinition(document: TextDocument, position: Position) { - const embedded = embeddedCSSDocuments.get(document); - return cssLanguageService.findDefinition(embedded, position, cssStylesheets.get(embedded)); - }, - async findReferences(document: TextDocument, position: Position) { - const embedded = embeddedCSSDocuments.get(document); - return cssLanguageService.findReferences(embedded, position, cssStylesheets.get(embedded)); - }, - async findDocumentColors(document: TextDocument) { - const embedded = embeddedCSSDocuments.get(document); - return cssLanguageService.findDocumentColors(embedded, cssStylesheets.get(embedded)); - }, - async getColorPresentations(document: TextDocument, color: Color, range: Range) { - const embedded = embeddedCSSDocuments.get(document); - return cssLanguageService.getColorPresentations(embedded, cssStylesheets.get(embedded), color, range); - }, - async getFoldingRanges(document: TextDocument) { - const embedded = embeddedCSSDocuments.get(document); - return cssLanguageService.getFoldingRanges(embedded, {}); - }, - async getSelectionRange(document: TextDocument, position: Position) { - const embedded = embeddedCSSDocuments.get(document); - return cssLanguageService.getSelectionRanges(embedded, [position], cssStylesheets.get(embedded))[0]; - }, - onDocumentRemoved(document: TextDocument) { - embeddedCSSDocuments.onDocumentRemoved(document); - cssStylesheets.onDocumentRemoved(document); - }, - dispose() { - embeddedCSSDocuments.dispose(); - cssStylesheets.dispose(); - } - }; -} diff --git a/extensions/html-language-features/server/src/modes/embeddedSupport.ts b/extensions/html-language-features/server/src/modes/embeddedSupport.ts index 26ef68439da96..64dc44824cefc 100644 --- a/extensions/html-language-features/server/src/modes/embeddedSupport.ts +++ b/extensions/html-language-features/server/src/modes/embeddedSupport.ts @@ -3,29 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TextDocument, Position, LanguageService, TokenType, Range } from './languageModes'; - -export interface LanguageRange extends Range { - languageId: string | undefined; - attributeValue?: boolean; -} +import { LanguageService, TokenType } from 'vscode-html-languageservice'; export interface HTMLDocumentRegions { - getEmbeddedDocument(languageId: string, ignoreAttributeValues?: boolean): TextDocument; - getLanguageRanges(range: Range): LanguageRange[]; - getLanguageAtPosition(position: Position): string | undefined; - getLanguagesInDocument(): string[]; + getEmbeddedRegions(): EmbeddedRegion[]; getImportedScripts(): string[]; } export const CSS_STYLE_RULE = '__'; -interface EmbeddedRegion { languageId: string | undefined; start: number; end: number; attributeValue?: boolean } +interface EmbeddedRegion { + languageId: string | undefined; + content: string; + start: number; + generatedStart: number; + length: number; + attributeValue?: boolean; +} -export function getDocumentRegions(languageService: LanguageService, document: TextDocument): HTMLDocumentRegions { +export function getDocumentRegions(languageService: LanguageService, text: string): HTMLDocumentRegions { const regions: EmbeddedRegion[] = []; - const scanner = languageService.createScanner(document.getText()); + const scanner = languageService.createScanner(text); let lastTagName: string = ''; let lastAttributeName: string | null = null; let languageIdFromType: string | undefined = undefined; @@ -40,10 +39,10 @@ export function getDocumentRegions(languageService: LanguageService, document: T languageIdFromType = 'javascript'; break; case TokenType.Styles: - regions.push({ languageId: 'css', start: scanner.getTokenOffset(), end: scanner.getTokenEnd() }); + regions.push(createEmbeddedRegion('css', scanner.getTokenOffset(), scanner.getTokenEnd())); break; case TokenType.Script: - regions.push({ languageId: languageIdFromType, start: scanner.getTokenOffset(), end: scanner.getTokenEnd() }); + regions.push(createEmbeddedRegion(languageIdFromType, scanner.getTokenOffset(), scanner.getTokenEnd())); break; case TokenType.AttributeName: lastAttributeName = scanner.getTokenText(); @@ -68,12 +67,12 @@ export function getDocumentRegions(languageService: LanguageService, document: T if (attributeLanguageId) { let start = scanner.getTokenOffset(); let end = scanner.getTokenEnd(); - const firstChar = document.getText()[start]; + const firstChar = text[start]; if (firstChar === '\'' || firstChar === '"') { start++; end--; } - regions.push({ languageId: attributeLanguageId, start, end, attributeValue: true }); + regions.push(createEmbeddedRegion(attributeLanguageId, start, end, true)); } } lastAttributeName = null; @@ -82,99 +81,25 @@ export function getDocumentRegions(languageService: LanguageService, document: T token = scanner.scan(); } return { - getLanguageRanges: (range: Range) => getLanguageRanges(document, regions, range), - getEmbeddedDocument: (languageId: string, ignoreAttributeValues: boolean) => getEmbeddedDocument(document, regions, languageId, ignoreAttributeValues), - getLanguageAtPosition: (position: Position) => getLanguageAtPosition(document, regions, position), - getLanguagesInDocument: () => getLanguagesInDocument(document, regions), + getEmbeddedRegions: () => regions, getImportedScripts: () => importedScripts }; -} - -function getLanguageRanges(document: TextDocument, regions: EmbeddedRegion[], range: Range): LanguageRange[] { - const result: LanguageRange[] = []; - let currentPos = range ? range.start : Position.create(0, 0); - let currentOffset = range ? document.offsetAt(range.start) : 0; - const endOffset = range ? document.offsetAt(range.end) : document.getText().length; - for (const region of regions) { - if (region.end > currentOffset && region.start < endOffset) { - const start = Math.max(region.start, currentOffset); - const startPos = document.positionAt(start); - if (currentOffset < region.start) { - result.push({ - start: currentPos, - end: startPos, - languageId: 'html' - }); - } - const end = Math.min(region.end, endOffset); - const endPos = document.positionAt(end); - if (end > region.start) { - result.push({ - start: startPos, - end: endPos, - languageId: region.languageId, - attributeValue: region.attributeValue - }); - } - currentOffset = end; - currentPos = endPos; - } - } - if (currentOffset < endOffset) { - const endPos = range ? range.end : document.positionAt(endOffset); - result.push({ - start: currentPos, - end: endPos, - languageId: 'html' - }); - } - return result; -} - -function getLanguagesInDocument(_document: TextDocument, regions: EmbeddedRegion[]): string[] { - const result = []; - for (const region of regions) { - if (region.languageId && result.indexOf(region.languageId) === -1) { - result.push(region.languageId); - if (result.length === 3) { - return result; - } - } + function createEmbeddedRegion(languageId: string | undefined, start: number, end: number, attributeValue?: boolean) { + const c: EmbeddedRegion = { + languageId, + start, + generatedStart: 0, + length: end - start, + attributeValue, + content: '', + }; + c.content += getPrefix(c); + c.generatedStart += c.content.length; + c.content += updateContent(c, text.substring(start, end)); + c.content += getSuffix(c); + return c; } - result.push('html'); - return result; -} - -function getLanguageAtPosition(document: TextDocument, regions: EmbeddedRegion[], position: Position): string | undefined { - const offset = document.offsetAt(position); - for (const region of regions) { - if (region.start <= offset) { - if (offset <= region.end) { - return region.languageId; - } - } else { - break; - } - } - return 'html'; -} - -function getEmbeddedDocument(document: TextDocument, contents: EmbeddedRegion[], languageId: string, ignoreAttributeValues: boolean): TextDocument { - let currentPos = 0; - const oldContent = document.getText(); - let result = ''; - let lastSuffix = ''; - for (const c of contents) { - if (c.languageId === languageId && (!ignoreAttributeValues || !c.attributeValue)) { - result = substituteWithWhitespace(result, currentPos, c.start, oldContent, lastSuffix, getPrefix(c)); - result += updateContent(c, oldContent.substring(c.start, c.end)); - currentPos = c.end; - lastSuffix = getSuffix(c); - } - } - result = substituteWithWhitespace(result, currentPos, oldContent.length, oldContent, lastSuffix, ''); - return TextDocument.create(document.uri, languageId, document.version, result); } function getPrefix(c: EmbeddedRegion) { @@ -201,35 +126,6 @@ function updateContent(c: EmbeddedRegion, content: string): string { return content; } -function substituteWithWhitespace(result: string, start: number, end: number, oldContent: string, before: string, after: string) { - result += before; - let accumulatedWS = -before.length; // start with a negative value to account for the before string - for (let i = start; i < end; i++) { - const ch = oldContent[i]; - if (ch === '\n' || ch === '\r') { - // only write new lines, skip the whitespace - accumulatedWS = 0; - result += ch; - } else { - accumulatedWS++; - } - } - result = append(result, ' ', accumulatedWS - after.length); - result += after; - return result; -} - -function append(result: string, str: string, n: number): string { - while (n > 0) { - if (n & 1) { - result += str; - } - n >>= 1; - str += str; - } - return result; -} - function getAttributeLanguage(attributeName: string): string | null { const match = attributeName.match(/^(style)$|^(on\w+)$/i); if (!match) { diff --git a/extensions/html-language-features/server/src/modes/formatting.ts b/extensions/html-language-features/server/src/modes/formatting.ts deleted file mode 100644 index 6b8c669a6cb5f..0000000000000 --- a/extensions/html-language-features/server/src/modes/formatting.ts +++ /dev/null @@ -1,96 +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 { LanguageModes, Settings, LanguageModeRange, TextDocument, Range, TextEdit, FormattingOptions, Position } from './languageModes'; -import { pushAll } from '../utils/arrays'; -import { isEOL } from '../utils/strings'; - -export async function format(languageModes: LanguageModes, document: TextDocument, formatRange: Range, formattingOptions: FormattingOptions, settings: Settings | undefined, enabledModes: { [mode: string]: boolean }) { - const result: TextEdit[] = []; - - const endPos = formatRange.end; - let endOffset = document.offsetAt(endPos); - const content = document.getText(); - if (endPos.character === 0 && endPos.line > 0 && endOffset !== content.length) { - // if selection ends after a new line, exclude that new line - const prevLineStart = document.offsetAt(Position.create(endPos.line - 1, 0)); - while (isEOL(content, endOffset - 1) && endOffset > prevLineStart) { - endOffset--; - } - formatRange = Range.create(formatRange.start, document.positionAt(endOffset)); - } - - - // run the html formatter on the full range and pass the result content to the embedded formatters. - // from the final content create a single edit - // advantages of this approach are - // - correct indents in the html document - // - correct initial indent for embedded formatters - // - no worrying of overlapping edits - - // make sure we start in html - const allRanges = languageModes.getModesInRange(document, formatRange); - let i = 0; - let startPos = formatRange.start; - const isHTML = (range: LanguageModeRange) => range.mode && range.mode.getId() === 'html'; - - while (i < allRanges.length && !isHTML(allRanges[i])) { - const range = allRanges[i]; - if (!range.attributeValue && range.mode && range.mode.format) { - const edits = await range.mode.format(document, Range.create(startPos, range.end), formattingOptions, settings); - pushAll(result, edits); - } - startPos = range.end; - i++; - } - if (i === allRanges.length) { - return result; - } - // modify the range - formatRange = Range.create(startPos, formatRange.end); - - // perform a html format and apply changes to a new document - const htmlMode = languageModes.getMode('html')!; - const htmlEdits = await htmlMode.format!(document, formatRange, formattingOptions, settings); - let htmlFormattedContent = TextDocument.applyEdits(document, htmlEdits); - if (formattingOptions.insertFinalNewline && endOffset === content.length && !htmlFormattedContent.endsWith('\n')) { - htmlFormattedContent = htmlFormattedContent + '\n'; - htmlEdits.push(TextEdit.insert(endPos, '\n')); - } - const newDocument = TextDocument.create(document.uri + '.tmp', document.languageId, document.version, htmlFormattedContent); - try { - // run embedded formatters on html formatted content: - formatters see correct initial indent - const afterFormatRangeLength = document.getText().length - document.offsetAt(formatRange.end); // length of unchanged content after replace range - const newFormatRange = Range.create(formatRange.start, newDocument.positionAt(htmlFormattedContent.length - afterFormatRangeLength)); - const embeddedRanges = languageModes.getModesInRange(newDocument, newFormatRange); - - const embeddedEdits: TextEdit[] = []; - - for (const r of embeddedRanges) { - const mode = r.mode; - if (mode && mode.format && enabledModes[mode.getId()] && !r.attributeValue) { - const edits = await mode.format(newDocument, r, formattingOptions, settings); - for (const edit of edits) { - embeddedEdits.push(edit); - } - } - } - - if (embeddedEdits.length === 0) { - pushAll(result, htmlEdits); - return result; - } - - // apply all embedded format edits and create a single edit for all changes - const resultContent = TextDocument.applyEdits(newDocument, embeddedEdits); - const resultReplaceText = resultContent.substring(document.offsetAt(formatRange.start), resultContent.length - afterFormatRangeLength); - - result.push(TextEdit.replace(formatRange, resultReplaceText)); - return result; - } finally { - languageModes.onDocumentRemoved(newDocument); - } - -} diff --git a/extensions/html-language-features/server/src/modes/htmlFolding.ts b/extensions/html-language-features/server/src/modes/htmlFolding.ts deleted file mode 100644 index 38a84e5048a6c..0000000000000 --- a/extensions/html-language-features/server/src/modes/htmlFolding.ts +++ /dev/null @@ -1,115 +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 { TextDocument, FoldingRange, Position, Range, LanguageModes, LanguageMode } from './languageModes'; -import { CancellationToken } from 'vscode-languageserver'; - -export async function getFoldingRanges(languageModes: LanguageModes, document: TextDocument, maxRanges: number | undefined, _cancellationToken: CancellationToken | null): Promise { - const htmlMode = languageModes.getMode('html'); - const range = Range.create(Position.create(0, 0), Position.create(document.lineCount, 0)); - let result: FoldingRange[] = []; - if (htmlMode && htmlMode.getFoldingRanges) { - result.push(... await htmlMode.getFoldingRanges(document)); - } - - // cache folding ranges per mode - const rangesPerMode: { [mode: string]: FoldingRange[] } = Object.create(null); - const getRangesForMode = async (mode: LanguageMode) => { - if (mode.getFoldingRanges) { - let ranges = rangesPerMode[mode.getId()]; - if (!Array.isArray(ranges)) { - ranges = await mode.getFoldingRanges(document) || []; - rangesPerMode[mode.getId()] = ranges; - } - return ranges; - } - return []; - }; - - const modeRanges = languageModes.getModesInRange(document, range); - for (const modeRange of modeRanges) { - const mode = modeRange.mode; - if (mode && mode !== htmlMode && !modeRange.attributeValue) { - const ranges = await getRangesForMode(mode); - result.push(...ranges.filter(r => r.startLine >= modeRange.start.line && r.endLine < modeRange.end.line)); - } - } - if (maxRanges && result.length > maxRanges) { - result = limitRanges(result, maxRanges); - } - return result; -} - -function limitRanges(ranges: FoldingRange[], maxRanges: number) { - ranges = ranges.sort((r1, r2) => { - let diff = r1.startLine - r2.startLine; - if (diff === 0) { - diff = r1.endLine - r2.endLine; - } - return diff; - }); - - // compute each range's nesting level in 'nestingLevels'. - // count the number of ranges for each level in 'nestingLevelCounts' - let top: FoldingRange | undefined = undefined; - const previous: FoldingRange[] = []; - const nestingLevels: number[] = []; - const nestingLevelCounts: number[] = []; - - const setNestingLevel = (index: number, level: number) => { - nestingLevels[index] = level; - if (level < 30) { - nestingLevelCounts[level] = (nestingLevelCounts[level] || 0) + 1; - } - }; - - // compute nesting levels and sanitize - for (let i = 0; i < ranges.length; i++) { - const entry = ranges[i]; - if (!top) { - top = entry; - setNestingLevel(i, 0); - } else { - if (entry.startLine > top.startLine) { - if (entry.endLine <= top.endLine) { - previous.push(top); - top = entry; - setNestingLevel(i, previous.length); - } else if (entry.startLine > top.endLine) { - do { - top = previous.pop(); - } while (top && entry.startLine > top.endLine); - if (top) { - previous.push(top); - } - top = entry; - setNestingLevel(i, previous.length); - } - } - } - } - let entries = 0; - let maxLevel = 0; - for (let i = 0; i < nestingLevelCounts.length; i++) { - const n = nestingLevelCounts[i]; - if (n) { - if (n + entries > maxRanges) { - maxLevel = i; - break; - } - entries += n; - } - } - const result = []; - for (let i = 0; i < ranges.length; i++) { - const level = nestingLevels[i]; - if (typeof level === 'number') { - if (level < maxLevel || (level === maxLevel && entries++ < maxRanges)) { - result.push(ranges[i]); - } - } - } - return result; -} diff --git a/extensions/html-language-features/server/src/modes/htmlMode.ts b/extensions/html-language-features/server/src/modes/htmlMode.ts deleted file mode 100644 index 58a3ded2beed2..0000000000000 --- a/extensions/html-language-features/server/src/modes/htmlMode.ts +++ /dev/null @@ -1,106 +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 { getLanguageModelCache } from '../languageModelCache'; -import { - LanguageService as HTMLLanguageService, HTMLDocument, DocumentContext, FormattingOptions, - HTMLFormatConfiguration, SelectionRange, - TextDocument, Position, Range, FoldingRange, - LanguageMode, Workspace, Settings -} from './languageModes'; - -export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace: Workspace): LanguageMode { - const htmlDocuments = getLanguageModelCache(10, 60, document => htmlLanguageService.parseHTMLDocument(document)); - return { - getId() { - return 'html'; - }, - async getSelectionRange(document: TextDocument, position: Position): Promise { - return htmlLanguageService.getSelectionRanges(document, [position])[0]; - }, - doComplete(document: TextDocument, position: Position, documentContext: DocumentContext, settings = workspace.settings) { - const htmlSettings = settings?.html; - const options = merge(htmlSettings?.suggest, {}); - options.hideAutoCompleteProposals = htmlSettings?.autoClosingTags === true; - options.attributeDefaultValue = htmlSettings?.completion?.attributeDefaultValue ?? 'doublequotes'; - - const htmlDocument = htmlDocuments.get(document); - const completionList = htmlLanguageService.doComplete2(document, position, htmlDocument, documentContext, options); - return completionList; - }, - async doHover(document: TextDocument, position: Position, settings?: Settings) { - return htmlLanguageService.doHover(document, position, htmlDocuments.get(document), settings?.html?.hover); - }, - async findDocumentHighlight(document: TextDocument, position: Position) { - return htmlLanguageService.findDocumentHighlights(document, position, htmlDocuments.get(document)); - }, - async findDocumentLinks(document: TextDocument, documentContext: DocumentContext) { - return htmlLanguageService.findDocumentLinks(document, documentContext); - }, - async findDocumentSymbols(document: TextDocument) { - return htmlLanguageService.findDocumentSymbols(document, htmlDocuments.get(document)); - }, - async format(document: TextDocument, range: Range, formatParams: FormattingOptions, settings = workspace.settings) { - const formatSettings: HTMLFormatConfiguration = merge(settings?.html?.format, {}); - if (formatSettings.contentUnformatted) { - formatSettings.contentUnformatted = formatSettings.contentUnformatted + ',script'; - } else { - formatSettings.contentUnformatted = 'script'; - } - merge(formatParams, formatSettings); - return htmlLanguageService.format(document, range, formatSettings); - }, - async getFoldingRanges(document: TextDocument): Promise { - return htmlLanguageService.getFoldingRanges(document); - }, - async doAutoInsert(document: TextDocument, position: Position, kind: 'autoQuote' | 'autoClose', settings = workspace.settings) { - const offset = document.offsetAt(position); - const text = document.getText(); - if (kind === 'autoQuote') { - if (offset > 0 && text.charAt(offset - 1) === '=') { - const htmlSettings = settings?.html; - const options = merge(htmlSettings?.suggest, {}); - options.attributeDefaultValue = htmlSettings?.completion?.attributeDefaultValue ?? 'doublequotes'; - - return htmlLanguageService.doQuoteComplete(document, position, htmlDocuments.get(document), options); - } - } else if (kind === 'autoClose') { - if (offset > 0 && text.charAt(offset - 1).match(/[>\/]/g)) { - return htmlLanguageService.doTagComplete(document, position, htmlDocuments.get(document)); - } - } - return null; - }, - async doRename(document: TextDocument, position: Position, newName: string) { - const htmlDocument = htmlDocuments.get(document); - return htmlLanguageService.doRename(document, position, newName, htmlDocument); - }, - async onDocumentRemoved(document: TextDocument) { - htmlDocuments.onDocumentRemoved(document); - }, - async findMatchingTagPosition(document: TextDocument, position: Position) { - const htmlDocument = htmlDocuments.get(document); - return htmlLanguageService.findMatchingTagPosition(document, position, htmlDocument); - }, - async doLinkedEditing(document: TextDocument, position: Position) { - const htmlDocument = htmlDocuments.get(document); - return htmlLanguageService.findLinkedEditingRanges(document, position, htmlDocument); - }, - dispose() { - htmlDocuments.dispose(); - } - }; -} - -function merge(src: any, dst: any): any { - if (src) { - for (const key in src) { - if (src.hasOwnProperty(key)) { - dst[key] = src[key]; - } - } - } - return dst; -} diff --git a/extensions/html-language-features/server/src/modes/javascriptLibs.ts b/extensions/html-language-features/server/src/modes/javascriptLibs.ts deleted file mode 100644 index 7abf94edf2233..0000000000000 --- a/extensions/html-language-features/server/src/modes/javascriptLibs.ts +++ /dev/null @@ -1,33 +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 { join, basename, dirname } from 'path'; -import { readFileSync } from 'fs'; - -const contents: { [name: string]: string } = {}; - -const serverFolder = basename(__dirname) === 'dist' ? dirname(__dirname) : dirname(dirname(__dirname)); -const TYPESCRIPT_LIB_SOURCE = join(serverFolder, '../../node_modules/typescript/lib'); -const JQUERY_PATH = join(serverFolder, 'lib/jquery.d.ts'); - -export function loadLibrary(name: string) { - let content = contents[name]; - if (typeof content !== 'string') { - let libPath; - if (name === 'jquery') { - libPath = JQUERY_PATH; - } else { - libPath = join(TYPESCRIPT_LIB_SOURCE, name); // from source - } - try { - content = readFileSync(libPath).toString(); - } catch (e) { - console.log(`Unable to load library ${name} at ${libPath}`); - content = ''; - } - contents[name] = content; - } - return content; -} diff --git a/extensions/html-language-features/server/src/modes/javascriptMode.ts b/extensions/html-language-features/server/src/modes/javascriptMode.ts deleted file mode 100644 index a540745428c5e..0000000000000 --- a/extensions/html-language-features/server/src/modes/javascriptMode.ts +++ /dev/null @@ -1,603 +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 { LanguageModelCache, getLanguageModelCache } from '../languageModelCache'; -import { - SymbolInformation, SymbolKind, CompletionItem, Location, SignatureHelp, SignatureInformation, ParameterInformation, - Definition, TextEdit, TextDocument, Diagnostic, DiagnosticSeverity, Range, CompletionItemKind, Hover, - DocumentHighlight, DocumentHighlightKind, CompletionList, Position, FormattingOptions, FoldingRange, FoldingRangeKind, SelectionRange, - LanguageMode, Settings, SemanticTokenData, Workspace, DocumentContext, CompletionItemData, isCompletionItemData -} from './languageModes'; -import { getWordAtText, isWhitespaceOnly, repeat } from '../utils/strings'; -import { HTMLDocumentRegions } from './embeddedSupport'; - -import * as ts from 'typescript'; -import { getSemanticTokens, getSemanticTokenLegend } from './javascriptSemanticTokens'; - -const JS_WORD_REGEX = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g; - -function getLanguageServiceHost(scriptKind: ts.ScriptKind) { - const compilerOptions: ts.CompilerOptions = { allowNonTsExtensions: true, allowJs: true, lib: ['lib.es2020.full.d.ts'], target: ts.ScriptTarget.Latest, moduleResolution: ts.ModuleResolutionKind.Classic, experimentalDecorators: false }; - - let currentTextDocument = TextDocument.create('init', 'javascript', 1, ''); - const jsLanguageService = import(/* webpackChunkName: "javascriptLibs" */ './javascriptLibs').then(libs => { - const host: ts.LanguageServiceHost = { - getCompilationSettings: () => compilerOptions, - getScriptFileNames: () => [currentTextDocument.uri, 'jquery'], - getScriptKind: (fileName) => { - if (fileName === currentTextDocument.uri) { - return scriptKind; - } - return fileName.substr(fileName.length - 2) === 'ts' ? ts.ScriptKind.TS : ts.ScriptKind.JS; - }, - getScriptVersion: (fileName: string) => { - if (fileName === currentTextDocument.uri) { - return String(currentTextDocument.version); - } - return '1'; // default lib an jquery.d.ts are static - }, - getScriptSnapshot: (fileName: string) => { - let text = ''; - if (fileName === currentTextDocument.uri) { - text = currentTextDocument.getText(); - } else { - text = libs.loadLibrary(fileName); - } - return { - getText: (start, end) => text.substring(start, end), - getLength: () => text.length, - getChangeRange: () => undefined - }; - }, - getCurrentDirectory: () => '', - getDefaultLibFileName: (_options: ts.CompilerOptions) => 'es2020.full', - readFile: (path: string, _encoding?: string | undefined): string | undefined => { - if (path === currentTextDocument.uri) { - return currentTextDocument.getText(); - } else { - return libs.loadLibrary(path); - } - }, - fileExists: (path: string): boolean => { - if (path === currentTextDocument.uri) { - return true; - } else { - return !!libs.loadLibrary(path); - } - }, - directoryExists: (path: string): boolean => { - // typescript tries to first find libraries in node_modules/@types and node_modules/@typescript - // there's no node_modules in our setup - if (path.startsWith('node_modules')) { - return false; - } - return true; - - } - }; - return ts.createLanguageService(host); - }); - return { - async getLanguageService(jsDocument: TextDocument): Promise { - currentTextDocument = jsDocument; - return jsLanguageService; - }, - getCompilationSettings() { - return compilerOptions; - }, - dispose() { - jsLanguageService.then(s => s.dispose()); - } - }; -} - -const ignoredErrors = [ - 1108, /* A_return_statement_can_only_be_used_within_a_function_body_1108 */ - 2792, /* Cannot_find_module_0_Did_you_mean_to_set_the_moduleResolution_option_to_node_or_to_add_aliases_to_the_paths_option */ -]; - -export function getJavaScriptMode(documentRegions: LanguageModelCache, languageId: 'javascript' | 'typescript', workspace: Workspace): LanguageMode { - const jsDocuments = getLanguageModelCache(10, 60, document => documentRegions.get(document).getEmbeddedDocument(languageId)); - - const host = getLanguageServiceHost(languageId === 'javascript' ? ts.ScriptKind.JS : ts.ScriptKind.TS); - const globalSettings: Settings = {}; - - function updateHostSettings(settings: Settings) { - const hostSettings = host.getCompilationSettings(); - hostSettings.experimentalDecorators = settings?.['js/ts']?.implicitProjectConfig?.experimentalDecorators; - hostSettings.strictNullChecks = settings?.['js/ts']?.implicitProjectConfig.strictNullChecks; - } - - return { - getId() { - return languageId; - }, - async doValidation(document: TextDocument, settings = workspace.settings): Promise { - updateHostSettings(settings); - - const jsDocument = jsDocuments.get(document); - const languageService = await host.getLanguageService(jsDocument); - const syntaxDiagnostics: ts.Diagnostic[] = languageService.getSyntacticDiagnostics(jsDocument.uri); - const semanticDiagnostics = languageService.getSemanticDiagnostics(jsDocument.uri); - return syntaxDiagnostics.concat(semanticDiagnostics).filter(d => !ignoredErrors.includes(d.code)).map((diag: ts.Diagnostic): Diagnostic => { - return { - range: convertRange(jsDocument, diag), - severity: DiagnosticSeverity.Error, - source: languageId, - message: ts.flattenDiagnosticMessageText(diag.messageText, '\n') - }; - }); - }, - async doComplete(document: TextDocument, position: Position, _documentContext: DocumentContext): Promise { - const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - const offset = jsDocument.offsetAt(position); - const completions = jsLanguageService.getCompletionsAtPosition(jsDocument.uri, offset, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); - if (!completions) { - return { isIncomplete: false, items: [] }; - } - const replaceRange = convertRange(jsDocument, getWordAtText(jsDocument.getText(), offset, JS_WORD_REGEX)); - return { - isIncomplete: false, - items: completions.entries.map(entry => { - const data: CompletionItemData = { // data used for resolving item details (see 'doResolve') - languageId, - uri: document.uri, - offset: offset - }; - return { - uri: document.uri, - position: position, - label: entry.name, - sortText: entry.sortText, - kind: convertKind(entry.kind), - textEdit: TextEdit.replace(replaceRange, entry.name), - data - }; - }) - }; - }, - async doResolve(document: TextDocument, item: CompletionItem): Promise { - if (isCompletionItemData(item.data)) { - const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - const details = jsLanguageService.getCompletionEntryDetails(jsDocument.uri, item.data.offset, item.label, undefined, undefined, undefined, undefined); - if (details) { - item.detail = ts.displayPartsToString(details.displayParts); - item.documentation = ts.displayPartsToString(details.documentation); - delete item.data; - } - } - return item; - }, - async doHover(document: TextDocument, position: Position): Promise { - const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - const info = jsLanguageService.getQuickInfoAtPosition(jsDocument.uri, jsDocument.offsetAt(position)); - if (info) { - const contents = ts.displayPartsToString(info.displayParts); - return { - range: convertRange(jsDocument, info.textSpan), - contents: ['```typescript', contents, '```'].join('\n') - }; - } - return null; - }, - async doSignatureHelp(document: TextDocument, position: Position): Promise { - const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - const signHelp = jsLanguageService.getSignatureHelpItems(jsDocument.uri, jsDocument.offsetAt(position), undefined); - if (signHelp) { - const ret: SignatureHelp = { - activeSignature: signHelp.selectedItemIndex, - activeParameter: signHelp.argumentIndex, - signatures: [] - }; - signHelp.items.forEach(item => { - - const signature: SignatureInformation = { - label: '', - documentation: undefined, - parameters: [] - }; - - signature.label += ts.displayPartsToString(item.prefixDisplayParts); - item.parameters.forEach((p, i, a) => { - const label = ts.displayPartsToString(p.displayParts); - const parameter: ParameterInformation = { - label: label, - documentation: ts.displayPartsToString(p.documentation) - }; - signature.label += label; - signature.parameters!.push(parameter); - if (i < a.length - 1) { - signature.label += ts.displayPartsToString(item.separatorDisplayParts); - } - }); - signature.label += ts.displayPartsToString(item.suffixDisplayParts); - ret.signatures.push(signature); - }); - return ret; - } - return null; - }, - async doRename(document: TextDocument, position: Position, newName: string) { - const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - const jsDocumentPosition = jsDocument.offsetAt(position); - const { canRename } = jsLanguageService.getRenameInfo(jsDocument.uri, jsDocumentPosition); - if (!canRename) { - return null; - } - const renameInfos = jsLanguageService.findRenameLocations(jsDocument.uri, jsDocumentPosition, false, false); - - const edits: TextEdit[] = []; - renameInfos?.map(renameInfo => { - edits.push({ - range: convertRange(jsDocument, renameInfo.textSpan), - newText: newName, - }); - }); - - return { - changes: { [document.uri]: edits }, - }; - }, - async findDocumentHighlight(document: TextDocument, position: Position): Promise { - const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - const highlights = jsLanguageService.getDocumentHighlights(jsDocument.uri, jsDocument.offsetAt(position), [jsDocument.uri]); - const out: DocumentHighlight[] = []; - for (const entry of highlights || []) { - for (const highlight of entry.highlightSpans) { - out.push({ - range: convertRange(jsDocument, highlight.textSpan), - kind: highlight.kind === 'writtenReference' ? DocumentHighlightKind.Write : DocumentHighlightKind.Text - }); - } - } - return out; - }, - async findDocumentSymbols(document: TextDocument): Promise { - const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - const items = jsLanguageService.getNavigationBarItems(jsDocument.uri); - if (items) { - const result: SymbolInformation[] = []; - const existing = Object.create(null); - const collectSymbols = (item: ts.NavigationBarItem, containerLabel?: string) => { - const sig = item.text + item.kind + item.spans[0].start; - if (item.kind !== 'script' && !existing[sig]) { - const symbol: SymbolInformation = { - name: item.text, - kind: convertSymbolKind(item.kind), - location: { - uri: document.uri, - range: convertRange(jsDocument, item.spans[0]) - }, - containerName: containerLabel - }; - existing[sig] = true; - result.push(symbol); - containerLabel = item.text; - } - - if (item.childItems && item.childItems.length > 0) { - for (const child of item.childItems) { - collectSymbols(child, containerLabel); - } - } - - }; - - items.forEach(item => collectSymbols(item)); - return result; - } - return []; - }, - async findDefinition(document: TextDocument, position: Position): Promise { - const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - const definition = jsLanguageService.getDefinitionAtPosition(jsDocument.uri, jsDocument.offsetAt(position)); - if (definition) { - return definition.filter(d => d.fileName === jsDocument.uri).map(d => { - return { - uri: document.uri, - range: convertRange(jsDocument, d.textSpan) - }; - }); - } - return null; - }, - async findReferences(document: TextDocument, position: Position): Promise { - const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - const references = jsLanguageService.getReferencesAtPosition(jsDocument.uri, jsDocument.offsetAt(position)); - if (references) { - return references.filter(d => d.fileName === jsDocument.uri).map(d => { - return { - uri: document.uri, - range: convertRange(jsDocument, d.textSpan) - }; - }); - } - return []; - }, - async getSelectionRange(document: TextDocument, position: Position): Promise { - const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - function convertSelectionRange(selectionRange: ts.SelectionRange): SelectionRange { - const parent = selectionRange.parent ? convertSelectionRange(selectionRange.parent) : undefined; - return SelectionRange.create(convertRange(jsDocument, selectionRange.textSpan), parent); - } - const range = jsLanguageService.getSmartSelectionRange(jsDocument.uri, jsDocument.offsetAt(position)); - return convertSelectionRange(range); - }, - async format(document: TextDocument, range: Range, formatParams: FormattingOptions, settings: Settings = globalSettings): Promise { - const jsDocument = documentRegions.get(document).getEmbeddedDocument('javascript', true); - const jsLanguageService = await host.getLanguageService(jsDocument); - - const formatterSettings = settings && settings.javascript && settings.javascript.format; - - const initialIndentLevel = computeInitialIndent(document, range, formatParams); - const formatSettings = convertOptions(formatParams, formatterSettings, initialIndentLevel + 1); - const start = jsDocument.offsetAt(range.start); - let end = jsDocument.offsetAt(range.end); - let lastLineRange = null; - if (range.end.line > range.start.line && (range.end.character === 0 || isWhitespaceOnly(jsDocument.getText().substr(end - range.end.character, range.end.character)))) { - end -= range.end.character; - lastLineRange = Range.create(Position.create(range.end.line, 0), range.end); - } - const edits = jsLanguageService.getFormattingEditsForRange(jsDocument.uri, start, end, formatSettings); - if (edits) { - const result = []; - for (const edit of edits) { - if (edit.span.start >= start && edit.span.start + edit.span.length <= end) { - result.push({ - range: convertRange(jsDocument, edit.span), - newText: edit.newText - }); - } - } - if (lastLineRange) { - result.push({ - range: lastLineRange, - newText: generateIndent(initialIndentLevel, formatParams) - }); - } - return result; - } - return []; - }, - async getFoldingRanges(document: TextDocument): Promise { - const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - const spans = jsLanguageService.getOutliningSpans(jsDocument.uri); - const ranges: FoldingRange[] = []; - for (const span of spans) { - const curr = convertRange(jsDocument, span.textSpan); - const startLine = curr.start.line; - const endLine = curr.end.line; - if (startLine < endLine) { - const foldingRange: FoldingRange = { startLine, endLine }; - const match = document.getText(curr).match(/^\s*\/(?:(\/\s*#(?:end)?region\b)|(\*|\/))/); - if (match) { - foldingRange.kind = match[1] ? FoldingRangeKind.Region : FoldingRangeKind.Comment; - } - ranges.push(foldingRange); - } - } - return ranges; - }, - onDocumentRemoved(document: TextDocument) { - jsDocuments.onDocumentRemoved(document); - }, - async getSemanticTokens(document: TextDocument): Promise { - const jsDocument = jsDocuments.get(document); - const jsLanguageService = await host.getLanguageService(jsDocument); - return [...getSemanticTokens(jsLanguageService, jsDocument, jsDocument.uri)]; - }, - getSemanticTokenLegend(): { types: string[]; modifiers: string[] } { - return getSemanticTokenLegend(); - }, - dispose() { - host.dispose(); - jsDocuments.dispose(); - } - }; -} - - - - -function convertRange(document: TextDocument, span: { start: number | undefined; length: number | undefined }): Range { - if (typeof span.start === 'undefined') { - const pos = document.positionAt(0); - return Range.create(pos, pos); - } - const startPosition = document.positionAt(span.start); - const endPosition = document.positionAt(span.start + (span.length || 0)); - return Range.create(startPosition, endPosition); -} - -function convertKind(kind: string): CompletionItemKind { - switch (kind) { - case Kind.primitiveType: - case Kind.keyword: - return CompletionItemKind.Keyword; - - case Kind.const: - case Kind.let: - case Kind.variable: - case Kind.localVariable: - case Kind.alias: - case Kind.parameter: - return CompletionItemKind.Variable; - - case Kind.memberVariable: - case Kind.memberGetAccessor: - case Kind.memberSetAccessor: - return CompletionItemKind.Field; - - case Kind.function: - case Kind.localFunction: - return CompletionItemKind.Function; - - case Kind.method: - case Kind.constructSignature: - case Kind.callSignature: - case Kind.indexSignature: - return CompletionItemKind.Method; - - case Kind.enum: - return CompletionItemKind.Enum; - - case Kind.enumMember: - return CompletionItemKind.EnumMember; - - case Kind.module: - case Kind.externalModuleName: - return CompletionItemKind.Module; - - case Kind.class: - case Kind.type: - return CompletionItemKind.Class; - - case Kind.interface: - return CompletionItemKind.Interface; - - case Kind.warning: - return CompletionItemKind.Text; - - case Kind.script: - return CompletionItemKind.File; - - case Kind.directory: - return CompletionItemKind.Folder; - - case Kind.string: - return CompletionItemKind.Constant; - - default: - return CompletionItemKind.Property; - } -} -const enum Kind { - alias = 'alias', - callSignature = 'call', - class = 'class', - const = 'const', - constructorImplementation = 'constructor', - constructSignature = 'construct', - directory = 'directory', - enum = 'enum', - enumMember = 'enum member', - externalModuleName = 'external module name', - function = 'function', - indexSignature = 'index', - interface = 'interface', - keyword = 'keyword', - let = 'let', - localFunction = 'local function', - localVariable = 'local var', - method = 'method', - memberGetAccessor = 'getter', - memberSetAccessor = 'setter', - memberVariable = 'property', - module = 'module', - primitiveType = 'primitive type', - script = 'script', - type = 'type', - variable = 'var', - warning = 'warning', - string = 'string', - parameter = 'parameter', - typeParameter = 'type parameter' -} - -function convertSymbolKind(kind: string): SymbolKind { - switch (kind) { - case Kind.module: return SymbolKind.Module; - case Kind.class: return SymbolKind.Class; - case Kind.enum: return SymbolKind.Enum; - case Kind.enumMember: return SymbolKind.EnumMember; - case Kind.interface: return SymbolKind.Interface; - case Kind.indexSignature: return SymbolKind.Method; - case Kind.callSignature: return SymbolKind.Method; - case Kind.method: return SymbolKind.Method; - case Kind.memberVariable: return SymbolKind.Property; - case Kind.memberGetAccessor: return SymbolKind.Property; - case Kind.memberSetAccessor: return SymbolKind.Property; - case Kind.variable: return SymbolKind.Variable; - case Kind.let: return SymbolKind.Variable; - case Kind.const: return SymbolKind.Variable; - case Kind.localVariable: return SymbolKind.Variable; - case Kind.alias: return SymbolKind.Variable; - case Kind.function: return SymbolKind.Function; - case Kind.localFunction: return SymbolKind.Function; - case Kind.constructSignature: return SymbolKind.Constructor; - case Kind.constructorImplementation: return SymbolKind.Constructor; - case Kind.typeParameter: return SymbolKind.TypeParameter; - case Kind.string: return SymbolKind.String; - default: return SymbolKind.Variable; - } -} - -function convertOptions(options: FormattingOptions, formatSettings: any, initialIndentLevel: number): ts.FormatCodeSettings { - return { - convertTabsToSpaces: options.insertSpaces, - tabSize: options.tabSize, - indentSize: options.tabSize, - indentStyle: ts.IndentStyle.Smart, - newLineCharacter: '\n', - baseIndentSize: options.tabSize * initialIndentLevel, - insertSpaceAfterCommaDelimiter: Boolean(!formatSettings || formatSettings.insertSpaceAfterCommaDelimiter), - insertSpaceAfterConstructor: Boolean(formatSettings && formatSettings.insertSpaceAfterConstructor), - insertSpaceAfterSemicolonInForStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterSemicolonInForStatements), - insertSpaceBeforeAndAfterBinaryOperators: Boolean(!formatSettings || formatSettings.insertSpaceBeforeAndAfterBinaryOperators), - insertSpaceAfterKeywordsInControlFlowStatements: Boolean(!formatSettings || formatSettings.insertSpaceAfterKeywordsInControlFlowStatements), - insertSpaceAfterFunctionKeywordForAnonymousFunctions: Boolean(!formatSettings || formatSettings.insertSpaceAfterFunctionKeywordForAnonymousFunctions), - insertSpaceBeforeFunctionParenthesis: Boolean(formatSettings && formatSettings.insertSpaceBeforeFunctionParenthesis), - insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis), - insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets), - insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces), - insertSpaceAfterOpeningAndBeforeClosingEmptyBraces: Boolean(!formatSettings || formatSettings.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces), - insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces), - insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: Boolean(formatSettings && formatSettings.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces), - insertSpaceAfterTypeAssertion: Boolean(formatSettings && formatSettings.insertSpaceAfterTypeAssertion), - placeOpenBraceOnNewLineForControlBlocks: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForFunctions), - placeOpenBraceOnNewLineForFunctions: Boolean(formatSettings && formatSettings.placeOpenBraceOnNewLineForControlBlocks), - semicolons: formatSettings?.semicolons - }; -} - -function computeInitialIndent(document: TextDocument, range: Range, options: FormattingOptions) { - const lineStart = document.offsetAt(Position.create(range.start.line, 0)); - const content = document.getText(); - - let i = lineStart; - let nChars = 0; - const tabSize = options.tabSize || 4; - while (i < content.length) { - const ch = content.charAt(i); - if (ch === ' ') { - nChars++; - } else if (ch === '\t') { - nChars += tabSize; - } else { - break; - } - i++; - } - return Math.floor(nChars / tabSize); -} - -function generateIndent(level: number, options: FormattingOptions) { - if (options.insertSpaces) { - return repeat(' ', level * options.tabSize); - } else { - return repeat('\t', level); - } -} diff --git a/extensions/html-language-features/server/src/modes/javascriptSemanticTokens.ts b/extensions/html-language-features/server/src/modes/javascriptSemanticTokens.ts deleted file mode 100644 index cbcf1b450829a..0000000000000 --- a/extensions/html-language-features/server/src/modes/javascriptSemanticTokens.ts +++ /dev/null @@ -1,109 +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 { TextDocument, SemanticTokenData } from './languageModes'; -import * as ts from 'typescript'; - -export function getSemanticTokenLegend() { - if (tokenTypes.length !== TokenType._) { - console.warn('TokenType has added new entries.'); - } - if (tokenModifiers.length !== TokenModifier._) { - console.warn('TokenModifier has added new entries.'); - } - return { types: tokenTypes, modifiers: tokenModifiers }; -} - -export function* getSemanticTokens(jsLanguageService: ts.LanguageService, document: TextDocument, fileName: string): Iterable { - const { spans } = jsLanguageService.getEncodedSemanticClassifications(fileName, { start: 0, length: document.getText().length }, '2020' as ts.SemanticClassificationFormat); - - for (let i = 0; i < spans.length;) { - const offset = spans[i++]; - const length = spans[i++]; - const tsClassification = spans[i++]; - - const tokenType = getTokenTypeFromClassification(tsClassification); - if (tokenType === undefined) { - continue; - } - - const tokenModifiers = getTokenModifierFromClassification(tsClassification); - const startPos = document.positionAt(offset); - yield { - start: startPos, - length: length, - typeIdx: tokenType, - modifierSet: tokenModifiers - }; - } -} - - -// typescript encodes type and modifiers in the classification: -// TSClassification = (TokenType + 1) << 8 + TokenModifier - -const enum TokenType { - class = 0, - enum = 1, - interface = 2, - namespace = 3, - typeParameter = 4, - type = 5, - parameter = 6, - variable = 7, - enumMember = 8, - property = 9, - function = 10, - method = 11, - _ = 12 -} - -const enum TokenModifier { - declaration = 0, - static = 1, - async = 2, - readonly = 3, - defaultLibrary = 4, - local = 5, - _ = 6 -} - -const enum TokenEncodingConsts { - typeOffset = 8, - modifierMask = 255 -} - -function getTokenTypeFromClassification(tsClassification: number): number | undefined { - if (tsClassification > TokenEncodingConsts.modifierMask) { - return (tsClassification >> TokenEncodingConsts.typeOffset) - 1; - } - return undefined; -} - -function getTokenModifierFromClassification(tsClassification: number) { - return tsClassification & TokenEncodingConsts.modifierMask; -} - -const tokenTypes: string[] = []; -tokenTypes[TokenType.class] = 'class'; -tokenTypes[TokenType.enum] = 'enum'; -tokenTypes[TokenType.interface] = 'interface'; -tokenTypes[TokenType.namespace] = 'namespace'; -tokenTypes[TokenType.typeParameter] = 'typeParameter'; -tokenTypes[TokenType.type] = 'type'; -tokenTypes[TokenType.parameter] = 'parameter'; -tokenTypes[TokenType.variable] = 'variable'; -tokenTypes[TokenType.enumMember] = 'enumMember'; -tokenTypes[TokenType.property] = 'property'; -tokenTypes[TokenType.function] = 'function'; -tokenTypes[TokenType.method] = 'method'; - -const tokenModifiers: string[] = []; -tokenModifiers[TokenModifier.async] = 'async'; -tokenModifiers[TokenModifier.declaration] = 'declaration'; -tokenModifiers[TokenModifier.readonly] = 'readonly'; -tokenModifiers[TokenModifier.static] = 'static'; -tokenModifiers[TokenModifier.local] = 'local'; -tokenModifiers[TokenModifier.defaultLibrary] = 'defaultLibrary'; diff --git a/extensions/html-language-features/server/src/modes/languageModes.ts b/extensions/html-language-features/server/src/modes/languageModes.ts deleted file mode 100644 index 4ab4a4a876ea9..0000000000000 --- a/extensions/html-language-features/server/src/modes/languageModes.ts +++ /dev/null @@ -1,188 +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 { getCSSLanguageService } from 'vscode-css-languageservice'; -import { - DocumentContext, getLanguageService as getHTMLLanguageService, IHTMLDataProvider, ClientCapabilities -} from 'vscode-html-languageservice'; -import { - SelectionRange, - CompletionItem, CompletionList, Definition, Diagnostic, DocumentHighlight, DocumentLink, FoldingRange, FormattingOptions, - Hover, Location, Position, Range, SignatureHelp, SymbolInformation, TextEdit, - Color, ColorInformation, ColorPresentation, WorkspaceEdit, - WorkspaceFolder -} from 'vscode-languageserver'; -import { TextDocument } from 'vscode-languageserver-textdocument'; - -import { getLanguageModelCache, LanguageModelCache } from '../languageModelCache'; -import { getCSSMode } from './cssMode'; -import { getDocumentRegions, HTMLDocumentRegions } from './embeddedSupport'; -import { getHTMLMode } from './htmlMode'; -import { getJavaScriptMode } from './javascriptMode'; -import { FileSystemProvider } from '../requests'; - -export { - WorkspaceFolder, CompletionItem, CompletionList, CompletionItemKind, Definition, Diagnostic, DocumentHighlight, DocumentHighlightKind, - DocumentLink, FoldingRange, FoldingRangeKind, FormattingOptions, - Hover, Location, Position, Range, SignatureHelp, SymbolInformation, SymbolKind, TextEdit, - Color, ColorInformation, ColorPresentation, WorkspaceEdit, - SignatureInformation, ParameterInformation, DiagnosticSeverity, - SelectionRange, TextDocumentIdentifier -} from 'vscode-languageserver'; - -export { ClientCapabilities, DocumentContext, LanguageService, HTMLDocument, HTMLFormatConfiguration, TokenType } from 'vscode-html-languageservice'; - -export { TextDocument } from 'vscode-languageserver-textdocument'; - -export interface Settings { - readonly css?: any; - readonly html?: any; - readonly javascript?: any; - readonly 'js/ts'?: any; -} - -export interface Workspace { - readonly settings: Settings; - readonly folders: WorkspaceFolder[]; -} - -export interface SemanticTokenData { - start: Position; - length: number; - typeIdx: number; - modifierSet: number; -} - -export type CompletionItemData = { - languageId: string; - uri: string; - offset: number; -}; - -export function isCompletionItemData(value: any): value is CompletionItemData { - return value && typeof value.languageId === 'string' && typeof value.uri === 'string' && typeof value.offset === 'number'; -} - -export interface LanguageMode { - getId(): string; - getSelectionRange?: (document: TextDocument, position: Position) => Promise; - doValidation?: (document: TextDocument, settings?: Settings) => Promise; - doComplete?: (document: TextDocument, position: Position, documentContext: DocumentContext, settings?: Settings) => Promise; - doResolve?: (document: TextDocument, item: CompletionItem) => Promise; - doHover?: (document: TextDocument, position: Position, settings?: Settings) => Promise; - doSignatureHelp?: (document: TextDocument, position: Position) => Promise; - doRename?: (document: TextDocument, position: Position, newName: string) => Promise; - doLinkedEditing?: (document: TextDocument, position: Position) => Promise; - findDocumentHighlight?: (document: TextDocument, position: Position) => Promise; - findDocumentSymbols?: (document: TextDocument) => Promise; - findDocumentLinks?: (document: TextDocument, documentContext: DocumentContext) => Promise; - findDefinition?: (document: TextDocument, position: Position) => Promise; - findReferences?: (document: TextDocument, position: Position) => Promise; - format?: (document: TextDocument, range: Range, options: FormattingOptions, settings?: Settings) => Promise; - findDocumentColors?: (document: TextDocument) => Promise; - getColorPresentations?: (document: TextDocument, color: Color, range: Range) => Promise; - doAutoInsert?: (document: TextDocument, position: Position, kind: 'autoClose' | 'autoQuote') => Promise; - findMatchingTagPosition?: (document: TextDocument, position: Position) => Promise; - getFoldingRanges?: (document: TextDocument) => Promise; - onDocumentRemoved(document: TextDocument): void; - getSemanticTokens?(document: TextDocument): Promise; - getSemanticTokenLegend?(): { types: string[]; modifiers: string[] }; - dispose(): void; -} - -export interface LanguageModes { - updateDataProviders(dataProviders: IHTMLDataProvider[]): void; - getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined; - getModesInRange(document: TextDocument, range: Range): LanguageModeRange[]; - getAllModes(): LanguageMode[]; - getAllModesInDocument(document: TextDocument): LanguageMode[]; - getMode(languageId: string): LanguageMode | undefined; - onDocumentRemoved(document: TextDocument): void; - dispose(): void; -} - -export interface LanguageModeRange extends Range { - mode: LanguageMode | undefined; - attributeValue?: boolean; -} - -export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean }, workspace: Workspace, clientCapabilities: ClientCapabilities, requestService: FileSystemProvider): LanguageModes { - const htmlLanguageService = getHTMLLanguageService({ clientCapabilities, fileSystemProvider: requestService }); - const cssLanguageService = getCSSLanguageService({ clientCapabilities, fileSystemProvider: requestService }); - - const documentRegions = getLanguageModelCache(10, 60, document => getDocumentRegions(htmlLanguageService, document)); - - let modelCaches: LanguageModelCache[] = []; - modelCaches.push(documentRegions); - - let modes = Object.create(null); - modes['html'] = getHTMLMode(htmlLanguageService, workspace); - if (supportedLanguages['css']) { - modes['css'] = getCSSMode(cssLanguageService, documentRegions, workspace); - } - if (supportedLanguages['javascript']) { - modes['javascript'] = getJavaScriptMode(documentRegions, 'javascript', workspace); - modes['typescript'] = getJavaScriptMode(documentRegions, 'typescript', workspace); - } - return { - async updateDataProviders(dataProviders: IHTMLDataProvider[]): Promise { - htmlLanguageService.setDataProviders(true, dataProviders); - }, - getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined { - const languageId = documentRegions.get(document).getLanguageAtPosition(position); - if (languageId) { - return modes[languageId]; - } - return undefined; - }, - getModesInRange(document: TextDocument, range: Range): LanguageModeRange[] { - return documentRegions.get(document).getLanguageRanges(range).map(r => { - return { - start: r.start, - end: r.end, - mode: r.languageId && modes[r.languageId], - attributeValue: r.attributeValue - }; - }); - }, - getAllModesInDocument(document: TextDocument): LanguageMode[] { - const result = []; - for (const languageId of documentRegions.get(document).getLanguagesInDocument()) { - const mode = modes[languageId]; - if (mode) { - result.push(mode); - } - } - return result; - }, - getAllModes(): LanguageMode[] { - const result = []; - for (const languageId in modes) { - const mode = modes[languageId]; - if (mode) { - result.push(mode); - } - } - return result; - }, - getMode(languageId: string): LanguageMode { - return modes[languageId]; - }, - onDocumentRemoved(document: TextDocument) { - modelCaches.forEach(mc => mc.onDocumentRemoved(document)); - for (const mode in modes) { - modes[mode].onDocumentRemoved(document); - } - }, - dispose(): void { - modelCaches.forEach(mc => mc.dispose()); - modelCaches = []; - for (const mode in modes) { - modes[mode].dispose(); - } - modes = {}; - } - }; -} diff --git a/extensions/html-language-features/server/src/modes/selectionRanges.ts b/extensions/html-language-features/server/src/modes/selectionRanges.ts deleted file mode 100644 index 8624f103e044b..0000000000000 --- a/extensions/html-language-features/server/src/modes/selectionRanges.ts +++ /dev/null @@ -1,26 +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 { LanguageModes, TextDocument, Position, Range, SelectionRange } from './languageModes'; -import { insideRangeButNotSame } from '../utils/positions'; - -export async function getSelectionRanges(languageModes: LanguageModes, document: TextDocument, positions: Position[]) { - const htmlMode = languageModes.getMode('html'); - return Promise.all(positions.map(async position => { - const htmlRange = await htmlMode!.getSelectionRange!(document, position); - const mode = languageModes.getModeAtPosition(document, position); - if (mode && mode.getSelectionRange) { - const range = await mode.getSelectionRange(document, position); - let top = range; - while (top.parent && insideRangeButNotSame(htmlRange.range, top.parent.range)) { - top = top.parent; - } - top.parent = htmlRange; - return range; - } - return htmlRange || SelectionRange.create(Range.create(position, position)); - })); -} - diff --git a/extensions/html-language-features/server/src/modes/semanticTokens.ts b/extensions/html-language-features/server/src/modes/semanticTokens.ts deleted file mode 100644 index dbfffcabdf5d4..0000000000000 --- a/extensions/html-language-features/server/src/modes/semanticTokens.ts +++ /dev/null @@ -1,137 +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 { SemanticTokenData, Range, TextDocument, LanguageModes, Position } from './languageModes'; -import { beforeOrSame } from '../utils/positions'; - -interface LegendMapping { - types: number[] | undefined; - modifiers: number[] | undefined; -} - -export interface SemanticTokenProvider { - readonly legend: { types: string[]; modifiers: string[] }; - getSemanticTokens(document: TextDocument, ranges?: Range[]): Promise; -} - - -export function newSemanticTokenProvider(languageModes: LanguageModes): SemanticTokenProvider { - - // combined legend across modes - const legend: { types: string[]; modifiers: string[] } = { types: [], modifiers: [] }; - const legendMappings: { [modeId: string]: LegendMapping } = {}; - - for (const mode of languageModes.getAllModes()) { - if (mode.getSemanticTokenLegend && mode.getSemanticTokens) { - const modeLegend = mode.getSemanticTokenLegend(); - legendMappings[mode.getId()] = { types: createMapping(modeLegend.types, legend.types), modifiers: createMapping(modeLegend.modifiers, legend.modifiers) }; - } - } - - return { - legend, - async getSemanticTokens(document: TextDocument, ranges?: Range[]): Promise { - const allTokens: SemanticTokenData[] = []; - for (const mode of languageModes.getAllModesInDocument(document)) { - if (mode.getSemanticTokens) { - const mapping = legendMappings[mode.getId()]; - const tokens = await mode.getSemanticTokens(document); - applyTypesMapping(tokens, mapping.types); - applyModifiersMapping(tokens, mapping.modifiers); - for (const token of tokens) { - allTokens.push(token); - } - } - } - return encodeTokens(allTokens, ranges, document); - } - }; -} - -function createMapping(origLegend: string[], newLegend: string[]): number[] | undefined { - const mapping: number[] = []; - let needsMapping = false; - for (let origIndex = 0; origIndex < origLegend.length; origIndex++) { - const entry = origLegend[origIndex]; - let newIndex = newLegend.indexOf(entry); - if (newIndex === -1) { - newIndex = newLegend.length; - newLegend.push(entry); - } - mapping.push(newIndex); - needsMapping = needsMapping || (newIndex !== origIndex); - } - return needsMapping ? mapping : undefined; -} - -function applyTypesMapping(tokens: SemanticTokenData[], typesMapping: number[] | undefined): void { - if (typesMapping) { - for (const token of tokens) { - token.typeIdx = typesMapping[token.typeIdx]; - } - } -} - -function applyModifiersMapping(tokens: SemanticTokenData[], modifiersMapping: number[] | undefined): void { - if (modifiersMapping) { - for (const token of tokens) { - let modifierSet = token.modifierSet; - if (modifierSet) { - let index = 0; - let result = 0; - while (modifierSet > 0) { - if ((modifierSet & 1) !== 0) { - result = result + (1 << modifiersMapping[index]); - } - index++; - modifierSet = modifierSet >> 1; - } - token.modifierSet = result; - } - } - } -} - -function encodeTokens(tokens: SemanticTokenData[], ranges: Range[] | undefined, document: TextDocument): number[] { - - const resultTokens = tokens.sort((d1, d2) => d1.start.line - d2.start.line || d1.start.character - d2.start.character); - if (ranges) { - ranges = ranges.sort((d1, d2) => d1.start.line - d2.start.line || d1.start.character - d2.start.character); - } else { - ranges = [Range.create(Position.create(0, 0), Position.create(document.lineCount, 0))]; - } - - let rangeIndex = 0; - let currRange = ranges[rangeIndex++]; - - let prefLine = 0; - let prevChar = 0; - - const encodedResult: number[] = []; - - for (let k = 0; k < resultTokens.length && currRange; k++) { - const curr = resultTokens[k]; - const start = curr.start; - while (currRange && beforeOrSame(currRange.end, start)) { - currRange = ranges[rangeIndex++]; - } - if (currRange && beforeOrSame(currRange.start, start) && beforeOrSame({ line: start.line, character: start.character + curr.length }, currRange.end)) { - // token inside a range - - if (prefLine !== start.line) { - prevChar = 0; - } - encodedResult.push(start.line - prefLine); // line delta - encodedResult.push(start.character - prevChar); // line delta - encodedResult.push(curr.length); // length - encodedResult.push(curr.typeIdx); // tokenType - encodedResult.push(curr.modifierSet); // tokenModifier - - prefLine = start.line; - prevChar = start.character; - } - } - return encodedResult; -} diff --git a/extensions/html-language-features/server/src/node/htmlServerMain.ts b/extensions/html-language-features/server/src/node/htmlServerMain.ts index 0367e11a2209f..9a19dbeee874b 100644 --- a/extensions/html-language-features/server/src/node/htmlServerMain.ts +++ b/extensions/html-language-features/server/src/node/htmlServerMain.ts @@ -3,34 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createConnection, Connection, Disposable } from 'vscode-languageserver/node'; -import { formatError } from '../utils/runner'; -import { RuntimeEnvironment, startServer } from '../htmlServer'; -import { getNodeFileFS } from './nodeFs'; +import { createServer, createConnection, createSimpleProjectProvider } from '@volar/language-server/node'; +import { create as createCssServicePlugin } from 'volar-service-css'; +import { create as createHtmlServicePlugin } from 'volar-service-html'; +import { htmlLanguagePlugin } from '../languagePlugin'; +const connection = createConnection(); +const server = createServer(connection); -// 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)); +connection.onInitialize(params => { + return server.initialize(params, createSimpleProjectProvider, { + getLanguagePlugins() { + return [htmlLanguagePlugin]; + }, + getServicePlugins() { + return [ + createCssServicePlugin(), + createHtmlServicePlugin(), + ]; + }, + }); }); -const runtime: RuntimeEnvironment = { - timer: { - setImmediate(callback: (...args: any[]) => void, ...args: any[]): Disposable { - const handle = setImmediate(callback, ...args); - return { dispose: () => clearImmediate(handle) }; - }, - setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): Disposable { - const handle = setTimeout(callback, ms, ...args); - return { dispose: () => clearTimeout(handle) }; - } - }, - fileFs: getNodeFileFS() -}; +connection.onInitialized(server.initialized); + +connection.onShutdown(server.shutdown); -startServer(connection, runtime); +connection.listen(); diff --git a/extensions/html-language-features/server/src/node/nodeFs.ts b/extensions/html-language-features/server/src/node/nodeFs.ts deleted file mode 100644 index edc9be776a65f..0000000000000 --- a/extensions/html-language-features/server/src/node/nodeFs.ts +++ /dev/null @@ -1,74 +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 { FileSystemProvider } from '../requests'; -import { URI as Uri } from 'vscode-uri'; - -import * as fs from 'fs'; -import { FileType } from 'vscode-css-languageservice'; - -export function getNodeFileFS(): FileSystemProvider { - function ensureFileUri(location: string) { - if (!location.startsWith('file:')) { - throw new Error('fileSystemProvider 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]; - } - })); - }); - }); - } - }; -} diff --git a/extensions/html-language-features/server/src/requests.ts b/extensions/html-language-features/server/src/requests.ts deleted file mode 100644 index 725f6f3b13579..0000000000000 --- a/extensions/html-language-features/server/src/requests.ts +++ /dev/null @@ -1,78 +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 { RequestType, Connection } from 'vscode-languageserver'; -import { RuntimeEnvironment } from './htmlServer'; - -export namespace FsStatRequest { - export const type: RequestType = new RequestType('fs/stat'); -} - -export namespace FsReadDirRequest { - export const type: RequestType = 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 FileSystemProvider { - stat(uri: string): Promise; - readDirectory(uri: string): Promise<[string, FileType][]>; -} - - -export function getFileSystemProvider(handledSchemas: string[], connection: Connection, runtime: RuntimeEnvironment): FileSystemProvider { - const fileFs = runtime.fileFs && handledSchemas.indexOf('file') !== -1 ? runtime.fileFs : undefined; - return { - async stat(uri: string): Promise { - if (fileFs && uri.startsWith('file:')) { - return fileFs.stat(uri); - } - const res = await connection.sendRequest(FsStatRequest.type, uri.toString()); - return res; - }, - readDirectory(uri: string): Promise<[string, FileType][]> { - if (fileFs && uri.startsWith('file:')) { - return fileFs.readDirectory(uri); - } - return connection.sendRequest(FsReadDirRequest.type, uri.toString()); - } - }; -} diff --git a/extensions/html-language-features/server/src/test/completions.test.ts b/extensions/html-language-features/server/src/test/completions.test.ts deleted file mode 100644 index fbad266e2dea2..0000000000000 --- a/extensions/html-language-features/server/src/test/completions.test.ts +++ /dev/null @@ -1,319 +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 'mocha'; -import * as assert from 'assert'; -import * as path from 'path'; -import { URI } from 'vscode-uri'; -import { getLanguageModes, WorkspaceFolder, TextDocument, CompletionList, CompletionItemKind, ClientCapabilities, TextEdit } from '../modes/languageModes'; -import { getNodeFileFS } from '../node/nodeFs'; -import { getDocumentContext } from '../utils/documentContext'; -export interface ItemDescription { - label: string; - documentation?: string; - kind?: CompletionItemKind; - resultText?: string; - command?: { title: string; command: string }; - notAvailable?: boolean; -} - -export function assertCompletion(completions: CompletionList, expected: ItemDescription, document: TextDocument) { - const matches = completions.items.filter(completion => { - return completion.label === expected.label; - }); - if (expected.notAvailable) { - assert.strictEqual(matches.length, 0, `${expected.label} should not existing is results`); - return; - } - - assert.strictEqual(matches.length, 1, `${expected.label} should only existing once: Actual: ${completions.items.map(c => c.label).join(', ')}`); - const match = matches[0]; - if (expected.documentation) { - assert.strictEqual(match.documentation, expected.documentation); - } - if (expected.kind) { - assert.strictEqual(match.kind, expected.kind); - } - if (expected.resultText && match.textEdit) { - const edit = TextEdit.is(match.textEdit) ? match.textEdit : TextEdit.replace(match.textEdit.replace, match.textEdit.newText); - assert.strictEqual(TextDocument.applyEdits(document, [edit]), expected.resultText); - } - if (expected.command) { - assert.deepStrictEqual(match.command, expected.command); - } -} - -const testUri = 'test://test/test.html'; - -export async function testCompletionFor(value: string, expected: { count?: number; items?: ItemDescription[] }, uri = testUri, workspaceFolders?: WorkspaceFolder[]): Promise { - const offset = value.indexOf('|'); - value = value.substr(0, offset) + value.substr(offset + 1); - - const workspace = { - settings: {}, - folders: workspaceFolders || [{ name: 'x', uri: uri.substr(0, uri.lastIndexOf('/')) }] - }; - - const document = TextDocument.create(uri, 'html', 0, value); - const position = document.positionAt(offset); - const context = getDocumentContext(uri, workspace.folders); - - const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFileFS()); - const mode = languageModes.getModeAtPosition(document, position)!; - - const list = await mode.doComplete!(document, position, context); - - if (expected.count) { - assert.strictEqual(list.items.length, expected.count); - } - if (expected.items) { - for (const item of expected.items) { - assertCompletion(list, item, document); - } - } -} - -suite('HTML Completion', () => { - test('HTML JavaScript Completions', async () => { - await testCompletionFor('', { - items: [ - { label: 'location', resultText: '' }, - ] - }); - await testCompletionFor('', { - items: [ - { label: 'getJSON', resultText: '' }, - ] - }); - await testCompletionFor('', { - items: [ - { label: 'a', resultText: '' }, - ] - }, 'test://test/test2.html'); - }); -}); - -suite('HTML Path Completion', () => { - const triggerSuggestCommand = { - title: 'Suggest', - command: 'editor.action.triggerSuggest' - }; - - const fixtureRoot = path.resolve(__dirname, '../../src/test/pathCompletionFixtures'); - const fixtureWorkspace = { name: 'fixture', uri: URI.file(fixtureRoot).toString() }; - const indexHtmlUri = URI.file(path.resolve(fixtureRoot, 'index.html')).toString(); - const aboutHtmlUri = URI.file(path.resolve(fixtureRoot, 'about/about.html')).toString(); - - test('Basics - Correct label/kind/result/command', async () => { - await testCompletionFor('', 'css', ' '); - assertEmbeddedLanguageContent('Hello', 'css', ' foo { } foo { } '); - assertEmbeddedLanguageContent('\n \n\n', 'css', '\n \n foo { } \n \n\n'); - - assertEmbeddedLanguageContent('
', 'css', ' __{color: red} '); - assertEmbeddedLanguageContent('
', 'css', ' __{color:red} '); - }); - - test('Scripts', function (): any { - assertLanguageId('|', 'html'); - assertLanguageId('', 'html'); - assertLanguageId('var i = 0;', 'html'); - assertLanguageId('', 'javascript'); - assertLanguageId('', 'javascript'); - assertLanguageId('', 'javascript'); - assertLanguageId('', 'javascript'); - assertLanguageId('', 'javascript'); - assertLanguageId('', 'javascript'); - assertLanguageId('', 'javascript'); - assertLanguageId('', undefined); - assertLanguageId('', 'javascript'); - }); - - test('Scripts in attribute', function (): any { - assertLanguageId('
', 'html'); - assertLanguageId('
', 'html'); - assertLanguageId('
', 'javascript'); - assertLanguageId('
', 'javascript'); - assertLanguageId('
', 'javascript'); - assertLanguageId('
', 'html'); - assertLanguageId('
', 'html'); - assertLanguageId('
', 'javascript'); - assertLanguageId('
', 'javascript'); - assertLanguageId('
', 'html'); - - assertLanguageId('
', 'html'); - assertLanguageId('
', 'javascript'); - assertLanguageId('
', 'javascript'); - assertLanguageId('
', 'javascript'); - assertLanguageId('
', 'javascript'); - assertLanguageId('
', 'html'); - - assertLanguageId('