jsonServerMain.ts 15.2 KB
Newer Older
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

import {
8
	createConnection, IConnection,
9
	TextDocuments, TextDocument, InitializeParams, InitializeResult, NotificationType, RequestType,
10
	DocumentRangeFormattingRequest, Disposable, ServerCapabilities, DocumentColorRequest, ColorPresentationRequest,
11 12
} from 'vscode-languageserver';

J
Johannes Rieken 已提交
13
import { xhr, XHRResponse, configure as configureHttpRequests, getErrorStatusDescription } from 'request-light';
14
import fs = require('fs');
M
Martin Aeschlimann 已提交
15
import URI from 'vscode-uri';
16
import * as URL from 'url';
17
import Strings = require('./utils/strings');
18 19
import { formatError, runSafe, runSafeAsync } from './utils/errors';
import { JSONDocument, JSONSchema, getLanguageService, DocumentLanguageSettings, SchemaConfiguration } from 'vscode-json-languageservice';
J
Johannes Rieken 已提交
20
import { getLanguageModelCache } from './languageModelCache';
21
import { createScanner, SyntaxKind } from 'jsonc-parser';
M
Martin Aeschlimann 已提交
22

23 24
import { FoldingRangeType, FoldingRangesRequest, FoldingRange, FoldingRangeList, FoldingProviderServerCapabilities } from './protocol/foldingProvider.proposed';

25 26 27 28
interface ISchemaAssociations {
	[pattern: string]: string[];
}

29
namespace SchemaAssociationNotification {
30
	export const type: NotificationType<ISchemaAssociations, any> = new NotificationType('json/schemaAssociations');
31 32 33
}

namespace VSCodeContentRequest {
34
	export const type: RequestType<string, string, any, any> = new RequestType('vscode/content');
35 36
}

37 38 39 40
namespace SchemaContentChangeNotification {
	export const type: NotificationType<string, any> = new NotificationType('json/schemaContent');
}

41
// Create a connection for the server
42
let connection: IConnection = createConnection();
43

44
process.on('unhandledRejection', (e: any) => {
45 46 47
	connection.console.error(formatError(`Unhandled exception`, e));
});

M
Martin Aeschlimann 已提交
48 49 50
console.log = connection.console.log.bind(connection.console);
console.error = connection.console.error.bind(connection.console);

51 52 53 54 55 56 57
// Create a simple text document manager. The text document manager
// supports full document sync only
let documents: TextDocuments = new TextDocuments();
// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);

58 59 60
let clientSnippetSupport = false;
let clientDynamicRegisterSupport = false;

61
// After the server has started the client sends an initilize request. The server receives
A
Andre Weinand 已提交
62
// in the passed params the rootPath of the workspace plus the client capabilities.
M
Martin Aeschlimann 已提交
63
connection.onInitialize((params: InitializeParams): InitializeResult => {
64

65
	function hasClientCapability(...keys: string[]) {
66
		let c = params.capabilities as any;
67 68 69 70 71 72 73 74
		for (let i = 0; c && i < keys.length; i++) {
			c = c[keys[i]];
		}
		return !!c;
	}

	clientSnippetSupport = hasClientCapability('textDocument', 'completion', 'completionItem', 'snippetSupport');
	clientDynamicRegisterSupport = hasClientCapability('workspace', 'symbol', 'dynamicRegistration');
75
	let capabilities: ServerCapabilities & FoldingProviderServerCapabilities = {
76 77
		// Tell the client that the server works in FULL text document sync mode
		textDocumentSync: documents.syncKind,
78
		completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: ['"', ':'] } : void 0,
79 80 81
		hoverProvider: true,
		documentSymbolProvider: true,
		documentRangeFormattingProvider: false,
82 83
		colorProvider: true,
		foldingProvider: true
A
tslint  
Alex Dima 已提交
84
	};
85 86

	return { capabilities };
87 88 89
});

let workspaceContext = {
90
	resolveRelativePath: (relativePath: string, resource: string) => {
91
		return URL.resolve(resource, relativePath);
92
	}
A
tslint  
Alex Dima 已提交
93
};
94

