html headless

This commit is contained in:
Martin Aeschlimann 2020-06-19 23:35:45 +02:00
parent c23285f8c8
commit d16e306c2e
32 changed files with 1529 additions and 967 deletions

View file

@ -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);
}
}

View file

@ -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<void>();
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<ExperimentalConfig>('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<string[]>('customData');
if (customDataInspect) {
collect(customDataInspect.workspaceFolderValue, folderUri);
if (i === 0) {
if (workspace.workspaceFile) {
collect(customDataInspect.workspaceValue, workspace.workspaceFile);
}
collect(customDataInspect.globalValue, folderUri);
}
}
}
return dataPaths;
}
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;
}

View file

@ -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<string[]> = new NotificationType('html/customDataChanged');
}
namespace TagCloseRequest {
export const type: RequestType<TextDocumentPositionParams, string, any, any> = 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<any> {
return telemetryReporter ? telemetryReporter.dispose() : Promise.resolve(null);
}

View file

@ -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: '' };
}
}

View file

@ -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];
}
}));
});
});
}
};
}

View file

@ -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<string, FileStat, any, any> = new RequestType('fs/stat');
}
export namespace FsReadDirRequest {
export const type: RequestType<string, [string, FileType][], any, any> = new RequestType('fs/readDir');
}
export 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<string>;
stat(uri: string): Promise<FileStat>;
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) });
}

View file

@ -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;

View file

@ -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')
}
});

View file

@ -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": {

View file

@ -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;

View file

@ -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'

View file

@ -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"
},

View file

@ -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, {});

View file

