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 index 06997d39fb0af..1559f3028b561 100644 --- a/extensions/html-language-features/client/src/browser/htmlClientMain.ts +++ b/extensions/html-language-features/client/src/browser/htmlClientMain.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as serverProtocol from '@volar/language-server/protocol'; +import { LanguageClient } from '@volar/vscode/browser'; import { Disposable, ExtensionContext, Uri, l10n } from 'vscode'; import { LanguageClientOptions } from 'vscode-languageclient'; -import { startClient, LanguageClientConstructor, AsyncDisposable } from '../htmlClient'; -import { LanguageClient } from 'vscode-languageclient/browser'; +import { AsyncDisposable, LanguageClientConstructor, startClient } from '../htmlClient'; +import { BaseLanguageClient, createLabsInfo } from '@volar/vscode'; let client: AsyncDisposable | undefined; @@ -17,8 +19,9 @@ export async function activate(context: ExtensionContext) { const worker = new Worker(serverMain.toString()); worker.postMessage({ i10lLocation: l10n.uri?.toString(false) ?? '' }); + let languageClient!: BaseLanguageClient; const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - return new LanguageClient(id, name, worker, clientOptions); + return languageClient = new LanguageClient(id, name, clientOptions, worker); }; const timer = { @@ -30,9 +33,13 @@ export async function activate(context: ExtensionContext) { client = await startClient(context, newLanguageClient, { TextDecoder, timer }); + const labsInfo = createLabsInfo(serverProtocol); + labsInfo.addLanguageClient(languageClient); + return labsInfo.extensionExports; } catch (e) { console.log(e); } + return undefined; } export async function deactivate(): Promise { @@ -41,4 +48,3 @@ export async function deactivate(): Promise { client = undefined; } } - diff --git a/extensions/html-language-features/client/src/htmlClient.ts b/extensions/html-language-features/client/src/htmlClient.ts index 7b69c795f9043..46308234613a8 100644 --- a/extensions/html-language-features/client/src/htmlClient.ts +++ b/extensions/html-language-features/client/src/htmlClient.ts @@ -6,16 +6,16 @@ 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 + Disposable, FormattingOptions, CancellationToken, ProviderResult, TextEdit, CompletionContext, CompletionList, + window, commands, OutputChannel, l10n } from 'vscode'; import { LanguageClientOptions, RequestType, DocumentRangeFormattingParams, - DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, TextDocumentIdentifier, RequestType0, Range as LspRange, Position as LspPosition, NotificationType, BaseLanguageClient + DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, NotificationType, BaseLanguageClient } from 'vscode-languageclient'; -import { FileSystemProvider, serveFileSystemRequests } from './requests'; +import { serveFileSystemRequests } from './requests'; import { getCustomDataSource } from './customData'; -import { activateAutoInsertion } from './autoInsertion'; +import { activateAutoInsertion } from '@volar/vscode'; import { getLanguageParticipants, LanguageParticipants } from './languageParticipants'; namespace CustomDataChangedNotification { @@ -26,37 +26,6 @@ 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'; @@ -77,7 +46,6 @@ 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; @@ -196,7 +164,7 @@ async function startClientWithParticipants(languageParticipants: LanguagePartici await client.start(); - toDispose.push(serveFileSystemRequests(client, runtime)); + toDispose.push(serveFileSystemRequests(client)); const customDataSource = getCustomDataSource(runtime, toDispose); @@ -207,16 +175,7 @@ async function startClientWithParticipants(languageParticipants: LanguagePartici 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); + const disposable = activateAutoInsertion(languageParticipants.documentSelector, client); toDispose.push(disposable); const disposable2 = client.onTelemetry(e => { @@ -229,31 +188,6 @@ async function startClientWithParticipants(languageParticipants: LanguagePartici 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) { diff --git a/extensions/html-language-features/client/src/node/htmlClientMain.ts b/extensions/html-language-features/client/src/node/htmlClientMain.ts index cdb995b3286db..b36d0d0d8672f 100644 --- a/extensions/html-language-features/client/src/node/htmlClientMain.ts +++ b/extensions/html-language-features/client/src/node/htmlClientMain.ts @@ -3,14 +3,13 @@ * 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 * as serverProtocol from '@volar/language-server/protocol'; +import { createLabsInfo, LanguageClientOptions } from '@volar/vscode'; +import { BaseLanguageClient, LanguageClient, ServerOptions, TransportKind } from '@volar/vscode/node'; import TelemetryReporter from '@vscode/extension-telemetry'; - +import * as fs from 'fs'; +import { Disposable, ExtensionContext, l10n } from 'vscode'; +import { AsyncDisposable, LanguageClientConstructor, startClient } from '../htmlClient'; let telemetry: TelemetryReporter | undefined; let client: AsyncDisposable | undefined; @@ -34,8 +33,9 @@ export async function activate(context: ExtensionContext) { debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } }; + let languageClient!: BaseLanguageClient; const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { - return new LanguageClient(id, name, serverOptions, clientOptions); + return languageClient = new LanguageClient(id, name, serverOptions, clientOptions); }; const timer = { @@ -49,7 +49,11 @@ export async function activate(context: ExtensionContext) { // 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 }); + client = await startClient(context, newLanguageClient, { TextDecoder, telemetry, timer }); + + const labsInfo = createLabsInfo(serverProtocol); + labsInfo.addLanguageClient(languageClient); + return labsInfo.extensionExports; } export async function deactivate(): Promise { 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 index 8106f0442280f..c92b56926f135 100644 --- a/extensions/html-language-features/client/src/requests.ts +++ b/extensions/html-language-features/client/src/requests.ts @@ -3,9 +3,9 @@ * 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'; +import type { FileStat, FileType } from '@volar/language-service'; +import { Disposable, Uri, workspace } from 'vscode'; +import { BaseLanguageClient, RequestType } from 'vscode-languageclient'; export namespace FsStatRequest { export const type: RequestType = new RequestType('fs/stat'); @@ -15,64 +15,15 @@ export namespace FsReadDirRequest { export const type: RequestType = new RequestType('fs/readDir'); } -export function serveFileSystemRequests(client: BaseLanguageClient, runtime: Runtime): Disposable { +export function serveFileSystemRequests(client: BaseLanguageClient): 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 ac026b973eb18..a172fb4966d78 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": "^10.0.0-next.8", - "vscode-uri": "^3.0.8" + "@volar/language-server": "~2.4.0", + "@volar/vscode": "~2.4.0", + "@vscode/extension-telemetry": "^0.9.0" }, "devDependencies": { "@types/node": "20.x" diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index c1ddc242fa4a3..e0eed513a7e9a 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -9,11 +9,16 @@ }, "main": "./out/node/htmlServerMain", "dependencies": { + "@volar/language-server": "~2.4.0", + "@volar/language-service": "~2.4.0", + "@volar/typescript": "~2.4.0", "@vscode/l10n": "^0.0.18", - "vscode-css-languageservice": "^6.3.0", + "volar-service-css": "0.0.61", + "volar-service-html": "0.0.61", + "volar-service-json": "0.0.61", + "volar-service-typescript": "0.0.61", "vscode-html-languageservice": "^5.3.0", - "vscode-languageserver": "^10.0.0-next.6", - "vscode-languageserver-textdocument": "^1.0.11", + "vscode-jsonrpc": "^8.2.1", "vscode-uri": "^3.0.8" }, "devDependencies": { diff --git a/extensions/html-language-features/server/src/browser/htmlServerMain.ts b/extensions/html-language-features/server/src/browser/htmlServerMain.ts index 3264513e0ff84..5d5e6c9ca09be 100644 --- a/extensions/html-language-features/server/src/browser/htmlServerMain.ts +++ b/extensions/html-language-features/server/src/browser/htmlServerMain.ts @@ -3,28 +3,21 @@ * 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 { createConnection, createServer } from '@volar/language-server/browser'; +import { startServer } from '../htmlServer'; +import { getFileSystemProvider } from '../requests'; -const messageReader = new BrowserMessageReader(self); -const messageWriter = new BrowserMessageWriter(self); +const connection = createConnection(); +const server = createServer(connection); +const installedFs = new Set(); -const connection = createConnection(messageReader, messageWriter); - -console.log = connection.console.log.bind(connection.console); -console.error = connection.console.error.bind(connection.console); - -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) }; +server.onInitialize(() => { + for (const folder of server.workspaceFolders.all) { + if (!installedFs.has(folder.scheme)) { + installedFs.add(folder.scheme); + server.fileSystem.install(folder.scheme, getFileSystemProvider(connection)); } } -}; +}); -startServer(connection, runtime); +startServer(server, connection); diff --git a/extensions/html-language-features/server/src/htmlServer.ts b/extensions/html-language-features/server/src/htmlServer.ts index 29aa041746cc5..d407398fd3992 100644 --- a/extensions/html-language-features/server/src/htmlServer.ts +++ b/extensions/html-language-features/server/src/htmlServer.ts @@ -3,29 +3,12 @@ * 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 { Connection, Disposable, DocumentFormattingRequest, DocumentRangeFormattingRequest, LanguageServer, NotificationType, RequestType } from '@volar/language-server'; +import { Emitter } from 'vscode-jsonrpc'; import { fetchHTMLDataProviders } from './customData'; -import { getSelectionRanges } from './modes/selectionRanges'; -import { SemanticTokenProvider, newSemanticTokenProvider } from './modes/semanticTokens'; -import { FileSystemProvider, getFileSystemProvider } from './requests'; +import { htmlLanguagePlugin } from './modes/languagePlugin'; +import { createHtmlProject } from './modes/project'; +import { getLanguageServicePlugins } from './modes/languageServicePlugins'; namespace CustomDataChangedNotification { export const type: NotificationType = new NotificationType('html/customDataChanged'); @@ -35,221 +18,30 @@ 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; +export function startServer(server: LanguageServer, connection: Connection) { let clientSnippetSupport = false; let dynamicFormatterRegistration = false; - let scopedSettingsSupport = false; - let workspaceFoldersSupport = false; - let foldingRangeLimit = Number.MAX_VALUE; let formatterMaxNumberOfEdits = Number.MAX_VALUE; + let dataPaths: string[]; + let formatterRegistrations: Promise[] | null = null; + const customDataChangedEmitter = new Emitter(); 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(); - + server.configurations.onDidChange(async () => { // dynamically enable & disable the formatter if (dynamicFormatterRegistration) { - const enableFormatter = globalSettings && globalSettings.html && globalSettings.html.format && globalSettings.html.format.enable; + const enableFormatter = await server.configurations.get('html.format.enable'); if (enableFormatter) { if (!formatterRegistrations) { const documentSelector = [{ language: 'html' }, { language: 'handlebars' }]; @@ -265,329 +57,58 @@ export function startServer(connection: Connection, runtime: RuntimeEnvironment) } }); - 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.onInitialize(params => { + const initializationOptions = params.initializationOptions as any || {}; - 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); - }); + dataPaths = initializationOptions?.dataPaths || []; - 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); + 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 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 c; } - 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); - }); + clientSnippetSupport = getClientCapability('textDocument.completion.completionItem.snippetSupport', false); + dynamicFormatterRegistration = getClientCapability('textDocument.rangeFormatting.dynamicRegistration', false) && (typeof initializationOptions?.provideFormatter !== 'boolean'); + formatterMaxNumberOfEdits = initializationOptions?.customCapabilities?.rangeFormatting?.editLimit || Number.MAX_VALUE; - let semanticTokensProvider: SemanticTokenProvider | undefined; - function getSemanticTokenProvider() { - if (!semanticTokensProvider) { - semanticTokensProvider = newSemanticTokenProvider(languageModes); + const initializeResult = server.initialize( + params, + createHtmlProject([htmlLanguagePlugin]), + getLanguageServicePlugins({ + supportedLanguages: initializationOptions?.embeddedLanguages || { css: true, javascript: true }, + getCustomData: () => fetchHTMLDataProviders(dataPaths, customDataRequestService), + onDidChangeCustomData: listener => customDataChangedEmitter.event(listener), + formatterMaxNumberOfEdits, + }) + ); + + if (!initializationOptions?.provideFormatter) { + initializeResult.capabilities.documentRangeFormattingProvider = undefined; + initializeResult.capabilities.documentFormattingProvider = undefined; + } + if (!clientSnippetSupport) { + initializeResult.capabilities.completionProvider = undefined; } - 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); + return initializeResult; }); - connection.onRequest(SemanticTokenLegendRequest.type, token => { - return runSafe(runtime, async () => { - return getSemanticTokenProvider().legend; - }, null, `Error while computing semantic tokens legend`, token); - }); + connection.onInitialized(server.initialized); + + connection.onShutdown(server.shutdown); - connection.onNotification(CustomDataChangedNotification.type, dataPaths => { - fetchHTMLDataProviders(dataPaths, customDataRequestService).then(dataProviders => { - languageModes.updateDataProviders(dataProviders); - }); + connection.onNotification(CustomDataChangedNotification.type, newDataPaths => { + dataPaths = newDataPaths; + customDataChangedEmitter.fire(); }); - // 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 5dd8e439f5cf4..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.Timeout | 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/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..093a2be57f7c5 100644 --- a/extensions/html-language-features/server/src/modes/embeddedSupport.ts +++ b/extensions/html-language-features/server/src/modes/embeddedSupport.ts @@ -3,32 +3,36 @@ * 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 } +export interface EmbeddedRegion { + tagName: string; + languageId: string | undefined; + content: string; + start: number; + generatedStart: number; + length: number; + attributeValue?: boolean; + moduleScript?: 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; + let isModuleScript = false; + let value: string; + let region: EmbeddedRegion; const importedScripts: string[] = []; let token = scanner.scan(); @@ -37,43 +41,56 @@ export function getDocumentRegions(languageService: LanguageService, document: T case TokenType.StartTag: lastTagName = scanner.getTokenText(); lastAttributeName = null; - languageIdFromType = 'javascript'; + isModuleScript = false; + languageIdFromType = lastTagName === 'style' ? 'css' : 'javascript'; break; case TokenType.Styles: - regions.push({ languageId: 'css', start: scanner.getTokenOffset(), end: scanner.getTokenEnd() }); + regions.push(createEmbeddedRegion(lastTagName, languageIdFromType, scanner.getTokenOffset(), scanner.getTokenEnd())); break; case TokenType.Script: - regions.push({ languageId: languageIdFromType, start: scanner.getTokenOffset(), end: scanner.getTokenEnd() }); + region = createEmbeddedRegion(lastTagName, languageIdFromType, scanner.getTokenOffset(), scanner.getTokenEnd()); + region.moduleScript = isModuleScript; + regions.push(region); break; case TokenType.AttributeName: lastAttributeName = scanner.getTokenText(); break; case TokenType.AttributeValue: + value = scanner.getTokenText(); + if ((value.startsWith('\'') && value.endsWith('\'')) || (value.startsWith('"') && value.endsWith('"'))) { + value = value.slice(1, -1); + } if (lastAttributeName === 'src' && lastTagName.toLowerCase() === 'script') { - let value = scanner.getTokenText(); - if (value[0] === '\'' || value[0] === '"') { - value = value.substr(1, value.length - 1); - } importedScripts.push(value); } else if (lastAttributeName === 'type' && lastTagName.toLowerCase() === 'script') { - if (/["'](module|(text|application)\/(java|ecma)script|text\/babel)["']/.test(scanner.getTokenText())) { + if (/(module|(text|application)\/(java|ecma)script|text\/babel)/.test(value)) { languageIdFromType = 'javascript'; - } else if (/["']text\/typescript["']/.test(scanner.getTokenText())) { + isModuleScript = true; + } else if (/text\/typescript/.test(value)) { languageIdFromType = 'typescript'; + isModuleScript = true; + } else if (/application\/json/.test(value)) { + languageIdFromType = 'json'; } else { languageIdFromType = undefined; } + } else if (lastAttributeName === 'type' && lastTagName.toLowerCase() === 'style') { + if (/text\/scss/.test(value)) { + languageIdFromType = 'scss'; + } else if (/text\/less/.test(value)) { + languageIdFromType = 'less'; + } } else { const attributeLanguageId = getAttributeLanguage(lastAttributeName!); 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(lastTagName, attributeLanguageId, start, end, true)); } } lastAttributeName = null; @@ -82,99 +99,26 @@ 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), - getImportedScripts: () => importedScripts + 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; - } - } - } - 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); - } + function createEmbeddedRegion(tagName: string, languageId: string | undefined, start: number, end: number, attributeValue?: boolean) { + const c: EmbeddedRegion = { + tagName, + 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 = substituteWithWhitespace(result, currentPos, oldContent.length, oldContent, lastSuffix, ''); - return TextDocument.create(document.uri, languageId, document.version, result); } function getPrefix(c: EmbeddedRegion) { @@ -201,35 +145,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 index 7abf94edf2233..87002f96cb1af 100644 --- a/extensions/html-language-features/server/src/modes/javascriptLibs.ts +++ b/extensions/html-language-features/server/src/modes/javascriptLibs.ts @@ -3,31 +3,7 @@ * 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 } = {}; +import { basename, dirname, join } from 'path'; 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; -} +export const JQUERY_PATH = join(serverFolder, 'lib/jquery.d.ts'); 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/languagePlugin.ts b/extensions/html-language-features/server/src/modes/languagePlugin.ts new file mode 100644 index 0000000000000..9a05fdcf2fa6c --- /dev/null +++ b/extensions/html-language-features/server/src/modes/languagePlugin.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { forEachEmbeddedCode } from '@volar/language-core'; +import type { LanguagePlugin } from '@volar/language-server'; +import type { TypeScriptExtraServiceScript } from '@volar/typescript'; +import type * as ts from 'typescript'; +import { URI } from 'vscode-uri'; +import { HTMLVirtualCode } from './virtualCode'; + +export const htmlLanguagePlugin: LanguagePlugin = { + getLanguageId(uri) { + if (uri.toString().endsWith('.html')) { + return 'html'; + } + return undefined; + }, + createVirtualCode(_uri, languageId, snapshot) { + if (languageId !== 'typescript' && languageId !== 'javascript' && languageId !== 'typescriptreact' && languageId !== 'javascriptreact' && languageId !== 'json') { + return new HTMLVirtualCode(snapshot); + } + return undefined; + }, + typescript: { + extraFileExtensions: [], + getServiceScript(rootCode) { + for (const code of forEachEmbeddedCode(rootCode)) { + if (code.id === 'global_script') { + return { + code, + extension: '.js', + scriptKind: 1, + }; + } + } + return undefined; + }, + getExtraServiceScripts(fileName, rootCode) { + const extraScripts: TypeScriptExtraServiceScript[] = []; + for (const code of forEachEmbeddedCode(rootCode)) { + if (code.id.startsWith('script_')) { + const ext = code.languageId === 'typescript' ? '.ts' : '.js'; + extraScripts.push({ + fileName: `${fileName}.embedded_${code.id}${ext}`, + code, + extension: ext, + scriptKind: ext === '.ts' + ? 3 satisfies ts.ScriptKind.TS + : 1 satisfies ts.ScriptKind.JS, + }); + } + } + return extraScripts; + }, + }, +}; diff --git a/extensions/html-language-features/server/src/modes/languageService.ts b/extensions/html-language-features/server/src/modes/languageService.ts new file mode 100644 index 0000000000000..d491e9455d551 --- /dev/null +++ b/extensions/html-language-features/server/src/modes/languageService.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { LanguageServer, ProjectContext } from '@volar/language-server'; +import { SnapshotDocument } from '@volar/language-server/lib/utils/snapshotDocument'; +import { createLanguageServiceEnvironment } from '@volar/language-server/browser'; +import { LanguagePlugin, LanguageService, createLanguageService as _createLanguageService, createLanguage, createUriMap } from '@volar/language-service'; +import { TypeScriptProjectHost, createLanguageServiceHost, createSys, resolveFileLanguageId } from '@volar/typescript'; +import * as ts from 'typescript'; +import { URI } from 'vscode-uri'; + +export function createLanguageService( + server: LanguageServer, + languagePlugins: LanguagePlugin[], + projectHost: TypeScriptProjectHost, + uriConverter: { + asUri(fileName: string): URI; + asFileName(uri: URI): string; + }, +): LanguageService { + const fsFileSnapshots = createUriMap<[number | undefined, ts.IScriptSnapshot | undefined]>(); + const serviceEnv = createLanguageServiceEnvironment(server, server.workspaceFolders.all); + const sys = createSys(ts.sys, serviceEnv, projectHost.getCurrentDirectory, uriConverter); + const docOpenWatcher = server.documents.onDidOpen(({ document }) => updateFsCacheFromSyncedDocument(document)); + const docSaveWatcher = server.documents.onDidSave(({ document }) => updateFsCacheFromSyncedDocument(document)); + const language = createLanguage( + [ + { getLanguageId: uri => server.documents.get(uri)?.languageId }, + ...languagePlugins, + { getLanguageId: uri => resolveFileLanguageId(uri.path) }, + ], + createUriMap(sys.useCaseSensitiveFileNames), + (uri, includeFsFiles) => { + let snapshot = server.documents.get(uri)?.getSnapshot(); + + if (!snapshot && includeFsFiles) { + const cache = fsFileSnapshots.get(uri); + const fileName = uriConverter.asFileName(uri); + const modifiedTime = sys.getModifiedTime?.(fileName)?.valueOf(); + if (!cache || cache[0] !== modifiedTime) { + if (sys.fileExists(fileName)) { + const text = sys.readFile(fileName); + const snapshot = text !== undefined ? ts.ScriptSnapshot.fromString(text) : undefined; + fsFileSnapshots.set(uri, [modifiedTime, snapshot]); + } + else { + fsFileSnapshots.set(uri, [modifiedTime, undefined]); + } + } + snapshot = fsFileSnapshots.get(uri)?.[1]; + } + + if (snapshot) { + language.scripts.set(uri, snapshot); + } + else { + language.scripts.delete(uri); + } + } + ); + const project: ProjectContext = { + typescript: { + configFileName: undefined, + sys, + uriConverter, + ...createLanguageServiceHost( + ts, + sys, + language, + fileName => uriConverter.asUri(fileName), + projectHost + ), + }, + }; + const languageService = _createLanguageService(language, server.languageServicePlugins, serviceEnv, project); + + return { + ...languageService, + dispose: () => { + sys.dispose(); + languageService?.dispose(); + docOpenWatcher.dispose(); + docSaveWatcher.dispose(); + }, + }; + + function updateFsCacheFromSyncedDocument(document: SnapshotDocument) { + const uri = URI.parse(document.uri); + const fileName = uriConverter.asFileName(uri); + if (fsFileSnapshots.has(uri) || sys.fileExists(fileName)) { + const modifiedTime = sys.getModifiedTime?.(fileName); + fsFileSnapshots.set(uri, [modifiedTime?.valueOf(), document.getSnapshot()]); + } + } +} diff --git a/extensions/html-language-features/server/src/modes/languageServicePlugins.ts b/extensions/html-language-features/server/src/modes/languageServicePlugins.ts new file mode 100644 index 0000000000000..00e2f16033cc3 --- /dev/null +++ b/extensions/html-language-features/server/src/modes/languageServicePlugins.ts @@ -0,0 +1,173 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { Disposable, LanguageServiceContext, LanguageServicePlugin, ProviderResult } from '@volar/language-service'; +import * as ts from 'typescript'; +import { create as createCssPlugin } from 'volar-service-css'; +import { create as createHtmlPlugin } from 'volar-service-html'; +import { create as createJsonPlugin } from 'volar-service-json'; +import { create as createTypeScriptPlugins } from 'volar-service-typescript'; +import { isTsDocument } from 'volar-service-typescript/lib/shared'; +import { IHTMLDataProvider, TextDocument, TextEdit } from 'vscode-html-languageservice'; + +export function getLanguageServicePlugins(options: { + supportedLanguages: { [languageId: string]: boolean }; + getCustomData: (context: LanguageServiceContext) => ProviderResult; + onDidChangeCustomData: (listener: () => void) => Disposable; + formatterMaxNumberOfEdits?: number; +}) { + const plugins: LanguageServicePlugin[] = [ + { + capabilities: {}, + create() { + return { + resolveEmbeddedCodeFormattingOptions(_sourceScript, embeddedCode, options) { + if (embeddedCode.id.startsWith('style_')) { + options.initialIndentLevel++; + } + return options; + }, + }; + }, + }, + ]; + const baseHtmlPlugin = createHtmlPlugin({ + async isFormattingEnabled(_document, context) { + return await context.env.getConfiguration?.('html.format.enable') ?? true; + }, + getCustomData: options.getCustomData, + onDidChangeCustomData: options.onDidChangeCustomData, + }); + plugins.push({ + ...baseHtmlPlugin, + create(context) { + const base = baseHtmlPlugin.create(context); + return { + ...base, + async provideDocumentFormattingEdits(document, ...args) { + const edits = await base.provideDocumentFormattingEdits?.(document, ...args); + if (edits && options.formatterMaxNumberOfEdits !== undefined && edits.length > options.formatterMaxNumberOfEdits) { + const newText = TextDocument.applyEdits(document, edits); + return [TextEdit.replace({ start: document.positionAt(0), end: document.positionAt(document.getText().length) }, newText)]; + } + return edits; + }, + }; + }, + }); + if (options.supportedLanguages['css']) { + plugins.push( + createCssPlugin({ + async isFormattingEnabled(_document, context) { + return await context.env.getConfiguration?.('html.format.enable') ?? true; + }, + async getLanguageSettings(document, context) { + return { + ...await context.env.getConfiguration?.(document.languageId), + validate: await context.env.getConfiguration?.('html.validate.styles') ?? true, + }; + }, + }), + ); + } + if (options.supportedLanguages['javascript']) { + const tsPlugins = createTypeScriptPlugins(ts, { + async isFormattingEnabled(_document, context) { + return await context.env.getConfiguration?.('html.format.enable') ?? true; + }, + async isValidationEnabled(_document, context) { + return await context.env.getConfiguration?.('html.validate.scripts') ?? true; + }, + }); + const patchedDocuments = new WeakMap(); + + for (const tsPlugin of tsPlugins) { + if (tsPlugin.name === 'typescript-syntactic') { + plugins.push({ + ...tsPlugin, + create(context) { + const base = tsPlugin.create(context); + return { + ...base, + async provideDocumentFormattingEdits(document, range, options, embeddedCodeContext, token) { + if (isTsDocument(document)) { + const [_version, prefix, newDocument] = getPatchedDocument(document); + if (newDocument === document) { + if (embeddedCodeContext) { + embeddedCodeContext.initialIndentLevel = 0; + } + return await base.provideDocumentFormattingEdits?.(newDocument, range, options, embeddedCodeContext, token); + } + const newRange = { ...range }; + if (document.offsetAt(range.start) !== 0 && document.offsetAt(range.end) !== document.getText().length) { + newRange.start = newDocument.positionAt(document.offsetAt(range.start) + prefix.length); + newRange.end = newDocument.positionAt(document.offsetAt(range.end) + prefix.length); + } + else { + newRange.end = newDocument.positionAt(newDocument.getText().length); + } + const edits = await base.provideDocumentFormattingEdits?.(newDocument, newRange, options, embeddedCodeContext, token); + if (edits) { + const modifiedEdits: TextEdit[] = []; + for (const edit of edits) { + if (edit.range.start.line === 0) { + edit.range.start.character -= prefix.length; + } + if (edit.range.end.line === 0) { + edit.range.end.character -= prefix.length; + } + if (edit.range.start.character < 0 || edit.range.end.character < 0) { + continue; + } + modifiedEdits.push(edit); + } + return modifiedEdits; + } + } + return undefined; + }, + }; + }, + }); + } + else { + plugins.push(tsPlugin); + } + } + plugins.push( + createJsonPlugin({ + async isFormattingEnabled(_document, context) { + return await context.env.getConfiguration?.('html.format.enable') ?? true; + }, + async getLanguageSettings(context) { + return { + ...await context.env.getConfiguration?.('json'), + validate: await context.env.getConfiguration?.('html.validate.scripts') ?? true, + }; + }, + }), + ); + + function getPatchedDocument(document: TextDocument) { + let patchedDocument = patchedDocuments.get(document); + if (!patchedDocument || patchedDocument[0] !== document.version) { + const lines = document.getText().split('\n'); + if (lines.length && !lines[0].trim() && !lines[lines.length - 1].trim()) { // wrap with {...} if is multi-line block + const prefix = '{'; + const suffix = '}'; + const newText = prefix + document.getText() + suffix; + const newDocument = TextDocument.create(document.uri, document.languageId, document.version, newText); + patchedDocument = [document.version, prefix, newDocument]; + } + else { + patchedDocument = [document.version, '', document]; + } + patchedDocuments.set(document, patchedDocument); + } + return patchedDocument; + } + } + return plugins; +} diff --git a/extensions/html-language-features/server/src/modes/project.ts b/extensions/html-language-features/server/src/modes/project.ts new file mode 100644 index 0000000000000..204b95f5f5832 --- /dev/null +++ b/extensions/html-language-features/server/src/modes/project.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { LanguageServer, LanguageServerProject } from '@volar/language-server'; +import { createUriConverter } from '@volar/language-server/browser'; +import { LanguagePlugin, LanguageService } from '@volar/language-service'; +import * as ts from 'typescript'; +import { URI, Utils } from 'vscode-uri'; +import { JQUERY_PATH } from './javascriptLibs'; +import { createLanguageService } from './languageService'; +import { HTMLVirtualCode } from './virtualCode'; + +export const compilerOptions: ts.CompilerOptions = { + allowNonTsExtensions: true, + allowJs: true, + lib: ['lib.es2020.full.d.ts'], + target: 99 satisfies ts.ScriptTarget.Latest, + moduleResolution: 1 satisfies ts.ModuleResolutionKind.Classic, + experimentalDecorators: false, +}; + +export function createHtmlProject(languagePlugins: LanguagePlugin[]): LanguageServerProject { + let server: LanguageServer; + let languageService: LanguageService | undefined; + let projectVersion = ''; + let currentDirectory = ''; + let tsLocalized: any; + let uriConverter: ReturnType; + + const currentRootFiles: string[] = []; + + return { + setup(_server) { + server = _server; + uriConverter = createUriConverter(server.workspaceFolders.all); + if (server.initializeParams.locale) { + try { + tsLocalized = require(`typescript/lib/${server.initializeParams.locale}/diagnosticMessages.generated.json`); + } catch { } + } + }, + async getLanguageService(uri) { + if (!languageService) { + languageService = createLanguageService( + server, + languagePlugins, + { + getCurrentDirectory() { + return currentDirectory; + }, + getProjectVersion() { + return projectVersion; + }, + getScriptFileNames() { + return currentRootFiles; + }, + getCompilationSettings() { + return compilerOptions; + }, + getLocalizedDiagnosticMessages: tsLocalized ? () => tsLocalized : undefined, + }, + uriConverter + ); + } + updateRootFiles(uri, languageService); + return languageService; + }, + async getExistingLanguageServices() { + return languageService ? [languageService] : []; + }, + reload() { + languageService?.dispose(); + languageService = undefined; + }, + }; + + function updateRootFiles(uri: URI, languageService: LanguageService) { + const document = server.documents.get(uri); + if (!document) { + return; + } + const newProjectVersion = document.uri.toString() + '::' + document.version; + if (newProjectVersion === projectVersion) { + return; + } + projectVersion = newProjectVersion; + currentDirectory = getRootFolder(uri) ?? ''; + + currentRootFiles.length = 0; + currentRootFiles.push(JQUERY_PATH); + currentRootFiles.push(uriConverter.asFileName(uri)); + + const sourceScript = languageService.context.language.scripts.get(uri); + if (sourceScript?.generated?.root instanceof HTMLVirtualCode) { + const regions = sourceScript.generated.root.documentRegions; + for (const script of regions.getImportedScripts()) { + if (script.startsWith('http://') || script.startsWith('https://') || script.startsWith('//')) { + continue; + } + else if (script.startsWith('file://')) { + const scriptUri = URI.parse(script); + currentRootFiles.push(uriConverter.asFileName(scriptUri)); + } + else { + const scriptUri = Utils.resolvePath(Utils.dirname(uri), script); + currentRootFiles.push(uriConverter.asFileName(scriptUri)); + } + } + } + } + + function getRootFolder(uri: URI) { + for (const folder of server.workspaceFolders.all) { + let folderURI = folder.toString(); + if (!folderURI.endsWith('/')) { + folderURI = folderURI + '/'; + } + if (uri.toString().startsWith(folderURI)) { + return folderURI; + } + } + return undefined; + } +} 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/modes/virtualCode.ts b/extensions/html-language-features/server/src/modes/virtualCode.ts new file mode 100644 index 0000000000000..3a469b833c548 --- /dev/null +++ b/extensions/html-language-features/server/src/modes/virtualCode.ts @@ -0,0 +1,161 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { CodeMapping, VirtualCode } from '@volar/language-server'; +import type * as ts from 'typescript'; +import { getLanguageService, LanguageService } from 'vscode-html-languageservice'; +import { EmbeddedRegion, getDocumentRegions, HTMLDocumentRegions } from './embeddedSupport'; + +let htmlLanguageService: LanguageService | undefined; + +export class HTMLVirtualCode implements VirtualCode { + documentRegions: HTMLDocumentRegions; + id = 'root'; + languageId = 'html'; + mappings: CodeMapping[]; + embeddedCodes: VirtualCode[]; + + constructor(public snapshot: ts.IScriptSnapshot) { + this.documentRegions = getDocumentRegions( + htmlLanguageService ??= getLanguageService(), + snapshot.getText(0, snapshot.getLength()) + ); + this.mappings = [{ + sourceOffsets: [0], + generatedOffsets: [0], + lengths: [snapshot.getLength()], + data: { verification: true, completion: true, semantic: true, navigation: true, structure: true, format: true }, + }]; + this.embeddedCodes = [ + ...getGlobalScriptVirtualCodes(this.documentRegions), + ...getOtherLanguageVirtualCodes(this.documentRegions), + ]; + } +} + +function* getGlobalScriptVirtualCodes(documentRegions: HTMLDocumentRegions): Generator { + const globalScripts = documentRegions + .getEmbeddedRegions() + .filter(isGlobalScript); + let scriptIndex = 0; + + if (globalScripts.length === 1) { + const globalScript = globalScripts[0]; + yield { + languageId: 'javascript', + id: 'global_script', + snapshot: { + getText(start, end) { + return globalScript.content.substring(start, end); + }, + getLength() { + return globalScript.content.length; + }, + getChangeRange() { + return undefined; + }, + }, + mappings: [{ + sourceOffsets: [globalScript.start], + generatedOffsets: [globalScript.generatedStart], + lengths: [globalScript.length], + data: { verification: true, completion: true, semantic: true, navigation: true, structure: true, format: true }, + }], + }; + } + else if (globalScripts.length >= 2) { + let text = ''; + const mappings: CodeMapping[] = []; + for (let i = 0; i < globalScripts.length; i++) { + const globalScript = globalScripts[i]; + mappings.push({ + sourceOffsets: [globalScript.start], + generatedOffsets: [text.length + globalScript.generatedStart], + lengths: [globalScript.length], + data: { verification: true, completion: true, semantic: true, navigation: true }, + }); + text += globalScript.content; + if (i < globalScripts.length - 1) { + text += '\n;\n'; + } + const index = scriptIndex++; + yield { + languageId: globalScript.languageId!, + id: 'global_script_' + index + '_syntax', + snapshot: { + getText(start, end) { + return globalScript.content.substring(start, end); + }, + getLength() { + return globalScript.content.length; + }, + getChangeRange() { + return undefined; + }, + }, + mappings: [{ + sourceOffsets: [globalScript.start], + generatedOffsets: [globalScript.generatedStart], + lengths: [globalScript.length], + data: { structure: true, format: true }, + }], + }; + } + yield { + languageId: 'javascript', + id: 'global_script', + snapshot: { + getText(start, end) { + return text.substring(start, end); + }, + getLength() { + return text.length; + }, + getChangeRange() { + return undefined; + }, + }, + mappings, + }; + } +} + +function* getOtherLanguageVirtualCodes(documentRegions: HTMLDocumentRegions): Generator { + const indexMap: Record = {}; + for (const documentRegion of documentRegions.getEmbeddedRegions()) { + if (!documentRegion.languageId || isGlobalScript(documentRegion)) { + continue; + } + indexMap[documentRegion.tagName] ??= 0; + const index = indexMap[documentRegion.tagName]++; + yield { + languageId: documentRegion.languageId, + id: documentRegion.tagName + '_' + index, + 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: true, completion: true, semantic: true, navigation: true, structure: true } + : { verification: true, completion: true, semantic: true, navigation: true, structure: true, format: true }, + }], + }; + } +} + +function isGlobalScript(region: EmbeddedRegion) { + return region.languageId === 'javascript' && !region.moduleScript; +} diff --git a/extensions/html-language-features/server/src/node/htmlServerMain.ts b/extensions/html-language-features/server/src/node/htmlServerMain.ts index 0367e11a2209f..3d19c338e078a 100644 --- a/extensions/html-language-features/server/src/node/htmlServerMain.ts +++ b/extensions/html-language-features/server/src/node/htmlServerMain.ts @@ -3,34 +3,28 @@ * 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 { provider as nodeFsProvider } from '@volar/language-server/lib/fileSystemProviders/node'; +import { createServerBase } from '@volar/language-server/lib/server'; +import { createConnection } from '@volar/language-server/node'; +import { startServer } from '../htmlServer'; +import { getFileSystemProvider } from '../requests'; +const connection = createConnection(); +const workspaceFsProvider = getFileSystemProvider(connection); +const server = createServerBase(connection); +const installedFs = new Set(); -// 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)); -}); - -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) }; +server.onInitialize(() => { + if (server.initializeParams.initializationOptions?.handledSchemas?.indexOf('file') !== -1) { + server.fileSystem.install('file', nodeFsProvider); + installedFs.add('file'); + } + for (const folder of server.workspaceFolders.all) { + if (!installedFs.has(folder.scheme)) { + installedFs.add(folder.scheme); + server.fileSystem.install(folder.scheme, workspaceFsProvider); } - }, - fileFs: getNodeFileFS() -}; + } +}); -startServer(connection, runtime); +startServer(server, connection); 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 index 725f6f3b13579..e2dfcdb27a450 100644 --- a/extensions/html-language-features/server/src/requests.ts +++ b/extensions/html-language-features/server/src/requests.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { RequestType, Connection } from 'vscode-languageserver'; -import { RuntimeEnvironment } from './htmlServer'; +import { FileStat, FileSystem, FileType } from '@volar/language-service'; export namespace FsStatRequest { export const type: RequestType = new RequestType('fs/stat'); @@ -14,65 +14,17 @@ 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; +export function getFileSystemProvider(connection: Connection): FileSystem { return { - async stat(uri: string): Promise { - if (fileFs && uri.startsWith('file:')) { - return fileFs.stat(uri); - } + async 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); - } + readDirectory(uri) { return connection.sendRequest(FsReadDirRequest.type, uri.toString()); - } + }, + readFile() { + return undefined; + }, }; } diff --git a/extensions/html-language-features/server/src/test/completions.test.ts b/extensions/html-language-features/server/src/test/completions.test.ts index fbad266e2dea2..67f1fd227d0f2 100644 --- a/extensions/html-language-features/server/src/test/completions.test.ts +++ b/extensions/html-language-features/server/src/test/completions.test.ts @@ -2,13 +2,13 @@ * 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 { CompletionItemKind, CompletionList, TextDocument, TextEdit } from '@volar/language-server'; import * as assert from 'assert'; +import 'mocha'; 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'; +import { getTestService } from './shared'; + export interface ItemDescription { label: string; documentation?: string; @@ -44,25 +44,18 @@ export function assertCompletion(completions: CompletionList, expected: ItemDesc } } -const testUri = 'test://test/test.html'; - -export async function testCompletionFor(value: string, expected: { count?: number; items?: ItemDescription[] }, uri = testUri, workspaceFolders?: WorkspaceFolder[]): Promise { +export async function testCompletionFor(value: string, expected: { count?: number; items?: ItemDescription[] }, uri?: string, workspaceFolders?: string[]): 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 { document, languageService } = await getTestService({ + uri, + workspaceFolders, + content: 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); + const list = await languageService.getCompletionItems(URI.parse(document.uri), position); + assert(!!list); if (expected.count) { assert.strictEqual(list.items.length, expected.count); @@ -101,7 +94,7 @@ suite('HTML Path Completion', () => { }; const fixtureRoot = path.resolve(__dirname, '../../src/test/pathCompletionFixtures'); - const fixtureWorkspace = { name: 'fixture', uri: URI.file(fixtureRoot).toString() }; + const fixtureWorkspace = 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(); diff --git a/extensions/html-language-features/server/src/test/documentContext.test.ts b/extensions/html-language-features/server/src/test/documentContext.test.ts index 2bddeda0eb307..c0014b9721daf 100644 --- a/extensions/html-language-features/server/src/test/documentContext.test.ts +++ b/extensions/html-language-features/server/src/test/documentContext.test.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { getDocumentContext } from '../utils/documentContext'; +import { resolveReference } from 'volar-service-html'; +import { URI } from 'vscode-uri'; suite('HTML Document Context', () => { test('Context', function (): any { - const docURI = 'file:///users/test/folder/test.html'; - const rootFolders = [{ name: '', uri: 'file:///users/test/' }]; + const docURI = URI.parse('file:///users/test/folder/test.html'); + const rootFolders = [URI.parse('file:///users/test/')]; - const context = getDocumentContext(docURI, rootFolders); - assert.strictEqual(context.resolveReference('/', docURI), 'file:///users/test/'); - assert.strictEqual(context.resolveReference('/message.html', docURI), 'file:///users/test/message.html'); - assert.strictEqual(context.resolveReference('message.html', docURI), 'file:///users/test/folder/message.html'); - assert.strictEqual(context.resolveReference('message.html', 'file:///users/test/'), 'file:///users/test/message.html'); + assert.strictEqual(resolveReference('/', docURI, rootFolders), 'file:///users/test/'); + assert.strictEqual(resolveReference('/message.html', docURI, rootFolders), 'file:///users/test/message.html'); + assert.strictEqual(resolveReference('message.html', docURI, rootFolders), 'file:///users/test/folder/message.html'); + assert.strictEqual(resolveReference('message.html', URI.parse('file:///users/test/'), rootFolders), 'file:///users/test/message.html'); }); }); diff --git a/extensions/html-language-features/server/src/test/embedded.test.ts b/extensions/html-language-features/server/src/test/embedded.test.ts index 87698f3971882..5e8990a66c12a 100644 --- a/extensions/html-language-features/server/src/test/embedded.test.ts +++ b/extensions/html-language-features/server/src/test/embedded.test.ts @@ -2,11 +2,13 @@ * 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 { forEachEmbeddedCode, defaultMapperFactory, VirtualCode } from '@volar/language-core'; import * as assert from 'assert'; -import * as embeddedSupport from '../modes/embeddedSupport'; +import 'mocha'; import { getLanguageService } from 'vscode-html-languageservice'; -import { TextDocument } from '../modes/languageModes'; +import { URI } from 'vscode-uri'; +import * as embeddedSupport from '../modes/embeddedSupport'; +import { htmlLanguagePlugin } from '../modes/languagePlugin'; suite('HTML Embedded Support', () => { @@ -16,22 +18,37 @@ suite('HTML Embedded Support', () => { const offset = value.indexOf('|'); value = value.substr(0, offset) + value.substr(offset + 1); - const document = TextDocument.create('test://test/test.html', 'html', 0, value); - - const position = document.positionAt(offset); - - const docRegions = embeddedSupport.getDocumentRegions(htmlLanguageService, document); - const languageId = docRegions.getLanguageAtPosition(position); - - assert.strictEqual(languageId, expectedLanguageId); + const virtualCode = htmlLanguagePlugin.createVirtualCode?.(URI.file(''), 'html', { + getText: (start, end) => value.substring(start, end), + getLength: () => value.length, + getChangeRange: () => undefined, + }, { getAssociatedScript: () => undefined }); + assert(!!virtualCode); + + let mappedCode: VirtualCode | undefined; + + for (const embeddedCode of [...forEachEmbeddedCode(virtualCode)].reverse()) { + const map = defaultMapperFactory(embeddedCode.mappings); + for (const _mapped of map.toGeneratedLocation(offset)) { + mappedCode = embeddedCode; + break; + } + if (mappedCode) { + break; + } + } + + assert(!!mappedCode); + assert.strictEqual(mappedCode.languageId, expectedLanguageId); } - function assertEmbeddedLanguageContent(value: string, languageId: string, expectedContent: string): void { - const document = TextDocument.create('test://test/test.html', 'html', 0, value); - - const docRegions = embeddedSupport.getDocumentRegions(htmlLanguageService, document); - const content = docRegions.getEmbeddedDocument(languageId); - assert.strictEqual(content.getText(), expectedContent); + function assertEmbeddedLanguageContent(value: string, languageId: string, expectedContents: string[]): void { + const docRegions = embeddedSupport.getDocumentRegions(htmlLanguageService, value); + const contents = docRegions.getEmbeddedRegions().filter(r => r.languageId === languageId); + assert.strictEqual(contents.length, expectedContents.length); + for (let i = 0; i < contents.length; i++) { + assert.strictEqual(contents[i].content, expectedContents[i]); + } } test('Styles', function (): any { @@ -67,13 +84,13 @@ suite('HTML Embedded Support', () => { }); test('Style content', function (): any { - assertEmbeddedLanguageContent('', 'css', ' foo { } '); - assertEmbeddedLanguageContent('', 'css', ' '); - assertEmbeddedLanguageContent('Hello', 'css', ' foo { } foo { } '); - assertEmbeddedLanguageContent('\n \n\n', 'css', '\n \n foo { } \n \n\n'); + assertEmbeddedLanguageContent('', 'css', ['foo { }']); + assertEmbeddedLanguageContent('', 'css', []); + assertEmbeddedLanguageContent('Hello', 'css', ['foo { }', 'foo { }']); + assertEmbeddedLanguageContent('\n \n\n', 'css', ['\n foo { } \n ']); - assertEmbeddedLanguageContent('
', 'css', ' __{color: red} '); - assertEmbeddedLanguageContent('
', 'css', ' __{color:red} '); + assertEmbeddedLanguageContent('
', 'css', ['__{color: red}']); + assertEmbeddedLanguageContent('
', 'css', ['__{color:red}']); }); test('Scripts', function (): any { @@ -89,7 +106,7 @@ suite('HTML Embedded Support', () => { assertLanguageId('', 'javascript'); assertLanguageId('', 'javascript'); assertLanguageId('', 'javascript'); - assertLanguageId('', undefined); + assertLanguageId('', 'html'); assertLanguageId('', 'javascript'); }); @@ -117,15 +134,14 @@ suite('HTML Embedded Support', () => { }); test('Script content', function (): any { - assertEmbeddedLanguageContent('', 'javascript', ' var i = 0; '); - assertEmbeddedLanguageContent('', 'javascript', ' var i = 0; '); - assertEmbeddedLanguageContent('', 'javascript', ' /* this comment should not give error */ '); - assertEmbeddedLanguageContent('', 'javascript', ' /* this comment should not give error */ console.log("logging"); '); - - assertEmbeddedLanguageContent('', 'javascript', ' var data=100; /* this comment should not give error */ '); - assertEmbeddedLanguageContent('
', 'javascript', ' foo(); bar(); '); - assertEmbeddedLanguageContent('
', 'javascript', ' return; '); - assertEmbeddedLanguageContent('
', 'javascript', ' return;\n foo(); '); + assertEmbeddedLanguageContent('', 'javascript', ['var i = 0;']); + assertEmbeddedLanguageContent('', 'javascript', ['var i = 0;']); + assertEmbeddedLanguageContent('', 'javascript', ['/* this comment should not give error */']); + assertEmbeddedLanguageContent('', 'javascript', ['/* this comment should not give error */ console.log("logging");']); + + assertEmbeddedLanguageContent('', 'javascript', ['var data=100; /* this comment should not give error */ ']); + assertEmbeddedLanguageContent('
', 'javascript', ['foo();', 'bar();']); + assertEmbeddedLanguageContent('
', 'javascript', ['return;']); + assertEmbeddedLanguageContent('
', 'javascript', ['return;', 'foo();']); }); - }); diff --git a/extensions/html-language-features/server/src/test/folding.test.ts b/extensions/html-language-features/server/src/test/folding.test.ts index ec33f7a5198f0..e70ca04e58ba5 100644 --- a/extensions/html-language-features/server/src/test/folding.test.ts +++ b/extensions/html-language-features/server/src/test/folding.test.ts @@ -3,12 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'mocha'; import * as assert from 'assert'; -import { getFoldingRanges } from '../modes/htmlFolding'; -import { TextDocument, getLanguageModes } from '../modes/languageModes'; -import { ClientCapabilities } from 'vscode-css-languageservice'; -import { getNodeFileFS } from '../node/nodeFs'; +import 'mocha'; +import { URI } from 'vscode-uri'; +import { getTestService } from './shared'; interface ExpectedIndentRange { startLine: number; @@ -17,13 +15,18 @@ interface ExpectedIndentRange { } async function assertRanges(lines: string[], expected: ExpectedIndentRange[], message?: string, nRanges?: number): Promise { - const document = TextDocument.create('test://foo/bar.html', 'html', 1, lines.join('\n')); - const workspace = { - settings: {}, - folders: [{ name: 'foo', uri: 'test://foo' }] - }; - const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFileFS()); - const actual = await getFoldingRanges(languageModes, document, nRanges, null); + const { languageService, document } = await getTestService({ + content: lines.join('\n'), + clientCapabilities: nRanges ? { + textDocument: { + foldingRange: { + rangeLimit: nRanges, + }, + }, + } : undefined, + }); + const actual = await languageService.getFoldingRanges(URI.parse(document.uri)); + assert(!!actual); let actualRanges = []; for (let i = 0; i < actual.length; i++) { @@ -50,7 +53,7 @@ suite('HTML Folding', () => { /*6*/'', /*7*/'', ]; - await await assertRanges(input, [r(0, 6), r(1, 5), r(2, 4), r(3, 4)]); + await assertRanges(input, [r(0, 6), r(1, 5), r(2, 4), r(3, 3)]); }); test('Embedded JavaScript - multiple areas', async () => { @@ -71,7 +74,7 @@ suite('HTML Folding', () => { /*13*/'', /*14*/'', ]; - await assertRanges(input, [r(0, 13), r(1, 12), r(2, 6), r(3, 6), r(8, 11), r(9, 11), r(9, 11)]); + await assertRanges(input, [r(0, 13), r(1, 12), r(2, 6), r(3, 5), r(5, 5), r(8, 11), r(9, 10), r(9, 10)]); }); test('Embedded JavaScript - incomplete', async () => { @@ -161,21 +164,20 @@ suite('HTML Folding', () => { await assertRanges(input, [r(0, 9), r(1, 8), r(2, 7), r(3, 7, 'region'), r(4, 6, 'region')]); }); - - // test('Embedded JavaScript - multi line comment', async () => { - // const input = [ - // /* 0*/'', - // /* 1*/'', - // /* 2*/'', - // /* 7*/'', - // /* 8*/'', - // ]; - // await assertRanges(input, [r(0, 7), r(1, 6), r(2, 5), r(3, 5, 'comment')]); - // }); + test('Embedded JavaScript - multi line comment', async () => { + const input = [ + /* 0*/'', + /* 1*/'', + /* 2*/'', + /* 7*/'', + /* 8*/'', + ]; + await assertRanges(input, [r(0, 7), r(1, 6), r(2, 5), r(3, 5, 'comment')]); + }); test('Test limit', async () => { const input = [ diff --git a/extensions/html-language-features/server/src/test/formatting.test.ts b/extensions/html-language-features/server/src/test/formatting.test.ts index adf9b21e17715..44042f8e1cb4b 100644 --- a/extensions/html-language-features/server/src/test/formatting.test.ts +++ b/extensions/html-language-features/server/src/test/formatting.test.ts @@ -2,24 +2,17 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { FormattingOptions, Range, TextDocument } from '@volar/language-server'; +import * as assert from 'assert'; +import * as fs from 'fs'; import 'mocha'; import * as path from 'path'; -import * as fs from 'fs'; - -import * as assert from 'assert'; -import { getLanguageModes, TextDocument, Range, FormattingOptions, ClientCapabilities } from '../modes/languageModes'; - -import { format } from '../modes/formatting'; -import { getNodeFileFS } from '../node/nodeFs'; +import { URI } from 'vscode-uri'; +import { getTestService } from './shared'; suite('HTML Embedded Formatting', () => { - async function assertFormat(value: string, expected: string, options?: any, formatOptions?: FormattingOptions, message?: string): Promise { - const workspace = { - settings: options, - folders: [{ name: 'foo', uri: 'test://foo' }] - }; - const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFileFS()); + async function assertFormat(value: string, expected: string, _options?: any, formatOptions?: FormattingOptions, message?: string): Promise { let rangeStartOffset = value.indexOf('|'); let rangeEndOffset; @@ -32,15 +25,14 @@ suite('HTML Embedded Formatting', () => { rangeStartOffset = 0; rangeEndOffset = value.length; } - const document = TextDocument.create('test://test/test.html', 'html', 0, value); + const { languageService, document } = await getTestService({ content: value }); const range = Range.create(document.positionAt(rangeStartOffset), document.positionAt(rangeEndOffset)); if (!formatOptions) { formatOptions = FormattingOptions.create(2, true); } - const result = await format(languageModes, document, range, formatOptions, undefined, { css: true, javascript: true }); - - const actual = TextDocument.applyEdits(document, result); + const result = await languageService.getDocumentFormattingEdits(URI.parse(document.uri), formatOptions, range, undefined); + const actual = TextDocument.applyEdits(document, result ?? []); assert.strictEqual(actual, expected, message); } @@ -88,15 +80,15 @@ suite('HTML Embedded Formatting', () => { const options: FormattingOptions = FormattingOptions.create(2, true); options.insertFinalNewline = true; - await assertFormat('

Hello

', '\n\n\n

Hello

\n\n\n\n', {}, options); - await assertFormat('|

Hello

|', '\n

Hello

\n', {}, options); - await assertFormat('|

Hello

|', '\n

Hello

\n\n\n\n', {}, options); - await assertFormat('', '\n\n\n \n\n\n\n', {}, options); + await assertFormat('

Hello

', '\n\n\n

Hello

\n\n\n\n', undefined, options); + await assertFormat('|

Hello

|', '\n

Hello

\n', undefined, options); + await assertFormat('|

Hello

|', '\n

Hello

\n\n\n\n', undefined, options); + await assertFormat('', '\n\n\n \n\n\n\n', undefined, options); }); test('Inside script', async () => { - await assertFormat('\n ', '\n '); - await assertFormat('\n ', '\n '); + await assertFormat('\n ', '\n '); + await assertFormat('\n ', '\n '); }); test('Range after new line', async () => { diff --git a/extensions/html-language-features/server/src/test/rename.test.ts b/extensions/html-language-features/server/src/test/rename.test.ts index 5c3d557224c2c..c71a722b140de 100644 --- a/extensions/html-language-features/server/src/test/rename.test.ts +++ b/extensions/html-language-features/server/src/test/rename.test.ts @@ -3,66 +3,45 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { TextDocument } from '@volar/language-server'; import * as assert from 'assert'; -import { WorkspaceEdit, TextDocument, getLanguageModes, ClientCapabilities } from '../modes/languageModes'; -import { getNodeFileFS } from '../node/nodeFs'; - +import { URI } from 'vscode-uri'; +import { getTestService } from './shared'; async function testRename(value: string, newName: string, expectedDocContent: string): Promise { const offset = value.indexOf('|'); value = value.substr(0, offset) + value.substr(offset + 1); - const document = TextDocument.create('test://test/test.html', 'html', 0, value); - const workspace = { - settings: {}, - folders: [{ name: 'foo', uri: 'test://foo' }] - }; - const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFileFS()); - const javascriptMode = languageModes.getMode('javascript'); + const { languageService, document } = await getTestService({ content: value }); const position = document.positionAt(offset); + const workspaceEdit = await languageService.getRenameEdits(URI.parse(document.uri), position, newName); - if (javascriptMode) { - const workspaceEdit: WorkspaceEdit | null = await javascriptMode.doRename!(document, position, newName); - - if (!workspaceEdit || !workspaceEdit.changes) { - assert.fail('No workspace edits'); - } - - const edits = workspaceEdit.changes[document.uri.toString()]; - if (!edits) { - assert.fail(`No edits for file at ${document.uri.toString()}`); - } + if (!workspaceEdit || !workspaceEdit.changes) { + assert.fail('No workspace edits'); + } - const newDocContent = TextDocument.applyEdits(document, edits); - assert.strictEqual(newDocContent, expectedDocContent, `Expected: ${expectedDocContent}\nActual: ${newDocContent}`); - } else { - assert.fail('should have javascriptMode but no'); + const edits = workspaceEdit.changes[document.uri.toString()]; + if (!edits) { + assert.fail(`No edits for file at ${document.uri.toString()}`); } + + const newDocContent = TextDocument.applyEdits(document, edits); + assert.strictEqual(newDocContent, expectedDocContent, `Expected: ${expectedDocContent}\nActual: ${newDocContent}`); } async function testNoRename(value: string, newName: string): Promise { const offset = value.indexOf('|'); value = value.substr(0, offset) + value.substr(offset + 1); - const document = TextDocument.create('test://test/test.html', 'html', 0, value); - const workspace = { - settings: {}, - folders: [{ name: 'foo', uri: 'test://foo' }] - }; - const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFileFS()); - const javascriptMode = languageModes.getMode('javascript'); + const { languageService, document } = await getTestService({ content: value }); const position = document.positionAt(offset); + const workspaceEdit = await languageService.getRenameEdits(URI.parse(document.uri), position, newName); - if (javascriptMode) { - const workspaceEdit: WorkspaceEdit | null = await javascriptMode.doRename!(document, position, newName); - - assert.ok(workspaceEdit?.changes === undefined, 'Should not rename but rename happened'); - } else { - assert.fail('should have javascriptMode but no'); - } + assert.ok(workspaceEdit?.changes === undefined, 'Should not rename but rename happened'); } suite('HTML Javascript Rename', () => { + test('Rename Variable', async () => { const input = [ '', diff --git a/extensions/html-language-features/server/src/test/selectionRanges.test.ts b/extensions/html-language-features/server/src/test/selectionRanges.test.ts index 28e32dc29032f..7a7f785c27368 100644 --- a/extensions/html-language-features/server/src/test/selectionRanges.test.ts +++ b/extensions/html-language-features/server/src/test/selectionRanges.test.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'mocha'; +import type { SelectionRange } from '@volar/language-server'; import * as assert from 'assert'; -import { getLanguageModes, ClientCapabilities, TextDocument, SelectionRange } from '../modes/languageModes'; -import { getSelectionRanges } from '../modes/selectionRanges'; -import { getNodeFileFS } from '../node/nodeFs'; +import 'mocha'; +import { URI } from 'vscode-uri'; +import { getTestService } from './shared'; async function assertRanges(content: string, expected: (number | string)[][]): Promise { let message = `${content} gives selection range:\n`; @@ -15,14 +15,10 @@ async function assertRanges(content: string, expected: (number | string)[][]): P const offset = content.indexOf('|'); content = content.substr(0, offset) + content.substr(offset + 1); - const workspace = { - settings: {}, - folders: [{ name: 'foo', uri: 'test://foo' }] - }; - const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFileFS()); + const { document, languageService } = await getTestService({ content }); + const actualRanges = await languageService.getSelectionRanges(URI.parse(document.uri), [document.positionAt(offset)]); + assert(!!actualRanges); - const document = TextDocument.create('test://foo.html', 'html', 1, content); - const actualRanges = await getSelectionRanges(languageModes, document, [document.positionAt(offset)]); assert.strictEqual(actualRanges.length, 1); const offsetPairs: [number, string][] = []; let curr: SelectionRange | undefined = actualRanges[0]; @@ -36,6 +32,7 @@ async function assertRanges(content: string, expected: (number | string)[][]): P } suite('HTML SelectionRange', () => { + test('Embedded JavaScript', async () => { await assertRanges('', [ [48, '1'], @@ -76,6 +73,4 @@ suite('HTML SelectionRange', () => { [0, '
'] ]); }); - - }); diff --git a/extensions/html-language-features/server/src/test/semanticTokens.test.ts b/extensions/html-language-features/server/src/test/semanticTokens.test.ts index 4f479d9163d44..a2a79d1248ac3 100644 --- a/extensions/html-language-features/server/src/test/semanticTokens.test.ts +++ b/extensions/html-language-features/server/src/test/semanticTokens.test.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'mocha'; +import { Position, Range, SemanticTokensLegend } from '@volar/language-server'; import * as assert from 'assert'; -import { TextDocument, getLanguageModes, ClientCapabilities, Range, Position } from '../modes/languageModes'; -import { newSemanticTokenProvider } from '../modes/semanticTokens'; -import { getNodeFileFS } from '../node/nodeFs'; +import 'mocha'; +import { URI } from 'vscode-uri'; +import { getTestService, languageServicePlugins } from './shared'; interface ExpectedToken { startLine: number; @@ -16,26 +16,24 @@ interface ExpectedToken { tokenClassifiction: string; } -async function assertTokens(lines: string[], expected: ExpectedToken[], ranges?: Range[], message?: string): Promise { - const document = TextDocument.create('test://foo/bar.html', 'html', 1, lines.join('\n')); - const workspace = { - settings: {}, - folders: [{ name: 'foo', uri: 'test://foo' }] - }; - const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFileFS()); - const semanticTokensProvider = newSemanticTokenProvider(languageModes); +const legend: SemanticTokensLegend = { + tokenTypes: languageServicePlugins.map(plugin => plugin.capabilities.semanticTokensProvider?.legend.tokenTypes ?? []).flat(), + tokenModifiers: languageServicePlugins.map(plugin => plugin.capabilities.semanticTokensProvider?.legend.tokenModifiers ?? []).flat(), +}; - const legend = semanticTokensProvider.legend; - const actual = await semanticTokensProvider.getSemanticTokens(document, ranges); +async function assertTokens(lines: string[], expected: ExpectedToken[], range?: Range, message?: string): Promise { + const { document, languageService } = await getTestService({ content: lines.join('\n') }); + const actual = await languageService.getSemanticTokens(URI.parse(document.uri), range, legend); + assert(!!actual); const actualRanges = []; let lastLine = 0; let lastCharacter = 0; - for (let i = 0; i < actual.length; i += 5) { - const lineDelta = actual[i], charDelta = actual[i + 1], len = actual[i + 2], typeIdx = actual[i + 3], modSet = actual[i + 4]; + for (let i = 0; i < actual.data.length; i += 5) { + const lineDelta = actual.data[i], charDelta = actual.data[i + 1], len = actual.data[i + 2], typeIdx = actual.data[i + 3], modSet = actual.data[i + 4]; const line = lastLine + lineDelta; const character = lineDelta === 0 ? lastCharacter + charDelta : charDelta; - const tokenClassifiction = [legend.types[typeIdx], ...legend.modifiers.filter((_, i) => modSet & 1 << i)].join('.'); + const tokenClassifiction = [legend.tokenTypes[typeIdx], ...legend.tokenModifiers.filter((_, i) => modSet & 1 << i)].join('.'); actualRanges.push(t(line, character, len, tokenClassifiction)); lastLine = line; lastCharacter = character; @@ -155,9 +153,9 @@ suite('HTML Semantic Tokens', () => { ]; await assertTokens(input, [ t(3, 8, 1, 'variable.declaration.readonly'), - t(4, 8, 1, 'class.declaration'), t(4, 28, 1, 'property.declaration.static.readonly'), t(4, 42, 3, 'property.declaration.static'), t(4, 47, 3, 'interface.defaultLibrary'), + t(4, 8, 1, 'class.declaration'), t(4, 28, 1, 'property.declaration.readonly.static'), t(4, 42, 3, 'property.declaration.static'), t(4, 47, 3, 'interface.defaultLibrary'), t(5, 13, 1, 'enum.declaration'), t(5, 17, 1, 'enumMember.declaration.readonly'), t(5, 24, 1, 'enumMember.declaration.readonly'), t(5, 28, 1, 'enumMember.readonly'), - t(6, 2, 7, 'variable.defaultLibrary'), t(6, 10, 3, 'method.defaultLibrary'), t(6, 14, 1, 'variable.readonly'), t(6, 18, 1, 'class'), t(6, 20, 1, 'property.static.readonly'), t(6, 24, 1, 'class'), t(6, 26, 3, 'property.static'), t(6, 30, 6, 'property.readonly.defaultLibrary'), + t(6, 2, 7, 'variable.defaultLibrary'), t(6, 10, 3, 'method.defaultLibrary'), t(6, 14, 1, 'variable.readonly'), t(6, 18, 1, 'class'), t(6, 20, 1, 'property.readonly.static'), t(6, 24, 1, 'class'), t(6, 26, 3, 'property.static'), t(6, 30, 6, 'property.readonly.defaultLibrary'), ]); }); @@ -216,13 +214,10 @@ suite('HTML Semantic Tokens', () => { ]; await assertTokens(input, [ t(3, 2, 6, 'variable.defaultLibrary'), t(3, 9, 5, 'method.defaultLibrary') - ], [Range.create(Position.create(2, 0), Position.create(4, 0))]); + ], Range.create(Position.create(2, 0), Position.create(4, 0))); await assertTokens(input, [ t(6, 2, 6, 'variable.defaultLibrary'), - ], [Range.create(Position.create(6, 2), Position.create(6, 8))]); + ], Range.create(Position.create(6, 2), Position.create(6, 8))); }); - - }); - diff --git a/extensions/html-language-features/server/src/test/shared.ts b/extensions/html-language-features/server/src/test/shared.ts new file mode 100644 index 0000000000000..4d81545432529 --- /dev/null +++ b/extensions/html-language-features/server/src/test/shared.ts @@ -0,0 +1,152 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ClientCapabilities, LanguageServiceEnvironment, ProjectContext, TextDocument } from '@volar/language-server'; +import { createUriConverter } from '@volar/language-server/node'; +import { provider as nodeFsProvider } from '@volar/language-server/lib/fileSystemProviders/node'; +import { createLanguage, createLanguageService, createUriMap, LanguageService } from '@volar/language-service'; +import { createLanguageServiceHost, resolveFileLanguageId, TypeScriptProjectHost } from '@volar/typescript'; +import * as ts from 'typescript'; +import { URI } from 'vscode-uri'; +import { JQUERY_PATH } from '../modes/javascriptLibs'; +import { htmlLanguagePlugin } from '../modes/languagePlugin'; +import { compilerOptions } from '../modes/project'; +import { getLanguageServicePlugins } from '../modes/languageServicePlugins'; + +let currentDocument: [URI, string, TextDocument, ts.IScriptSnapshot]; +let languageService: LanguageService; +let uriConverter: ReturnType; + +const serviceEnv: LanguageServiceEnvironment = { + workspaceFolders: [], + fs: { + stat(uri) { + if (uri.scheme === 'file') { + return nodeFsProvider.stat(uri); + } + return undefined; + }, + readDirectory(uri) { + if (uri.scheme === 'file') { + return nodeFsProvider.readDirectory(uri); + } + return []; + }, + readFile(uri, encoding) { + if (uri.scheme === 'file') { + return nodeFsProvider.readFile(uri, encoding); + } + return ''; + } + } +}; +const libSnapshots = new Map(); +const projectHost: TypeScriptProjectHost = { + getCompilationSettings: () => compilerOptions, + getScriptFileNames: () => [currentDocument[1], JQUERY_PATH], + getCurrentDirectory: () => '', + getProjectVersion: () => currentDocument[1] + ',' + currentDocument[2].version, +}; + +export const languageServicePlugins = getLanguageServicePlugins({ + supportedLanguages: { css: true, javascript: true }, + getCustomData: () => [], + onDidChangeCustomData: () => ({ dispose() { } }), +}); + +export async function getTestService({ + uri = 'test://test/test.html', + languageId = 'html', + content, + workspaceFolders = [uri.substr(0, uri.lastIndexOf('/'))], + clientCapabilities, +}: { + uri?: string; + languageId?: string; + content: string; + workspaceFolders?: string[]; + clientCapabilities?: ClientCapabilities; +}) { + serviceEnv.workspaceFolders = workspaceFolders.map(folder => URI.parse(folder)); + serviceEnv.clientCapabilities = clientCapabilities; + uriConverter = createUriConverter(serviceEnv.workspaceFolders); + const parsedUri = URI.parse(uri); + currentDocument = [ + parsedUri, + uriConverter.asFileName(parsedUri), + TextDocument.create(uri, languageId, (currentDocument?.[2].version ?? 0) + 1, content), + ts.ScriptSnapshot.fromString(content), + ]; + if (!languageService) { + const language = createLanguage( + [ + htmlLanguagePlugin, + { + getLanguageId(uri) { + if (uri.toString() === currentDocument[0].toString()) { + return currentDocument[2].languageId; + } + const tsLanguageId = resolveFileLanguageId(uri.toString()); + if (tsLanguageId) { + return tsLanguageId; + } + return undefined; + }, + } + ], + createUriMap(), + uri => { + let snapshot: ts.IScriptSnapshot | undefined; + + const fileName = uriConverter.asFileName(uri); + if (fileName === currentDocument[1]) { + snapshot = currentDocument[3]; + } + else { + if (!libSnapshots.has(fileName)) { + const text = ts.sys.readFile(fileName); + if (text !== undefined) { + libSnapshots.set(fileName, { + getText: (start, end) => text.substring(start, end), + getLength: () => text.length, + getChangeRange: () => undefined, + }); + } + else { + libSnapshots.set(fileName, undefined); + } + } + snapshot = libSnapshots.get(fileName); + } + + if (snapshot) { + language.scripts.set(uri, snapshot); + } + else { + language.scripts.delete(uri); + } + }, + ); + const project: ProjectContext = { + typescript: { + configFileName: undefined, + sys: ts.sys, + uriConverter, + ...createLanguageServiceHost( + ts, + ts.sys, + language, + fileName => uriConverter.asUri(fileName), + projectHost + ), + }, + }; + languageService = createLanguageService(language, languageServicePlugins, serviceEnv, project); + } + return { + document: currentDocument[2], + languageService, + }; +} diff --git a/extensions/html-language-features/server/src/test/words.test.ts b/extensions/html-language-features/server/src/test/words.test.ts deleted file mode 100644 index 132589e686776..0000000000000 --- a/extensions/html-language-features/server/src/test/words.test.ts +++ /dev/null @@ -1,71 +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 assert from 'assert'; -import * as words from '../utils/strings'; -import * as fs from 'fs'; -import * as path from 'path'; - -suite('HTML Language Configuration', () => { - const config = JSON.parse((fs.readFileSync(path.join(__dirname, '../../../../html/language-configuration.json')).toString())); - - function createRegex(str: string | { pattern: string; flags: string }): RegExp { - if (typeof str === 'string') { - return new RegExp(str, 'g'); - } - return new RegExp(str.pattern, str.flags); - } - - const wordRegex = createRegex(config.wordPattern); - - function assertWord(value: string, expected: string): void { - const offset = value.indexOf('|'); - value = value.substr(0, offset) + value.substring(offset + 1); - - const actualRange = words.getWordAtText(value, offset, wordRegex); - assert(actualRange.start <= offset); - assert(actualRange.start + actualRange.length >= offset); - assert.strictEqual(value.substr(actualRange.start, actualRange.length), expected); - } - - test('Words Basic', function (): any { - assertWord('|var x1 = new F(a, b);', 'var'); - assertWord('v|ar x1 = new F(a, b);', 'var'); - assertWord('var| x1 = new F(a, b);', 'var'); - assertWord('var |x1 = new F(a, b);', 'x1'); - assertWord('var x1| = new F(a, b);', 'x1'); - assertWord('var x1 = new |F(a, b);', 'F'); - assertWord('var x1 = new F<|A>(a, b);', 'A'); - assertWord('var x1 = new F(|a, b);', 'a'); - assertWord('var x1 = new F(a, b|);', 'b'); - assertWord('var x1 = new F(a, b)|;', ''); - assertWord('var x1 = new F(a, b)|;|', ''); - assertWord('var x1 = | new F(a, b)|;|', ''); - }); - - test('Words Multiline', function (): any { - assertWord('console.log("hello");\n|var x1 = new F(a, b);', 'var'); - assertWord('console.log("hello");\n|\nvar x1 = new F(a, b);', ''); - assertWord('console.log("hello");\n\r |var x1 = new F(a, b);', 'var'); - }); - - const onEnterBeforeRules: RegExp[] = config.onEnterRules.map((r: any) => createRegex(r.beforeText)); - - function assertBeforeRule(text: string, expectedMatch: boolean): void { - for (const reg of onEnterBeforeRules) { - const start = new Date().getTime(); - assert.strictEqual(reg.test(text), expectedMatch); - const totalTime = new Date().getTime() - start; - assert.ok(totalTime < 200, `Evaluation of ${reg.source} on ${text} took ${totalTime}ms]`); - } - } - - test('OnEnter Before', function (): any { - assertBeforeRule('', false); - }); - -}); diff --git a/extensions/html-language-features/server/src/utils/arrays.ts b/extensions/html-language-features/server/src/utils/arrays.ts deleted file mode 100644 index 265e8bd4d0aa0..0000000000000 --- a/extensions/html-language-features/server/src/utils/arrays.ts +++ /dev/null @@ -1,76 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export function pushAll(to: T[], from: T[]) { - if (from) { - for (const e of from) { - to.push(e); - } - } -} - -export function contains(arr: T[], val: T) { - return arr.indexOf(val) !== -1; -} - -/** - * Like `Array#sort` but always stable. Usually runs a little slower `than Array#sort` - * so only use this when actually needing stable sort. - */ -export function mergeSort(data: T[], compare: (a: T, b: T) => number): T[] { - _divideAndMerge(data, compare); - return data; -} - -function _divideAndMerge(data: T[], compare: (a: T, b: T) => number): void { - if (data.length <= 1) { - // sorted - return; - } - const p = (data.length / 2) | 0; - const left = data.slice(0, p); - const right = data.slice(p); - - _divideAndMerge(left, compare); - _divideAndMerge(right, compare); - - let leftIdx = 0; - let rightIdx = 0; - let i = 0; - while (leftIdx < left.length && rightIdx < right.length) { - const ret = compare(left[leftIdx], right[rightIdx]); - if (ret <= 0) { - // smaller_equal -> take left to preserve order - data[i++] = left[leftIdx++]; - } else { - // greater -> take right - data[i++] = right[rightIdx++]; - } - } - while (leftIdx < left.length) { - data[i++] = left[leftIdx++]; - } - while (rightIdx < right.length) { - data[i++] = right[rightIdx++]; - } -} - -export function binarySearch(array: T[], key: T, comparator: (op1: T, op2: T) => number): number { - let low = 0, - high = array.length - 1; - - while (low <= high) { - const mid = ((low + high) / 2) | 0; - const comp = comparator(array[mid], key); - if (comp < 0) { - low = mid + 1; - } else if (comp > 0) { - high = mid - 1; - } else { - return mid; - } - } - return -(low + 1); -} diff --git a/extensions/html-language-features/server/src/utils/documentContext.ts b/extensions/html-language-features/server/src/utils/documentContext.ts deleted file mode 100644 index 9cf8ce9ea76be..0000000000000 --- a/extensions/html-language-features/server/src/utils/documentContext.ts +++ /dev/null @@ -1,43 +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 { DocumentContext } from 'vscode-css-languageservice'; -import { endsWith, startsWith } from '../utils/strings'; -import { WorkspaceFolder } from 'vscode-languageserver'; -import { URI, Utils } from 'vscode-uri'; - -export function getDocumentContext(documentUri: string, workspaceFolders: WorkspaceFolder[]): DocumentContext { - function getRootFolder(): string | undefined { - for (const folder of workspaceFolders) { - let folderURI = folder.uri; - if (!endsWith(folderURI, '/')) { - folderURI = folderURI + '/'; - } - if (startsWith(documentUri, folderURI)) { - return folderURI; - } - } - return undefined; - } - - return { - resolveReference: (ref: string, base = documentUri) => { - if (ref.match(/^\w[\w\d+.-]*:/)) { - // starts with a schema - return ref; - } - if (ref[0] === '/') { // resolve absolute path against the current workspace folder - const folderUri = getRootFolder(); - if (folderUri) { - return folderUri + ref.substr(1); - } - } - const baseUri = URI.parse(base); - const baseUriDir = baseUri.path.endsWith('/') ? baseUri : Utils.dirname(baseUri); - return Utils.resolvePath(baseUriDir, ref).toString(true); - }, - }; -} - diff --git a/extensions/html-language-features/server/src/utils/positions.ts b/extensions/html-language-features/server/src/utils/positions.ts deleted file mode 100644 index eefd7f61fd42f..0000000000000 --- a/extensions/html-language-features/server/src/utils/positions.ts +++ /dev/null @@ -1,16 +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 { Position, Range } from '../modes/languageModes'; - -export function beforeOrSame(p1: Position, p2: Position) { - return p1.line < p2.line || p1.line === p2.line && p1.character <= p2.character; -} -export function insideRangeButNotSame(r1: Range, r2: Range) { - return beforeOrSame(r1.start, r2.start) && beforeOrSame(r2.end, r1.end) && !equalRange(r1, r2); -} -export function equalRange(r1: Range, r2: Range) { - return r1.start.line === r2.start.line && r1.start.character === r2.start.character && r1.end.line === r2.end.line && r1.end.character === r2.end.character; -} diff --git a/extensions/html-language-features/server/src/utils/runner.ts b/extensions/html-language-features/server/src/utils/runner.ts deleted file mode 100644 index 6be8c9f21284c..0000000000000 --- a/extensions/html-language-features/server/src/utils/runner.ts +++ /dev/null @@ -1,47 +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 { ResponseError, CancellationToken, LSPErrorCodes } from 'vscode-languageserver'; -import { RuntimeEnvironment } from '../htmlServer'; - -export function formatError(message: string, err: any): string { - if (err instanceof Error) { - const error = err; - return `${message}: ${error.message}\n${error.stack}`; - } else if (typeof err === 'string') { - return `${message}: ${err}`; - } else if (err) { - return `${message}: ${err.toString()}`; - } - return message; -} - -export function runSafe(runtime: RuntimeEnvironment, func: () => Thenable, errorVal: T, errorMessage: string, token: CancellationToken): Thenable> { - return new Promise>((resolve) => { - runtime.timer.setImmediate(() => { - if (token.isCancellationRequested) { - resolve(cancelValue()); - return; - } - return func().then(result => { - if (token.isCancellationRequested) { - resolve(cancelValue()); - return; - } else { - resolve(result); - } - }, e => { - console.error(formatError(errorMessage, e)); - resolve(errorVal); - }); - }); - }); -} - - - -function cancelValue() { - return new ResponseError(LSPErrorCodes.RequestCancelled, 'Request cancelled'); -} diff --git a/extensions/html-language-features/server/src/utils/strings.ts b/extensions/html-language-features/server/src/utils/strings.ts deleted file mode 100644 index c79f41df8f2df..0000000000000 --- a/extensions/html-language-features/server/src/utils/strings.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. - *--------------------------------------------------------------------------------------------*/ - -export function getWordAtText(text: string, offset: number, wordDefinition: RegExp): { start: number; length: number } { - let lineStart = offset; - while (lineStart > 0 && !isNewlineCharacter(text.charCodeAt(lineStart - 1))) { - lineStart--; - } - const offsetInLine = offset - lineStart; - const lineText = text.substr(lineStart); - - // make a copy of the regex as to not keep the state - const flags = wordDefinition.ignoreCase ? 'gi' : 'g'; - wordDefinition = new RegExp(wordDefinition.source, flags); - - let match = wordDefinition.exec(lineText); - while (match && match.index + match[0].length < offsetInLine) { - match = wordDefinition.exec(lineText); - } - if (match && match.index <= offsetInLine) { - return { start: match.index + lineStart, length: match[0].length }; - } - - return { start: offset, length: 0 }; -} - -export function startsWith(haystack: string, needle: string): boolean { - if (haystack.length < needle.length) { - return false; - } - - for (let i = 0; i < needle.length; i++) { - if (haystack[i] !== needle[i]) { - return false; - } - } - - return true; -} - -export function endsWith(haystack: string, needle: string): boolean { - const diff = haystack.length - needle.length; - if (diff > 0) { - return haystack.indexOf(needle, diff) === diff; - } else if (diff === 0) { - return haystack === needle; - } else { - return false; - } -} - -export function repeat(value: string, count: number) { - let s = ''; - while (count > 0) { - if ((count & 1) === 1) { - s += value; - } - value += value; - count = count >>> 1; - } - return s; -} - -export function isWhitespaceOnly(str: string) { - return /^\s*$/.test(str); -} - -export function isEOL(content: string, offset: number) { - return isNewlineCharacter(content.charCodeAt(offset)); -} - -const CR = '\r'.charCodeAt(0); -const NL = '\n'.charCodeAt(0); -export function isNewlineCharacter(charCode: number) { - return charCode === CR || charCode === NL; -} \ No newline at end of file diff --git a/extensions/html-language-features/server/src/utils/validation.ts b/extensions/html-language-features/server/src/utils/validation.ts deleted file mode 100644 index adb13086391fa..0000000000000 --- a/extensions/html-language-features/server/src/utils/validation.ts +++ /dev/null @@ -1,108 +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 { CancellationToken, Connection, Diagnostic, Disposable, DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportKind, TextDocuments } from 'vscode-languageserver'; -import { TextDocument } from 'vscode-html-languageservice'; -import { formatError, runSafe } from './runner'; -import { RuntimeEnvironment } from '../htmlServer'; - -export type Validator = (textDocument: TextDocument) => Promise; -export type DiagnosticsSupport = { - dispose(): void; - requestRefresh(): void; -}; - -export function registerDiagnosticsPushSupport(documents: TextDocuments, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticsSupport { - - const pendingValidationRequests: { [uri: string]: Disposable } = {}; - const validationDelayMs = 500; - - const disposables: Disposable[] = []; - - // The content of a text document has changed. This event is emitted - // when the text document first opened or when its content has changed. - documents.onDidChangeContent(change => { - triggerValidation(change.document); - }, undefined, disposables); - - // a document has closed: clear all diagnostics - documents.onDidClose(event => { - cleanPendingValidation(event.document); - connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); - }, undefined, disposables); - - function cleanPendingValidation(textDocument: TextDocument): void { - const request = pendingValidationRequests[textDocument.uri]; - if (request) { - request.dispose(); - delete pendingValidationRequests[textDocument.uri]; - } - } - - function triggerValidation(textDocument: TextDocument): void { - cleanPendingValidation(textDocument); - const request = pendingValidationRequests[textDocument.uri] = runtime.timer.setTimeout(async () => { - if (request === pendingValidationRequests[textDocument.uri]) { - try { - const diagnostics = await validate(textDocument); - if (request === pendingValidationRequests[textDocument.uri]) { - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); - } - delete pendingValidationRequests[textDocument.uri]; - } catch (e) { - connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); - } - } - }, validationDelayMs); - } - - return { - requestRefresh: () => { - documents.all().forEach(triggerValidation); - }, - dispose: () => { - disposables.forEach(d => d.dispose()); - disposables.length = 0; - const keys = Object.keys(pendingValidationRequests); - for (const key of keys) { - pendingValidationRequests[key].dispose(); - delete pendingValidationRequests[key]; - } - } - }; -} - -export function registerDiagnosticsPullSupport(documents: TextDocuments, connection: Connection, runtime: RuntimeEnvironment, validate: Validator): DiagnosticsSupport { - - function newDocumentDiagnosticReport(diagnostics: Diagnostic[]): DocumentDiagnosticReport { - return { - kind: DocumentDiagnosticReportKind.Full, - items: diagnostics - }; - } - - const registration = connection.languages.diagnostics.on(async (params: DocumentDiagnosticParams, token: CancellationToken) => { - return runSafe(runtime, async () => { - const document = documents.get(params.textDocument.uri); - if (document) { - return newDocumentDiagnosticReport(await validate(document)); - } - return newDocumentDiagnosticReport([]); - - }, newDocumentDiagnosticReport([]), `Error while computing diagnostics for ${params.textDocument.uri}`, token); - }); - - function requestRefresh(): void { - connection.languages.diagnostics.refresh(); - } - - return { - requestRefresh, - dispose: () => { - registration.dispose(); - } - }; - -} diff --git a/extensions/html-language-features/server/yarn.lock b/extensions/html-language-features/server/yarn.lock index caaf929d89547..a7b158f275cd8 100644 --- a/extensions/html-language-features/server/yarn.lock +++ b/extensions/html-language-features/server/yarn.lock @@ -14,16 +14,127 @@ dependencies: undici-types "~5.26.4" +"@volar/language-core@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.4.0.tgz#962efc66ff9198ee2412786e99528bf77cdad100" + integrity sha512-FTla+khE+sYK0qJP+6hwPAAUwiNHVMph4RUXpxf/FIPKUP61NFrVZorml4mjFShnueR2y9/j8/vnh09YwVdH7A== + dependencies: + "@volar/source-map" "2.4.0" + +"@volar/language-server@~2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@volar/language-server/-/language-server-2.4.0.tgz#717727df01e0c3da544d7171c9d5b88691b396fb" + integrity sha512-rmGIjAxWekWQiGH97Mosb4juiD/hfFYNQKV5Py9r7vDOLSkbIwRhITbwHm88NJKs8P6TNc6w/PfBXN6yjKadJg== + dependencies: + "@volar/language-core" "2.4.0" + "@volar/language-service" "2.4.0" + "@volar/typescript" "2.4.0" + path-browserify "^1.0.1" + request-light "^0.7.0" + vscode-languageserver "^9.0.1" + vscode-languageserver-protocol "^3.17.5" + vscode-languageserver-textdocument "^1.0.11" + vscode-uri "^3.0.8" + +"@volar/language-service@2.4.0", "@volar/language-service@~2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@volar/language-service/-/language-service-2.4.0.tgz#56f14e6888fe63f6beb1ed67d2dfc84877c251d0" + integrity sha512-4P3yeQXIL68mLfS3n6P3m02IRg3GnLHUU9k/1PCHEfm5FG9bySkDOc72dbBn2vAa2BxOqm18bmmZXrsWuQ5AOw== + dependencies: + "@volar/language-core" "2.4.0" + vscode-languageserver-protocol "^3.17.5" + vscode-languageserver-textdocument "^1.0.11" + vscode-uri "^3.0.8" + +"@volar/source-map@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.4.0.tgz#b6690f06c600eaf587bbc81b0153203e4f6db72a" + integrity sha512-2ceY8/NEZvN6F44TXw2qRP6AQsvCYhV2bxaBPWxV9HqIfkbRydSksTFObCF1DBDNBfKiZTS8G/4vqV6cvjdOIQ== + +"@volar/typescript@2.4.0", "@volar/typescript@~2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.4.0.tgz#f909d20dfe43dd846d30695f6e5467276ff4418e" + integrity sha512-9zx3lQWgHmVd+JRRAHUSRiEhe4TlzL7U7e6ulWXOxHH/WNYxzKwCvZD7WYWEZFdw4dHfTD9vUR0yPQO6GilCaQ== + dependencies: + "@volar/language-core" "2.4.0" + path-browserify "^1.0.1" + vscode-uri "^3.0.8" + "@vscode/l10n@^0.0.18": version "0.0.18" resolved "https://registry.yarnpkg.com/@vscode/l10n/-/l10n-0.0.18.tgz#916d3a5e960dbab47c1c56f58a7cb5087b135c95" integrity sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ== +jsonc-parser@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz#f2a524b4f7fd11e3d791e559977ad60b98b798b4" + integrity sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ== + +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + +request-light@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.7.0.tgz#885628bb2f8040c26401ebf258ec51c4ae98ac2a" + integrity sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q== + +semver@^7.3.8, semver@^7.6.2: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + +typescript-auto-import-cache@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.3.tgz#45f376313d1eb0929ce47ef1d1aae5a353d060a3" + integrity sha512-ojEC7+Ci1ij9eE6hp8Jl9VUNnsEKzztktP5gtYNRMrTmfXVwA1PITYYAkpxCvvupdSYa/Re51B6KMcv1CTZEUA== + dependencies: + semver "^7.3.8" + undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +volar-service-css@0.0.61: + version "0.0.61" + resolved "https://registry.yarnpkg.com/volar-service-css/-/volar-service-css-0.0.61.tgz#848bae953c023337795fba4b9fb78fd3a5a759c3" + integrity sha512-Ct9L/w+IB1JU8F4jofcNCGoHy6TF83aiapfZq9A0qYYpq+Kk5dH+ONS+rVZSsuhsunq8UvAuF8Gk6B8IFLfniw== + dependencies: + vscode-css-languageservice "^6.3.0" + vscode-languageserver-textdocument "^1.0.11" + vscode-uri "^3.0.8" + +volar-service-html@0.0.61: + version "0.0.61" + resolved "https://registry.yarnpkg.com/volar-service-html/-/volar-service-html-0.0.61.tgz#e57de0afcee8019aafe5ab3851cb609765a1a18a" + integrity sha512-yFE+YmmgqIL5HI4ORqP++IYb1QaGcv+xBboI0WkCxJJ/M35HZj7f5rbT3eQ24ECLXFbFCFanckwyWJVz5KmN3Q== + dependencies: + vscode-html-languageservice "^5.3.0" + vscode-languageserver-textdocument "^1.0.11" + vscode-uri "^3.0.8" + +volar-service-json@0.0.61: + version "0.0.61" + resolved "https://registry.yarnpkg.com/volar-service-json/-/volar-service-json-0.0.61.tgz#97ada31b62082185a2f495950328dc378e46f21c" + integrity sha512-9PpEib6XE99gFjjjl8IkITktSvfPW39jFAGsHggT6SdlIb8zC7J0+rMjkyVUAUOpWvY5jPqkByX43LcxZvkrdQ== + dependencies: + vscode-json-languageservice "^5.4.0" + vscode-uri "^3.0.8" + +volar-service-typescript@0.0.61: + version "0.0.61" + resolved "https://registry.yarnpkg.com/volar-service-typescript/-/volar-service-typescript-0.0.61.tgz#5c4572e34b11f57fe7087fd7242617f21eec25c5" + integrity sha512-4kRHxVbW7wFBHZWRU6yWxTgiKETBDIJNwmJUAWeP0mHaKpnDGj/astdRFKqGFRYVeEYl45lcUPhdJyrzanjsdQ== + dependencies: + path-browserify "^1.0.1" + semver "^7.6.2" + typescript-auto-import-cache "^0.3.3" + vscode-languageserver-textdocument "^1.0.11" + vscode-nls "^5.2.0" + vscode-uri "^3.0.8" + vscode-css-languageservice@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-6.3.0.tgz#51724d193d19b1a9075b1cef5cfeea6a555d2aa4" @@ -44,18 +155,34 @@ vscode-html-languageservice@^5.3.0: vscode-languageserver-types "^3.17.5" vscode-uri "^3.0.8" -vscode-jsonrpc@9.0.0-next.4: - version "9.0.0-next.4" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" - integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== +vscode-json-languageservice@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-5.4.0.tgz#caf1aabc81b1df9faf6a97e4c34e13a2d10a8cdf" + integrity sha512-NCkkCr63OHVkE4lcb0xlUAaix6vE5gHQW4NrswbLEh3ArXj81lrGuFTsGEYEUXlNHdnc53vWPcjeSy/nMTrfXg== + dependencies: + "@vscode/l10n" "^0.0.18" + jsonc-parser "^3.3.0" + vscode-languageserver-textdocument "^1.0.11" + vscode-languageserver-types "^3.17.5" + vscode-uri "^3.0.8" + +vscode-jsonrpc@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" + integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== -vscode-languageserver-protocol@3.17.6-next.6: - version "3.17.6-next.6" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" - integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== +vscode-jsonrpc@^8.2.1: + version "8.2.1" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz#a322cc0f1d97f794ffd9c4cd2a898a0bde097f34" + integrity sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ== + +vscode-languageserver-protocol@3.17.5, vscode-languageserver-protocol@^3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" + integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== dependencies: - vscode-jsonrpc "9.0.0-next.4" - vscode-languageserver-types "3.17.6-next.4" + vscode-jsonrpc "8.2.0" + vscode-languageserver-types "3.17.5" vscode-languageserver-textdocument@^1.0.11: version "1.0.11" @@ -67,17 +194,17 @@ vscode-languageserver-types@3.17.5, vscode-languageserver-types@^3.17.5: resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== -vscode-languageserver-types@3.17.6-next.4: - version "3.17.6-next.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" - integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== - -vscode-languageserver@^10.0.0-next.6: - version "10.0.0-next.6" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-10.0.0-next.6.tgz#0db118a93fe010c6b40cd04e91a15d09e7b60b60" - integrity sha512-0Lh1nhQfSxo5Ob+ayYO1QTIsDix2/Lc72Urm1KZrCFxK5zIFYaEh3QFeM9oZih4Rzs0ZkQPXXnoHtpvs5GT+Zw== +vscode-languageserver@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz#500aef82097eb94df90d008678b0b6b5f474015b" + integrity sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g== dependencies: - vscode-languageserver-protocol "3.17.6-next.6" + vscode-languageserver-protocol "3.17.5" + +vscode-nls@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.2.0.tgz#3cb6893dd9bd695244d8a024bdf746eea665cc3f" + integrity sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng== vscode-uri@^3.0.8: version "3.0.8" diff --git a/extensions/html-language-features/yarn.lock b/extensions/html-language-features/yarn.lock index aa2ea1c684073..c2f56ea328a9a 100644 --- a/extensions/html-language-features/yarn.lock +++ b/extensions/html-language-features/yarn.lock @@ -102,6 +102,62 @@ dependencies: undici-types "~5.26.4" +"@volar/language-core@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.4.0.tgz#962efc66ff9198ee2412786e99528bf77cdad100" + integrity sha512-FTla+khE+sYK0qJP+6hwPAAUwiNHVMph4RUXpxf/FIPKUP61NFrVZorml4mjFShnueR2y9/j8/vnh09YwVdH7A== + dependencies: + "@volar/source-map" "2.4.0" + +"@volar/language-server@2.4.0", "@volar/language-server@~2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@volar/language-server/-/language-server-2.4.0.tgz#717727df01e0c3da544d7171c9d5b88691b396fb" + integrity sha512-rmGIjAxWekWQiGH97Mosb4juiD/hfFYNQKV5Py9r7vDOLSkbIwRhITbwHm88NJKs8P6TNc6w/PfBXN6yjKadJg== + dependencies: + "@volar/language-core" "2.4.0" + "@volar/language-service" "2.4.0" + "@volar/typescript" "2.4.0" + path-browserify "^1.0.1" + request-light "^0.7.0" + vscode-languageserver "^9.0.1" + vscode-languageserver-protocol "^3.17.5" + vscode-languageserver-textdocument "^1.0.11" + vscode-uri "^3.0.8" + +"@volar/language-service@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@volar/language-service/-/language-service-2.4.0.tgz#56f14e6888fe63f6beb1ed67d2dfc84877c251d0" + integrity sha512-4P3yeQXIL68mLfS3n6P3m02IRg3GnLHUU9k/1PCHEfm5FG9bySkDOc72dbBn2vAa2BxOqm18bmmZXrsWuQ5AOw== + dependencies: + "@volar/language-core" "2.4.0" + vscode-languageserver-protocol "^3.17.5" + vscode-languageserver-textdocument "^1.0.11" + vscode-uri "^3.0.8" + +"@volar/source-map@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.4.0.tgz#b6690f06c600eaf587bbc81b0153203e4f6db72a" + integrity sha512-2ceY8/NEZvN6F44TXw2qRP6AQsvCYhV2bxaBPWxV9HqIfkbRydSksTFObCF1DBDNBfKiZTS8G/4vqV6cvjdOIQ== + +"@volar/typescript@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.4.0.tgz#f909d20dfe43dd846d30695f6e5467276ff4418e" + integrity sha512-9zx3lQWgHmVd+JRRAHUSRiEhe4TlzL7U7e6ulWXOxHH/WNYxzKwCvZD7WYWEZFdw4dHfTD9vUR0yPQO6GilCaQ== + dependencies: + "@volar/language-core" "2.4.0" + path-browserify "^1.0.1" + vscode-uri "^3.0.8" + +"@volar/vscode@~2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@volar/vscode/-/vscode-2.4.0.tgz#4deab9cefe635594a6efc370584cb301638b17b2" + integrity sha512-VOnUgtmu+xGOqVKouRM8ZSeVOFPqmcTDfi3wif5peXpkOPsCgNdS/zns0xunuh9J6Ck5SV+QffPfmNW9XARnxw== + dependencies: + "@volar/language-server" "2.4.0" + path-browserify "^1.0.1" + vscode-languageclient "^9.0.1" + vscode-nls "^5.2.0" + "@vscode/extension-telemetry@^0.9.0": version "0.9.0" resolved "https://registry.yarnpkg.com/@vscode/extension-telemetry/-/extension-telemetry-0.9.0.tgz#8c6c61e253ff304f46045f04edd60059b144417a" @@ -123,65 +179,78 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -minimatch@^9.0.3: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== +minimatch@^5.1.0: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== dependencies: brace-expansion "^2.0.1" -semver@^7.6.0: - version "7.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" - integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== - dependencies: - lru-cache "^6.0.0" +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + +request-light@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/request-light/-/request-light-0.7.0.tgz#885628bb2f8040c26401ebf258ec51c4ae98ac2a" + integrity sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q== + +semver@^7.3.7: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== -vscode-jsonrpc@9.0.0-next.4: - version "9.0.0-next.4" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-9.0.0-next.4.tgz#ba403ddb3b82ca578179963dbe08e120a935f50d" - integrity sha512-zSVIr58lJSMYKIsZ5P7GtBbv1eEx25eNyOf0NmEzxmn1GhUNJAVAb5hkA1poKUwj1FRMwN6CeyWxZypmr8SsQQ== +vscode-jsonrpc@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" + integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA== -vscode-languageclient@^10.0.0-next.8: - version "10.0.0-next.8" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-10.0.0-next.8.tgz#5afa0ced3b2ac68d31cc1c48edc4f289744542a0" - integrity sha512-D9inIHgqKayO9Tv0MeLb3XIL76yTuWmKdHqcGZKzjtQrMGJgASJDYWTapu+yAjEpDp0gmVOaCYyIlLB86ncDoQ== +vscode-languageclient@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz#cdfe20267726c8d4db839dc1e9d1816e1296e854" + integrity sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA== dependencies: - minimatch "^9.0.3" - semver "^7.6.0" - vscode-languageserver-protocol "3.17.6-next.6" + minimatch "^5.1.0" + semver "^7.3.7" + vscode-languageserver-protocol "3.17.5" -vscode-languageserver-protocol@3.17.6-next.6: - version "3.17.6-next.6" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.6-next.6.tgz#8863a4dc8b395a8c31106ffdc945a00f9163b68b" - integrity sha512-naxM9kc/phpl0kAFNVPejMUWUtzFXdPYY/BtQTYtfbBbHf8sceHOrKkmf6yynZRu1A4oFtRZNqV3wyFRTWqUHw== +vscode-languageserver-protocol@3.17.5, vscode-languageserver-protocol@^3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz#864a8b8f390835572f4e13bd9f8313d0e3ac4bea" + integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg== dependencies: - vscode-jsonrpc "9.0.0-next.4" - vscode-languageserver-types "3.17.6-next.4" + vscode-jsonrpc "8.2.0" + vscode-languageserver-types "3.17.5" -vscode-languageserver-types@3.17.6-next.4: - version "3.17.6-next.4" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.6-next.4.tgz#6670939eb98f00aa7b05021dc3dd7fe9aa4453ea" - integrity sha512-SeJTpH/S14EbxOAVaOUoGVqPToqpRTld5QO5Ghig3AlbFJTFF9Wu7srHMfa85L0SX1RYAuuCSFKJVVCxDIk1/Q== +vscode-languageserver-textdocument@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf" + integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA== + +vscode-languageserver-types@3.17.5: + version "3.17.5" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz#3273676f0cf2eab40b3f44d085acbb7f08a39d8a" + integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg== + +vscode-languageserver@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz#500aef82097eb94df90d008678b0b6b5f474015b" + integrity sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g== + dependencies: + vscode-languageserver-protocol "3.17.5" + +vscode-nls@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.2.0.tgz#3cb6893dd9bd695244d8a024bdf746eea665cc3f" + integrity sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng== vscode-uri@^3.0.8: version "3.0.8" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==