diff --git a/extensions/html-language-features/client/src/browser/htmlClientMain.ts b/extensions/html-language-features/client/src/browser/htmlClientMain.ts new file mode 100644 index 00000000000..1623bea9e20 --- /dev/null +++ b/extensions/html-language-features/client/src/browser/htmlClientMain.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtensionContext } from 'vscode'; +import { LanguageClientOptions } from 'vscode-languageclient'; +import { startClient, LanguageClientConstructor } from '../htmlClient'; +import { LanguageClient } from 'vscode-languageclient/browser'; + +declare const Worker: { + new(stringUrl: string): any; +}; +declare const TextDecoder: { + new(encoding?: string): { decode(buffer: ArrayBuffer): string; }; +}; + +// this method is called when vs code is activated +export function activate(context: ExtensionContext) { + const serverMain = context.asAbsolutePath('server/dist/browser/htmlServerMain.js'); + try { + const worker = new Worker(serverMain); + const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { + return new LanguageClient(id, name, clientOptions, worker); + }; + + startClient(context, newLanguageClient, { TextDecoder }); + + } catch (e) { + console.log(e); + } +} diff --git a/extensions/html-language-features/client/src/customData.ts b/extensions/html-language-features/client/src/customData.ts index 38b51d37596..ecf964056e5 100644 --- a/extensions/html-language-features/client/src/customData.ts +++ b/extensions/html-language-features/client/src/customData.ts @@ -3,55 +3,86 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import { workspace, WorkspaceFolder, extensions } from 'vscode'; +import { workspace, extensions, Uri, EventEmitter, Disposable } from 'vscode'; +import { resolvePath, joinPath } from './requests'; -interface ExperimentalConfig { - customData?: string[]; - experimental?: { - customData?: string[]; +export function getCustomDataSource(toDispose: Disposable[]) { + let pathsInWorkspace = getCustomDataPathsInAllWorkspaces(); + let pathsInExtensions = getCustomDataPathsFromAllExtensions(); + + const onChange = new EventEmitter(); + + toDispose.push(extensions.onDidChange(_ => { + const newPathsInExtensions = getCustomDataPathsFromAllExtensions(); + if (newPathsInExtensions.length !== pathsInExtensions.length || !newPathsInExtensions.every((val, idx) => val === pathsInExtensions[idx])) { + pathsInExtensions = newPathsInExtensions; + onChange.fire(); + } + })); + toDispose.push(workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('html.customData')) { + pathsInWorkspace = getCustomDataPathsInAllWorkspaces(); + onChange.fire(); + } + })); + + return { + get uris() { + return pathsInWorkspace.concat(pathsInExtensions); + }, + get onDidChange() { + return onChange.event; + } }; } -export function getCustomDataPathsInAllWorkspaces(workspaceFolders: readonly WorkspaceFolder[] | undefined): string[] { + +function getCustomDataPathsInAllWorkspaces(): string[] { + const workspaceFolders = workspace.workspaceFolders; + const dataPaths: string[] = []; if (!workspaceFolders) { return dataPaths; } - workspaceFolders.forEach(wf => { - const allHtmlConfig = workspace.getConfiguration(undefined, wf.uri); - const wfHtmlConfig = allHtmlConfig.inspect('html'); - - if (wfHtmlConfig && wfHtmlConfig.workspaceFolderValue && wfHtmlConfig.workspaceFolderValue.customData) { - const customData = wfHtmlConfig.workspaceFolderValue.customData; - if (Array.isArray(customData)) { - customData.forEach(t => { - if (typeof t === 'string') { - dataPaths.push(path.resolve(wf.uri.fsPath, t)); - } - }); + const collect = (paths: string[] | undefined, rootFolder: Uri) => { + if (Array.isArray(paths)) { + for (const path of paths) { + if (typeof path === 'string') { + dataPaths.push(resolvePath(rootFolder, path).toString()); + } } } - }); + }; + for (let i = 0; i < workspaceFolders.length; i++) { + const folderUri = workspaceFolders[i].uri; + const allHtmlConfig = workspace.getConfiguration('html', folderUri); + const customDataInspect = allHtmlConfig.inspect('customData'); + if (customDataInspect) { + collect(customDataInspect.workspaceFolderValue, folderUri); + if (i === 0) { + if (workspace.workspaceFile) { + collect(customDataInspect.workspaceValue, workspace.workspaceFile); + } + collect(customDataInspect.globalValue, folderUri); + } + } + + } return dataPaths; } -export function getCustomDataPathsFromAllExtensions(): string[] { +function getCustomDataPathsFromAllExtensions(): string[] { const dataPaths: string[] = []; - for (const extension of extensions.all) { - const contributes = extension.packageJSON && extension.packageJSON.contributes; - - if (contributes && contributes.html && contributes.html.customData && Array.isArray(contributes.html.customData)) { - const relativePaths: string[] = contributes.html.customData; - relativePaths.forEach(rp => { - dataPaths.push(path.resolve(extension.extensionPath, rp)); - }); + const customData = extension.packageJSON?.contributes?.html?.customData; + if (Array.isArray(customData)) { + for (const rp of customData) { + dataPaths.push(joinPath(extension.extensionUri, rp).toString()); + } } } - return dataPaths; } diff --git a/extensions/html-language-features/client/src/htmlMain.ts b/extensions/html-language-features/client/src/htmlClient.ts similarity index 84% rename from extensions/html-language-features/client/src/htmlMain.ts rename to extensions/html-language-features/client/src/htmlClient.ts index 913b43378b0..e818d1a2d02 100644 --- a/extensions/html-language-features/client/src/htmlMain.ts +++ b/extensions/html-language-features/client/src/htmlClient.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); @@ -13,13 +12,17 @@ import { DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider, SemanticTokens, window, commands } from 'vscode'; import { - LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, RequestType, TextDocumentPositionParams, DocumentRangeFormattingParams, - DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, TextDocumentIdentifier, RequestType0, Range as LspRange + LanguageClientOptions, RequestType, TextDocumentPositionParams, DocumentRangeFormattingParams, + DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, TextDocumentIdentifier, RequestType0, Range as LspRange, NotificationType, CommonLanguageClient } from 'vscode-languageclient'; import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared'; import { activateTagClosing } from './tagClosing'; -import TelemetryReporter from 'vscode-extension-telemetry'; -import { getCustomDataPathsInAllWorkspaces, getCustomDataPathsFromAllExtensions } from './customData'; +import { RequestService } from './requests'; +import { getCustomDataSource } from './customData'; + +namespace CustomDataChangedNotification { + export const type: NotificationType = new NotificationType('html/customDataChanged'); +} namespace TagCloseRequest { export const type: RequestType = new RequestType('html/tag'); @@ -46,44 +49,33 @@ namespace SettingIds { } -interface IPackageInfo { - name: string; - version: string; - aiKey: string; - main: string; +export interface TelemetryReporter { + sendTelemetryEvent(eventName: string, properties?: { + [key: string]: string; + }, measurements?: { + [key: string]: number; + }): void; } -let telemetryReporter: TelemetryReporter | null; +export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => CommonLanguageClient; +export interface Runtime { + TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string; } }; + fs?: RequestService; + telemetry?: TelemetryReporter; +} + +export function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime) { -export function activate(context: ExtensionContext) { let toDispose = context.subscriptions; - let clientPackageJSON = getPackageInfo(context); - telemetryReporter = new TelemetryReporter(clientPackageJSON.name, clientPackageJSON.version, clientPackageJSON.aiKey); - - const serverMain = `./server/${clientPackageJSON.main.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/htmlServerMain`; - const serverModule = context.asAbsolutePath(serverMain); - - // The debug options for the server - let debugOptions = { execArgv: ['--nolazy', '--inspect=6045'] }; - - // If the extension is launch in debug mode the debug server options are use - // Otherwise the run options are used - let serverOptions: ServerOptions = { - run: { module: serverModule, transport: TransportKind.ipc }, - debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } - }; let documentSelector = ['html', 'handlebars']; let embeddedLanguages = { css: true, javascript: true }; let rangeFormatting: Disposable | undefined = undefined; - let dataPaths = [ - ...getCustomDataPathsInAllWorkspaces(workspace.workspaceFolders), - ...getCustomDataPathsFromAllExtensions() - ]; + const customDataSource = getCustomDataSource(context.subscriptions); // Options to control the language client let clientOptions: LanguageClientOptions = { @@ -93,7 +85,7 @@ export function activate(context: ExtensionContext) { }, initializationOptions: { embeddedLanguages, - dataPaths, + handledSchemas: ['file'], provideFormatter: false, // tell the server to not provide formatting capability and ignore the `html.format.enable` setting. }, middleware: { @@ -123,12 +115,18 @@ export function activate(context: ExtensionContext) { }; // Create the language client and start the client. - let client = new LanguageClient('html', localize('htmlserver.name', 'HTML Language Server'), serverOptions, clientOptions); + let client = newLanguageClient('html', localize('htmlserver.name', 'HTML Language Server'), clientOptions); client.registerProposedFeatures(); let disposable = client.start(); toDispose.push(disposable); client.onReady().then(() => { + + client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); + customDataSource.onDidChange(() => { + client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); + }); + let tagRequestor = (document: TextDocument, position: Position) => { let param = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position); return client.sendRequest(TagCloseRequest.type, param); @@ -137,9 +135,7 @@ export function activate(context: ExtensionContext) { toDispose.push(disposable); disposable = client.onTelemetry(e => { - if (telemetryReporter) { - telemetryReporter.sendTelemetryEvent(e.key, e.data); - } + runtime.telemetry?.sendTelemetryEvent(e.key, e.data); }); toDispose.push(disposable); @@ -201,7 +197,7 @@ export function activate(context: ExtensionContext) { return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then( client.protocol2CodeConverter.asTextEdits, (error) => { - client.logFailedRequest(DocumentRangeFormattingRequest.type, error); + client.handleFailedRequest(DocumentRangeFormattingRequest.type, error, []); return Promise.resolve([]); } ); @@ -319,17 +315,3 @@ export function activate(context: ExtensionContext) { toDispose.push(); } - -function getPackageInfo(context: ExtensionContext): IPackageInfo { - const location = context.asAbsolutePath('./package.json'); - try { - return JSON.parse(fs.readFileSync(location).toString()); - } catch (e) { - console.log(`Problems reading ${location}: ${e}`); - return { name: '', version: '', aiKey: '', main: '' }; - } -} - -export function deactivate(): Promise { - return telemetryReporter ? telemetryReporter.dispose() : Promise.resolve(null); -} diff --git a/extensions/html-language-features/client/src/node/htmlClientMain.ts b/extensions/html-language-features/client/src/node/htmlClientMain.ts new file mode 100644 index 00000000000..c58515b3712 --- /dev/null +++ b/extensions/html-language-features/client/src/node/htmlClientMain.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 { getNodeFSRequestService } from './nodeFs'; +import { ExtensionContext } from 'vscode'; +import { startClient, LanguageClientConstructor } from '../htmlClient'; +import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; +import { TextDecoder } from 'util'; +import * as fs from 'fs'; +import TelemetryReporter from 'vscode-extension-telemetry'; + + +let telemetry: TelemetryReporter | undefined; + +// this method is called when vs code is activated +export function activate(context: ExtensionContext) { + + let clientPackageJSON = getPackageInfo(context); + telemetry = new TelemetryReporter(clientPackageJSON.name, clientPackageJSON.version, clientPackageJSON.aiKey); + + const serverMain = `./server/${clientPackageJSON.main.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/htmlServerMain`; + const serverModule = context.asAbsolutePath(serverMain); + + // The debug options for the server + const debugOptions = { execArgv: ['--nolazy', '--inspect=6044'] }; + + // If the extension is launch in debug mode the debug server options are use + // Otherwise the run options are used + const serverOptions: ServerOptions = { + run: { module: serverModule, transport: TransportKind.ipc }, + debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } + }; + + const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { + return new LanguageClient(id, name, serverOptions, clientOptions); + }; + + startClient(context, newLanguageClient, { fs: getNodeFSRequestService(), TextDecoder, telemetry }); +} + +interface IPackageInfo { + name: string; + version: string; + aiKey: string; + main: string; +} + +function getPackageInfo(context: ExtensionContext): IPackageInfo { + const location = context.asAbsolutePath('./package.json'); + try { + return JSON.parse(fs.readFileSync(location).toString()); + } catch (e) { + console.log(`Problems reading ${location}: ${e}`); + return { name: '', version: '', aiKey: '', main: '' }; + } +} diff --git a/extensions/html-language-features/client/src/node/nodeFs.ts b/extensions/html-language-features/client/src/node/nodeFs.ts new file mode 100644 index 00000000000..c13ef2e1c08 --- /dev/null +++ b/extensions/html-language-features/client/src/node/nodeFs.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { getScheme, RequestService, FileType } from '../requests'; + +export function getNodeFSRequestService(): RequestService { + function ensureFileUri(location: string) { + if (getScheme(location) !== 'file') { + throw new Error('fileRequestService can only handle file URLs'); + } + } + return { + getContent(location: string, encoding?: string) { + ensureFileUri(location); + return new Promise((c, e) => { + const uri = Uri.parse(location); + fs.readFile(uri.fsPath, encoding, (err, buf) => { + if (err) { + return e(err); + } + c(buf.toString()); + + }); + }); + }, + stat(location: string) { + ensureFileUri(location); + return new Promise((c, e) => { + const uri = Uri.parse(location); + fs.stat(uri.fsPath, (err, stats) => { + if (err) { + if (err.code === 'ENOENT') { + return c({ type: FileType.Unknown, ctime: -1, mtime: -1, size: -1 }); + } else { + return e(err); + } + } + + let type = FileType.Unknown; + if (stats.isFile()) { + type = FileType.File; + } else if (stats.isDirectory()) { + type = FileType.Directory; + } else if (stats.isSymbolicLink()) { + type = FileType.SymbolicLink; + } + + c({ + type, + ctime: stats.ctime.getTime(), + mtime: stats.mtime.getTime(), + size: stats.size + }); + }); + }); + }, + readDirectory(location: string) { + ensureFileUri(location); + return new Promise((c, e) => { + const path = Uri.parse(location).fsPath; + + fs.readdir(path, { withFileTypes: true }, (err, children) => { + if (err) { + return e(err); + } + c(children.map(stat => { + if (stat.isSymbolicLink()) { + return [stat.name, FileType.SymbolicLink]; + } else if (stat.isDirectory()) { + return [stat.name, FileType.Directory]; + } else if (stat.isFile()) { + return [stat.name, FileType.File]; + } else { + return [stat.name, FileType.Unknown]; + } + })); + }); + }); + } + }; +} diff --git a/extensions/html-language-features/client/src/requests.ts b/extensions/html-language-features/client/src/requests.ts new file mode 100644 index 00000000000..ac966636314 --- /dev/null +++ b/extensions/html-language-features/client/src/requests.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Uri, workspace } from 'vscode'; +import { RequestType, CommonLanguageClient } from 'vscode-languageclient'; +import { Runtime } from './htmlClient'; + +export namespace FsContentRequest { + export const type: RequestType<{ uri: string; encoding?: string; }, string, any, any> = new RequestType('fs/content'); +} +export namespace FsStatRequest { + export const type: RequestType = new RequestType('fs/stat'); +} + +export namespace FsReadDirRequest { + export const type: RequestType = new RequestType('fs/readDir'); +} + +export function serveFileSystemRequests(client: CommonLanguageClient, runtime: Runtime) { + client.onRequest(FsContentRequest.type, (param: { uri: string; encoding?: string; }) => { + const uri = Uri.parse(param.uri); + if (uri.scheme === 'file' && runtime.fs) { + return runtime.fs.getContent(param.uri); + } + return workspace.fs.readFile(uri).then(buffer => { + return new runtime.TextDecoder(param.encoding).decode(buffer); + }); + }); + client.onRequest(FsReadDirRequest.type, (uriString: string) => { + const uri = Uri.parse(uriString); + if (uri.scheme === 'file' && runtime.fs) { + return runtime.fs.readDirectory(uriString); + } + return workspace.fs.readDirectory(uri); + }); + client.onRequest(FsStatRequest.type, (uriString: string) => { + const uri = Uri.parse(uriString); + if (uri.scheme === 'file' && runtime.fs) { + return runtime.fs.stat(uriString); + } + return workspace.fs.stat(uri); + }); +} + +export enum FileType { + /** + * The file type is unknown. + */ + Unknown = 0, + /** + * A regular file. + */ + File = 1, + /** + * A directory. + */ + Directory = 2, + /** + * A symbolic link to a file. + */ + SymbolicLink = 64 +} +export interface FileStat { + /** + * The type of the file, e.g. is a regular file, a directory, or symbolic link + * to a file. + */ + type: FileType; + /** + * The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ + ctime: number; + /** + * The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ + mtime: number; + /** + * The size in bytes. + */ + size: number; +} + +export interface RequestService { + getContent(uri: string, encoding?: string): Promise; + + stat(uri: string): Promise; + readDirectory(uri: string): Promise<[string, FileType][]>; +} + +export function getScheme(uri: string) { + return uri.substr(0, uri.indexOf(':')); +} + +export function dirname(uri: string) { + const lastIndexOfSlash = uri.lastIndexOf('/'); + return lastIndexOfSlash !== -1 ? uri.substr(0, lastIndexOfSlash) : ''; +} + +export function basename(uri: string) { + const lastIndexOfSlash = uri.lastIndexOf('/'); + return uri.substr(lastIndexOfSlash + 1); +} + +const Slash = '/'.charCodeAt(0); +const Dot = '.'.charCodeAt(0); + +export function isAbsolutePath(path: string) { + return path.charCodeAt(0) === Slash; +} + +export function resolvePath(uri: Uri, path: string): Uri { + if (isAbsolutePath(path)) { + return uri.with({ path: normalizePath(path.split('/')) }); + } + return joinPath(uri, path); +} + +export function normalizePath(parts: string[]): string { + const newParts: string[] = []; + for (const part of parts) { + if (part.length === 0 || part.length === 1 && part.charCodeAt(0) === Dot) { + // ignore + } else if (part.length === 2 && part.charCodeAt(0) === Dot && part.charCodeAt(1) === Dot) { + newParts.pop(); + } else { + newParts.push(part); + } + } + if (parts.length > 1 && parts[parts.length - 1].length === 0) { + newParts.push(''); + } + let res = newParts.join('/'); + if (parts[0].length === 0) { + res = '/' + res; + } + return res; +} + + +export function joinPath(uri: Uri, ...paths: string[]): Uri { + const parts = uri.path.split('/'); + for (let path of paths) { + parts.push(...path.split('/')); + } + return uri.with({ path: normalizePath(parts) }); +} diff --git a/extensions/html-language-features/extension-browser.webpack.config.js b/extensions/html-language-features/extension-browser.webpack.config.js new file mode 100644 index 00000000000..ec6c3e60eb3 --- /dev/null +++ b/extensions/html-language-features/extension-browser.webpack.config.js @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); +const path = require('path'); + +const clientConfig = withDefaults({ + target: 'webworker', + context: path.join(__dirname, 'client'), + entry: { + extension: './src/browser/htmlClientMain.ts' + }, + output: { + filename: 'htmlClientMain.js', + path: path.join(__dirname, 'client', 'dist', 'browser') + }, + performance: { + hints: false + }, + resolve: { + alias: { + 'vscode-nls': path.resolve(__dirname, '../../build/polyfills/vscode-nls.js') + } + } +}); +clientConfig.module.rules[0].use.shift(); // remove nls loader + +module.exports = clientConfig; diff --git a/extensions/html-language-features/extension.webpack.config.js b/extensions/html-language-features/extension.webpack.config.js index 9624295ff5b..6af444db930 100644 --- a/extensions/html-language-features/extension.webpack.config.js +++ b/extensions/html-language-features/extension.webpack.config.js @@ -13,10 +13,10 @@ const path = require('path'); module.exports = withDefaults({ context: path.join(__dirname, 'client'), entry: { - extension: './src/htmlMain.ts', + extension: './src/node/htmlClientMain.ts', }, output: { - filename: 'htmlMain.js', - path: path.join(__dirname, 'client', 'dist') + filename: 'htmlClientMain.js', + path: path.join(__dirname, 'client', 'dist', 'node') } }); diff --git a/extensions/html-language-features/package.json b/extensions/html-language-features/package.json index c86a74e2cc0..1e223641b13 100644 --- a/extensions/html-language-features/package.json +++ b/extensions/html-language-features/package.json @@ -15,7 +15,8 @@ "onLanguage:html", "onLanguage:handlebars" ], - "main": "./client/out/htmlMain", + "main": "./client/out/node/htmlClientMain", + "browser": "./client/dist/browser/htmlClientMain", "scripts": { "compile": "npx gulp compile-extension:html-language-features-client compile-extension:html-language-features-server", "watch": "npx gulp watch-extension:html-language-features-client watch-extension:html-language-features-server", @@ -202,7 +203,7 @@ }, "dependencies": { "vscode-extension-telemetry": "0.1.1", - "vscode-languageclient": "^6.1.3", + "vscode-languageclient": "7.0.0-next.5", "vscode-nls": "^4.1.2" }, "devDependencies": { diff --git a/extensions/html-language-features/server/extension-browser.webpack.config.js b/extensions/html-language-features/server/extension-browser.webpack.config.js new file mode 100644 index 00000000000..2220a6c3ace --- /dev/null +++ b/extensions/html-language-features/server/extension-browser.webpack.config.js @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../../shared.webpack.config'); +const path = require('path'); + +const serverConfig = withDefaults({ + target: 'webworker', + context: __dirname, + entry: { + extension: './src/browser/htmlServerMain.ts', + }, + output: { + filename: 'htmlServerMain.js', + path: path.join(__dirname, 'dist', 'browser'), + libraryTarget: 'var' + }, + performance: { + hints: false + }, + resolve: { + alias: { + 'vscode-nls': path.resolve(__dirname, '../../../build/polyfills/vscode-nls.js') + } + } +}); +serverConfig.module.rules[0].use.shift(); // remove nls loader +serverConfig.module.noParse = /typescript\/lib\/typescript\.js/; + +module.exports = serverConfig; diff --git a/extensions/html-language-features/server/extension.webpack.config.js b/extensions/html-language-features/server/extension.webpack.config.js index 77b86e718b1..33cb0b4f0a1 100644 --- a/extensions/html-language-features/server/extension.webpack.config.js +++ b/extensions/html-language-features/server/extension.webpack.config.js @@ -13,11 +13,11 @@ const path = require('path'); module.exports = withDefaults({ context: path.join(__dirname), entry: { - extension: './src/htmlServerMain.ts', + extension: './src/node/htmlServerMain.ts', }, output: { filename: 'htmlServerMain.js', - path: path.join(__dirname, 'dist'), + path: path.join(__dirname, 'dist', 'node'), }, externals: { 'typescript': 'commonjs typescript' diff --git a/extensions/html-language-features/server/package.json b/extensions/html-language-features/server/package.json index 70665bf8497..4d3b5ba9a65 100644 --- a/extensions/html-language-features/server/package.json +++ b/extensions/html-language-features/server/package.json @@ -7,11 +7,11 @@ "engines": { "node": "*" }, - "main": "./out/htmlServerMain", + "main": "./out/node/htmlServerMain", "dependencies": { - "vscode-css-languageservice": "^4.1.2", - "vscode-html-languageservice": "^3.1.0-next.2", - "vscode-languageserver": "^6.1.1", + "vscode-css-languageservice": "4.3.0-next.2", + "vscode-html-languageservice": "3.1.0-next.2", + "vscode-languageserver": "7.0.0-next.3", "vscode-nls": "^4.1.2", "vscode-uri": "^2.1.2" }, diff --git a/extensions/html-language-features/server/src/browser/htmlServerMain.ts b/extensions/html-language-features/server/src/browser/htmlServerMain.ts new file mode 100644 index 00000000000..1d38d33db37 --- /dev/null +++ b/extensions/html-language-features/server/src/browser/htmlServerMain.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createConnection, BrowserMessageReader, BrowserMessageWriter } from 'vscode-languageserver/browser'; +import { startServer } from '../htmlServer'; + +declare let self: any; + +const messageReader = new BrowserMessageReader(self); +const messageWriter = new BrowserMessageWriter(self); + +const connection = createConnection(messageReader, messageWriter); + +startServer(connection, {}); diff --git a/extensions/html-language-features/server/src/customData.ts b/extensions/html-language-features/server/src/customData.ts index 1d550eddf9f..3ef347a23d2 100644 --- a/extensions/html-language-features/server/src/customData.ts +++ b/extensions/html-language-features/server/src/customData.ts @@ -4,26 +4,35 @@ *--------------------------------------------------------------------------------------------*/ import { IHTMLDataProvider, newHTMLDataProvider } from 'vscode-html-languageservice'; -import * as fs from 'fs'; +import { RequestService } from './requests'; -export function getDataProviders(dataPaths?: string[]): IHTMLDataProvider[] { - if (!dataPaths) { - return []; - } - - const providers: IHTMLDataProvider[] = []; - - dataPaths.forEach((path, i) => { +export function fetchHTMLDataProviders(dataPaths: string[], requestService: RequestService): Promise { + const providers = dataPaths.map(async p => { try { - if (fs.existsSync(path)) { - const htmlData = JSON.parse(fs.readFileSync(path, 'utf-8')); - - providers.push(newHTMLDataProvider(`customProvider${i}`, htmlData)); - } - } catch (err) { - console.log(`Failed to load tag from ${path}`); + const content = await requestService.getContent(p); + return parseHTMLData(p, content); + } catch (e) { + return newHTMLDataProvider(p, { version: 1 }); } }); - return providers; -} \ No newline at end of file + return Promise.all(providers); +} + +function parseHTMLData(id: string, source: string): IHTMLDataProvider { + let rawData: any; + + try { + rawData = JSON.parse(source); + } catch (err) { + return newHTMLDataProvider(id, { version: 1 }); + } + + return newHTMLDataProvider(id, { + version: rawData.version || 1, + tags: rawData.tags || [], + globalAttributes: rawData.globalAttributes || [], + valueSets: rawData.valueSets || [] + }); +} + diff --git a/extensions/html-language-features/server/src/htmlServer.ts b/extensions/html-language-features/server/src/htmlServer.ts new file mode 100644 index 00000000000..680fc4f5352 --- /dev/null +++ b/extensions/html-language-features/server/src/htmlServer.ts @@ -0,0 +1,559 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + Connection, TextDocuments, InitializeParams, InitializeResult, RequestType, + DocumentRangeFormattingRequest, Disposable, DocumentSelector, TextDocumentPositionParams, ServerCapabilities, + ConfigurationRequest, ConfigurationParams, DidChangeWorkspaceFoldersNotification, + DocumentColorRequest, ColorPresentationRequest, TextDocumentSyncKind, NotificationType +} from 'vscode-languageserver'; +import { + getLanguageModes, LanguageModes, Settings, TextDocument, Position, Diagnostic, WorkspaceFolder, ColorInformation, + Range, DocumentLink, SymbolInformation, TextDocumentIdentifier +} from './modes/languageModes'; + +import { format } from './modes/formatting'; +import { pushAll } from './utils/arrays'; +import { getDocumentContext } from './utils/documentContext'; +import { URI } from 'vscode-uri'; +import { formatError, runSafe, runSafeAsync } from './utils/runner'; + +import { getFoldingRanges } from './modes/htmlFolding'; +import { fetchHTMLDataProviders } from './customData'; +import { getSelectionRanges } from './modes/selectionRanges'; +import { SemanticTokenProvider, newSemanticTokenProvider } from './modes/semanticTokens'; +import { RequestService, getRequestService } from './requests'; + +namespace CustomDataChangedNotification { + export const type: NotificationType = new NotificationType('html/customDataChanged'); +} + +namespace TagCloseRequest { + export const type: RequestType = new RequestType('html/tag'); +} +namespace OnTypeRenameRequest { + export const type: RequestType = new RequestType('html/onTypeRename'); +} + +// experimental: semantic tokens +interface SemanticTokenParams { + textDocument: TextDocumentIdentifier; + ranges?: Range[]; +} +namespace SemanticTokenRequest { + export const type: RequestType = new RequestType('html/semanticTokens'); +} +namespace SemanticTokenLegendRequest { + export const type: RequestType = new RequestType('html/semanticTokenLegend'); +} + +export interface RuntimeEnvironment { + file?: RequestService; + http?: RequestService + configureHttpRequests?(proxy: string, strictSSL: boolean): void; +} + +export function startServer(connection: Connection, runtime: RuntimeEnvironment) { + + // Create a text document manager. + const documents = new TextDocuments(TextDocument); + // Make the text document manager listen on the connection + // for open, change and close text document events + documents.listen(connection); + + let workspaceFolders: WorkspaceFolder[] = []; + + let languageModes: LanguageModes; + + let clientSnippetSupport = false; + let dynamicFormatterRegistration = false; + let scopedSettingsSupport = false; + let workspaceFoldersSupport = false; + let foldingRangeLimit = Number.MAX_VALUE; + + const notReady = () => Promise.reject('Not Ready'); + let requestService: RequestService = { getContent: notReady, stat: notReady, readDirectory: notReady }; + + + + let globalSettings: Settings = {}; + let documentSettings: { [key: string]: Thenable } = {}; + // 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 configRequestParam: ConfigurationParams = { items: [{ scopeUri, section: 'css' }, { scopeUri, section: 'html' }, { scopeUri, section: 'javascript' }] }; + promise = connection.sendRequest(ConfigurationRequest.type, configRequestParam).then(s => ({ css: s[0], html: s[1], javascript: s[2] })); + documentSettings[textDocument.uri] = promise; + } + return promise; + } + return Promise.resolve(undefined); + } + + // After the server has started the client sends an initialize request. The server receives + // in the passed params the rootPath of the workspace plus the client capabilities + connection.onInitialize((params: InitializeParams): InitializeResult => { + const initializationOptions = params.initializationOptions; + + workspaceFolders = (params).workspaceFolders; + if (!Array.isArray(workspaceFolders)) { + workspaceFolders = []; + if (params.rootPath) { + workspaceFolders.push({ name: '', uri: URI.file(params.rootPath).toString() }); + } + } + + requestService = getRequestService(params.initializationOptions.handledSchemas || ['file'], connection, runtime); + + const workspace = { + get settings() { return globalSettings; }, + get folders() { return workspaceFolders; } + }; + + languageModes = getLanguageModes(initializationOptions ? initializationOptions.embeddedLanguages : { css: true, javascript: true }, workspace, params.capabilities, requestService); + + const dataPaths: string[] = params.initializationOptions.dataPaths || []; + fetchHTMLDataProviders(dataPaths, requestService).then(dataProviders => { + languageModes.updateDataProviders(dataProviders); + }); + + documents.onDidClose(e => { + languageModes.onDocumentRemoved(e.document); + }); + connection.onShutdown(() => { + languageModes.dispose(); + }); + + function getClientCapability(name: string, def: T) { + const keys = name.split('.'); + let c: any = params.capabilities; + for (let i = 0; c && i < keys.length; i++) { + if (!c.hasOwnProperty(keys[i])) { + return def; + } + c = c[keys[i]]; + } + return c; + } + + clientSnippetSupport = getClientCapability('textDocument.completion.completionItem.snippetSupport', false); + dynamicFormatterRegistration = getClientCapability('textDocument.rangeFormatting.dynamicRegistration', false) && (typeof params.initializationOptions.provideFormatter !== 'boolean'); + scopedSettingsSupport = getClientCapability('workspace.configuration', false); + workspaceFoldersSupport = getClientCapability('workspace.workspaceFolders', false); + foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE); + const capabilities: ServerCapabilities = { + textDocumentSync: TextDocumentSyncKind.Incremental, + completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/'] } : undefined, + hoverProvider: true, + documentHighlightProvider: true, + documentRangeFormattingProvider: params.initializationOptions.provideFormatter === true, + documentLinkProvider: { resolveProvider: false }, + documentSymbolProvider: true, + definitionProvider: true, + signatureHelpProvider: { triggerCharacters: ['('] }, + referencesProvider: true, + colorProvider: {}, + foldingRangeProvider: true, + selectionRangeProvider: true, + renameProvider: true + }; + return { capabilities }; + }); + + connection.onInitialized(() => { + if (workspaceFoldersSupport) { + connection.client.register(DidChangeWorkspaceFoldersNotification.type); + + connection.onNotification(DidChangeWorkspaceFoldersNotification.type, e => { + const toAdd = e.event.added; + const toRemove = e.event.removed; + const updatedFolders = []; + if (workspaceFolders) { + for (const folder of workspaceFolders) { + if (!toRemove.some(r => r.uri === folder.uri) && !toAdd.some(r => r.uri === folder.uri)) { + updatedFolders.push(folder); + } + } + } + workspaceFolders = updatedFolders.concat(toAdd); + documents.all().forEach(triggerValidation); + }); + } + }); + + let formatterRegistration: Thenable | null = null; + + // The settings have changed. Is send on server activation as well. + connection.onDidChangeConfiguration((change) => { + globalSettings = change.settings; + documentSettings = {}; // reset all document settings + documents.all().forEach(triggerValidation); + + // dynamically enable & disable the formatter + if (dynamicFormatterRegistration) { + const enableFormatter = globalSettings && globalSettings.html && globalSettings.html.format && globalSettings.html.format.enable; + if (enableFormatter) { + if (!formatterRegistration) { + const documentSelector: DocumentSelector = [{ language: 'html' }, { language: 'handlebars' }]; + formatterRegistration = connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector }); + } + } else if (formatterRegistration) { + formatterRegistration.then(r => r.dispose()); + formatterRegistration = null; + } + } + }); + + const pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {}; + const validationDelayMs = 500; + + // The content of a text document has changed. This event is emitted + // when the text document first opened or when its content has changed. + documents.onDidChangeContent(change => { + triggerValidation(change.document); + }); + + // a document has closed: clear all diagnostics + documents.onDidClose(event => { + cleanPendingValidation(event.document); + connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); + }); + + function cleanPendingValidation(textDocument: TextDocument): void { + const request = pendingValidationRequests[textDocument.uri]; + if (request) { + clearTimeout(request); + delete pendingValidationRequests[textDocument.uri]; + } + } + + function triggerValidation(textDocument: TextDocument): void { + cleanPendingValidation(textDocument); + pendingValidationRequests[textDocument.uri] = setTimeout(() => { + delete pendingValidationRequests[textDocument.uri]; + validateTextDocument(textDocument); + }, validationDelayMs); + } + + function isValidationEnabled(languageId: string, settings: Settings = globalSettings) { + const validationSettings = settings && settings.html && settings.html.validate; + if (validationSettings) { + return languageId === 'css' && validationSettings.styles !== false || languageId === 'javascript' && validationSettings.scripts !== false; + } + return true; + } + + async function validateTextDocument(textDocument: TextDocument) { + try { + const version = textDocument.version; + const diagnostics: Diagnostic[] = []; + if (textDocument.languageId === 'html') { + const modes = languageModes.getAllModesInDocument(textDocument); + const settings = await getDocumentSettings(textDocument, () => modes.some(m => !!m.doValidation)); + const latestTextDocument = documents.get(textDocument.uri); + if (latestTextDocument && latestTextDocument.version === version) { // check no new version has come in after in after the async op + modes.forEach(mode => { + if (mode.doValidation && isValidationEnabled(mode.getId(), settings)) { + pushAll(diagnostics, mode.doValidation(latestTextDocument, settings)); + } + }); + connection.sendDiagnostics({ uri: latestTextDocument.uri, diagnostics }); + } + } + } catch (e) { + connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); + } + } + + connection.onCompletion(async (textDocumentPosition, token) => { + return runSafeAsync(async () => { + const document = documents.get(textDocumentPosition.textDocument.uri); + if (!document) { + return null; + } + const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position); + if (!mode || !mode.doComplete) { + return { isIncomplete: true, items: [] }; + } + const doComplete = mode.doComplete!; + + if (mode.getId() !== 'html') { + /* __GDPR__ + "html.embbedded.complete" : { + "languageId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + connection.telemetry.logEvent({ key: 'html.embbedded.complete', value: { languageId: mode.getId() } }); + } + + const settings = await getDocumentSettings(document, () => doComplete.length > 2); + const documentContext = getDocumentContext(document.uri, workspaceFolders); + return doComplete(document, textDocumentPosition.position, documentContext, settings); + + }, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`, token); + }); + + connection.onCompletionResolve((item, token) => { + return runSafe(() => { + const data = item.data; + if (data && data.languageId && data.uri) { + const mode = languageModes.getMode(data.languageId); + const document = documents.get(data.uri); + if (mode && mode.doResolve && document) { + return mode.doResolve(document, item); + } + } + return item; + }, item, `Error while resolving completion proposal`, token); + }); + + connection.onHover((textDocumentPosition, token) => { + return runSafe(() => { + const document = documents.get(textDocumentPosition.textDocument.uri); + if (document) { + const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position); + if (mode && mode.doHover) { + return mode.doHover(document, textDocumentPosition.position); + } + } + return null; + }, null, `Error while computing hover for ${textDocumentPosition.textDocument.uri}`, token); + }); + + connection.onDocumentHighlight((documentHighlightParams, token) => { + return runSafe(() => { + const document = documents.get(documentHighlightParams.textDocument.uri); + if (document) { + const mode = languageModes.getModeAtPosition(document, documentHighlightParams.position); + if (mode && mode.findDocumentHighlight) { + return mode.findDocumentHighlight(document, documentHighlightParams.position); + } + } + return []; + }, [], `Error while computing document highlights for ${documentHighlightParams.textDocument.uri}`, token); + }); + + connection.onDefinition((definitionParams, token) => { + return runSafe(() => { + const document = documents.get(definitionParams.textDocument.uri); + if (document) { + const mode = languageModes.getModeAtPosition(document, definitionParams.position); + if (mode && mode.findDefinition) { + return mode.findDefinition(document, definitionParams.position); + } + } + return []; + }, null, `Error while computing definitions for ${definitionParams.textDocument.uri}`, token); + }); + + connection.onReferences((referenceParams, token) => { + return runSafe(() => { + const document = documents.get(referenceParams.textDocument.uri); + if (document) { + const mode = languageModes.getModeAtPosition(document, referenceParams.position); + if (mode && mode.findReferences) { + return mode.findReferences(document, referenceParams.position); + } + } + return []; + }, [], `Error while computing references for ${referenceParams.textDocument.uri}`, token); + }); + + connection.onSignatureHelp((signatureHelpParms, token) => { + return runSafe(() => { + const document = documents.get(signatureHelpParms.textDocument.uri); + if (document) { + const mode = languageModes.getModeAtPosition(document, signatureHelpParms.position); + if (mode && mode.doSignatureHelp) { + return mode.doSignatureHelp(document, signatureHelpParms.position); + } + } + return null; + }, null, `Error while computing signature help for ${signatureHelpParms.textDocument.uri}`, token); + }); + + connection.onDocumentRangeFormatting(async (formatParams, token) => { + return runSafeAsync(async () => { + const document = documents.get(formatParams.textDocument.uri); + if (document) { + let settings = await getDocumentSettings(document, () => true); + if (!settings) { + settings = globalSettings; + } + const unformattedTags: string = settings && settings.html && settings.html.format && settings.html.format.unformatted || ''; + const enabledModes = { css: !unformattedTags.match(/\bstyle\b/), javascript: !unformattedTags.match(/\bscript\b/) }; + + return format(languageModes, document, formatParams.range, formatParams.options, settings, enabledModes); + } + return []; + }, [], `Error while formatting range for ${formatParams.textDocument.uri}`, token); + }); + + connection.onDocumentLinks((documentLinkParam, token) => { + return runSafe(() => { + const document = documents.get(documentLinkParam.textDocument.uri); + const links: DocumentLink[] = []; + if (document) { + const documentContext = getDocumentContext(document.uri, workspaceFolders); + languageModes.getAllModesInDocument(document).forEach(m => { + if (m.findDocumentLinks) { + pushAll(links, m.findDocumentLinks(document, documentContext)); + } + }); + } + return links; + }, [], `Error while document links for ${documentLinkParam.textDocument.uri}`, token); + }); + + connection.onDocumentSymbol((documentSymbolParms, token) => { + return runSafe(() => { + const document = documents.get(documentSymbolParms.textDocument.uri); + const symbols: SymbolInformation[] = []; + if (document) { + languageModes.getAllModesInDocument(document).forEach(m => { + if (m.findDocumentSymbols) { + pushAll(symbols, m.findDocumentSymbols(document)); + } + }); + } + return symbols; + }, [], `Error while computing document symbols for ${documentSymbolParms.textDocument.uri}`, token); + }); + + connection.onRequest(DocumentColorRequest.type, (params, token) => { + return runSafe(() => { + const infos: ColorInformation[] = []; + const document = documents.get(params.textDocument.uri); + if (document) { + languageModes.getAllModesInDocument(document).forEach(m => { + if (m.findDocumentColors) { + pushAll(infos, m.findDocumentColors(document)); + } + }); + } + return infos; + }, [], `Error while computing document colors for ${params.textDocument.uri}`, token); + }); + + connection.onRequest(ColorPresentationRequest.type, (params, token) => { + return runSafe(() => { + const document = documents.get(params.textDocument.uri); + if (document) { + const mode = languageModes.getModeAtPosition(document, params.range.start); + if (mode && mode.getColorPresentations) { + return mode.getColorPresentations(document, params.color, params.range); + } + } + return []; + }, [], `Error while computing color presentations for ${params.textDocument.uri}`, token); + }); + + connection.onRequest(TagCloseRequest.type, (params, token) => { + return runSafe(() => { + const document = documents.get(params.textDocument.uri); + if (document) { + const pos = params.position; + if (pos.character > 0) { + const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1)); + if (mode && mode.doAutoClose) { + return mode.doAutoClose(document, pos); + } + } + } + return null; + }, null, `Error while computing tag close actions for ${params.textDocument.uri}`, token); + }); + + connection.onFoldingRanges((params, token) => { + return runSafe(() => { + const document = documents.get(params.textDocument.uri); + if (document) { + return getFoldingRanges(languageModes, document, foldingRangeLimit, token); + } + return null; + }, null, `Error while computing folding regions for ${params.textDocument.uri}`, token); + }); + + connection.onSelectionRanges((params, token) => { + return runSafe(() => { + const document = documents.get(params.textDocument.uri); + if (document) { + return getSelectionRanges(languageModes, document, params.positions); + } + return []; + }, [], `Error while computing selection ranges for ${params.textDocument.uri}`, token); + }); + + connection.onRenameRequest((params, token) => { + return runSafe(() => { + const document = documents.get(params.textDocument.uri); + const position: Position = params.position; + + if (document) { + const htmlMode = languageModes.getMode('html'); + if (htmlMode && htmlMode.doRename) { + return htmlMode.doRename(document, position, params.newName); + } + } + return null; + }, null, `Error while computing rename for ${params.textDocument.uri}`, token); + }); + + connection.onRequest(OnTypeRenameRequest.type, (params, token) => { + return runSafe(() => { + const document = documents.get(params.textDocument.uri); + if (document) { + const pos = params.position; + if (pos.character > 0) { + const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1)); + if (mode && mode.doOnTypeRename) { + return mode.doOnTypeRename(document, pos); + } + } + } + return null; + }, null, `Error while computing synced regions for ${params.textDocument.uri}`, token); + }); + + let semanticTokensProvider: SemanticTokenProvider | undefined; + function getSemanticTokenProvider() { + if (!semanticTokensProvider) { + semanticTokensProvider = newSemanticTokenProvider(languageModes); + } + return semanticTokensProvider; + } + + connection.onRequest(SemanticTokenRequest.type, (params, token) => { + return runSafe(() => { + const document = documents.get(params.textDocument.uri); + if (document) { + return getSemanticTokenProvider().getSemanticTokens(document, params.ranges); + } + return null; + }, null, `Error while computing semantic tokens for ${params.textDocument.uri}`, token); + }); + + connection.onRequest(SemanticTokenLegendRequest.type, (_params, token) => { + return runSafe(() => { + return getSemanticTokenProvider().legend; + }, null, `Error while computing semantic tokens legend`, token); + }); + + connection.onNotification(CustomDataChangedNotification.type, dataPaths => { + fetchHTMLDataProviders(dataPaths, requestService).then(dataProviders => { + languageModes.updateDataProviders(dataProviders); + }); + }); + + // Listen on the connection + connection.listen(); +} diff --git a/extensions/html-language-features/server/src/htmlServerMain.ts b/extensions/html-language-features/server/src/htmlServerMain.ts deleted file mode 100644 index 78385d54719..00000000000 --- a/extensions/html-language-features/server/src/htmlServerMain.ts +++ /dev/null @@ -1,544 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { - createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, RequestType, - DocumentRangeFormattingRequest, Disposable, DocumentSelector, TextDocumentPositionParams, ServerCapabilities, - ConfigurationRequest, ConfigurationParams, DidChangeWorkspaceFoldersNotification, - DocumentColorRequest, ColorPresentationRequest, TextDocumentSyncKind -} from 'vscode-languageserver'; -import { - getLanguageModes, LanguageModes, Settings, TextDocument, Position, Diagnostic, WorkspaceFolder, ColorInformation, - Range, DocumentLink, SymbolInformation, TextDocumentIdentifier -} from './modes/languageModes'; - -import { format } from './modes/formatting'; -import { pushAll } from './utils/arrays'; -import { getDocumentContext } from './utils/documentContext'; -import { URI } from 'vscode-uri'; -import { formatError, runSafe, runSafeAsync } from './utils/runner'; - -import { getFoldingRanges } from './modes/htmlFolding'; -import { getDataProviders } from './customData'; -import { getSelectionRanges } from './modes/selectionRanges'; -import { SemanticTokenProvider, newSemanticTokenProvider } from './modes/semanticTokens'; - -namespace TagCloseRequest { - export const type: RequestType = new RequestType('html/tag'); -} -namespace OnTypeRenameRequest { - export const type: RequestType = new RequestType('html/onTypeRename'); -} - -// experimental: semantic tokens -interface SemanticTokenParams { - textDocument: TextDocumentIdentifier; - ranges?: Range[]; -} -namespace SemanticTokenRequest { - export const type: RequestType = new RequestType('html/semanticTokens'); -} -namespace SemanticTokenLegendRequest { - export const type: RequestType = new RequestType('html/semanticTokenLegend'); -} - -// Create a connection for the server -const connection: IConnection = createConnection(); - -console.log = connection.console.log.bind(connection.console); -console.error = connection.console.error.bind(connection.console); - -process.on('unhandledRejection', (e: any) => { - console.error(formatError(`Unhandled exception`, e)); -}); -process.on('uncaughtException', (e: any) => { - console.error(formatError(`Unhandled exception`, e)); -}); - -// Create a text document manager. -const documents = new TextDocuments(TextDocument); -// Make the text document manager listen on the connection -// for open, change and close text document events -documents.listen(connection); - -let workspaceFolders: WorkspaceFolder[] = []; - -let languageModes: LanguageModes; - -let clientSnippetSupport = false; -let dynamicFormatterRegistration = false; -let scopedSettingsSupport = false; -let workspaceFoldersSupport = false; -let foldingRangeLimit = Number.MAX_VALUE; - -let globalSettings: Settings = {}; -let documentSettings: { [key: string]: Thenable } = {}; -// 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 configRequestParam: ConfigurationParams = { items: [{ scopeUri, section: 'css' }, { scopeUri, section: 'html' }, { scopeUri, section: 'javascript' }] }; - promise = connection.sendRequest(ConfigurationRequest.type, configRequestParam).then(s => ({ css: s[0], html: s[1], javascript: s[2] })); - documentSettings[textDocument.uri] = promise; - } - return promise; - } - return Promise.resolve(undefined); -} - -// After the server has started the client sends an initialize request. The server receives -// in the passed params the rootPath of the workspace plus the client capabilities -connection.onInitialize((params: InitializeParams): InitializeResult => { - const initializationOptions = params.initializationOptions; - - workspaceFolders = (params).workspaceFolders; - if (!Array.isArray(workspaceFolders)) { - workspaceFolders = []; - if (params.rootPath) { - workspaceFolders.push({ name: '', uri: URI.file(params.rootPath).toString() }); - } - } - - const dataPaths: string[] = params.initializationOptions.dataPaths; - const providers = getDataProviders(dataPaths); - - const workspace = { - get settings() { return globalSettings; }, - get folders() { return workspaceFolders; } - }; - - languageModes = getLanguageModes(initializationOptions ? initializationOptions.embeddedLanguages : { css: true, javascript: true }, workspace, params.capabilities, providers); - - documents.onDidClose(e => { - languageModes.onDocumentRemoved(e.document); - }); - connection.onShutdown(() => { - languageModes.dispose(); - }); - - function getClientCapability(name: string, def: T) { - const keys = name.split('.'); - let c: any = params.capabilities; - for (let i = 0; c && i < keys.length; i++) { - if (!c.hasOwnProperty(keys[i])) { - return def; - } - c = c[keys[i]]; - } - return c; - } - - clientSnippetSupport = getClientCapability('textDocument.completion.completionItem.snippetSupport', false); - dynamicFormatterRegistration = getClientCapability('textDocument.rangeFormatting.dynamicRegistration', false) && (typeof params.initializationOptions.provideFormatter !== 'boolean'); - scopedSettingsSupport = getClientCapability('workspace.configuration', false); - workspaceFoldersSupport = getClientCapability('workspace.workspaceFolders', false); - foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE); - const capabilities: ServerCapabilities = { - textDocumentSync: TextDocumentSyncKind.Incremental, - completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/'] } : undefined, - hoverProvider: true, - documentHighlightProvider: true, - documentRangeFormattingProvider: params.initializationOptions.provideFormatter === true, - documentLinkProvider: { resolveProvider: false }, - documentSymbolProvider: true, - definitionProvider: true, - signatureHelpProvider: { triggerCharacters: ['('] }, - referencesProvider: true, - colorProvider: {}, - foldingRangeProvider: true, - selectionRangeProvider: true, - renameProvider: true - }; - return { capabilities }; -}); - -connection.onInitialized(() => { - if (workspaceFoldersSupport) { - connection.client.register(DidChangeWorkspaceFoldersNotification.type); - - connection.onNotification(DidChangeWorkspaceFoldersNotification.type, e => { - const toAdd = e.event.added; - const toRemove = e.event.removed; - const updatedFolders = []; - if (workspaceFolders) { - for (const folder of workspaceFolders) { - if (!toRemove.some(r => r.uri === folder.uri) && !toAdd.some(r => r.uri === folder.uri)) { - updatedFolders.push(folder); - } - } - } - workspaceFolders = updatedFolders.concat(toAdd); - documents.all().forEach(triggerValidation); - }); - } -}); - -let formatterRegistration: Thenable | null = null; - -// The settings have changed. Is send on server activation as well. -connection.onDidChangeConfiguration((change) => { - globalSettings = change.settings; - documentSettings = {}; // reset all document settings - documents.all().forEach(triggerValidation); - - // dynamically enable & disable the formatter - if (dynamicFormatterRegistration) { - const enableFormatter = globalSettings && globalSettings.html && globalSettings.html.format && globalSettings.html.format.enable; - if (enableFormatter) { - if (!formatterRegistration) { - const documentSelector: DocumentSelector = [{ language: 'html' }, { language: 'handlebars' }]; - formatterRegistration = connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector }); - } - } else if (formatterRegistration) { - formatterRegistration.then(r => r.dispose()); - formatterRegistration = null; - } - } -}); - -const pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {}; -const validationDelayMs = 500; - -// The content of a text document has changed. This event is emitted -// when the text document first opened or when its content has changed. -documents.onDidChangeContent(change => { - triggerValidation(change.document); -}); - -// a document has closed: clear all diagnostics -documents.onDidClose(event => { - cleanPendingValidation(event.document); - connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); -}); - -function cleanPendingValidation(textDocument: TextDocument): void { - const request = pendingValidationRequests[textDocument.uri]; - if (request) { - clearTimeout(request); - delete pendingValidationRequests[textDocument.uri]; - } -} - -function triggerValidation(textDocument: TextDocument): void { - cleanPendingValidation(textDocument); - pendingValidationRequests[textDocument.uri] = setTimeout(() => { - delete pendingValidationRequests[textDocument.uri]; - validateTextDocument(textDocument); - }, validationDelayMs); -} - -function isValidationEnabled(languageId: string, settings: Settings = globalSettings) { - const validationSettings = settings && settings.html && settings.html.validate; - if (validationSettings) { - return languageId === 'css' && validationSettings.styles !== false || languageId === 'javascript' && validationSettings.scripts !== false; - } - return true; -} - -async function validateTextDocument(textDocument: TextDocument) { - try { - const version = textDocument.version; - const diagnostics: Diagnostic[] = []; - if (textDocument.languageId === 'html') { - const modes = languageModes.getAllModesInDocument(textDocument); - const settings = await getDocumentSettings(textDocument, () => modes.some(m => !!m.doValidation)); - const latestTextDocument = documents.get(textDocument.uri); - if (latestTextDocument && latestTextDocument.version === version) { // check no new version has come in after in after the async op - modes.forEach(mode => { - if (mode.doValidation && isValidationEnabled(mode.getId(), settings)) { - pushAll(diagnostics, mode.doValidation(latestTextDocument, settings)); - } - }); - connection.sendDiagnostics({ uri: latestTextDocument.uri, diagnostics }); - } - } - } catch (e) { - connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); - } -} - -connection.onCompletion(async (textDocumentPosition, token) => { - return runSafeAsync(async () => { - const document = documents.get(textDocumentPosition.textDocument.uri); - if (!document) { - return null; - } - const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position); - if (!mode || !mode.doComplete) { - return { isIncomplete: true, items: [] }; - } - const doComplete = mode.doComplete!; - - if (mode.getId() !== 'html') { - /* __GDPR__ - "html.embbedded.complete" : { - "languageId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - connection.telemetry.logEvent({ key: 'html.embbedded.complete', value: { languageId: mode.getId() } }); - } - - const settings = await getDocumentSettings(document, () => doComplete.length > 2); - const result = doComplete(document, textDocumentPosition.position, settings); - return result; - - }, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`, token); -}); - -connection.onCompletionResolve((item, token) => { - return runSafe(() => { - const data = item.data; - if (data && data.languageId && data.uri) { - const mode = languageModes.getMode(data.languageId); - const document = documents.get(data.uri); - if (mode && mode.doResolve && document) { - return mode.doResolve(document, item); - } - } - return item; - }, item, `Error while resolving completion proposal`, token); -}); - -connection.onHover((textDocumentPosition, token) => { - return runSafe(() => { - const document = documents.get(textDocumentPosition.textDocument.uri); - if (document) { - const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position); - if (mode && mode.doHover) { - return mode.doHover(document, textDocumentPosition.position); - } - } - return null; - }, null, `Error while computing hover for ${textDocumentPosition.textDocument.uri}`, token); -}); - -connection.onDocumentHighlight((documentHighlightParams, token) => { - return runSafe(() => { - const document = documents.get(documentHighlightParams.textDocument.uri); - if (document) { - const mode = languageModes.getModeAtPosition(document, documentHighlightParams.position); - if (mode && mode.findDocumentHighlight) { - return mode.findDocumentHighlight(document, documentHighlightParams.position); - } - } - return []; - }, [], `Error while computing document highlights for ${documentHighlightParams.textDocument.uri}`, token); -}); - -connection.onDefinition((definitionParams, token) => { - return runSafe(() => { - const document = documents.get(definitionParams.textDocument.uri); - if (document) { - const mode = languageModes.getModeAtPosition(document, definitionParams.position); - if (mode && mode.findDefinition) { - return mode.findDefinition(document, definitionParams.position); - } - } - return []; - }, null, `Error while computing definitions for ${definitionParams.textDocument.uri}`, token); -}); - -connection.onReferences((referenceParams, token) => { - return runSafe(() => { - const document = documents.get(referenceParams.textDocument.uri); - if (document) { - const mode = languageModes.getModeAtPosition(document, referenceParams.position); - if (mode && mode.findReferences) { - return mode.findReferences(document, referenceParams.position); - } - } - return []; - }, [], `Error while computing references for ${referenceParams.textDocument.uri}`, token); -}); - -connection.onSignatureHelp((signatureHelpParms, token) => { - return runSafe(() => { - const document = documents.get(signatureHelpParms.textDocument.uri); - if (document) { - const mode = languageModes.getModeAtPosition(document, signatureHelpParms.position); - if (mode && mode.doSignatureHelp) { - return mode.doSignatureHelp(document, signatureHelpParms.position); - } - } - return null; - }, null, `Error while computing signature help for ${signatureHelpParms.textDocument.uri}`, token); -}); - -connection.onDocumentRangeFormatting(async (formatParams, token) => { - return runSafeAsync(async () => { - const document = documents.get(formatParams.textDocument.uri); - if (document) { - let settings = await getDocumentSettings(document, () => true); - if (!settings) { - settings = globalSettings; - } - const unformattedTags: string = settings && settings.html && settings.html.format && settings.html.format.unformatted || ''; - const enabledModes = { css: !unformattedTags.match(/\bstyle\b/), javascript: !unformattedTags.match(/\bscript\b/) }; - - return format(languageModes, document, formatParams.range, formatParams.options, settings, enabledModes); - } - return []; - }, [], `Error while formatting range for ${formatParams.textDocument.uri}`, token); -}); - -connection.onDocumentLinks((documentLinkParam, token) => { - return runSafe(() => { - const document = documents.get(documentLinkParam.textDocument.uri); - const links: DocumentLink[] = []; - if (document) { - const documentContext = getDocumentContext(document.uri, workspaceFolders); - languageModes.getAllModesInDocument(document).forEach(m => { - if (m.findDocumentLinks) { - pushAll(links, m.findDocumentLinks(document, documentContext)); - } - }); - } - return links; - }, [], `Error while document links for ${documentLinkParam.textDocument.uri}`, token); -}); - -connection.onDocumentSymbol((documentSymbolParms, token) => { - return runSafe(() => { - const document = documents.get(documentSymbolParms.textDocument.uri); - const symbols: SymbolInformation[] = []; - if (document) { - languageModes.getAllModesInDocument(document).forEach(m => { - if (m.findDocumentSymbols) { - pushAll(symbols, m.findDocumentSymbols(document)); - } - }); - } - return symbols; - }, [], `Error while computing document symbols for ${documentSymbolParms.textDocument.uri}`, token); -}); - -connection.onRequest(DocumentColorRequest.type, (params, token) => { - return runSafe(() => { - const infos: ColorInformation[] = []; - const document = documents.get(params.textDocument.uri); - if (document) { - languageModes.getAllModesInDocument(document).forEach(m => { - if (m.findDocumentColors) { - pushAll(infos, m.findDocumentColors(document)); - } - }); - } - return infos; - }, [], `Error while computing document colors for ${params.textDocument.uri}`, token); -}); - -connection.onRequest(ColorPresentationRequest.type, (params, token) => { - return runSafe(() => { - const document = documents.get(params.textDocument.uri); - if (document) { - const mode = languageModes.getModeAtPosition(document, params.range.start); - if (mode && mode.getColorPresentations) { - return mode.getColorPresentations(document, params.color, params.range); - } - } - return []; - }, [], `Error while computing color presentations for ${params.textDocument.uri}`, token); -}); - -connection.onRequest(TagCloseRequest.type, (params, token) => { - return runSafe(() => { - const document = documents.get(params.textDocument.uri); - if (document) { - const pos = params.position; - if (pos.character > 0) { - const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1)); - if (mode && mode.doAutoClose) { - return mode.doAutoClose(document, pos); - } - } - } - return null; - }, null, `Error while computing tag close actions for ${params.textDocument.uri}`, token); -}); - -connection.onFoldingRanges((params, token) => { - return runSafe(() => { - const document = documents.get(params.textDocument.uri); - if (document) { - return getFoldingRanges(languageModes, document, foldingRangeLimit, token); - } - return null; - }, null, `Error while computing folding regions for ${params.textDocument.uri}`, token); -}); - -connection.onSelectionRanges((params, token) => { - return runSafe(() => { - const document = documents.get(params.textDocument.uri); - if (document) { - return getSelectionRanges(languageModes, document, params.positions); - } - return []; - }, [], `Error while computing selection ranges for ${params.textDocument.uri}`, token); -}); - -connection.onRenameRequest((params, token) => { - return runSafe(() => { - const document = documents.get(params.textDocument.uri); - const position: Position = params.position; - - if (document) { - const htmlMode = languageModes.getMode('html'); - if (htmlMode && htmlMode.doRename) { - return htmlMode.doRename(document, position, params.newName); - } - } - return null; - }, null, `Error while computing rename for ${params.textDocument.uri}`, token); -}); - -connection.onRequest(OnTypeRenameRequest.type, (params, token) => { - return runSafe(() => { - const document = documents.get(params.textDocument.uri); - if (document) { - const pos = params.position; - if (pos.character > 0) { - const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1)); - if (mode && mode.doOnTypeRename) { - return mode.doOnTypeRename(document, pos); - } - } - } - return null; - }, null, `Error while computing synced regions for ${params.textDocument.uri}`, token); -}); - -let semanticTokensProvider: SemanticTokenProvider | undefined; -function getSemanticTokenProvider() { - if (!semanticTokensProvider) { - semanticTokensProvider = newSemanticTokenProvider(languageModes); - } - return semanticTokensProvider; -} - -connection.onRequest(SemanticTokenRequest.type, (params, token) => { - return runSafe(() => { - const document = documents.get(params.textDocument.uri); - if (document) { - return getSemanticTokenProvider().getSemanticTokens(document, params.ranges); - } - return null; - }, null, `Error while computing semantic tokens for ${params.textDocument.uri}`, token); -}); - -connection.onRequest(SemanticTokenLegendRequest.type, (_params, token) => { - return runSafe(() => { - return getSemanticTokenProvider().legend; - }, null, `Error while computing semantic tokens legend`, token); -}); - - -// Listen on the connection -connection.listen(); diff --git a/extensions/html-language-features/server/src/modes/cssMode.ts b/extensions/html-language-features/server/src/modes/cssMode.ts index e705f44bbab..dafc79f0feb 100644 --- a/extensions/html-language-features/server/src/modes/cssMode.ts +++ b/extensions/html-language-features/server/src/modes/cssMode.ts @@ -5,7 +5,7 @@ import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache'; import { Stylesheet, LanguageService as CSSLanguageService } from 'vscode-css-languageservice'; -import { FoldingRange, LanguageMode, Workspace, Color, TextDocument, Position, Range, CompletionList } from './languageModes'; +import { FoldingRange, LanguageMode, Workspace, Color, TextDocument, Position, Range, CompletionList, DocumentContext } from './languageModes'; import { HTMLDocumentRegions, CSS_STYLE_RULE } from './embeddedSupport'; export function getCSSMode(cssLanguageService: CSSLanguageService, documentRegions: LanguageModelCache, workspace: Workspace): LanguageMode { @@ -20,10 +20,10 @@ export function getCSSMode(cssLanguageService: CSSLanguageService, documentRegio let embedded = embeddedCSSDocuments.get(document); return cssLanguageService.doValidation(embedded, cssStylesheets.get(embedded), settings && settings.css); }, - doComplete(document: TextDocument, position: Position, _settings = workspace.settings) { + doComplete(document: TextDocument, position: Position, documentContext: DocumentContext, _settings = workspace.settings) { let embedded = embeddedCSSDocuments.get(document); const stylesheet = cssStylesheets.get(embedded); - return cssLanguageService.doComplete(embedded, position, stylesheet) || CompletionList.create(); + return cssLanguageService.doComplete2(embedded, position, stylesheet, documentContext) || CompletionList.create(); }, doHover(document: TextDocument, position: Position) { let embedded = embeddedCSSDocuments.get(document); diff --git a/extensions/html-language-features/server/src/modes/htmlMode.ts b/extensions/html-language-features/server/src/modes/htmlMode.ts index 4a3be8244a4..8faf51e15fb 100644 --- a/extensions/html-language-features/server/src/modes/htmlMode.ts +++ b/extensions/html-language-features/server/src/modes/htmlMode.ts @@ -7,10 +7,9 @@ import { getLanguageModelCache } from '../languageModelCache'; import { LanguageService as HTMLLanguageService, HTMLDocument, DocumentContext, FormattingOptions, HTMLFormatConfiguration, SelectionRange, - TextDocument, Position, Range, CompletionItem, FoldingRange, + TextDocument, Position, Range, FoldingRange, LanguageMode, Workspace } from './languageModes'; -import { getPathCompletionParticipant } from './pathCompletion'; export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace: Workspace): LanguageMode { let htmlDocuments = getLanguageModelCache(10, 60, document => htmlLanguageService.parseHTMLDocument(document)); @@ -21,19 +20,15 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace: getSelectionRange(document: TextDocument, position: Position): SelectionRange { return htmlLanguageService.getSelectionRanges(document, [position])[0]; }, - doComplete(document: TextDocument, position: Position, settings = workspace.settings) { + doComplete(document: TextDocument, position: Position, documentContext: DocumentContext, settings = workspace.settings) { let options = settings && settings.html && settings.html.suggest; let doAutoComplete = settings && settings.html && settings.html.autoClosingTags; if (doAutoComplete) { options.hideAutoCompleteProposals = true; } - let pathCompletionProposals: CompletionItem[] = []; - let participants = [getPathCompletionParticipant(document, workspace.folders, pathCompletionProposals)]; - htmlLanguageService.setCompletionParticipants(participants); const htmlDocument = htmlDocuments.get(document); - let completionList = htmlLanguageService.doComplete(document, position, htmlDocument, options); - completionList.items.push(...pathCompletionProposals); + let completionList = htmlLanguageService.doComplete2(document, position, htmlDocument, documentContext, options); return completionList; }, doHover(document: TextDocument, position: Position) { diff --git a/extensions/html-language-features/server/src/modes/javascriptMode.ts b/extensions/html-language-features/server/src/modes/javascriptMode.ts index 9841c7167ec..789fb4b6332 100644 --- a/extensions/html-language-features/server/src/modes/javascriptMode.ts +++ b/extensions/html-language-features/server/src/modes/javascriptMode.ts @@ -8,20 +8,21 @@ import { SymbolInformation, SymbolKind, CompletionItem, Location, SignatureHelp, SignatureInformation, ParameterInformation, Definition, TextEdit, TextDocument, Diagnostic, DiagnosticSeverity, Range, CompletionItemKind, Hover, MarkedString, DocumentHighlight, DocumentHighlightKind, CompletionList, Position, FormattingOptions, FoldingRange, FoldingRangeKind, SelectionRange, - LanguageMode, Settings, SemanticTokenData, Workspace + LanguageMode, Settings, SemanticTokenData, Workspace, DocumentContext } from './languageModes'; import { getWordAtText, startsWith, isWhitespaceOnly, repeat } from '../utils/strings'; import { HTMLDocumentRegions } from './embeddedSupport'; import * as ts from 'typescript'; -import { join } from 'path'; import { getSemanticTokens, getSemanticTokenLegend } from './javascriptSemanticTokens'; +import { joinPath } from '../requests'; + const JS_WORD_REGEX = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g; -let jquery_d_ts = join(__dirname, '../lib/jquery.d.ts'); // when packaged +let jquery_d_ts = joinPath(__dirname, '../lib/jquery.d.ts'); // when packaged if (!ts.sys.fileExists(jquery_d_ts)) { - jquery_d_ts = join(__dirname, '../../lib/jquery.d.ts'); // from source + jquery_d_ts = joinPath(__dirname, '../../lib/jquery.d.ts'); // from source } export function getJavaScriptMode(documentRegions: LanguageModelCache, languageId: 'javascript' | 'typescript', workspace: Workspace): LanguageMode { @@ -64,7 +65,8 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache '', - getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options) + getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options), + }; let jsLanguageService = ts.createLanguageService(host); @@ -88,7 +90,7 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { updateCurrentTextDocument(document); let offset = currentTextDocument.offsetAt(position); let completions = jsLanguageService.getCompletionsAtPosition(workingFile, offset, { includeExternalModuleExports: false, includeInsertTextCompletions: false }); diff --git a/extensions/html-language-features/server/src/modes/languageModes.ts b/extensions/html-language-features/server/src/modes/languageModes.ts index a69a2516dcf..69ec4ce7a2a 100644 --- a/extensions/html-language-features/server/src/modes/languageModes.ts +++ b/extensions/html-language-features/server/src/modes/languageModes.ts @@ -16,6 +16,7 @@ import { getCSSMode } from './cssMode'; import { getDocumentRegions, HTMLDocumentRegions } from './embeddedSupport'; import { getHTMLMode } from './htmlMode'; import { getJavaScriptMode } from './javascriptMode'; +import { RequestService } from '../requests'; export * from 'vscode-html-languageservice'; export { WorkspaceFolder } from 'vscode-languageserver'; @@ -42,7 +43,7 @@ export interface LanguageMode { getId(): string; getSelectionRange?: (document: TextDocument, position: Position) => SelectionRange; doValidation?: (document: TextDocument, settings?: Settings) => Diagnostic[]; - doComplete?: (document: TextDocument, position: Position, settings?: Settings) => CompletionList; + doComplete?: (document: TextDocument, position: Position, documentContext: DocumentContext, settings?: Settings) => Promise; doResolve?: (document: TextDocument, item: CompletionItem) => CompletionItem; doHover?: (document: TextDocument, position: Position) => Hover | null; doSignatureHelp?: (document: TextDocument, position: Position) => SignatureHelp | null; @@ -66,6 +67,7 @@ export interface LanguageMode { } export interface LanguageModes { + updateDataProviders(dataProviders: IHTMLDataProvider[]): void; getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined; getModesInRange(document: TextDocument, range: Range): LanguageModeRange[]; getAllModes(): LanguageMode[]; @@ -80,9 +82,9 @@ export interface LanguageModeRange extends Range { attributeValue?: boolean; } -export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }, workspace: Workspace, clientCapabilities: ClientCapabilities, customDataProviders?: IHTMLDataProvider[]): LanguageModes { - const htmlLanguageService = getHTMLLanguageService({ customDataProviders, clientCapabilities }); - const cssLanguageService = getCSSLanguageService({ clientCapabilities }); +export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }, workspace: Workspace, clientCapabilities: ClientCapabilities, requestService: RequestService): LanguageModes { + const htmlLanguageService = getHTMLLanguageService({ clientCapabilities, fileSystemProvider: requestService }); + const cssLanguageService = getCSSLanguageService({ clientCapabilities, fileSystemProvider: requestService }); let documentRegions = getLanguageModelCache(10, 60, document => getDocumentRegions(htmlLanguageService, document)); @@ -99,6 +101,9 @@ export function getLanguageModes(supportedLanguages: { [languageId: string]: boo modes['typescript'] = getJavaScriptMode(documentRegions, 'typescript', workspace); } return { + async updateDataProviders(dataProviders: IHTMLDataProvider[]): Promise { + htmlLanguageService.setDataProviders(true, dataProviders); + }, getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined { let languageId = documentRegions.get(document).getLanguageAtPosition(position); if (languageId) { diff --git a/extensions/html-language-features/server/src/modes/pathCompletion.ts b/extensions/html-language-features/server/src/modes/pathCompletion.ts deleted file mode 100644 index d522efdc0df..00000000000 --- a/extensions/html-language-features/server/src/modes/pathCompletion.ts +++ /dev/null @@ -1,183 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as path from 'path'; -import * as fs from 'fs'; -import { URI } from 'vscode-uri'; -import { ICompletionParticipant, TextDocument, CompletionItemKind, CompletionItem, TextEdit, Range, Position, WorkspaceFolder } from './languageModes'; -import { startsWith } from '../utils/strings'; -import { contains } from '../utils/arrays'; - -export function getPathCompletionParticipant( - document: TextDocument, - workspaceFolders: WorkspaceFolder[], - result: CompletionItem[] -): ICompletionParticipant { - return { - onHtmlAttributeValue: ({ tag, attribute, value: valueBeforeCursor, range }) => { - const fullValue = stripQuotes(document.getText(range)); - - if (shouldDoPathCompletion(tag, attribute, fullValue)) { - if (workspaceFolders.length === 0) { - return; - } - const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders); - - const paths = providePaths(valueBeforeCursor, URI.parse(document.uri).fsPath, workspaceRoot); - result.push(...paths.map(p => pathToSuggestion(p, valueBeforeCursor, fullValue, range))); - } - } - }; -} - -function stripQuotes(fullValue: string) { - if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) { - return fullValue.slice(1, -1); - } else { - return fullValue; - } -} - -function shouldDoPathCompletion(tag: string, attr: string, value: string) { - if (startsWith(value, 'http') || startsWith(value, 'https') || startsWith(value, '//')) { - return false; - } - - if (PATH_TAG_AND_ATTR[tag]) { - if (typeof PATH_TAG_AND_ATTR[tag] === 'string') { - return PATH_TAG_AND_ATTR[tag] === attr; - } else { - return contains(PATH_TAG_AND_ATTR[tag], attr); - } - } - - return false; -} - -/** - * Get a list of path suggestions. Folder suggestions are suffixed with a slash. - */ -function providePaths(valueBeforeCursor: string, activeDocFsPath: string, root?: string): string[] { - const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/'); - const valueBeforeLastSlash = valueBeforeCursor.slice(0, lastIndexOfSlash + 1); - - const startsWithSlash = startsWith(valueBeforeCursor, '/'); - let parentDir: string; - if (startsWithSlash) { - if (!root) { - return []; - } - parentDir = path.resolve(root, '.' + valueBeforeLastSlash); - } else { - parentDir = path.resolve(activeDocFsPath, '..', valueBeforeLastSlash); - } - - try { - const paths = fs.readdirSync(parentDir).map(f => { - return isDir(path.resolve(parentDir, f)) - ? f + '/' - : f; - }); - return paths.filter(p => p[0] !== '.'); - } catch (e) { - return []; - } -} - -function isDir(p: string) { - try { - return fs.statSync(p).isDirectory(); - } catch (e) { - return false; - } -} - -function pathToSuggestion(p: string, valueBeforeCursor: string, fullValue: string, range: Range): CompletionItem { - const isDir = p[p.length - 1] === '/'; - - let replaceRange: Range; - const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/'); - if (lastIndexOfSlash === -1) { - replaceRange = shiftRange(range, 1, -1); - } else { - // For cases where cursor is in the middle of attribute value, like ', { + test('HTML JavaScript Completions', async () => { + await testCompletionFor('', { items: [ { label: 'location', resultText: '' }, ] }); - testCompletionFor('', { + await testCompletionFor('', { items: [ { label: 'getJSON', resultText: '' }, ] @@ -96,8 +100,8 @@ suite('HTML Path Completion', () => { const indexHtmlUri = URI.file(path.resolve(fixtureRoot, 'index.html')).toString(); const aboutHtmlUri = URI.file(path.resolve(fixtureRoot, 'about/about.html')).toString(); - test('Basics - Correct label/kind/result/command', () => { - testCompletionFor('