@ -4,26 +4,35 @@
*--------------------------------------------------------------------------------------------*/
import { IHTMLDataProvider, newHTMLDataProvider } from 'vscode-html-languageservice';
import * as fs from 'fs';
import { RequestService } from './requests';
export function getDataProviders(dataPaths?: string[]): IHTMLDataProvider[] {
if (!dataPaths) {
return [];
}
const providers: IHTMLDataProvider[] = [];
dataPaths.forEach((path, i) => {
export function fetchHTMLDataProviders(dataPaths: string[], requestService: RequestService): Promise<IHTMLDataProvider[]> {
const providers = dataPaths.map(async p => {
try {
if (fs.existsSync(path)) {
const htmlData = JSON.parse(fs.readFileSync(path, 'utf-8'));
providers.push(newHTMLDataProvider(`customProvider${i}`, htmlData));
}
} catch (err) {
console.log(`Failed to load tag from ${path}`);
const content = await requestService.getContent(p);
return parseHTMLData(p, content);
} catch (e) {
return newHTMLDataProvider(p, { version: 1 });
}
});
return providers;
}
return Promise.all(providers);
}
function parseHTMLData(id: string, source: string): IHTMLDataProvider {
let rawData: any;
try {
rawData = JSON.parse(source);
} catch (err) {
return newHTMLDataProvider(id, { version: 1 });
}
return newHTMLDataProvider(id, {
version: rawData.version || 1,
tags: rawData.tags || [],
globalAttributes: rawData.globalAttributes || [],
valueSets: rawData.valueSets || []
});
}

View file

@ -0,0 +1,559 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
Connection, TextDocuments, InitializeParams, InitializeResult, RequestType,
DocumentRangeFormattingRequest, Disposable, DocumentSelector, TextDocumentPositionParams, ServerCapabilities,
ConfigurationRequest, ConfigurationParams, DidChangeWorkspaceFoldersNotification,
DocumentColorRequest, ColorPresentationRequest, TextDocumentSyncKind, NotificationType
} from 'vscode-languageserver';
import {
getLanguageModes, LanguageModes, Settings, TextDocument, Position, Diagnostic, WorkspaceFolder, ColorInformation,
Range, DocumentLink, SymbolInformation, TextDocumentIdentifier
} from './modes/languageModes';
import { format } from './modes/formatting';
import { pushAll } from './utils/arrays';
import { getDocumentContext } from './utils/documentContext';
import { URI } from 'vscode-uri';
import { formatError, runSafe, runSafeAsync } from './utils/runner';
import { getFoldingRanges } from './modes/htmlFolding';
import { fetchHTMLDataProviders } from './customData';
import { getSelectionRanges } from './modes/selectionRanges';
import { SemanticTokenProvider, newSemanticTokenProvider } from './modes/semanticTokens';
import { RequestService, getRequestService } from './requests';
namespace CustomDataChangedNotification {
export const type: NotificationType<string[]> = new NotificationType('html/customDataChanged');
}
namespace TagCloseRequest {
export const type: RequestType<TextDocumentPositionParams, string | null, any, any> = new RequestType('html/tag');
}
namespace OnTypeRenameRequest {
export const type: RequestType<TextDocumentPositionParams, Range[] | null, any, any> = new RequestType('html/onTypeRename');
}
// experimental: semantic tokens
interface SemanticTokenParams {
textDocument: TextDocumentIdentifier;
ranges?: Range[];
}
namespace SemanticTokenRequest {
export const type: RequestType<SemanticTokenParams, number[] | null, any, any> = new RequestType('html/semanticTokens');
}
namespace SemanticTokenLegendRequest {
export const type: RequestType<void, { types: string[]; modifiers: string[] } | null, any, any> = new RequestType('html/semanticTokenLegend');
}
export interface RuntimeEnvironment {
file?: RequestService;
http?: RequestService
configureHttpRequests?(proxy: string, strictSSL: boolean): void;
}
export function startServer(connection: Connection, runtime: RuntimeEnvironment) {
// Create a text document manager.
const documents = new TextDocuments(TextDocument);
// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);
let workspaceFolders: WorkspaceFolder[] = [];
let languageModes: LanguageModes;
let clientSnippetSupport = false;
let dynamicFormatterRegistration = false;
let scopedSettingsSupport = false;
let workspaceFoldersSupport = false;
let foldingRangeLimit = Number.MAX_VALUE;
const notReady = () => Promise.reject('Not Ready');
let requestService: RequestService = { getContent: notReady, stat: notReady, readDirectory: notReady };
let globalSettings: Settings = {};
let documentSettings: { [key: string]: Thenable<Settings> } = {};
// remove document settings on close
documents.onDidClose(e => {
delete documentSettings[e.document.uri];
});
function getDocumentSettings(textDocument: TextDocument, needsDocumentSettings: () => boolean): Thenable<Settings | undefined> {
if (scopedSettingsSupport && needsDocumentSettings()) {
let promise = documentSettings[textDocument.uri];
if (!promise) {
const scopeUri = textDocument.uri;
const configRequestParam: ConfigurationParams = { items: [{ scopeUri, section: 'css' }, { scopeUri, section: 'html' }, { scopeUri, section: 'javascript' }] };
promise = connection.sendRequest(ConfigurationRequest.type, configRequestParam).then(s => ({ css: s[0], html: s[1], javascript: s[2] }));
documentSettings[textDocument.uri] = promise;
}
return promise;
}
return Promise.resolve(undefined);
}
// After the server has started the client sends an initialize request. The server receives
// in the passed params the rootPath of the workspace plus the client capabilities
connection.onInitialize((params: InitializeParams): InitializeResult => {
const initializationOptions = params.initializationOptions;
workspaceFolders = (<any>params).workspaceFolders;
if (!Array.isArray(workspaceFolders)) {
workspaceFolders = [];
if (params.rootPath) {
workspaceFolders.push({ name: '', uri: URI.file(params.rootPath).toString() });
}
}
requestService = getRequestService(params.initializationOptions.handledSchemas || ['file'], connection, runtime);
const workspace = {
get settings() { return globalSettings; },
get folders() { return workspaceFolders; }
};
languageModes = getLanguageModes(initializationOptions ? initializationOptions.embeddedLanguages : { css: true, javascript: true }, workspace, params.capabilities, requestService);
const dataPaths: string[] = params.initializationOptions.dataPaths || [];
fetchHTMLDataProviders(dataPaths, requestService).then(dataProviders => {
languageModes.updateDataProviders(dataProviders);
});
documents.onDidClose(e => {
languageModes.onDocumentRemoved(e.document);
});
connection.onShutdown(() => {
languageModes.dispose();
});
function getClientCapability<T>(name: string, def: T) {
const keys = name.split('.');
let c: any = params.capabilities;
for (let i = 0; c && i < keys.length; i++) {
if (!c.hasOwnProperty(keys[i])) {
return def;
}
c = c[keys[i]];
}
return c;
}
clientSnippetSupport = getClientCapability('textDocument.completion.completionItem.snippetSupport', false);
dynamicFormatterRegistration = getClientCapability('textDocument.rangeFormatting.dynamicRegistration', false) && (typeof params.initializationOptions.provideFormatter !== 'boolean');
scopedSettingsSupport = getClientCapability('workspace.configuration', false);
workspaceFoldersSupport = getClientCapability('workspace.workspaceFolders', false);
foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE);
const capabilities: ServerCapabilities = {
textDocumentSync: TextDocumentSyncKind.Incremental,
completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/'] } : undefined,
hoverProvider: true,
documentHighlightProvider: true,
documentRangeFormattingProvider: params.initializationOptions.provideFormatter === true,
documentLinkProvider: { resolveProvider: false },
documentSymbolProvider: true,
definitionProvider: true,
signatureHelpProvider: { triggerCharacters: ['('] },
referencesProvider: true,
colorProvider: {},
foldingRangeProvider: true,
selectionRangeProvider: true,
renameProvider: true
};
return { capabilities };
});
connection.onInitialized(() => {
if (workspaceFoldersSupport) {
connection.client.register(DidChangeWorkspaceFoldersNotification.type);
connection.onNotification(DidChangeWorkspaceFoldersNotification.type, e => {
const toAdd = e.event.added;
const toRemove = e.event.removed;
const updatedFolders = [];
if (workspaceFolders) {
for (const folder of workspaceFolders) {
if (!toRemove.some(r => r.uri === folder.uri) && !toAdd.some(r => r.uri === folder.uri)) {
updatedFolders.push(folder);
}
}
}
workspaceFolders = updatedFolders.concat(toAdd);
documents.all().forEach(triggerValidation);
});
}
});
let formatterRegistration: Thenable<Disposable> | null = null;
// The settings have changed. Is send on server activation as well.
connection.onDidChangeConfiguration((change) => {
globalSettings = change.settings;
documentSettings = {}; // reset all document settings
documents.all().forEach(triggerValidation);
// dynamically enable & disable the formatter
if (dynamicFormatterRegistration) {
const enableFormatter = globalSettings && globalSettings.html && globalSettings.html.format && globalSettings.html.format.enable;
if (enableFormatter) {
if (!formatterRegistration) {
const documentSelector: DocumentSelector = [{ language: 'html' }, { language: 'handlebars' }];
formatterRegistration = connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector });
}
} else if (formatterRegistration) {
formatterRegistration.then(r => r.dispose());
formatterRegistration = null;
}
}
});
const pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {};
const validationDelayMs = 500;
// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(change => {
triggerValidation(change.document);
});
// a document has closed: clear all diagnostics
documents.onDidClose(event => {
cleanPendingValidation(event.document);
connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
});
function cleanPendingValidation(textDocument: TextDocument): void {
const request = pendingValidationRequests[textDocument.uri];
if (request) {
clearTimeout(request);
delete pendingValidationRequests[textDocument.uri];
}
}
function triggerValidation(textDocument: TextDocument): void {
cleanPendingValidation(textDocument);
pendingValidationRequests[textDocument.uri] = setTimeout(() => {
delete pendingValidationRequests[textDocument.uri];
validateTextDocument(textDocument);
}, validationDelayMs);
}
function isValidationEnabled(languageId: string, settings: Settings = globalSettings) {
const validationSettings = settings && settings.html && settings.html.validate;
if (validationSettings) {
return languageId === 'css' && validationSettings.styles !== false || languageId === 'javascript' && validationSettings.scripts !== false;
}
return true;
}
async function validateTextDocument(textDocument: TextDocument) {
try {
const version = textDocument.version;
const diagnostics: Diagnostic[] = [];
if (textDocument.languageId === 'html') {
const modes = languageModes.getAllModesInDocument(textDocument);
const settings = await getDocumentSettings(textDocument, () => modes.some(m => !!m.doValidation));
const latestTextDocument = documents.get(textDocument.uri);
if (latestTextDocument && latestTextDocument.version === version) { // check no new version has come in after in after the async op
modes.forEach(mode => {
if (mode.doValidation && isValidationEnabled(mode.getId(), settings)) {
pushAll(diagnostics, mode.doValidation(latestTextDocument, settings));
}
});
connection.sendDiagnostics({ uri: latestTextDocument.uri, diagnostics });
}
}
} catch (e) {
connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e));
}
}
connection.onCompletion(async (textDocumentPosition, token) => {
return runSafeAsync(async () => {
const document = documents.get(textDocumentPosition.textDocument.uri);
if (!document) {
return null;
}
const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
if (!mode || !mode.doComplete) {
return { isIncomplete: true, items: [] };
}
const doComplete = mode.doComplete!;
if (mode.getId() !== 'html') {
/* __GDPR__
"html.embbedded.complete" : {
"languageId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
connection.telemetry.logEvent({ key: 'html.embbedded.complete', value: { languageId: mode.getId() } });
}
const settings = await getDocumentSettings(document, () => doComplete.length > 2);
const documentContext = getDocumentContext(document.uri, workspaceFolders);
return doComplete(document, textDocumentPosition.position, documentContext, settings);
}, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`, token);
});
connection.onCompletionResolve((item, token) => {
return runSafe(() => {
const data = item.data;
if (data && data.languageId && data.uri) {
const mode = languageModes.getMode(data.languageId);
const document = documents.get(data.uri);
if (mode && mode.doResolve && document) {
return mode.doResolve(document, item);
}
}
return item;
}, item, `Error while resolving completion proposal`, token);
});
connection.onHover((textDocumentPosition, token) => {
return runSafe(() => {
const document = documents.get(textDocumentPosition.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
if (mode && mode.doHover) {
return mode.doHover(document, textDocumentPosition.position);
}
}
return null;
}, null, `Error while computing hover for ${textDocumentPosition.textDocument.uri}`, token);
});
connection.onDocumentHighlight((documentHighlightParams, token) => {
return runSafe(() => {
const document = documents.get(documentHighlightParams.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, documentHighlightParams.position);
if (mode && mode.findDocumentHighlight) {
return mode.findDocumentHighlight(document, documentHighlightParams.position);
}
}
return [];
}, [], `Error while computing document highlights for ${documentHighlightParams.textDocument.uri}`, token);
});
connection.onDefinition((definitionParams, token) => {
return runSafe(() => {
const document = documents.get(definitionParams.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, definitionParams.position);
if (mode && mode.findDefinition) {
return mode.findDefinition(document, definitionParams.position);
}
}
return [];
}, null, `Error while computing definitions for ${definitionParams.textDocument.uri}`, token);
});
connection.onReferences((referenceParams, token) => {
return runSafe(() => {
const document = documents.get(referenceParams.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, referenceParams.position);
if (mode && mode.findReferences) {
return mode.findReferences(document, referenceParams.position);
}
}
return [];
}, [], `Error while computing references for ${referenceParams.textDocument.uri}`, token);
});
connection.onSignatureHelp((signatureHelpParms, token) => {
return runSafe(() => {
const document = documents.get(signatureHelpParms.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, signatureHelpParms.position);
if (mode && mode.doSignatureHelp) {
return mode.doSignatureHelp(document, signatureHelpParms.position);
}
}
return null;
}, null, `Error while computing signature help for ${signatureHelpParms.textDocument.uri}`, token);
});
connection.onDocumentRangeFormatting(async (formatParams, token) => {
return runSafeAsync(async () => {
const document = documents.get(formatParams.textDocument.uri);
if (document) {
let settings = await getDocumentSettings(document, () => true);
if (!settings) {
settings = globalSettings;
}
const unformattedTags: string = settings && settings.html && settings.html.format && settings.html.format.unformatted || '';
const enabledModes = { css: !unformattedTags.match(/\bstyle\b/), javascript: !unformattedTags.match(/\bscript\b/) };
return format(languageModes, document, formatParams.range, formatParams.options, settings, enabledModes);
}
return [];
}, [], `Error while formatting range for ${formatParams.textDocument.uri}`, token);
});
connection.onDocumentLinks((documentLinkParam, token) => {
return runSafe(() => {
const document = documents.get(documentLinkParam.textDocument.uri);
const links: DocumentLink[] = [];
if (document) {
const documentContext = getDocumentContext(document.uri, workspaceFolders);
languageModes.getAllModesInDocument(document).forEach(m => {
if (m.findDocumentLinks) {
pushAll(links, m.findDocumentLinks(document, documentContext));
}
});
}
return links;
}, [], `Error while document links for ${documentLinkParam.textDocument.uri}`, token);
});
connection.onDocumentSymbol((documentSymbolParms, token) => {
return runSafe(() => {
const document = documents.get(documentSymbolParms.textDocument.uri);
const symbols: SymbolInformation[] = [];
if (document) {
languageModes.getAllModesInDocument(document).forEach(m => {
if (m.findDocumentSymbols) {
pushAll(symbols, m.findDocumentSymbols(document));
}
});
}
return symbols;
}, [], `Error while computing document symbols for ${documentSymbolParms.textDocument.uri}`, token);
});
connection.onRequest(DocumentColorRequest.type, (params, token) => {
return runSafe(() => {
const infos: ColorInformation[] = [];
const document = documents.get(params.textDocument.uri);
if (document) {
languageModes.getAllModesInDocument(document).forEach(m => {
if (m.findDocumentColors) {
pushAll(infos, m.findDocumentColors(document));
}
});
}
return infos;
}, [], `Error while computing document colors for ${params.textDocument.uri}`, token);
});
connection.onRequest(ColorPresentationRequest.type, (params, token) => {
return runSafe(() => {
const document = documents.get(params.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, params.range.start);
if (mode && mode.getColorPresentations) {
return mode.getColorPresentations(document, params.color, params.range);
}
}
return [];
}, [], `Error while computing color presentations for ${params.textDocument.uri}`, token);
});
connection.onRequest(TagCloseRequest.type, (params, token) => {
return runSafe(() => {
const document = documents.get(params.textDocument.uri);
if (document) {
const pos = params.position;
if (pos.character > 0) {
const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1));
if (mode && mode.doAutoClose) {
return mode.doAutoClose(document, pos);
}
}
}
return null;
}, null, `Error while computing tag close actions for ${params.textDocument.uri}`, token);
});
connection.onFoldingRanges((params, token) => {
return runSafe(() => {
const document = documents.get(params.textDocument.uri);
if (document) {
return getFoldingRanges(languageModes, document, foldingRangeLimit, token);
}
return null;
}, null, `Error while computing folding regions for ${params.textDocument.uri}`, token);
});
connection.onSelectionRanges((params, token) => {
return runSafe(() => {
const document = documents.get(params.textDocument.uri);
if (document) {
return getSelectionRanges(languageModes, document, params.positions);
}
return [];
}, [], `Error while computing selection ranges for ${params.textDocument.uri}`, token);
});
connection.onRenameRequest((params, token) => {
return runSafe(() => {
const document = documents.get(params.textDocument.uri);
const position: Position = params.position;
if (document) {
const htmlMode = languageModes.getMode('html');
if (htmlMode && htmlMode.doRename) {
return htmlMode.doRename(document, position, params.newName);
}
}
return null;
}, null, `Error while computing rename for ${params.textDocument.uri}`, token);
});
connection.onRequest(OnTypeRenameRequest.type, (params, token) => {
return runSafe(() => {
const document = documents.get(params.textDocument.uri);
if (document) {
const pos = params.position;
if (pos.character > 0) {
const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1));
if (mode && mode.doOnTypeRename) {
return mode.doOnTypeRename(document, pos);
}
}
}
return null;
}, null, `Error while computing synced regions for ${params.textDocument.uri}`, token);
});
let semanticTokensProvider: SemanticTokenProvider | undefined;
function getSemanticTokenProvider() {
if (!semanticTokensProvider) {
semanticTokensProvider = newSemanticTokenProvider(languageModes);
}
return semanticTokensProvider;
}
connection.onRequest(SemanticTokenRequest.type, (params, token) => {
return runSafe(() => {
const document = documents.get(params.textDocument.uri);
if (document) {
return getSemanticTokenProvider().getSemanticTokens(document, params.ranges);
}
return null;
}, null, `Error while computing semantic tokens for ${params.textDocument.uri}`, token);
});
connection.onRequest(SemanticTokenLegendRequest.type, (_params, token) => {
return runSafe(() => {
return getSemanticTokenProvider().legend;
}, null, `Error while computing semantic tokens legend`, token);
});
connection.onNotification(CustomDataChangedNotification.type, dataPaths => {
fetchHTMLDataProviders(dataPaths, requestService).then(dataProviders => {
languageModes.updateDataProviders(dataProviders);
});
});
// Listen on the connection
connection.listen();
}

View file

@ -1,544 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, RequestType,
DocumentRangeFormattingRequest, Disposable, DocumentSelector, TextDocumentPositionParams, ServerCapabilities,
ConfigurationRequest, ConfigurationParams, DidChangeWorkspaceFoldersNotification,
DocumentColorRequest, ColorPresentationRequest, TextDocumentSyncKind
} from 'vscode-languageserver';
import {
getLanguageModes, LanguageModes, Settings, TextDocument, Position, Diagnostic, WorkspaceFolder, ColorInformation,
Range, DocumentLink, SymbolInformation, TextDocumentIdentifier
} from './modes/languageModes';
import { format } from './modes/formatting';
import { pushAll } from './utils/arrays';
import { getDocumentContext } from './utils/documentContext';
import { URI } from 'vscode-uri';
import { formatError, runSafe, runSafeAsync } from './utils/runner';
import { getFoldingRanges } from './modes/htmlFolding';
import { getDataProviders } from './customData';
import { getSelectionRanges } from './modes/selectionRanges';
import { SemanticTokenProvider, newSemanticTokenProvider } from './modes/semanticTokens';
namespace TagCloseRequest {
export const type: RequestType<TextDocumentPositionParams, string | null, any, any> = new RequestType('html/tag');
}
namespace OnTypeRenameRequest {
export const type: RequestType<TextDocumentPositionParams, Range[] | null, any, any> = new RequestType('html/onTypeRename');
}
// experimental: semantic tokens
interface SemanticTokenParams {
textDocument: TextDocumentIdentifier;
ranges?: Range[];
}
namespace SemanticTokenRequest {
export const type: RequestType<SemanticTokenParams, number[] | null, any, any> = new RequestType('html/semanticTokens');
}
namespace SemanticTokenLegendRequest {
export const type: RequestType<void, { types: string[]; modifiers: string[] } | null, any, any> = new RequestType('html/semanticTokenLegend');
}
// Create a connection for the server
const connection: IConnection = createConnection();
console.log = connection.console.log.bind(connection.console);
console.error = connection.console.error.bind(connection.console);
process.on('unhandledRejection', (e: any) => {
console.error(formatError(`Unhandled exception`, e));
});
process.on('uncaughtException', (e: any) => {
console.error(formatError(`Unhandled exception`, e));
});
// Create a text document manager.
const documents = new TextDocuments(TextDocument);
// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);
let workspaceFolders: WorkspaceFolder[] = [];
let languageModes: LanguageModes;
let clientSnippetSupport = false;
let dynamicFormatterRegistration = false;
let scopedSettingsSupport = false;
let workspaceFoldersSupport = false;
let foldingRangeLimit = Number.MAX_VALUE;
let globalSettings: Settings = {};
let documentSettings: { [key: string]: Thenable<Settings> } = {};
// remove document settings on close
documents.onDidClose(e => {
delete documentSettings[e.document.uri];
});
function getDocumentSettings(textDocument: TextDocument, needsDocumentSettings: () => boolean): Thenable<Settings | undefined> {
if (scopedSettingsSupport && needsDocumentSettings()) {
let promise = documentSettings[textDocument.uri];
if (!promise) {
const scopeUri = textDocument.uri;
const configRequestParam: ConfigurationParams = { items: [{ scopeUri, section: 'css' }, { scopeUri, section: 'html' }, { scopeUri, section: 'javascript' }] };
promise = connection.sendRequest(ConfigurationRequest.type, configRequestParam).then(s => ({ css: s[0], html: s[1], javascript: s[2] }));
documentSettings[textDocument.uri] = promise;
}
return promise;
}
return Promise.resolve(undefined);
}
// After the server has started the client sends an initialize request. The server receives
// in the passed params the rootPath of the workspace plus the client capabilities
connection.onInitialize((params: InitializeParams): InitializeResult => {
const initializationOptions = params.initializationOptions;
workspaceFolders = (<any>params).workspaceFolders;
if (!Array.isArray(workspaceFolders)) {
workspaceFolders = [];
if (params.rootPath) {
workspaceFolders.push({ name: '', uri: URI.file(params.rootPath).toString() });
}
}
const dataPaths: string[] = params.initializationOptions.dataPaths;
const providers = getDataProviders(dataPaths);
const workspace = {
get settings() { return globalSettings; },
get folders() { return workspaceFolders; }
};
languageModes = getLanguageModes(initializationOptions ? initializationOptions.embeddedLanguages : { css: true, javascript: true }, workspace, params.capabilities, providers);
documents.onDidClose(e => {
languageModes.onDocumentRemoved(e.document);
});
connection.onShutdown(() => {
languageModes.dispose();
});
function getClientCapability<T>(name: string, def: T) {
const keys = name.split('.');
let c: any = params.capabilities;
for (let i = 0; c && i < keys.length; i++) {
if (!c.hasOwnProperty(keys[i])) {
return def;
}
c = c[keys[i]];
}
return c;
}
clientSnippetSupport = getClientCapability('textDocument.completion.completionItem.snippetSupport', false);
dynamicFormatterRegistration = getClientCapability('textDocument.rangeFormatting.dynamicRegistration', false) && (typeof params.initializationOptions.provideFormatter !== 'boolean');
scopedSettingsSupport = getClientCapability('workspace.configuration', false);
workspaceFoldersSupport = getClientCapability('workspace.workspaceFolders', false);
foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE);
const capabilities: ServerCapabilities = {
textDocumentSync: TextDocumentSyncKind.Incremental,
completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['.', ':', '<', '"', '=', '/'] } : undefined,
hoverProvider: true,
documentHighlightProvider: true,
documentRangeFormattingProvider: params.initializationOptions.provideFormatter === true,
documentLinkProvider: { resolveProvider: false },
documentSymbolProvider: true,
definitionProvider: true,
signatureHelpProvider: { triggerCharacters: ['('] },
referencesProvider: true,
colorProvider: {},
foldingRangeProvider: true,
selectionRangeProvider: true,
renameProvider: true
};
return { capabilities };
});
connection.onInitialized(() => {
if (workspaceFoldersSupport) {
connection.client.register(DidChangeWorkspaceFoldersNotification.type);
connection.onNotification(DidChangeWorkspaceFoldersNotification.type, e => {
const toAdd = e.event.added;
const toRemove = e.event.removed;
const updatedFolders = [];
if (workspaceFolders) {
for (const folder of workspaceFolders) {
if (!toRemove.some(r => r.uri === folder.uri) && !toAdd.some(r => r.uri === folder.uri)) {
updatedFolders.push(folder);
}
}
}
workspaceFolders = updatedFolders.concat(toAdd);
documents.all().forEach(triggerValidation);
});
}
});
let formatterRegistration: Thenable<Disposable> | null = null;
// The settings have changed. Is send on server activation as well.
connection.onDidChangeConfiguration((change) => {
globalSettings = change.settings;
documentSettings = {}; // reset all document settings
documents.all().forEach(triggerValidation);
// dynamically enable & disable the formatter
if (dynamicFormatterRegistration) {
const enableFormatter = globalSettings && globalSettings.html && globalSettings.html.format && globalSettings.html.format.enable;
if (enableFormatter) {
if (!formatterRegistration) {
const documentSelector: DocumentSelector = [{ language: 'html' }, { language: 'handlebars' }];
formatterRegistration = connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector });
}
} else if (formatterRegistration) {
formatterRegistration.then(r => r.dispose());
formatterRegistration = null;
}
}
});
const pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {};
const validationDelayMs = 500;
// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(change => {
triggerValidation(change.document);
});
// a document has closed: clear all diagnostics
documents.onDidClose(event => {
cleanPendingValidation(event.document);
connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
});
function cleanPendingValidation(textDocument: TextDocument): void {
const request = pendingValidationRequests[textDocument.uri];
if (request) {
clearTimeout(request);
delete pendingValidationRequests[textDocument.uri];
}
}
function triggerValidation(textDocument: TextDocument): void {
cleanPendingValidation(textDocument);
pendingValidationRequests[textDocument.uri] = setTimeout(() => {
delete pendingValidationRequests[textDocument.uri];
validateTextDocument(textDocument);
}, validationDelayMs);
}
function isValidationEnabled(languageId: string, settings: Settings = globalSettings) {
const validationSettings = settings && settings.html && settings.html.validate;
if (validationSettings) {
return languageId === 'css' && validationSettings.styles !== false || languageId === 'javascript' && validationSettings.scripts !== false;
}
return true;
}
async function validateTextDocument(textDocument: TextDocument) {
try {
const version = textDocument.version;
const diagnostics: Diagnostic[] = [];
if (textDocument.languageId === 'html') {
const modes = languageModes.getAllModesInDocument(textDocument);
const settings = await getDocumentSettings(textDocument, () => modes.some(m => !!m.doValidation));
const latestTextDocument = documents.get(textDocument.uri);
if (latestTextDocument && latestTextDocument.version === version) { // check no new version has come in after in after the async op
modes.forEach(mode => {
if (mode.doValidation && isValidationEnabled(mode.getId(), settings)) {
pushAll(diagnostics, mode.doValidation(latestTextDocument, settings));
}
});
connection.sendDiagnostics({ uri: latestTextDocument.uri, diagnostics });
}
}
} catch (e) {
connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e));
}
}
connection.onCompletion(async (textDocumentPosition, token) => {
return runSafeAsync(async () => {
const document = documents.get(textDocumentPosition.textDocument.uri);
if (!document) {
return null;
}
const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
if (!mode || !mode.doComplete) {
return { isIncomplete: true, items: [] };
}
const doComplete = mode.doComplete!;
if (mode.getId() !== 'html') {
/* __GDPR__
"html.embbedded.complete" : {
"languageId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
connection.telemetry.logEvent({ key: 'html.embbedded.complete', value: { languageId: mode.getId() } });
}
const settings = await getDocumentSettings(document, () => doComplete.length > 2);
const result = doComplete(document, textDocumentPosition.position, settings);
return result;
}, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`, token);
});
connection.onCompletionResolve((item, token) => {
return runSafe(() => {
const data = item.data;
if (data && data.languageId && data.uri) {
const mode = languageModes.getMode(data.languageId);
const document = documents.get(data.uri);
if (mode && mode.doResolve && document) {
return mode.doResolve(document, item);
}
}
return item;
}, item, `Error while resolving completion proposal`, token);
});
connection.onHover((textDocumentPosition, token) => {
return runSafe(() => {
const document = documents.get(textDocumentPosition.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, textDocumentPosition.position);
if (mode && mode.doHover) {
return mode.doHover(document, textDocumentPosition.position);
}
}
return null;
}, null, `Error while computing hover for ${textDocumentPosition.textDocument.uri}`, token);
});
connection.onDocumentHighlight((documentHighlightParams, token) => {
return runSafe(() => {
const document = documents.get(documentHighlightParams.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, documentHighlightParams.position);
if (mode && mode.findDocumentHighlight) {
return mode.findDocumentHighlight(document, documentHighlightParams.position);
}
}
return [];
}, [], `Error while computing document highlights for ${documentHighlightParams.textDocument.uri}`, token);
});
connection.onDefinition((definitionParams, token) => {
return runSafe(() => {
const document = documents.get(definitionParams.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, definitionParams.position);
if (mode && mode.findDefinition) {
return mode.findDefinition(document, definitionParams.position);
}
}
return [];
}, null, `Error while computing definitions for ${definitionParams.textDocument.uri}`, token);
});
connection.onReferences((referenceParams, token) => {
return runSafe(() => {
const document = documents.get(referenceParams.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, referenceParams.position);
if (mode && mode.findReferences) {
return mode.findReferences(document, referenceParams.position);
}
}
return [];
}, [], `Error while computing references for ${referenceParams.textDocument.uri}`, token);
});
connection.onSignatureHelp((signatureHelpParms, token) => {
return runSafe(() => {
const document = documents.get(signatureHelpParms.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, signatureHelpParms.position);
if (mode && mode.doSignatureHelp) {
return mode.doSignatureHelp(document, signatureHelpParms.position);
}
}
return null;
}, null, `Error while computing signature help for ${signatureHelpParms.textDocument.uri}`, token);
});
connection.onDocumentRangeFormatting(async (formatParams, token) => {
return runSafeAsync(async () => {
const document = documents.get(formatParams.textDocument.uri);
if (document) {
let settings = await getDocumentSettings(document, () => true);
if (!settings) {
settings = globalSettings;
}
const unformattedTags: string = settings && settings.html && settings.html.format && settings.html.format.unformatted || '';
const enabledModes = { css: !unformattedTags.match(/\bstyle\b/), javascript: !unformattedTags.match(/\bscript\b/) };
return format(languageModes, document, formatParams.range, formatParams.options, settings, enabledModes);
}
return [];
}, [], `Error while formatting range for ${formatParams.textDocument.uri}`, token);
});
connection.onDocumentLinks((documentLinkParam, token) => {
return runSafe(() => {
const document = documents.get(documentLinkParam.textDocument.uri);
const links: DocumentLink[] = [];
if (document) {
const documentContext = getDocumentContext(document.uri, workspaceFolders);
languageModes.getAllModesInDocument(document).forEach(m => {
if (m.findDocumentLinks) {
pushAll(links, m.findDocumentLinks(document, documentContext));
}
});
}
return links;
}, [], `Error while document links for ${documentLinkParam.textDocument.uri}`, token);
});
connection.onDocumentSymbol((documentSymbolParms, token) => {
return runSafe(() => {
const document = documents.get(documentSymbolParms.textDocument.uri);
const symbols: SymbolInformation[] = [];
if (document) {
languageModes.getAllModesInDocument(document).forEach(m => {
if (m.findDocumentSymbols) {
pushAll(symbols, m.findDocumentSymbols(document));
}
});
}
return symbols;
}, [], `Error while computing document symbols for ${documentSymbolParms.textDocument.uri}`, token);
});
connection.onRequest(DocumentColorRequest.type, (params, token) => {
return runSafe(() => {
const infos: ColorInformation[] = [];
const document = documents.get(params.textDocument.uri);
if (document) {
languageModes.getAllModesInDocument(document).forEach(m => {
if (m.findDocumentColors) {
pushAll(infos, m.findDocumentColors(document));
}
});
}
return infos;
}, [], `Error while computing document colors for ${params.textDocument.uri}`, token);
});
connection.onRequest(ColorPresentationRequest.type, (params, token) => {
return runSafe(() => {
const document = documents.get(params.textDocument.uri);
if (document) {
const mode = languageModes.getModeAtPosition(document, params.range.start);
if (mode && mode.getColorPresentations) {
return mode.getColorPresentations(document, params.color, params.range);
}
}
return [];
}, [], `Error while computing color presentations for ${params.textDocument.uri}`, token);
});
connection.onRequest(TagCloseRequest.type, (params, token) => {
return runSafe(() => {
const document = documents.get(params.textDocument.uri);
if (document) {
const pos = params.position;
if (pos.character > 0) {
const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1));
if (mode && mode.doAutoClose) {
return mode.doAutoClose(document, pos);
}
}
}
return null;
}, null, `Error while computing tag close actions for ${params.textDocument.uri}`, token);
});
connection.onFoldingRanges((params, token) => {
return runSafe(() => {
const document = documents.get(params.textDocument.uri);
if (document) {
return getFoldingRanges(languageModes, document, foldingRangeLimit, token);
}
return null;
}, null, `Error while computing folding regions for ${params.textDocument.uri}`, token);
});
connection.onSelectionRanges((params, token) => {
return runSafe(() => {
const document = documents.get(params.textDocument.uri);
if (document) {
return getSelectionRanges(languageModes, document, params.positions);
}
return [];
}, [], `Error while computing selection ranges for ${params.textDocument.uri}`, token);
});
connection.onRenameRequest((params, token) => {
return runSafe(() => {
const document = documents.get(params.textDocument.uri);
const position: Position = params.position;
if (document) {
const htmlMode = languageModes.getMode('html');
if (htmlMode && htmlMode.doRename) {
return htmlMode.doRename(document, position, params.newName);
}
}
return null;
}, null, `Error while computing rename for ${params.textDocument.uri}`, token);
});
connection.onRequest(OnTypeRenameRequest.type, (params, token) => {
return runSafe(() => {
const document = documents.get(params.textDocument.uri);
if (document) {
const pos = params.position;
if (pos.character > 0) {
const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1));
if (mode && mode.doOnTypeRename) {
return mode.doOnTypeRename(document, pos);
}
}
}
return null;
}, null, `Error while computing synced regions for ${params.textDocument.uri}`, token);
});
let semanticTokensProvider: SemanticTokenProvider | undefined;
function getSemanticTokenProvider() {
if (!semanticTokensProvider) {
semanticTokensProvider = newSemanticTokenProvider(languageModes);
}
return semanticTokensProvider;
}
connection.onRequest(SemanticTokenRequest.type, (params, token) => {
return runSafe(() => {
const document = documents.get(params.textDocument.uri);
if (document) {
return getSemanticTokenProvider().getSemanticTokens(document, params.ranges);
}
return null;
}, null, `Error while computing semantic tokens for ${params.textDocument.uri}`, token);
});
connection.onRequest(SemanticTokenLegendRequest.type, (_params, token) => {
return runSafe(() => {
return getSemanticTokenProvider().legend;
}, null, `Error while computing semantic tokens legend`, token);
});
// Listen on the connection
connection.listen();

View file

@ -5,7 +5,7 @@
import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache';
import { Stylesheet, LanguageService as CSSLanguageService } from 'vscode-css-languageservice';
import { FoldingRange, LanguageMode, Workspace, Color, TextDocument, Position, Range, CompletionList } from './languageModes';
import { FoldingRange, LanguageMode, Workspace, Color, TextDocument, Position, Range, CompletionList, DocumentContext } from './languageModes';
import { HTMLDocumentRegions, CSS_STYLE_RULE } from './embeddedSupport';
export function getCSSMode(cssLanguageService: CSSLanguageService, documentRegions: LanguageModelCache<HTMLDocumentRegions>, workspace: Workspace): LanguageMode {
@ -20,10 +20,10 @@ export function getCSSMode(cssLanguageService: CSSLanguageService, documentRegio
let embedded = embeddedCSSDocuments.get(document);
return cssLanguageService.doValidation(embedded, cssStylesheets.get(embedded), settings && settings.css);
},
doComplete(document: TextDocument, position: Position, _settings = workspace.settings) {
doComplete(document: TextDocument, position: Position, documentContext: DocumentContext, _settings = workspace.settings) {
let embedded = embeddedCSSDocuments.get(document);
const stylesheet = cssStylesheets.get(embedded);
return cssLanguageService.doComplete(embedded, position, stylesheet) || CompletionList.create();
return cssLanguageService.doComplete2(embedded, position, stylesheet, documentContext) || CompletionList.create();
},
doHover(document: TextDocument, position: Position) {
let embedded = embeddedCSSDocuments.get(document);

View file

@ -7,10 +7,9 @@ import { getLanguageModelCache } from '../languageModelCache';
import {
LanguageService as HTMLLanguageService, HTMLDocument, DocumentContext, FormattingOptions,
HTMLFormatConfiguration, SelectionRange,
TextDocument, Position, Range, CompletionItem, FoldingRange,
TextDocument, Position, Range, FoldingRange,
LanguageMode, Workspace
} from './languageModes';
import { getPathCompletionParticipant } from './pathCompletion';
export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace: Workspace): LanguageMode {
let htmlDocuments = getLanguageModelCache<HTMLDocument>(10, 60, document => htmlLanguageService.parseHTMLDocument(document));
@ -21,19 +20,15 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace:
getSelectionRange(document: TextDocument, position: Position): SelectionRange {
return htmlLanguageService.getSelectionRanges(document, [position])[0];
},
doComplete(document: TextDocument, position: Position, settings = workspace.settings) {
doComplete(document: TextDocument, position: Position, documentContext: DocumentContext, settings = workspace.settings) {
let options = settings && settings.html && settings.html.suggest;
let doAutoComplete = settings && settings.html && settings.html.autoClosingTags;
if (doAutoComplete) {
options.hideAutoCompleteProposals = true;
}
let pathCompletionProposals: CompletionItem[] = [];
let participants = [getPathCompletionParticipant(document, workspace.folders, pathCompletionProposals)];
htmlLanguageService.setCompletionParticipants(participants);
const htmlDocument = htmlDocuments.get(document);
let completionList = htmlLanguageService.doComplete(document, position, htmlDocument, options);
completionList.items.push(...pathCompletionProposals);
let completionList = htmlLanguageService.doComplete2(document, position, htmlDocument, documentContext, options);
return completionList;
},
doHover(document: TextDocument, position: Position) {

View file

@ -8,20 +8,21 @@ import {
SymbolInformation, SymbolKind, CompletionItem, Location, SignatureHelp, SignatureInformation, ParameterInformation,
Definition, TextEdit, TextDocument, Diagnostic, DiagnosticSeverity, Range, CompletionItemKind, Hover, MarkedString,
DocumentHighlight, DocumentHighlightKind, CompletionList, Position, FormattingOptions, FoldingRange, FoldingRangeKind, SelectionRange,
LanguageMode, Settings, SemanticTokenData, Workspace
LanguageMode, Settings, SemanticTokenData, Workspace, DocumentContext
} from './languageModes';
import { getWordAtText, startsWith, isWhitespaceOnly, repeat } from '../utils/strings';
import { HTMLDocumentRegions } from './embeddedSupport';
import * as ts from 'typescript';
import { join } from 'path';
import { getSemanticTokens, getSemanticTokenLegend } from './javascriptSemanticTokens';
import { joinPath } from '../requests';
const JS_WORD_REGEX = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g;
let jquery_d_ts = join(__dirname, '../lib/jquery.d.ts'); // when packaged
let jquery_d_ts = joinPath(__dirname, '../lib/jquery.d.ts'); // when packaged
if (!ts.sys.fileExists(jquery_d_ts)) {
jquery_d_ts = join(__dirname, '../../lib/jquery.d.ts'); // from source
jquery_d_ts = joinPath(__dirname, '../../lib/jquery.d.ts'); // from source
}
export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocumentRegions>, languageId: 'javascript' | 'typescript', workspace: Workspace): LanguageMode {
@ -64,7 +65,8 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
};
},
getCurrentDirectory: () => '',
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options)
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
};
let jsLanguageService = ts.createLanguageService(host);
@ -88,7 +90,7 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache<HTMLDocume
};
});
},
doComplete(document: TextDocument, position: Position): CompletionList {
async doComplete(document: TextDocument, position: Position, _documentContext: DocumentContext): Promise<CompletionList> {
updateCurrentTextDocument(document);
let offset = currentTextDocument.offsetAt(position);
let completions = jsLanguageService.getCompletionsAtPosition(workingFile, offset, { includeExternalModuleExports: false, includeInsertTextCompletions: false });

View file

@ -16,6 +16,7 @@ import { getCSSMode } from './cssMode';
import { getDocumentRegions, HTMLDocumentRegions } from './embeddedSupport';
import { getHTMLMode } from './htmlMode';
import { getJavaScriptMode } from './javascriptMode';
import { RequestService } from '../requests';
export * from 'vscode-html-languageservice';
export { WorkspaceFolder } from 'vscode-languageserver';
@ -42,7 +43,7 @@ export interface LanguageMode {
getId(): string;
getSelectionRange?: (document: TextDocument, position: Position) => SelectionRange;
doValidation?: (document: TextDocument, settings?: Settings) => Diagnostic[];
doComplete?: (document: TextDocument, position: Position, settings?: Settings) => CompletionList;
doComplete?: (document: TextDocument, position: Position, documentContext: DocumentContext, settings?: Settings) => Promise<CompletionList>;
doResolve?: (document: TextDocument, item: CompletionItem) => CompletionItem;
doHover?: (document: TextDocument, position: Position) => Hover | null;
doSignatureHelp?: (document: TextDocument, position: Position) => SignatureHelp | null;
@ -66,6 +67,7 @@ export interface LanguageMode {
}
export interface LanguageModes {
updateDataProviders(dataProviders: IHTMLDataProvider[]): void;
getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined;
getModesInRange(document: TextDocument, range: Range): LanguageModeRange[];
getAllModes(): LanguageMode[];
@ -80,9 +82,9 @@ export interface LanguageModeRange extends Range {
attributeValue?: boolean;
}
export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }, workspace: Workspace, clientCapabilities: ClientCapabilities, customDataProviders?: IHTMLDataProvider[]): LanguageModes {
const htmlLanguageService = getHTMLLanguageService({ customDataProviders, clientCapabilities });
const cssLanguageService = getCSSLanguageService({ clientCapabilities });
export function getLanguageModes(supportedLanguages: { [languageId: string]: boolean; }, workspace: Workspace, clientCapabilities: ClientCapabilities, requestService: RequestService): LanguageModes {
const htmlLanguageService = getHTMLLanguageService({ clientCapabilities, fileSystemProvider: requestService });
const cssLanguageService = getCSSLanguageService({ clientCapabilities, fileSystemProvider: requestService });
let documentRegions = getLanguageModelCache<HTMLDocumentRegions>(10, 60, document => getDocumentRegions(htmlLanguageService, document));
@ -99,6 +101,9 @@ export function getLanguageModes(supportedLanguages: { [languageId: string]: boo
modes['typescript'] = getJavaScriptMode(documentRegions, 'typescript', workspace);
}
return {
async updateDataProviders(dataProviders: IHTMLDataProvider[]): Promise<void> {
htmlLanguageService.setDataProviders(true, dataProviders);
},
getModeAtPosition(document: TextDocument, position: Position): LanguageMode | undefined {
let languageId = documentRegions.get(document).getLanguageAtPosition(position);
if (languageId) {

View file

@ -1,183 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as fs from 'fs';
import { URI } from 'vscode-uri';
import { ICompletionParticipant, TextDocument, CompletionItemKind, CompletionItem, TextEdit, Range, Position, WorkspaceFolder } from './languageModes';
import { startsWith } from '../utils/strings';
import { contains } from '../utils/arrays';
export function getPathCompletionParticipant(
document: TextDocument,
workspaceFolders: WorkspaceFolder[],
result: CompletionItem[]
): ICompletionParticipant {
return {
onHtmlAttributeValue: ({ tag, attribute, value: valueBeforeCursor, range }) => {
const fullValue = stripQuotes(document.getText(range));
if (shouldDoPathCompletion(tag, attribute, fullValue)) {
if (workspaceFolders.length === 0) {
return;
}
const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders);
const paths = providePaths(valueBeforeCursor, URI.parse(document.uri).fsPath, workspaceRoot);
result.push(...paths.map(p => pathToSuggestion(p, valueBeforeCursor, fullValue, range)));
}
}
};
}
function stripQuotes(fullValue: string) {
if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) {
return fullValue.slice(1, -1);
} else {
return fullValue;
}
}
function shouldDoPathCompletion(tag: string, attr: string, value: string) {
if (startsWith(value, 'http') || startsWith(value, 'https') || startsWith(value, '//')) {
return false;
}
if (PATH_TAG_AND_ATTR[tag]) {
if (typeof PATH_TAG_AND_ATTR[tag] === 'string') {
return PATH_TAG_AND_ATTR[tag] === attr;
} else {
return contains(<string[]>PATH_TAG_AND_ATTR[tag], attr);
}
}
return false;
}
/**
* Get a list of path suggestions. Folder suggestions are suffixed with a slash.
*/
function providePaths(valueBeforeCursor: string, activeDocFsPath: string, root?: string): string[] {
const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/');
const valueBeforeLastSlash = valueBeforeCursor.slice(0, lastIndexOfSlash + 1);
const startsWithSlash = startsWith(valueBeforeCursor, '/');
let parentDir: string;
if (startsWithSlash) {
if (!root) {
return [];
}
parentDir = path.resolve(root, '.' + valueBeforeLastSlash);
} else {
parentDir = path.resolve(activeDocFsPath, '..', valueBeforeLastSlash);
}
try {
const paths = fs.readdirSync(parentDir).map(f => {
return isDir(path.resolve(parentDir, f))
? f + '/'
: f;
});
return paths.filter(p => p[0] !== '.');
} catch (e) {
return [];
}
}
function isDir(p: string) {
try {
return fs.statSync(p).isDirectory();
} catch (e) {
return false;
}
}
function pathToSuggestion(p: string, valueBeforeCursor: string, fullValue: string, range: Range): CompletionItem {
const isDir = p[p.length - 1] === '/';
let replaceRange: Range;
const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/');
if (lastIndexOfSlash === -1) {
replaceRange = shiftRange(range, 1, -1);
} else {
// For cases where cursor is in the middle of attribute value, like <script src="./s|rc/test.js">
// Find the last slash before cursor, and calculate the start of replace range from there
const valueAfterLastSlash = fullValue.slice(lastIndexOfSlash + 1);
const startPos = shiftPosition(range.end, -1 - valueAfterLastSlash.length);
// If whitespace exists, replace until there is no more
const whitespaceIndex = valueAfterLastSlash.indexOf(' ');
let endPos;
if (whitespaceIndex !== -1) {
endPos = shiftPosition(startPos, whitespaceIndex);
} else {
endPos = shiftPosition(range.end, -1);
}
replaceRange = Range.create(startPos, endPos);
}
if (isDir) {
return {
label: p,
kind: CompletionItemKind.Folder,
textEdit: TextEdit.replace(replaceRange, p),
command: {
title: 'Suggest',
command: 'editor.action.triggerSuggest'
}
};
} else {
return {
label: p,
kind: CompletionItemKind.File,
textEdit: TextEdit.replace(replaceRange, p)
};
}
}
function resolveWorkspaceRoot(activeDoc: TextDocument, workspaceFolders: WorkspaceFolder[]): string | undefined {
for (const folder of workspaceFolders) {
if (startsWith(activeDoc.uri, folder.uri)) {
return path.resolve(URI.parse(folder.uri).fsPath);
}
}
return undefined;
}
function shiftPosition(pos: Position, offset: number): Position {
return Position.create(pos.line, pos.character + offset);
}
function shiftRange(range: Range, startOffset: number, endOffset: number): Range {
const start = shiftPosition(range.start, startOffset);
const end = shiftPosition(range.end, endOffset);
return Range.create(start, end);
}
// Selected from https://stackoverflow.com/a/2725168/1780148
const PATH_TAG_AND_ATTR: { [tag: string]: string | string[] } = {
// HTML 4
a: 'href',
area: 'href',
body: 'background',
del: 'cite',
form: 'action',
frame: ['src', 'longdesc'],
img: ['src', 'longdesc'],
ins: 'cite',
link: 'href',
object: 'data',
q: 'cite',
script: 'src',
// HTML 5
audio: 'src',
button: 'formaction',
command: 'icon',
embed: 'src',
html: 'manifest',
input: ['src', 'formaction'],
source: 'src',
track: 'src',
video: ['src', 'poster']
};

View file

@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createConnection, Connection } from 'vscode-languageserver/node';
import { formatError } from '../utils/runner';
import { startServer } from '../htmlServer';
import { getNodeFSRequestService } from './nodeFs';
// Create a connection for the server.
const connection: Connection = createConnection();
console.log = connection.console.log.bind(connection.console);
console.error = connection.console.error.bind(connection.console);
process.on('unhandledRejection', (e: any) => {
connection.console.error(formatError(`Unhandled exception`, e));
});
startServer(connection, { file: getNodeFSRequestService() });

View file

@ -0,0 +1,87 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { RequestService, getScheme } from '../requests';
import { URI as Uri } from 'vscode-uri';
import * as fs from 'fs';
import { FileType } from 'vscode-css-languageservice';
export function getNodeFSRequestService(): RequestService {
function ensureFileUri(location: string) {
if (getScheme(location) !== 'file') {
throw new Error('fileRequestService can only handle file URLs');
}
}
return {
getContent(location: string, encoding?: string) {
ensureFileUri(location);
return new Promise((c, e) => {
const uri = Uri.parse(location);
fs.readFile(uri.fsPath, encoding, (err, buf) => {
if (err) {
return e(err);
}
c(buf.toString());
});
});
},
stat(location: string) {
ensureFileUri(location);
return new Promise((c, e) => {
const uri = Uri.parse(location);
fs.stat(uri.fsPath, (err, stats) => {
if (err) {
if (err.code === 'ENOENT') {
return c({ type: FileType.Unknown, ctime: -1, mtime: -1, size: -1 });
} else {
return e(err);
}
}
let type = FileType.Unknown;
if (stats.isFile()) {
type = FileType.File;
} else if (stats.isDirectory()) {
type = FileType.Directory;
} else if (stats.isSymbolicLink()) {
type = FileType.SymbolicLink;
}
c({
type,
ctime: stats.ctime.getTime(),
mtime: stats.mtime.getTime(),
size: stats.size
});
});
});
},
readDirectory(location: string) {
ensureFileUri(location);
return new Promise((c, e) => {
const path = Uri.parse(location).fsPath;
fs.readdir(path, { withFileTypes: true }, (err, children) => {
if (err) {
return e(err);
}
c(children.map(stat => {
if (stat.isSymbolicLink()) {
return [stat.name, FileType.SymbolicLink];
} else if (stat.isDirectory()) {
return [stat.name, FileType.Directory];
} else if (stat.isFile()) {
return [stat.name, FileType.File];
} else {
return [stat.name, FileType.Unknown];
}
}));
});
});
}
};
}

View file

@ -0,0 +1,177 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vscode-uri';
import { RequestType, Connection } from 'vscode-languageserver';
import { RuntimeEnvironment } from './htmlServer';
export namespace FsContentRequest {
export const type: RequestType<{ uri: string; encoding?: string; }, string, any, any> = new RequestType('fs/content');
}
export namespace FsStatRequest {
export const type: RequestType<string, FileStat, any, any> = new RequestType('fs/stat');
}
export namespace FsReadDirRequest {
export const type: RequestType<string, [string, FileType][], any, any> = new RequestType('fs/readDir');
}
export enum FileType {
/**
* The file type is unknown.
*/
Unknown = 0,
/**
* A regular file.
*/
File = 1,
/**
* A directory.
*/
Directory = 2,
/**
* A symbolic link to a file.
*/
SymbolicLink = 64
}
export interface FileStat {
/**
* The type of the file, e.g. is a regular file, a directory, or symbolic link
* to a file.
*/
type: FileType;
/**
* The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
*/
ctime: number;
/**
* The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
*/
mtime: number;
/**
* The size in bytes.
*/
size: number;
}
export interface RequestService {
getContent(uri: string, encoding?: string): Promise<string>;
stat(uri: string): Promise<FileStat>;
readDirectory(uri: string): Promise<[string, FileType][]>;
}
export function getRequestService(handledSchemas: string[], connection: Connection, runtime: RuntimeEnvironment): RequestService {
const builtInHandlers: { [protocol: string]: RequestService | undefined } = {};
for (let protocol of handledSchemas) {
if (protocol === 'file') {
builtInHandlers[protocol] = runtime.file;
} else if (protocol === 'http' || protocol === 'https') {
builtInHandlers[protocol] = runtime.http;
}
}
return {
async stat(uri: string): Promise<FileStat> {
const handler = builtInHandlers[getScheme(uri)];
if (handler) {
return handler.stat(uri);
}
const res = await connection.sendRequest(FsStatRequest.type, uri.toString());
return res;
},
readDirectory(uri: string): Promise<[string, FileType][]> {
const handler = builtInHandlers[getScheme(uri)];
if (handler) {
return handler.readDirectory(uri);
}
return connection.sendRequest(FsReadDirRequest.type, uri.toString());
},
getContent(uri: string, encoding?: string): Promise<string> {
const handler = builtInHandlers[getScheme(uri)];
if (handler) {
return handler.getContent(uri, encoding);
}
return connection.sendRequest(FsContentRequest.type, { uri: uri.toString(), encoding });
}
};
}
export function getScheme(uri: string) {
return uri.substr(0, uri.indexOf(':'));
}
export function dirname(uri: string) {
const lastIndexOfSlash = uri.lastIndexOf('/');
return lastIndexOfSlash !== -1 ? uri.substr(0, lastIndexOfSlash) : '';
}
export function basename(uri: string) {
const lastIndexOfSlash = uri.lastIndexOf('/');
return uri.substr(lastIndexOfSlash + 1);
}
const Slash = '/'.charCodeAt(0);
const Dot = '.'.charCodeAt(0);
export function extname(uri: string) {
for (let i = uri.length - 1; i >= 0; i--) {
const ch = uri.charCodeAt(i);
if (ch === Dot) {
if (i > 0 && uri.charCodeAt(i - 1) !== Slash) {
return uri.substr(i);
} else {
break;
}
} else if (ch === Slash) {
break;
}
}
return '';
}
export function isAbsolutePath(path: string) {
return path.charCodeAt(0) === Slash;
}
export function resolvePath(uriString: string, path: string): string {
if (isAbsolutePath(path)) {
const uri = URI.parse(uriString);
const parts = path.split('/');
return uri.with({ path: normalizePath(parts) }).toString();
}
return joinPath(uriString, path);
}
export function normalizePath(parts: string[]): string {
const newParts: string[] = [];
for (const part of parts) {
if (part.length === 0 || part.length === 1 && part.charCodeAt(0) === Dot) {
// ignore
} else if (part.length === 2 && part.charCodeAt(0) === Dot && part.charCodeAt(1) === Dot) {
newParts.pop();
} else {
newParts.push(part);
}
}
if (parts.length > 1 && parts[parts.length - 1].length === 0) {
newParts.push('');
}
let res = newParts.join('/');
if (parts[0].length === 0) {
res = '/' + res;
}
return res;
}
export function joinPath(uriString: string, ...paths: string[]): string {
const uri = URI.parse(uriString);
const parts = uri.path.split('/');
for (let path of paths) {
parts.push(...path.split('/'));
}
return uri.with({ path: normalizePath(parts) }).toString();
}

View file

@ -6,7 +6,9 @@ import 'mocha';
import * as assert from 'assert';
import * as path from 'path';
import { URI } from 'vscode-uri';
import { getLanguageModes, WorkspaceFolder, TextDocument, CompletionList, CompletionItemKind, ClientCapabilities} from '../modes/languageModes';
import { getLanguageModes, WorkspaceFolder, TextDocument, CompletionList, CompletionItemKind, ClientCapabilities, TextEdit } from '../modes/languageModes';
import { getNodeFSRequestService } from '../node/nodeFs';
import { getDocumentContext } from '../utils/documentContext';
export interface ItemDescription {
label: string;
documentation?: string;
@ -34,7 +36,8 @@ export function assertCompletion(completions: CompletionList, expected: ItemDesc
assert.equal(match.kind, expected.kind);
}
if (expected.resultText && match.textEdit) {
assert.equal(TextDocument.applyEdits(document, [match.textEdit]), expected.resultText);
const edit = TextEdit.is(match.textEdit) ? match.textEdit : TextEdit.replace(match.textEdit.replace, match.textEdit.newText);
assert.equal(TextDocument.applyEdits(document, [edit]), expected.resultText);
}
if (expected.command) {
assert.deepEqual(match.command, expected.command);
@ -43,7 +46,7 @@ export function assertCompletion(completions: CompletionList, expected: ItemDesc
const testUri = 'test://test/test.html';
export function testCompletionFor(value: string, expected: { count?: number, items?: ItemDescription[] }, uri = testUri, workspaceFolders?: WorkspaceFolder[]): void {
export async function testCompletionFor(value: string, expected: { count?: number, items?: ItemDescription[] }, uri = testUri, workspaceFolders?: WorkspaceFolder[]): Promise<void> {
let offset = value.indexOf('|');
value = value.substr(0, offset) + value.substr(offset + 1);
@ -54,11 +57,12 @@ export function testCompletionFor(value: string, expected: { count?: number, ite
let document = TextDocument.create(uri, 'html', 0, value);
let position = document.positionAt(offset);
const context = getDocumentContext(uri, workspace.folders)
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST);
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
const mode = languageModes.getModeAtPosition(document, position)!;
let list = mode.doComplete!(document, position);
let list = await mode.doComplete!(document, position, context);
if (expected.count) {
assert.equal(list.items.length, expected.count);
@ -71,13 +75,13 @@ export function testCompletionFor(value: string, expected: { count?: number, ite
}
suite('HTML Completion', () => {
test('HTML JavaScript Completions', function (): any {
testCompletionFor('<html><script>window.|</script></html>', {
test('HTML JavaScript Completions', async () => {
await testCompletionFor('<html><script>window.|</script></html>', {
items: [
{ label: 'location', resultText: '<html><script>window.location</script></html>' },
]
});
testCompletionFor('<html><script>$.|</script></html>', {
await testCompletionFor('<html><script>$.|</script></html>', {
items: [
{ label: 'getJSON', resultText: '<html><script>$.getJSON</script></html>' },
]
@ -96,8 +100,8 @@ suite('HTML Path Completion', () => {
const indexHtmlUri = URI.file(path.resolve(fixtureRoot, 'index.html')).toString();
const aboutHtmlUri = URI.file(path.resolve(fixtureRoot, 'about/about.html')).toString();
test('Basics - Correct label/kind/result/command', () => {
testCompletionFor('<script src="./|">', {
test('Basics - Correct label/kind/result/command', async () => {
await testCompletionFor('<script src="./|">', {
items: [
{ label: 'about/', kind: CompletionItemKind.Folder, resultText: '<script src="./about/">', command: triggerSuggestCommand },
{ label: 'index.html', kind: CompletionItemKind.File, resultText: '<script src="./index.html">' },
@ -106,8 +110,8 @@ suite('HTML Path Completion', () => {
}, indexHtmlUri);
});
test('Basics - Single Quote', () => {
testCompletionFor(`<script src='./|'>`, {
test('Basics - Single Quote', async () => {
await testCompletionFor(`<script src='./|'>`, {
items: [
{ label: 'about/', kind: CompletionItemKind.Folder, resultText: `<script src='./about/'>`, command: triggerSuggestCommand },
{ label: 'index.html', kind: CompletionItemKind.File, resultText: `<script src='./index.html'>` },
@ -116,18 +120,18 @@ suite('HTML Path Completion', () => {
}, indexHtmlUri);
});
test('No completion for remote paths', () => {
testCompletionFor('<script src="http:">', { items: [] });
testCompletionFor('<script src="http:/|">', { items: [] });
testCompletionFor('<script src="http://|">', { items: [] });
testCompletionFor('<script src="https:|">', { items: [] });
testCompletionFor('<script src="https:/|">', { items: [] });
testCompletionFor('<script src="https://|">', { items: [] });
testCompletionFor('<script src="//|">', { items: [] });
test('No completion for remote paths', async () => {
await testCompletionFor('<script src="http:">', { items: [] });
await testCompletionFor('<script src="http:/|">', { items: [] });
await testCompletionFor('<script src="http://|">', { items: [] });
await testCompletionFor('<script src="https:|">', { items: [] });
await testCompletionFor('<script src="https:/|">', { items: [] });
await testCompletionFor('<script src="https://|">', { items: [] });
await testCompletionFor('<script src="//|">', { items: [] });
});
test('Relative Path', () => {
testCompletionFor('<script src="../|">', {
test('Relative Path', async () => {
await testCompletionFor('<script src="../|">', {
items: [
{ label: 'about/', resultText: '<script src="../about/">' },
{ label: 'index.html', resultText: '<script src="../index.html">' },
@ -135,7 +139,7 @@ suite('HTML Path Completion', () => {
]
}, aboutHtmlUri);
testCompletionFor('<script src="../src/|">', {
await testCompletionFor('<script src="../src/|">', {
items: [
{ label: 'feature.js', resultText: '<script src="../src/feature.js">' },
{ label: 'test.js', resultText: '<script src="../src/test.js">' },
@ -143,8 +147,8 @@ suite('HTML Path Completion', () => {
}, aboutHtmlUri);
});
test('Absolute Path', () => {
testCompletionFor('<script src="/|">', {
test('Absolute Path', async () => {
await testCompletionFor('<script src="/|">', {
items: [
{ label: 'about/', resultText: '<script src="/about/">' },
{ label: 'index.html', resultText: '<script src="/index.html">' },
@ -152,7 +156,7 @@ suite('HTML Path Completion', () => {
]
}, indexHtmlUri);
testCompletionFor('<script src="/src/|">', {
await testCompletionFor('<script src="/src/|">', {
items: [
{ label: 'feature.js', resultText: '<script src="/src/feature.js">' },
{ label: 'test.js', resultText: '<script src="/src/test.js">' },
@ -160,9 +164,9 @@ suite('HTML Path Completion', () => {
}, aboutHtmlUri, [fixtureWorkspace]);
});
test('Empty Path Value', () => {
test('Empty Path Value', async () => {
// document: index.html
testCompletionFor('<script src="|">', {
await testCompletionFor('<script src="|">', {
items: [
{ label: 'about/', resultText: '<script src="about/">' },
{ label: 'index.html', resultText: '<script src="index.html">' },
@ -170,7 +174,7 @@ suite('HTML Path Completion', () => {
]
}, indexHtmlUri);
// document: about.html
testCompletionFor('<script src="|">', {
await testCompletionFor('<script src="|">', {
items: [
{ label: 'about.css', resultText: '<script src="about.css">' },
{ label: 'about.html', resultText: '<script src="about.html">' },
@ -178,15 +182,15 @@ suite('HTML Path Completion', () => {
]
}, aboutHtmlUri);
});
test('Incomplete Path', () => {
testCompletionFor('<script src="/src/f|">', {
test('Incomplete Path', async () => {
await testCompletionFor('<script src="/src/f|">', {
items: [
{ label: 'feature.js', resultText: '<script src="/src/feature.js">' },
{ label: 'test.js', resultText: '<script src="/src/test.js">' },
]
}, aboutHtmlUri, [fixtureWorkspace]);
testCompletionFor('<script src="../src/f|">', {
await testCompletionFor('<script src="../src/f|">', {
items: [
{ label: 'feature.js', resultText: '<script src="../src/feature.js">' },
{ label: 'test.js', resultText: '<script src="../src/test.js">' },
@ -194,9 +198,9 @@ suite('HTML Path Completion', () => {
}, aboutHtmlUri, [fixtureWorkspace]);
});
test('No leading dot or slash', () => {
test('No leading dot or slash', async () => {
// document: index.html
testCompletionFor('<script src="s|">', {
await testCompletionFor('<script src="s|">', {
items: [
{ label: 'about/', resultText: '<script src="about/">' },
{ label: 'index.html', resultText: '<script src="index.html">' },
@ -204,14 +208,14 @@ suite('HTML Path Completion', () => {
]
}, indexHtmlUri, [fixtureWorkspace]);
testCompletionFor('<script src="src/|">', {
await testCompletionFor('<script src="src/|">', {
items: [
{ label: 'feature.js', resultText: '<script src="src/feature.js">' },
{ label: 'test.js', resultText: '<script src="src/test.js">' },
]
}, indexHtmlUri, [fixtureWorkspace]);
testCompletionFor('<script src="src/f|">', {
await testCompletionFor('<script src="src/f|">', {
items: [
{ label: 'feature.js', resultText: '<script src="src/feature.js">' },
{ label: 'test.js', resultText: '<script src="src/test.js">' },
@ -219,7 +223,7 @@ suite('HTML Path Completion', () => {
}, indexHtmlUri, [fixtureWorkspace]);
// document: about.html
testCompletionFor('<script src="s|">', {
await testCompletionFor('<script src="s|">', {
items: [
{ label: 'about.css', resultText: '<script src="about.css">' },
{ label: 'about.html', resultText: '<script src="about.html">' },
@ -227,29 +231,29 @@ suite('HTML Path Completion', () => {
]
}, aboutHtmlUri, [fixtureWorkspace]);
testCompletionFor('<script src="media/|">', {
await testCompletionFor('<script src="media/|">', {
items: [
{ label: 'icon.pic', resultText: '<script src="media/icon.pic">' }
]
}, aboutHtmlUri, [fixtureWorkspace]);
testCompletionFor('<script src="media/f|">', {
await testCompletionFor('<script src="media/f|">', {
items: [
{ label: 'icon.pic', resultText: '<script src="media/icon.pic">' }
]
}, aboutHtmlUri, [fixtureWorkspace]);
});
test('Trigger completion in middle of path', () => {
test('Trigger completion in middle of path', async () => {
// document: index.html
testCompletionFor('<script src="src/f|eature.js">', {
await testCompletionFor('<script src="src/f|eature.js">', {
items: [
{ label: 'feature.js', resultText: '<script src="src/feature.js">' },
{ label: 'test.js', resultText: '<script src="src/test.js">' },
]
}, indexHtmlUri, [fixtureWorkspace]);
testCompletionFor('<script src="s|rc/feature.js">', {
await testCompletionFor('<script src="s|rc/feature.js">', {
items: [
{ label: 'about/', resultText: '<script src="about/">' },
{ label: 'index.html', resultText: '<script src="index.html">' },
@ -258,13 +262,13 @@ suite('HTML Path Completion', () => {
}, indexHtmlUri, [fixtureWorkspace]);
// document: about.html
testCompletionFor('<script src="media/f|eature.js">', {
await testCompletionFor('<script src="media/f|eature.js">', {
items: [
{ label: 'icon.pic', resultText: '<script src="media/icon.pic">' }
]
}, aboutHtmlUri, [fixtureWorkspace]);
testCompletionFor('<script src="m|edia/feature.js">', {
await testCompletionFor('<script src="m|edia/feature.js">', {
items: [
{ label: 'about.css', resultText: '<script src="about.css">' },
{ label: 'about.html', resultText: '<script src="about.html">' },
@ -274,8 +278,8 @@ suite('HTML Path Completion', () => {
});
test('Trigger completion in middle of path and with whitespaces', () => {
testCompletionFor('<script src="./| about/about.html>', {
test('Trigger completion in middle of path and with whitespaces', async () => {
await testCompletionFor('<script src="./| about/about.html>', {
items: [
{ label: 'about/', resultText: '<script src="./about/ about/about.html>' },
{ label: 'index.html', resultText: '<script src="./index.html about/about.html>' },
@ -283,7 +287,7 @@ suite('HTML Path Completion', () => {
]
}, indexHtmlUri, [fixtureWorkspace]);
testCompletionFor('<script src="./a|bout /about.html>', {
await testCompletionFor('<script src="./a|bout /about.html>', {
items: [
{ label: 'about/', resultText: '<script src="./about/ /about.html>' },
{ label: 'index.html', resultText: '<script src="./index.html /about.html>' },
@ -292,13 +296,13 @@ suite('HTML Path Completion', () => {
}, indexHtmlUri, [fixtureWorkspace]);
});
test('Completion should ignore files/folders starting with dot', () => {
testCompletionFor('<script src="./|"', {
test('Completion should ignore files/folders starting with dot', async () => {
await testCompletionFor('<script src="./|"', {
count: 3
}, indexHtmlUri, [fixtureWorkspace]);
});
test('Unquoted Path', () => {
test('Unquoted Path', async () => {
/* Unquoted value is not supported in html language service yet
testCompletionFor(`<div><a href=about/|>`, {
items: [

View file

@ -8,6 +8,7 @@ import * as assert from 'assert';
import { getFoldingRanges } from '../modes/htmlFolding';
import { TextDocument, getLanguageModes } from '../modes/languageModes';
import { ClientCapabilities } from 'vscode-css-languageservice';
import { getNodeFSRequestService } from '../node/nodeFs';
interface ExpectedIndentRange {
startLine: number;
@ -21,7 +22,7 @@ function assertRanges(lines: string[], expected: ExpectedIndentRange[], message?
settings: {},
folders: [{ name: 'foo', uri: 'test://foo' }]
};
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST);
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
const actual = getFoldingRanges(languageModes, document, nRanges, null);
let actualRanges = [];

View file

@ -10,6 +10,7 @@ import * as assert from 'assert';
import { getLanguageModes, TextDocument, Range, FormattingOptions, ClientCapabilities } from '../modes/languageModes';
import { format } from '../modes/formatting';
import { getNodeFSRequestService } from '../node/nodeFs';
suite('HTML Embedded Formatting', () => {
@ -18,7 +19,7 @@ suite('HTML Embedded Formatting', () => {
settings: options,
folders: [{ name: 'foo', uri: 'test://foo' }]
};
let languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST);
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
let rangeStartOffset = value.indexOf('|');
let rangeEndOffset;

View file

@ -7,6 +7,7 @@ import 'mocha';
import * as assert from 'assert';
import { getLanguageModes, ClientCapabilities, TextDocument, SelectionRange} from '../modes/languageModes';
import { getSelectionRanges } from '../modes/selectionRanges';
import { getNodeFSRequestService } from '../node/nodeFs';
function assertRanges(content: string, expected: (number | string)[][]): void {
let message = `${content} gives selection range:\n`;
@ -18,7 +19,7 @@ function assertRanges(content: string, expected: (number | string)[][]): void {
settings: {},
folders: [{ name: 'foo', uri: 'test://foo' }]
};
let languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST);
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
const document = TextDocument.create('test://foo.html', 'html', 1, content);
const actualRanges = getSelectionRanges(languageModes, document, [document.positionAt(offset)]);

View file

@ -7,6 +7,7 @@ import 'mocha';
import * as assert from 'assert';
import { TextDocument, getLanguageModes, ClientCapabilities, Range, Position } from '../modes/languageModes';
import { newSemanticTokenProvider } from '../modes/semanticTokens';
import { getNodeFSRequestService } from '../node/nodeFs';
interface ExpectedToken {
startLine: number;
@ -21,7 +22,7 @@ function assertTokens(lines: string[], expected: ExpectedToken[], ranges?: Range
settings: {},
folders: [{ name: 'foo', uri: 'test://foo' }]
};
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST);
const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST, getNodeFSRequestService());
const semanticTokensProvider = newSemanticTokenProvider(languageModes);
const legend = semanticTokensProvider.legend;

View file

@ -3,9 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DocumentContext, WorkspaceFolder } from '../modes/languageModes';
import { DocumentContext } from 'vscode-css-languageservice';
import { endsWith, startsWith } from '../utils/strings';
import * as url from 'url';
import { WorkspaceFolder } from 'vscode-languageserver';
import { resolvePath } from '../requests';
export function getDocumentContext(documentUri: string, workspaceFolders: WorkspaceFolder[]): DocumentContext {
function getRootFolder(): string | undefined {
@ -22,20 +23,15 @@ export function getDocumentContext(documentUri: string, workspaceFolders: Worksp
}
return {
resolveReference: (ref, base = documentUri) => {
resolveReference: (ref: string, base = documentUri) => {
if (ref[0] === '/') { // resolve absolute path against the current workspace folder
if (startsWith(base, 'file://')) {
let folderUri = getRootFolder();
if (folderUri) {
return folderUri + ref.substr(1);
}
let folderUri = getRootFolder();
if (folderUri) {
return folderUri + ref.substr(1);
}
}
try {
return url.resolve(base, ref);
} catch {
return '';
}
base = base.substr(0, base.lastIndexOf('/') + 1);
return resolvePath(base, ref);
},
};
}

View file

@ -726,17 +726,17 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
vscode-css-languageservice@^4.1.2:
version "4.2.0"
resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.2.0.tgz#56081374857ce8aa4dd4c98f97e4e10a30b7242f"
integrity sha512-HIjl5bofrrxMMF05K/nq83270EdvteuAIio44FWd6tDdfhgg4vbofiAuXRSpXFi335f5+ekKdrzvPZm9ahqzsg==
vscode-css-languageservice@4.3.0-next.2:
version "4.3.0-next.2"
resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.3.0-next.2.tgz#a7a1289d8d68ddcdee55d4f18b12a455acaf5962"
integrity sha512-4h/s/N7wt6If/5EUNMtfAbwWwImH6EvveqZMf9SmQdMMMqekZkRLA68E98hGzuzI13rHEiLckwlAC+RNLq6FXg==
dependencies:
vscode-languageserver-textdocument "^1.0.1"
vscode-languageserver-types "^3.15.1"
vscode-languageserver-types "3.16.0-next.2"
vscode-nls "^4.1.2"
vscode-uri "^2.1.1"
vscode-uri "^2.1.2"
vscode-html-languageservice@^3.1.0-next.2:
vscode-html-languageservice@3.1.0-next.2:
version "3.1.0-next.2"
resolved "https://registry.yarnpkg.com/vscode-html-languageservice/-/vscode-html-languageservice-3.1.0-next.2.tgz#a6ad42ed0ad0adda9ad0c5d34b2ac0d05076190e"
integrity sha512-cohfk2Ez8MrnT/8upnKsOc2FK2T+lU2LsBgg0L2P2BHjVzq4LMCOiYcwNeq+u9y2L5ck9E6SFw1BTdyZ5377KQ==
@ -746,35 +746,40 @@ vscode-html-languageservice@^3.1.0-next.2:
vscode-nls "^4.1.2"
vscode-uri "^2.1.1"
vscode-jsonrpc@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz#9bab9c330d89f43fc8c1e8702b5c36e058a01794"
integrity sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A==
vscode-jsonrpc@6.0.0-next.2:
version "6.0.0-next.2"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0-next.2.tgz#3d73f86d812304cb91b9fb1efee40ec60b09ed7f"
integrity sha512-dKQXRYNUY6BHALQJBJlyZyv9oWlYpbJ2vVoQNNVNPLAYQ3hzNp4zy+iSo7zGx1BPXByArJQDWTKLQh8dz3dnNw==
vscode-languageserver-protocol@^3.15.3:
version "3.15.3"
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz#3fa9a0702d742cf7883cb6182a6212fcd0a1d8bb"
integrity sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==
vscode-languageserver-protocol@3.16.0-next.4:
version "3.16.0-next.4"
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0-next.4.tgz#8f8b1b831d4dfd9b26aa1ba3d2a32c427a91c99f"
integrity sha512-6GmPUp2MhJy2H1CTWp2B40Pa9BeC9glrXWmQWVG6A/0V9UbcAjVC9m56znm2GL32iyLDIprTBe8gBvvvcjbpaQ==
dependencies:
vscode-jsonrpc "^5.0.1"
vscode-languageserver-types "3.15.1"
vscode-jsonrpc "6.0.0-next.2"
vscode-languageserver-types "3.16.0-next.2"
vscode-languageserver-textdocument@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz#178168e87efad6171b372add1dea34f53e5d330f"
integrity sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA==
vscode-languageserver-types@3.15.1, vscode-languageserver-types@^3.15.1:
vscode-languageserver-types@3.16.0-next.2:
version "3.16.0-next.2"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0-next.2.tgz#940bd15c992295a65eae8ab6b8568a1e8daa3083"
integrity sha512-QjXB7CKIfFzKbiCJC4OWC8xUncLsxo19FzGVp/ADFvvi87PlmBSCAtZI5xwGjF5qE0xkLf0jjKUn3DzmpDP52Q==
vscode-languageserver-types@^3.15.1:
version "3.15.1"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz#17be71d78d2f6236d414f0001ce1ef4d23e6b6de"
integrity sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ==
vscode-languageserver@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-6.1.1.tgz#d76afc68172c27d4327ee74332b468fbc740d762"
integrity sha512-DueEpkUAkD5XTR4MLYNr6bQIp/UFR0/IPApgXU3YfCBCB08u2sm9hRCs6DxYZELkk++STPjpcjksR2H8qI3cDQ==
vscode-languageserver@7.0.0-next.3:
version "7.0.0-next.3"
resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-7.0.0-next.3.tgz#3833bd09259a4a085baeba90783f1e4d06d81095"
integrity sha512-qSt8eb546iFuoFIN+9MPl4Avru6Iz2/JP0UmS/3djf40ICa31Np/yJ7anX2j0Az5rCzb0fak8oeKwDioGeVOYg==
dependencies:
vscode-languageserver-protocol "^3.15.3"
vscode-languageserver-protocol "3.16.0-next.4"
vscode-nls@^4.1.2:
version "4.1.2"

View file

@ -45,31 +45,31 @@ vscode-extension-telemetry@0.1.1:
dependencies:
applicationinsights "1.0.8"
vscode-jsonrpc@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz#9bab9c330d89f43fc8c1e8702b5c36e058a01794"
integrity sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A==
vscode-jsonrpc@6.0.0-next.2:
version "6.0.0-next.2"
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0-next.2.tgz#3d73f86d812304cb91b9fb1efee40ec60b09ed7f"
integrity sha512-dKQXRYNUY6BHALQJBJlyZyv9oWlYpbJ2vVoQNNVNPLAYQ3hzNp4zy+iSo7zGx1BPXByArJQDWTKLQh8dz3dnNw==
vscode-languageclient@^6.1.3:
version "6.1.3"
resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-6.1.3.tgz#c979c5bb5855714a0307e998c18ca827c1b3953a"
integrity sha512-YciJxk08iU5LmWu7j5dUt9/1OLjokKET6rME3cI4BRpiF6HZlusm2ZwPt0MYJ0lV5y43sZsQHhyon2xBg4ZJVA==
vscode-languageclient@7.0.0-next.5:
version "7.0.0-next.5"
resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0-next.5.tgz#7ae84c598dff360bd2bc64322b74e10e5d0b9cd6"
integrity sha512-ec+fJg+JiNBIdbeKbzssSuORUaVdtLValtiYdNEUCUjpYE+Y6xXPtXwiZOlS/0OB9pC/RLCMxsj16UwWncQhYQ==
dependencies:
semver "^6.3.0"
vscode-languageserver-protocol "^3.15.3"
vscode-languageserver-protocol "3.16.0-next.4"
vscode-languageserver-protocol@^3.15.3:
version "3.15.3"
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz#3fa9a0702d742cf7883cb6182a6212fcd0a1d8bb"
integrity sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==
vscode-languageserver-protocol@3.16.0-next.4:
version "3.16.0-next.4"
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0-next.4.tgz#8f8b1b831d4dfd9b26aa1ba3d2a32c427a91c99f"
integrity sha512-6GmPUp2MhJy2H1CTWp2B40Pa9BeC9glrXWmQWVG6A/0V9UbcAjVC9m56znm2GL32iyLDIprTBe8gBvvvcjbpaQ==
dependencies:
vscode-jsonrpc "^5.0.1"
vscode-languageserver-types "3.15.1"
vscode-jsonrpc "6.0.0-next.2"
vscode-languageserver-types "3.16.0-next.2"
vscode-languageserver-types@3.15.1:
version "3.15.1"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz#17be71d78d2f6236d414f0001ce1ef4d23e6b6de"
integrity sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ==
vscode-languageserver-types@3.16.0-next.2:
version "3.16.0-next.2"
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0-next.2.tgz#940bd15c992295a65eae8ab6b8568a1e8daa3083"
integrity sha512-QjXB7CKIfFzKbiCJC4OWC8xUncLsxo19FzGVp/ADFvvi87PlmBSCAtZI5xwGjF5qE0xkLf0jjKUn3DzmpDP52Q==
vscode-nls@^4.1.2:
version "4.1.2"