diff --git a/extensions/html/client/src/htmlMain.ts b/extensions/html/client/src/htmlMain.ts
index 1ddf5f1b665..75dc6fcbbff 100644
--- a/extensions/html/client/src/htmlMain.ts
+++ b/extensions/html/client/src/htmlMain.ts
@@ -8,12 +8,14 @@ import * as path from 'path';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
-import { languages, ExtensionContext, IndentAction, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString } from 'vscode';
-import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, RequestType, TextDocumentPositionParams } from 'vscode-languageclient';
+import { languages, ExtensionContext, IndentAction, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, FoldingRangeList, FoldingRange, workspace } from 'vscode';
+import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, RequestType, TextDocumentPositionParams, Disposable } from 'vscode-languageclient';
import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared';
import { activateTagClosing } from './tagClosing';
import TelemetryReporter from 'vscode-extension-telemetry';
+import { FoldingRangesRequest } from './protocol/foldingProvider.proposed';
+
namespace TagCloseRequest {
export const type: RequestType = new RequestType('html/tag');
}
@@ -26,6 +28,9 @@ interface IPackageInfo {
let telemetryReporter: TelemetryReporter | null;
+let foldingProviderRegistration: Disposable | undefined = void 0;
+const foldingSetting = 'html.experimental.syntaxFolding';
+
export function activate(context: ExtensionContext) {
let toDispose = context.subscriptions;
@@ -78,6 +83,14 @@ export function activate(context: ExtensionContext) {
}
});
toDispose.push(disposable);
+
+ initFoldingProvider();
+ toDispose.push(workspace.onDidChangeConfiguration(c => {
+ if (c.affectsConfiguration(foldingSetting)) {
+ initFoldingProvider();
+ }
+ }));
+ toDispose.push({ dispose: () => foldingProviderRegistration && foldingProviderRegistration.dispose() });
});
languages.setLanguageConfiguration('html', {
@@ -153,6 +166,29 @@ export function activate(context: ExtensionContext) {
return null;
}
});
+
+ function initFoldingProvider() {
+ let enable = workspace.getConfiguration().get(foldingSetting);
+ if (enable) {
+ if (!foldingProviderRegistration) {
+ foldingProviderRegistration = languages.registerFoldingProvider(documentSelector, {
+ provideFoldingRanges(document: TextDocument) {
+ return client.sendRequest(FoldingRangesRequest.type, { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document) }).then(res => {
+ if (res && Array.isArray(res.ranges)) {
+ return new FoldingRangeList(res.ranges.map(r => new FoldingRange(r.startLine, r.endLine, r.type)));
+ }
+ return null;
+ });
+ }
+ });
+ }
+ } else {
+ if (foldingProviderRegistration) {
+ foldingProviderRegistration.dispose();
+ foldingProviderRegistration = void 0;
+ }
+ }
+ }
}
function getPackageInfo(context: ExtensionContext): IPackageInfo | null {
diff --git a/extensions/html/client/src/protocol/foldingProvider.proposed.ts b/extensions/html/client/src/protocol/foldingProvider.proposed.ts
new file mode 100644
index 00000000000..86209eac344
--- /dev/null
+++ b/extensions/html/client/src/protocol/foldingProvider.proposed.ts
@@ -0,0 +1,89 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { TextDocumentIdentifier } from 'vscode-languageserver-types';
+import { RequestType, TextDocumentRegistrationOptions, StaticRegistrationOptions } from 'vscode-languageserver-protocol';
+
+// ---- capabilities
+
+export interface FoldingProviderClientCapabilities {
+ /**
+ * The text document client capabilities
+ */
+ textDocument?: {
+ /**
+ * Capabilities specific to the foldingProvider
+ */
+ foldingProvider?: {
+ /**
+ * Whether implementation supports dynamic registration. If this is set to `true`
+ * the client supports the new `(FoldingProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ * return value for the corresponding server capability as well.
+ */
+ dynamicRegistration?: boolean;
+ };
+ };
+}
+
+export interface FoldingProviderOptions {
+}
+
+export interface FoldingProviderServerCapabilities {
+ /**
+ * The server provides folding provider support.
+ */
+ foldingProvider?: FoldingProviderOptions | (FoldingProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions);
+}
+
+export interface FoldingRangeList {
+ /**
+ * The folding ranges.
+ */
+ ranges: FoldingRange[];
+}
+
+export enum FoldingRangeType {
+ /**
+ * Folding range for a comment
+ */
+ Comment = 'comment',
+ /**
+ * Folding range for a imports or includes
+ */
+ Imports = 'imports',
+ /**
+ * Folding range for a region (e.g. `#region`)
+ */
+ Region = 'region'
+}
+
+export interface FoldingRange {
+
+ /**
+ * The start line number
+ */
+ startLine: number;
+
+ /**
+ * The end line number
+ */
+ endLine: number;
+
+ /**
+ * The actual color value for this folding range.
+ */
+ type?: FoldingRangeType | string;
+}
+
+export interface FoldingRangeRequestParam {
+ /**
+ * The text document.
+ */
+ textDocument: TextDocumentIdentifier;
+}
+
+export namespace FoldingRangesRequest {
+ export const type: RequestType = new RequestType('textDocument/foldingRanges');
+}
diff --git a/extensions/html/package.json b/extensions/html/package.json
index d7a3611d25e..81e6e702eaf 100644
--- a/extensions/html/package.json
+++ b/extensions/html/package.json
@@ -213,6 +213,11 @@
"default": true,
"description": "%html.autoClosingTags%"
},
+ "html.experimental.syntaxFolding": {
+ "type": "boolean",
+ "default": false,
+ "description": "%html.experimental.syntaxFolding%"
+ },
"html.trace.server": {
"type": "string",
"scope": "window",
diff --git a/extensions/html/package.nls.json b/extensions/html/package.nls.json
index 4b59c6fba33..b41b1236299 100644
--- a/extensions/html/package.nls.json
+++ b/extensions/html/package.nls.json
@@ -22,5 +22,6 @@
"html.trace.server.desc": "Traces the communication between VS Code and the HTML language server.",
"html.validate.scripts": "Configures if the built-in HTML language support validates embedded scripts.",
"html.validate.styles": "Configures if the built-in HTML language support validates embedded styles.",
+ "html.experimental.syntaxFolding": "Enables/disables syntax aware folding markers.",
"html.autoClosingTags": "Enable/disable autoclosing of HTML tags."
}
\ No newline at end of file
diff --git a/extensions/html/server/src/htmlServerMain.ts b/extensions/html/server/src/htmlServerMain.ts
index 8d04a8a8590..e657c392487 100644
--- a/extensions/html/server/src/htmlServerMain.ts
+++ b/extensions/html/server/src/htmlServerMain.ts
@@ -19,6 +19,8 @@ import uri from 'vscode-uri';
import { formatError, runSafe } from './utils/errors';
import { doComplete as emmetDoComplete, updateExtensionsPath as updateEmmetExtensionsPath, getEmmetCompletionParticipants } from 'vscode-emmet-helper';
+import { FoldingRangesRequest, FoldingProviderServerCapabilities } from './protocol/foldingProvider.proposed';
+
namespace TagCloseRequest {
export const type: RequestType = new RequestType('html/tag');
}
@@ -32,6 +34,9 @@ console.error = connection.console.error.bind(connection.console);
process.on('unhandledRejection', (e: any) => {
connection.console.error(formatError(`Unhandled exception`, e));
});
+process.on('uncaughtException', (e) => {
+ connection.console.error(formatError(`Unhandled exception`, e));
+});
// Create a simple text document manager. The text document manager
// supports full document sync only
@@ -107,7 +112,7 @@ connection.onInitialize((params: InitializeParams): InitializeResult => {
clientDynamicRegisterSupport = hasClientCapability('workspace', 'symbol', 'dynamicRegistration');
scopedSettingsSupport = hasClientCapability('workspace', 'configuration');
workspaceFoldersSupport = hasClientCapability('workspace', 'workspaceFolders');
- let capabilities: ServerCapabilities & CPServerCapabilities = {
+ let capabilities: ServerCapabilities & CPServerCapabilities & FoldingProviderServerCapabilities = {
// Tell the client that the server works in FULL text document sync mode
textDocumentSync: documents.syncKind,
completionProvider: clientSnippetSupport ? { resolveProvider: true, triggerCharacters: [...emmetTriggerCharacters, '.', ':', '<', '"', '=', '/'] } : undefined,
@@ -119,7 +124,8 @@ connection.onInitialize((params: InitializeParams): InitializeResult => {
definitionProvider: true,
signatureHelpProvider: { triggerCharacters: ['('] },
referencesProvider: true,
- colorProvider: true
+ colorProvider: true,
+ foldingProvider: true
};
return { capabilities };
});
@@ -448,6 +454,20 @@ connection.onRequest(TagCloseRequest.type, params => {
}, null, `Error while computing tag close actions for ${params.textDocument.uri}`);
});
+connection.onRequest(FoldingRangesRequest.type, params => {
+ return runSafe(() => {
+ let document = documents.get(params.textDocument.uri);
+ if (document) {
+ let mode = languageModes.getMode('html');
+ if (mode && mode.getFoldingRanges) {
+ return mode.getFoldingRanges(document);
+ }
+ return null;
+ }
+ return null;
+ }, null, `Error while computing folding regions for ${params.textDocument.uri}`);
+});
+
// Listen on the connection
connection.listen();
\ No newline at end of file
diff --git a/extensions/html/server/src/modes/htmlMode.ts b/extensions/html/server/src/modes/htmlMode.ts
index 24dcd14d49c..99e543f626b 100644
--- a/extensions/html/server/src/modes/htmlMode.ts
+++ b/extensions/html/server/src/modes/htmlMode.ts
@@ -9,6 +9,8 @@ import { LanguageService as HTMLLanguageService, HTMLDocument, DocumentContext,
import { TextDocument, Position, Range } from 'vscode-languageserver-types';
import { LanguageMode, Settings } from './languageModes';
+import { FoldingRangeType, FoldingRange, FoldingRangeList } from '../protocol/foldingProvider.proposed';
+
export function getHTMLMode(htmlLanguageService: HTMLLanguageService): LanguageMode {
let globalSettings: Settings = {};
let htmlDocuments = getLanguageModelCache(10, 60, document => htmlLanguageService.parseHTMLDocument(document));
@@ -71,6 +73,80 @@ export function getHTMLMode(htmlLanguageService: HTMLLanguageService): LanguageM
formatSettings = merge(formatParams, formatSettings);
return htmlLanguageService.format(document, range, formatSettings);
},
+ getFoldingRanges(document: TextDocument): FoldingRangeList {
+ const scanner = htmlLanguageService.createScanner(document.getText());
+ let token = scanner.scan();
+ let ranges: FoldingRange[] = [];
+ let stack: FoldingRange[] = [];
+ let elementNames: string[] = [];
+ let lastTagName = null;
+ let prevStart = -1;
+ while (token !== TokenType.EOS) {
+ switch (token) {
+ case TokenType.StartTagOpen: {
+ let startLine = document.positionAt(scanner.getTokenOffset()).line;
+ let range = { startLine, endLine: startLine };
+ stack.push(range);
+ break;
+ }
+ case TokenType.StartTag: {
+ lastTagName = scanner.getTokenText();
+ elementNames.push(lastTagName);
+ break;
+ }
+ case TokenType.EndTag: {
+ lastTagName = scanner.getTokenText();
+ break;
+ }
+ case TokenType.EndTagClose:
+ case TokenType.StartTagSelfClose: {
+ let name = elementNames.pop();
+ let range = stack.pop();
+ while (name && name !== lastTagName) {
+ name = elementNames.pop();
+ 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;
+ }
+ case TokenType.Comment: {
+ let text = scanner.getTokenText();
+ 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);
+ elementNames.push('');
+ } 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;
+ }
+ }
+ token = scanner.scan();
+ }
+ return { ranges };
+ },
+
doAutoClose(document: TextDocument, position: Position) {
let offset = document.offsetAt(position);
let text = document.getText();
diff --git a/extensions/html/server/src/modes/languageModes.ts b/extensions/html/server/src/modes/languageModes.ts
index 721ee9a4e65..88fe447c72e 100644
--- a/extensions/html/server/src/modes/languageModes.ts
+++ b/extensions/html/server/src/modes/languageModes.ts
@@ -20,6 +20,8 @@ import { getHTMLMode } from './htmlMode';
export { ColorInformation, ColorPresentation, Color };
+import { FoldingRangeList } from '../protocol/foldingProvider.proposed';
+
export interface Settings {
css?: any;
html?: any;
@@ -49,6 +51,7 @@ export interface LanguageMode {
findDocumentColors?: (document: TextDocument) => ColorInformation[];
getColorPresentations?: (document: TextDocument, color: Color, range: Range) => ColorPresentation[];
doAutoClose?: (document: TextDocument, position: Position) => string | null;
+ getFoldingRanges?: (document: TextDocument) => FoldingRangeList | null;
onDocumentRemoved(document: TextDocument): void;
dispose(): void;
}
diff --git a/extensions/html/server/src/protocol/foldingProvider.proposed.ts b/extensions/html/server/src/protocol/foldingProvider.proposed.ts
new file mode 100644
index 00000000000..86209eac344
--- /dev/null
+++ b/extensions/html/server/src/protocol/foldingProvider.proposed.ts
@@ -0,0 +1,89 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { TextDocumentIdentifier } from 'vscode-languageserver-types';
+import { RequestType, TextDocumentRegistrationOptions, StaticRegistrationOptions } from 'vscode-languageserver-protocol';
+
+// ---- capabilities
+
+export interface FoldingProviderClientCapabilities {
+ /**
+ * The text document client capabilities
+ */
+ textDocument?: {
+ /**
+ * Capabilities specific to the foldingProvider
+ */
+ foldingProvider?: {
+ /**
+ * Whether implementation supports dynamic registration. If this is set to `true`
+ * the client supports the new `(FoldingProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ * return value for the corresponding server capability as well.
+ */
+ dynamicRegistration?: boolean;
+ };
+ };
+}
+
+export interface FoldingProviderOptions {
+}
+
+export interface FoldingProviderServerCapabilities {
+ /**
+ * The server provides folding provider support.
+ */
+ foldingProvider?: FoldingProviderOptions | (FoldingProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions);
+}
+
+export interface FoldingRangeList {
+ /**
+ * The folding ranges.
+ */
+ ranges: FoldingRange[];
+}
+
+export enum FoldingRangeType {
+ /**
+ * Folding range for a comment
+ */
+ Comment = 'comment',
+ /**
+ * Folding range for a imports or includes
+ */
+ Imports = 'imports',
+ /**
+ * Folding range for a region (e.g. `#region`)
+ */
+ Region = 'region'
+}
+
+export interface FoldingRange {
+
+ /**
+ * The start line number
+ */
+ startLine: number;
+
+ /**
+ * The end line number
+ */
+ endLine: number;
+
+ /**
+ * The actual color value for this folding range.
+ */
+ type?: FoldingRangeType | string;
+}
+
+export interface FoldingRangeRequestParam {
+ /**
+ * The text document.
+ */
+ textDocument: TextDocumentIdentifier;
+}
+
+export namespace FoldingRangesRequest {
+ export const type: RequestType = new RequestType('textDocument/foldingRanges');
+}