J
Johannes Rieken 已提交
95
let schemaRequestService = (uri: string): Thenable<string> => {
96 97 98
	if (Strings.startsWith(uri, 'file://')) {
		let fsPath = URI.parse(uri).fsPath;
		return new Promise<string>((c, e) => {
99
			fs.readFile(fsPath, 'UTF-8', (err, result) => {
100
				err ? e('') : c(result.toString());
101 102
			});
		});
103 104 105
	} else if (Strings.startsWith(uri, 'vscode://')) {
		return connection.sendRequest(VSCodeContentRequest.type, uri).then(responseText => {
			return responseText;
106
		}, error => {
M
Martin Aeschlimann 已提交
107
			return Promise.reject(error.message);
108
		});
109
	}
110
	if (uri.indexOf('//schema.management.azure.com/') !== -1) {
111 112 113 114 115
		/* __GDPR__
			"json.schema" : {
				"schemaURL" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
			}
		 */
116 117 118 119 120 121 122
		connection.telemetry.logEvent({
			key: 'json.schema',
			value: {
				schemaURL: uri
			}
		});
	}
123 124
	let headers = { 'Accept-Encoding': 'gzip, deflate' };
	return xhr({ url: uri, followRedirects: 5, headers }).then(response => {
125 126
		return response.responseText;
	}, (error: XHRResponse) => {
127
		return Promise.reject(error.responseText || getErrorStatusDescription(error.status) || error.toString());
128
	});
A
tslint  
Alex Dima 已提交
129
};
130

131
// create the JSON language service
132 133 134
let languageService = getLanguageService({
	schemaRequestService,
	workspaceContext,
135
	contributions: []
136 137
});

138
// The settings interface describes the server relevant settings part
139
interface Settings {
140
	json: {
A
tslint  
Alex Dima 已提交
141
		schemas: JSONSchemaSettings[];
142
		format: { enable: boolean; };
A
tslint  
Alex Dima 已提交
143
	};
M
Martin Aeschlimann 已提交
144
	http: {
A
tslint  
Alex Dima 已提交
145 146 147
		proxy: string;
		proxyStrictSSL: boolean;
	};
148 149
}

150
interface JSONSchemaSettings {
A
tslint  
Alex Dima 已提交
151 152
	fileMatch?: string[];
	url?: string;
153
	schema?: JSONSchema;
154 155
}

156 157 158
let jsonConfigurationSettings: JSONSchemaSettings[] | undefined = void 0;
let schemaAssociations: ISchemaAssociations | undefined = void 0;
let formatterRegistration: Thenable<Disposable> | null = null;
159

160 161
// The settings have changed. Is send on server activation as well.
connection.onDidChangeConfiguration((change) => {
A
tslint  
Alex Dima 已提交
162
	var settings = <Settings>change.settings;
163
	configureHttpRequests(settings.http && settings.http.proxy, settings.http && settings.http.proxyStrictSSL);
A
tslint  
Alex Dima 已提交
164

165
	jsonConfigurationSettings = settings.json && settings.json.schemas;
166
	updateConfiguration();
167 168 169 170 171 172

	// dynamically enable & disable the formatter
	if (clientDynamicRegisterSupport) {
		let enableFormatter = settings && settings.json && settings.json.format && settings.json.format.enable;
		if (enableFormatter) {
			if (!formatterRegistration) {
173
				formatterRegistration = connection.client.register(DocumentRangeFormattingRequest.type, { documentSelector: [{ language: 'json' }, { language: 'jsonc' }] });
174 175 176 177 178 179
			}
		} else if (formatterRegistration) {
			formatterRegistration.then(r => r.dispose());
			formatterRegistration = null;
		}
	}
180
});
181

182
// The jsonValidation extension configuration has changed
183
connection.onNotification(SchemaAssociationNotification.type, associations => {
184 185 186 187
	schemaAssociations = associations;
	updateConfiguration();
});

188 189 190 191 192
// A schema has changed
connection.onNotification(SchemaContentChangeNotification.type, uri => {
	languageService.resetSchema(uri);
});

193
function updateConfiguration() {
194
	let languageSettings = {
195 196
		validate: true,
		allowComments: true,
197
		schemas: new Array<SchemaConfiguration>()
198
	};
199 200 201 202
	if (schemaAssociations) {
		for (var pattern in schemaAssociations) {
			let association = schemaAssociations[pattern];
			if (Array.isArray(association)) {
M
Martin Aeschlimann 已提交
203
				association.forEach(uri => {
204
					languageSettings.schemas.push({ uri, fileMatch: [pattern] });
A
tslint  
Alex Dima 已提交
205
				});
206 207 208 209
			}
		}
	}
	if (jsonConfigurationSettings) {
210
		jsonConfigurationSettings.forEach((schema, index) => {
211 212
			let uri = schema.url;
			if (!uri && schema.schema) {
213
				uri = schema.schema.id || `vscode://schemas/custom/${index}`;
214 215 216
			}
			if (uri) {
				languageSettings.schemas.push({ uri, fileMatch: schema.fileMatch, schema: schema.schema });
217 218 219
			}
		});
	}
220
	languageService.configure(languageSettings);
M
Martin Aeschlimann 已提交
221

222
	// Revalidate any open text documents
223
	documents.all().forEach(triggerValidation);
224 225
}

