Skip to content

Commit d16e306

Browse files
committed
html headless
1 parent c23285f commit d16e306

32 files changed

+1528
-966
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { ExtensionContext } from 'vscode';
7+
import { LanguageClientOptions } from 'vscode-languageclient';
8+
import { startClient, LanguageClientConstructor } from '../htmlClient';
9+
import { LanguageClient } from 'vscode-languageclient/browser';
10+
11+
declare const Worker: {
12+
new(stringUrl: string): any;
13+
};
14+
declare const TextDecoder: {
15+
new(encoding?: string): { decode(buffer: ArrayBuffer): string; };
16+
};
17+
18+
// this method is called when vs code is activated
19+
export function activate(context: ExtensionContext) {
20+
const serverMain = context.asAbsolutePath('server/dist/browser/htmlServerMain.js');
21+
try {
22+
const worker = new Worker(serverMain);
23+
const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => {
24+
return new LanguageClient(id, name, clientOptions, worker);
25+
};
26+
27+
startClient(context, newLanguageClient, { TextDecoder });
28+
29+
} catch (e) {
30+
console.log(e);
31+
}
32+
}

extensions/html-language-features/client/src/customData.ts

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,55 +3,86 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import * as path from 'path';
7-
import { workspace, WorkspaceFolder, extensions } from 'vscode';
6+
import { workspace, extensions, Uri, EventEmitter, Disposable } from 'vscode';
7+
import { resolvePath, joinPath } from './requests';
88

