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