226 227 228 229 230 231 232 233 234 235 236 237
// 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: [] });
});

J
Johannes Rieken 已提交
238
let pendingValidationRequests: { [uri: string]: NodeJS.Timer; } = {};
239
const validationDelayMs = 500;
240

241
function cleanPendingValidation(textDocument: TextDocument): void {
242 243 244
	let request = pendingValidationRequests[textDocument.uri];
	if (request) {
		clearTimeout(request);
245
		delete pendingValidationRequests[textDocument.uri];
246
	}
247 248 249 250
}

function triggerValidation(textDocument: TextDocument): void {
	cleanPendingValidation(textDocument);
251 252 253 254 255
	pendingValidationRequests[textDocument.uri] = setTimeout(() => {
		delete pendingValidationRequests[textDocument.uri];
		validateTextDocument(textDocument);
	}, validationDelayMs);
}
256

257
function validateTextDocument(textDocument: TextDocument): void {
258 259 260 261 262
	if (textDocument.getText().length === 0) {
		// ignore empty documents
		connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: [] });
		return;
	}
263 264
	try {
		let jsonDocument = getJSONDocument(textDocument);
265

266 267 268 269 270 271 272 273
		let documentSettings: DocumentLanguageSettings = textDocument.languageId === 'jsonc' ? { comments: 'ignore', trailingCommas: 'ignore' } : { comments: 'error', trailingCommas: 'error' };
		languageService.doValidation(textDocument, jsonDocument, documentSettings).then(diagnostics => {
			// Send the computed diagnostics to VSCode.
			connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
		});
	} catch (e) {
		connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e));
	}
274 275 276
}

connection.onDidChangeWatchedFiles((change) => {
277
	// Monitored files have changed in VSCode
278 279
	let hasChanges = false;
	change.changes.forEach(c => {
M
Martin Aeschlimann 已提交
280
		if (languageService.resetSchema(c.uri)) {
281 282 283 284 285 286 287 288
			hasChanges = true;
		}
	});
	if (hasChanges) {
		documents.all().forEach(validateTextDocument);
	}
});

289
let jsonDocuments = getLanguageModelCache<JSONDocument>(10, 60, document => languageService.parseJSONDocument(document));
290 291 292 293 294 295
documents.onDidClose(e => {
	jsonDocuments.onDocumentRemoved(e.document);
});
connection.onShutdown(() => {
	jsonDocuments.dispose();
});
296

297
function getJSONDocument(document: TextDocument): JSONDocument {
298
	return jsonDocuments.get(document);
299 300
}