9-
interface ExperimentalConfig {
10-
customData?: string[];
11-
experimental?: {
12-
customData?: string[];
9+
export function getCustomDataSource(toDispose: Disposable[]) {
10+
let pathsInWorkspace = getCustomDataPathsInAllWorkspaces();
11+
let pathsInExtensions = getCustomDataPathsFromAllExtensions();
12+
13+
const onChange = new EventEmitter<void>();
14+
15+
toDispose.push(extensions.onDidChange(_ => {
16+
const newPathsInExtensions = getCustomDataPathsFromAllExtensions();
17+
if (newPathsInExtensions.length !== pathsInExtensions.length || !newPathsInExtensions.every((val, idx) => val === pathsInExtensions[idx])) {
18+
pathsInExtensions = newPathsInExtensions;
19+
onChange.fire();
20+
}
21+
}));
22+
toDispose.push(workspace.onDidChangeConfiguration(e => {
23+
if (e.affectsConfiguration('html.customData')) {
24+
pathsInWorkspace = getCustomDataPathsInAllWorkspaces();
25+
onChange.fire();
26+
}
27+
}));
28+
29+
return {
30+
get uris() {
31+
return pathsInWorkspace.concat(pathsInExtensions);
32+
},
33+
get onDidChange() {
34+
return onChange.event;
35+
}
1336
};
1437
}
1538

16-
export function getCustomDataPathsInAllWorkspaces(workspaceFolders: readonly WorkspaceFolder[] | undefined): string[] {
39+
40+
function getCustomDataPathsInAllWorkspaces(): string[] {
41+
const workspaceFolders = workspace.workspaceFolders;
42+
1743
const dataPaths: string[] = [];
1844

1945
if (!workspaceFolders) {
2046
return dataPaths;
2147
}
2248

23-
workspaceFolders.forEach(wf => {
24-
const allHtmlConfig = workspace.getConfiguration(undefined, wf.uri);
25-
const wfHtmlConfig = allHtmlConfig.inspect<ExperimentalConfig>('html');
26-
27-
if (wfHtmlConfig && wfHtmlConfig.workspaceFolderValue && wfHtmlConfig.workspaceFolderValue.customData) {
28-
const customData = wfHtmlConfig.workspaceFolderValue.customData;
29-
if (Array.isArray(customData)) {
30-
customData.forEach(t => {
31-
if (typeof t === 'string') {
32-
dataPaths.push(path.resolve(wf.uri.fsPath, t));
33-
}
34-
});
49+
const collect = (paths: string[] | undefined, rootFolder: Uri) => {
50+
if (Array.isArray(paths)) {
51+
for (const path of paths) {
52+
if (typeof path === 'string') {
53+
dataPaths.push(resolvePath(rootFolder, path).toString());
54+
}
3555
}
3656
}
37-
});
57+
};
3858

59+
for (let i = 0; i < workspaceFolders.length; i++) {
60+
const folderUri = workspaceFolders[i].uri;
61+
const allHtmlConfig = workspace.getConfiguration('html', folderUri);
62+
const customDataInspect = allHtmlConfig.inspect<string[]>('customData');
63+
if (customDataInspect) {
64+
collect(customDataInspect.workspaceFolderValue, folderUri);
65+
if (i === 0) {
66+
if (workspace.workspaceFile) {
67+
collect(customDataInspect.workspaceValue, workspace.workspaceFile);
68+
}
69+
collect(customDataInspect.globalValue, folderUri);
70+
}
71+
}
72+
73+
}
3974
return dataPaths;
4075
}
4176

42-
export function getCustomDataPathsFromAllExtensions(): string[] {
77+
function getCustomDataPathsFromAllExtensions(): string[] {
4378
const dataPaths: string[] = [];
44-
4579
for (const extension of extensions.all) {
46-
const contributes = extension.packageJSON && extension.packageJSON.contributes;
47-
48-
if (contributes && contributes.html && contributes.html.customData && Array.isArray(contributes.html.customData)) {
49-
const relativePaths: string[] = contributes.html.customData;
50-
relativePaths.forEach(rp => {
51-
dataPaths.push(path.resolve(extension.extensionPath, rp));
52-
});
80+
const customData = extension.packageJSON?.contributes?.html?.customData;
81+
if (Array.isArray(customData)) {
82+
for (const rp of customData) {
83+
dataPaths.push(joinPath(extension.extensionUri, rp).toString());
84+
}
5385
}
5486
}
55-
5687
return dataPaths;
5788
}

extensions/html-language-features/client/src/htmlMain.ts renamed to extensions/html-language-features/client/src/htmlClient.ts

Lines changed: 33 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import * as fs from 'fs';
76
import * as nls from 'vscode-nls';
87
const localize = nls.loadMessageBundle();
98

@@ -13,13 +12,17 @@ import {
1312
DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider, SemanticTokens, window, commands
1413
} from 'vscode';
1514
import {
16-
LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, RequestType, TextDocumentPositionParams, DocumentRangeFormattingParams,
17-
DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, TextDocumentIdentifier, RequestType0, Range as LspRange
15+
LanguageClientOptions, RequestType, TextDocumentPositionParams, DocumentRangeFormattingParams,
16+
DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, TextDocumentIdentifier, RequestType0, Range as LspRange, NotificationType, CommonLanguageClient
1817
} from 'vscode-languageclient';
1918
import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared';
2019
import { activateTagClosing } from './tagClosing';
21-
import TelemetryReporter from 'vscode-extension-telemetry';
22-
import { getCustomDataPathsInAllWorkspaces, getCustomDataPathsFromAllExtensions } from './customData';
20+
import { RequestService } from './requests';
21+
import { getCustomDataSource } from './customData';
22+
23+
namespace CustomDataChangedNotification {
24+
export const type: NotificationType<string[]> = new NotificationType('html/customDataChanged');
25+
}
2326

2427
namespace TagCloseRequest {
2528
export const type: RequestType<TextDocumentPositionParams, string, any, any> = new RequestType('html/tag');
@@ -46,44 +49,33 @@ namespace SettingIds {
4649

4750
}
4851

49-
interface IPackageInfo {
50-
name: string;
51-
version: string;
52-
aiKey: string;
53-
main: string;
52+
export interface TelemetryReporter {
53+
sendTelemetryEvent(eventName: string, properties?: {
54+
[key: string]: string;
55+
}, measurements?: {
56+
[key: string]: number;
57+
}): void;
5458
}
5559

56-
let telemetryReporter: TelemetryReporter | null;
57-
58-
59-
export function activate(context: ExtensionContext) {
60-
let toDispose = context.subscriptions;
60+
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => CommonLanguageClient;
6161

62-
let clientPackageJSON = getPackageInfo(context);
63-
telemetryReporter = new TelemetryReporter(clientPackageJSON.name, clientPackageJSON.version, clientPackageJSON.aiKey);
62+
export interface Runtime {
63+
TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string; } };
64+
fs?: RequestService;
65+
telemetry?: TelemetryReporter;
66+
}
6467

65-
const serverMain = `./server/${clientPackageJSON.main.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/htmlServerMain`;
66-
const serverModule = context.asAbsolutePath(serverMain);
68+
export function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime) {
6769

68-
// The debug options for the server
69-
let debugOptions = { execArgv: ['--nolazy', '--inspect=6045'] };
70+
let toDispose = context.subscriptions;
7071

71-
// If the extension is launch in debug mode the debug server options are use
72-
// Otherwise the run options are used
73-
let serverOptions: ServerOptions = {
74-
run: { module: serverModule, transport: TransportKind.ipc },
75-
debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }
76-
};
7772

7873
let documentSelector = ['html', 'handlebars'];
7974
let embeddedLanguages = { css: true, javascript: true };
8075

8176
let rangeFormatting: Disposable | undefined = undefined;
8277

83-
let dataPaths = [
84-
...getCustomDataPathsInAllWorkspaces(workspace.workspaceFolders),
85-
...getCustomDataPathsFromAllExtensions()
86-
];
78+
const customDataSource = getCustomDataSource(context.subscriptions);
8779

8880
// Options to control the language client
8981
let clientOptions: LanguageClientOptions = {
@@ -93,7 +85,7 @@ export function activate(context: ExtensionContext) {
9385
},
9486
initializationOptions: {
9587
embeddedLanguages,
96-
dataPaths,
88+
handledSchemas: ['file'],
9789
provideFormatter: false, // tell the server to not provide formatting capability and ignore the `html.format.enable` setting.
9890
},
9991
middleware: {
@@ -123,12 +115,18 @@ export function activate(context: ExtensionContext) {
123115
};
124116

125117
// Create the language client and start the client.
126-
let client = new LanguageClient('html', localize('htmlserver.name', 'HTML Language Server'), serverOptions, clientOptions);
118+
let client = newLanguageClient('html', localize('htmlserver.name', 'HTML Language Server'), clientOptions);
127119
client.registerProposedFeatures();
128120

129121
let disposable = client.start();
130122
toDispose.push(disposable);
131123
client.onReady().then(() => {
124+
125+
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
126+
customDataSource.onDidChange(() => {
127+
client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris);
128+
});
129+
132130
let tagRequestor = (document: TextDocument, position: Position) => {
133131
let param = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position);
134132
return client.sendRequest(TagCloseRequest.type, param);
@@ -137,9 +135,7 @@ export function activate(context: ExtensionContext) {
137135
toDispose.push(disposable);
138136

139137
disposable = client.onTelemetry(e => {
140-
if (telemetryReporter) {
141-
telemetryReporter.sendTelemetryEvent(e.key, e.data);
142-
}
138+
runtime.telemetry?.sendTelemetryEvent(e.key, e.data);
143139
});
144140
toDispose.push(disposable);
145141

@@ -201,7 +197,7 @@ export function activate(context: ExtensionContext) {
201197
return client.sendRequest(DocumentRangeFormattingRequest.type, params, token).then(
202198
client.protocol2CodeConverter.asTextEdits,
203199
(error) => {
204-
client.logFailedRequest(DocumentRangeFormattingRequest.type, error);
200+
client.handleFailedRequest(DocumentRangeFormattingRequest.type, error, []);
205201
return Promise.resolve([]);
206202
}
207203
);
@@ -319,17 +315,3 @@ export function activate(context: ExtensionContext) {
319315

320316
toDispose.push();
321317
}
322-
323-
function getPackageInfo(context: ExtensionContext): IPackageInfo {
324-
const location = context.asAbsolutePath('./package.json');
325-
try {
326-
return JSON.parse(fs.readFileSync(location).toString());
327-
} catch (e) {
328-
console.log(`Problems reading ${location}: ${e}`);
329-
return { name: '', version: '', aiKey: '', main: '' };
330-
}
331-
}
332-
333-
export function deactivate(): Promise<any> {
334-
return telemetryReporter ? telemetryReporter.dispose() : Promise.resolve(null);
335-
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { getNodeFSRequestService } from './nodeFs';
7+
import { ExtensionContext } from 'vscode';
8+
import { startClient, LanguageClientConstructor } from '../htmlClient';
9+
import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node';
10+
import { TextDecoder } from 'util';
11+
import * as fs from 'fs';
12+
import TelemetryReporter from 'vscode-extension-telemetry';
13+
14+
15+
let telemetry: TelemetryReporter | undefined;
16+
17+
// this method is called when vs code is activated
18+
export function activate(context: ExtensionContext) {
19+
20+
let clientPackageJSON = getPackageInfo(context);
21+
telemetry = new TelemetryReporter(clientPackageJSON.name, clientPackageJSON.version, clientPackageJSON.aiKey);
22+
23+
const serverMain = `./server/${clientPackageJSON.main.indexOf('/dist/') !== -1 ? 'dist' : 'out'}/node/htmlServerMain`;
24+
const serverModule = context.asAbsolutePath(serverMain);
25+
26+
// The debug options for the server
27+
const debugOptions = { execArgv: ['--nolazy', '--inspect=6044'] };
28+
29+
// If the extension is launch in debug mode the debug server options are use
30+
// Otherwise the run options are used
31+
const serverOptions: ServerOptions = {
32+
run: { module: serverModule, transport: TransportKind.ipc },
33+
debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions }
34+
};
35+
36+
const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => {
37+
return new LanguageClient(id, name, serverOptions, clientOptions);
38+
};
39+
40+
startClient(context, newLanguageClient, { fs: getNodeFSRequestService(), TextDecoder, telemetry });
41+
}
42+
43+
interface IPackageInfo {
44+
name: string;
45+
version: string;
46+
aiKey: string;
47+
main: string;
48+
}
49+
50+
function getPackageInfo(context: ExtensionContext): IPackageInfo {
51+
const location = context.asAbsolutePath('./package.json');
52+
try {
53+
return JSON.parse(fs.readFileSync(location).toString());
54+
} catch (e) {
55+
console.log(`Problems reading ${location}: ${e}`);
56+
return { name: '', version: '', aiKey: '', main: '' };
57+
}
58+
}

0 commit comments

Comments
 (0)