M
Martin Aeschlimann 已提交
301
connection.onCompletion(textDocumentPosition => {
302
	return runSafeAsync(() => {
303 304 305 306
		let document = documents.get(textDocumentPosition.textDocument.uri);
		let jsonDocument = getJSONDocument(document);
		return languageService.doComplete(document, textDocumentPosition.position, jsonDocument);
	}, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`);
307 308
});

M
Martin Aeschlimann 已提交
309
connection.onCompletionResolve(completionItem => {
310
	return runSafeAsync(() => {
311
		return languageService.doResolve(completionItem);
312
	}, completionItem, `Error while resolving completion proposal`);
313 314
});

M
Martin Aeschlimann 已提交
315
connection.onHover(textDocumentPositionParams => {
316
	return runSafeAsync(() => {
317 318 319 320
		let document = documents.get(textDocumentPositionParams.textDocument.uri);
		let jsonDocument = getJSONDocument(document);
		return languageService.doHover(document, textDocumentPositionParams.position, jsonDocument);
	}, null, `Error while computing hover for ${textDocumentPositionParams.textDocument.uri}`);
321 322
});

M
Martin Aeschlimann 已提交
323
connection.onDocumentSymbol(documentSymbolParams => {
324 325 326 327 328
	return runSafe(() => {
		let document = documents.get(documentSymbolParams.textDocument.uri);
		let jsonDocument = getJSONDocument(document);
		return languageService.findDocumentSymbols(document, jsonDocument);
	}, [], `Error while computing document symbols for ${documentSymbolParams.textDocument.uri}`);
329 330
});

M
Martin Aeschlimann 已提交
331
connection.onDocumentRangeFormatting(formatParams => {
332 333 334 335
	return runSafe(() => {
		let document = documents.get(formatParams.textDocument.uri);
		return languageService.format(document, formatParams.range, formatParams.options);
	}, [], `Error while formatting range for ${formatParams.textDocument.uri}`);
336 337
});

338
connection.onRequest(DocumentColorRequest.type, params => {
339
	return runSafeAsync(() => {
340 341 342 343 344
		let document = documents.get(params.textDocument.uri);
		if (document) {
			let jsonDocument = getJSONDocument(document);
			return languageService.findDocumentColors(document, jsonDocument);
		}
345
		return Promise.resolve([]);
346
	}, [], `Error while computing document colors for ${params.textDocument.uri}`);
M
Martin Aeschlimann 已提交
347 348
});

349
connection.onRequest(ColorPresentationRequest.type, params => {
350 351 352 353 354 355 356
	return runSafe(() => {
		let document = documents.get(params.textDocument.uri);
		if (document) {
			let jsonDocument = getJSONDocument(document);
			return languageService.getColorPresentations(document, jsonDocument, params.color, params.range);
		}
		return [];
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376
	}, [], `Error while computing color presentations for ${params.textDocument.uri}`);
});

connection.onRequest(FoldingRangesRequest.type, params => {
	return runSafe(() => {
		let document = documents.get(params.textDocument.uri);
		if (document) {
			let ranges: FoldingRange[] = [];
			let stack: FoldingRange[] = [];
			let prevStart = -1;
			let scanner = createScanner(document.getText(), false);
			let token = scanner.scan();
			while (token !== SyntaxKind.EOF) {
				switch (token) {
					case SyntaxKind.OpenBraceToken:
					case SyntaxKind.OpenBracketToken: {
						let startLine = document.positionAt(scanner.getTokenOffset()).line;
						let range = { startLine, endLine: startLine, type: token === SyntaxKind.OpenBraceToken ? 'object' : 'array' };
						stack.push(range);
						break;
377
					}
378 379 380 381 382 383 384 385 386 387 388 389 390
					case SyntaxKind.CloseBraceToken:
					case SyntaxKind.CloseBracketToken: {
						let type = token === SyntaxKind.CloseBraceToken ? 'object' : 'array';
						if (stack.length > 0 && stack[stack.length - 1].type === type) {
							let range = stack.pop();
							let line = document.positionAt(scanner.getTokenOffset()).line;
							if (range && line > range.startLine + 1 && prevStart !== range.startLine) {
								range.endLine = line - 1;
								ranges.push(range);
								prevStart = range.startLine;
							}
						}
						break;
391 392
					}

393 394 395 396 397 398 399 400
					case SyntaxKind.BlockCommentTrivia: {
						let startLine = document.positionAt(scanner.getTokenOffset()).line;
						let endLine = document.positionAt(scanner.getTokenOffset() + scanner.getTokenLength()).line;
						if (startLine < endLine) {
							ranges.push({ startLine, endLine, type: FoldingRangeType.Comment });
							prevStart = startLine;
						}
						break;
401 402
					}

403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
					case SyntaxKind.LineCommentTrivia: {
						let text = document.getText().substr(scanner.getTokenOffset(), scanner.getTokenLength());
						let m = text.match(/^\/\/\s*#(region\b)|(endregion\b)/);
						if (m) {
							let line = document.positionAt(scanner.getTokenOffset()).line;
							if (m[1]) { // start pattern match
								let range = { startLine: line, endLine: line, type: FoldingRangeType.Region };
								stack.push(range);
							} else {
								let i = stack.length - 1;
								while (i >= 0 && stack[i].type !== FoldingRangeType.Region) {
									i--;
								}
								if (i >= 0) {
									let range = stack[i];
									stack.length = i;
									if (line > range.startLine && prevStart !== range.startLine) {
										range.endLine = line;
										ranges.push(range);
										prevStart = range.startLine;
									}
								}
							}
						}
						break;
428 429
					}

430 431 432 433 434 435 436
				}
				token = scanner.scan();
			}
			return <FoldingRangeList>{ ranges };
		}
		return null;
	}, null, `Error while computing folding ranges for ${params.textDocument.uri}`);
437 438
});

439 440
// Listen on the connection
connection.listen();