From 0faa5d117cced88ab9d507cadf697c2acd3dcbff Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Thu, 22 Feb 2018 14:12:57 +0100 Subject: [PATCH] folding provider: first version --- extensions/json/client/src/jsonMain.ts | 66 +++++++- extensions/json/server/src/jsonServerMain.ts | 132 ++++++++++++++- src/vs/editor/common/modes.ts | 66 ++++++++ src/vs/editor/contrib/folding/folding.ts | 36 +++- src/vs/editor/contrib/folding/foldingModel.ts | 68 ++++---- .../editor/contrib/folding/foldingRanges.ts | 4 +- .../contrib/folding/hiddenRangeModel.ts | 4 +- .../contrib/folding/indentRangeProvider.ts | 32 ++-- .../contrib/folding/syntaxRangeProvider.ts | 154 ++++++++++++++++++ .../contrib/folding/test/foldingModel.test.ts | 4 +- .../standalone/browser/standaloneLanguages.ts | 8 + src/vs/monaco.d.ts | 3 + src/vs/vscode.proposed.d.ts | 74 +++++++++ .../mainThreadLanguageFeatures.ts | 11 ++ src/vs/workbench/api/node/extHost.api.impl.ts | 8 +- src/vs/workbench/api/node/extHost.protocol.ts | 2 + .../api/node/extHostLanguageFeatures.ts | 31 +++- .../api/node/extHostTypeConverters.ts | 8 + src/vs/workbench/api/node/extHostTypes.ts | 43 +++++ 19 files changed, 689 insertions(+), 65 deletions(-) create mode 100644 src/vs/editor/contrib/folding/syntaxRangeProvider.ts diff --git a/extensions/json/client/src/jsonMain.ts b/extensions/json/client/src/jsonMain.ts index 9491dc03e3e..949a1207c0a 100644 --- a/extensions/json/client/src/jsonMain.ts +++ b/extensions/json/client/src/jsonMain.ts @@ -8,8 +8,8 @@ import * as path from 'path'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -import { workspace, languages, ExtensionContext, extensions, Uri, LanguageConfiguration } from 'vscode'; -import { LanguageClient, LanguageClientOptions, RequestType, ServerOptions, TransportKind, NotificationType, DidChangeConfigurationNotification } from 'vscode-languageclient'; +import { workspace, languages, ExtensionContext, extensions, Uri, LanguageConfiguration, TextDocument, FoldingRangeList as VSFoldingRangeList, FoldingRange as VSFoldingRange } from 'vscode'; +import { LanguageClient, LanguageClientOptions, RequestType, ServerOptions, TransportKind, NotificationType, DidChangeConfigurationNotification, TextDocumentIdentifier } from 'vscode-languageclient'; import TelemetryReporter from 'vscode-extension-telemetry'; import { hash } from './utils/hash'; @@ -30,6 +30,57 @@ namespace SchemaAssociationNotification { export const type: NotificationType = new NotificationType('json/schemaAssociations'); } +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' +} + +interface FoldingRange { + + /** + * The start line number + */ + startLine: number; + + /** + * The end line number + */ + endLine: number; + + /** + * The actual color value for this color range. + */ + type?: FoldingRangeType; +} + +interface FoldingRangeRequest { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; +} + +namespace FoldingRangesRequest { + export const type: RequestType = new RequestType('textDocument/foldingRanges'); +} + interface IPackageInfo { name: string; version: string; @@ -124,6 +175,17 @@ export function activate(context: ExtensionContext) { toDispose.push(workspace.onDidCloseTextDocument(d => handleContentChange(d.uri))); client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociation(context)); + + 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 VSFoldingRangeList(res.ranges.map(r => new VSFoldingRange(r.startLine, r.endLine, r.type))); + } + return null; + }); + } + }); }); let languageConfiguration: LanguageConfiguration = { diff --git a/extensions/json/server/src/jsonServerMain.ts b/extensions/json/server/src/jsonServerMain.ts index 1adbcfbf546..bd3a7fb03f0 100644 --- a/extensions/json/server/src/jsonServerMain.ts +++ b/extensions/json/server/src/jsonServerMain.ts @@ -7,7 +7,7 @@ import { createConnection, IConnection, TextDocuments, TextDocument, InitializeParams, InitializeResult, NotificationType, RequestType, - DocumentRangeFormattingRequest, Disposable, ServerCapabilities + DocumentRangeFormattingRequest, Disposable, ServerCapabilities, TextDocumentIdentifier } from 'vscode-languageserver'; import { DocumentColorRequest, ServerCapabilities as CPServerCapabilities, ColorPresentationRequest } from 'vscode-languageserver-protocol/lib/protocol.colorProvider.proposed'; @@ -20,6 +20,7 @@ import Strings = require('./utils/strings'); import { formatError, runSafe, runSafeAsync } from './utils/errors'; import { JSONDocument, JSONSchema, getLanguageService, DocumentLanguageSettings, SchemaConfiguration } from 'vscode-json-languageservice'; import { getLanguageModelCache } from './languageModelCache'; +import { createScanner, SyntaxKind } from 'jsonc-parser'; interface ISchemaAssociations { [pattern: string]: string[]; @@ -37,6 +38,57 @@ namespace SchemaContentChangeNotification { export const type: NotificationType = new NotificationType('json/schemaContent'); } +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' +} + +interface FoldingRange { + + /** + * The start line number + */ + startLine: number; + + /** + * The end line number + */ + endLine: number; + + /** + * The actual color value for this color range. + */ + type?: FoldingRangeType | string; +} + +interface FoldingRangeRequest { + /** + * The text document. + */ + textDocument: TextDocumentIdentifier; +} + +namespace FoldingRangesRequest { + export const type: RequestType = new RequestType('textDocument/foldingRanges'); +} + // Create a connection for the server let connection: IConnection = createConnection(); @@ -347,7 +399,83 @@ connection.onRequest(ColorPresentationRequest.type, params => { return languageService.getColorPresentations(document, jsonDocument, params.color, params.range); } return []; - }, [], `Error while computing color presentationsd for ${params.textDocument.uri}`); + }, [], `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; + 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; + 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; + 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; + } + token = scanner.scan(); + } + return { ranges }; + } + return null; + }, null, `Error while computing folding ranges for ${params.textDocument.uri}`); }); // Listen on the connection diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index c7fd1b7b243..33c9cbcce55 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -826,6 +826,67 @@ export interface DocumentColorProvider { provideColorPresentations(model: model.ITextModel, colorInfo: IColorInformation, token: CancellationToken): IColorPresentation[] | Thenable; } +/** + * A provider of colors for editor models. + */ +/** + * @internal + */ +export interface FoldingProvider { + /** + * Provides the color ranges for a specific model. + */ + provideFoldingRanges(model: model.ITextModel, token: CancellationToken): IFoldingRangeList | Thenable; +} +/** + * @internal + */ +export interface IFoldingRangeList { + + ranges: IFoldingRange[]; +} +/** + * @internal + */ +export interface IFoldingRange { + + /** + * The start line number + */ + startLineNumber: number; + + /** + * The end line number + */ + endLineNumber: number; + + /** + * The optional type of the folding range + */ + type?: FoldingRangeType | string; + + // auto-collapse + // header span + +} +/** + * @internal + */ +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' +} + /** * @internal */ @@ -971,6 +1032,11 @@ export const LinkProviderRegistry = new LanguageFeatureRegistry(); */ export const ColorProviderRegistry = new LanguageFeatureRegistry(); +/** + * @internal + */ +export const FoldingProviderRegistry = new LanguageFeatureRegistry(); + /** * @internal */ diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index 9fd6f41c706..2eaa4756dba 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -19,17 +19,24 @@ import { registerEditorAction, registerEditorContribution, ServicesAccessor, Edi import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { FoldingModel, setCollapseStateAtLevel, CollapseMemento, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateForMatchingLines } from 'vs/editor/contrib/folding/foldingModel'; import { FoldingDecorationProvider } from './foldingDecorations'; +import { FoldingRegions } from './foldingRanges'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions'; import { IMarginData, IEmptyContentData } from 'vs/editor/browser/controller/mouseTarget'; import { HiddenRangeModel } from 'vs/editor/contrib/folding/hiddenRangeModel'; import { IRange } from 'vs/editor/common/core/range'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; -import { computeRanges as computeIndentRanges } from 'vs/editor/contrib/folding/indentRangeProvider'; +import { IndentRangeProvider } from 'vs/editor/contrib/folding/indentRangeProvider'; import { IPosition } from 'vs/editor/common/core/position'; +import { FoldingProviderRegistry } from 'vs/editor/common/modes'; +import { SyntaxRangeProvider } from './syntaxRangeProvider'; export const ID = 'editor.contrib.folding'; +export interface RangeProvider { + compute(editorModel: ITextModel): TPromise; +} + export class FoldingController implements IEditorContribution { static MAX_FOLDING_REGIONS = 5000; @@ -48,6 +55,8 @@ export class FoldingController implements IEditorContribution { private foldingModel: FoldingModel; private hiddenRangeModel: HiddenRangeModel; + private rangeProvider: RangeProvider; + private foldingModelPromise: TPromise; private updateScheduler: Delayer; @@ -69,6 +78,7 @@ export class FoldingController implements IEditorContribution { this.foldingDecorationProvider.autoHideFoldingControls = this._autoHideFoldingControls; this.globalToDispose.push(this.editor.onDidChangeModel(() => this.onModelChanged())); + this.globalToDispose.push(FoldingProviderRegistry.onDidChange(() => this.onModelChanged())); this.globalToDispose.push(this.editor.onDidChangeConfiguration((e: IConfigurationChangedEvent) => { if (e.contribInfo) { @@ -167,17 +177,22 @@ export class FoldingController implements IEditorContribution { this.foldingModelPromise = null; this.hiddenRangeModel = null; this.cursorChangedScheduler = null; + this.rangeProvider = null; } }); this.onModelContentChanged(); } - private computeRanges(editorModel: ITextModel) { - let foldingRules = LanguageConfigurationRegistry.getFoldingRules(editorModel.getLanguageIdentifier().id); - let offSide = foldingRules && foldingRules.offSide; - let markers = foldingRules && foldingRules.markers; - let ranges = computeIndentRanges(editorModel, offSide, markers); - return ranges; + private getRangeProvider(): RangeProvider { + if (!this.rangeProvider) { + let foldingProviders = FoldingProviderRegistry.ordered(this.foldingModel.textModel); + if (foldingProviders.length) { + this.rangeProvider = new SyntaxRangeProvider(foldingProviders); + } else { + this.rangeProvider = new IndentRangeProvider(); + } + } + return this.rangeProvider; } public getFoldingModel() { @@ -191,9 +206,12 @@ export class FoldingController implements IEditorContribution { // some cursors might have moved into hidden regions, make sure they are in expanded regions let selections = this.editor.getSelections(); let selectionLineNumbers = selections ? selections.map(s => s.startLineNumber) : []; - this.foldingModel.update(this.computeRanges(this.foldingModel.textModel), selectionLineNumbers); + return this.getRangeProvider().compute(this.foldingModel.textModel).then(foldingRanges => { + this.foldingModel.update(foldingRanges, selectionLineNumbers); + return this.foldingModel; + }); } - return this.foldingModel; + return null; }); } } diff --git a/src/vs/editor/contrib/folding/foldingModel.ts b/src/vs/editor/contrib/folding/foldingModel.ts index 33e1826061c..5fbb9f2289a 100644 --- a/src/vs/editor/contrib/folding/foldingModel.ts +++ b/src/vs/editor/contrib/folding/foldingModel.ts @@ -5,7 +5,7 @@ import { ITextModel, IModelDecorationOptions, IModelDeltaDecoration, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; import Event, { Emitter } from 'vs/base/common/event'; -import { FoldingRanges, ILineRange, FoldingRegion } from './foldingRanges'; +import { FoldingRegions, ILineRange, FoldingRegion } from './foldingRanges'; export interface IDecorationProvider { getDecorationOption(isCollapsed: boolean): IModelDecorationOptions; @@ -24,13 +24,13 @@ export class FoldingModel { private _textModel: ITextModel; private _decorationProvider: IDecorationProvider; - private _ranges: FoldingRanges; + private _regions: FoldingRegions; private _editorDecorationIds: string[]; private _isInitialized: boolean; private _updateEventEmitter = new Emitter(); - public get ranges(): FoldingRanges { return this._ranges; } + public get regions(): FoldingRegions { return this._regions; } public get onDidChange(): Event { return this._updateEventEmitter.event; } public get textModel() { return this._textModel; } public get isInitialized() { return this._isInitialized; } @@ -38,7 +38,7 @@ export class FoldingModel { constructor(textModel: ITextModel, decorationProvider: IDecorationProvider) { this._textModel = textModel; this._decorationProvider = decorationProvider; - this._ranges = new FoldingRanges(new Uint32Array(0), new Uint32Array(0)); + this._regions = new FoldingRegions(new Uint32Array(0), new Uint32Array(0)); this._editorDecorationIds = []; this._isInitialized = false; } @@ -54,8 +54,8 @@ export class FoldingModel { let editorDecorationId = this._editorDecorationIds[index]; if (editorDecorationId && !processed[editorDecorationId]) { processed[editorDecorationId] = true; - let newCollapseState = !this._ranges.isCollapsed(index); - this._ranges.setCollapsed(index, newCollapseState); + let newCollapseState = !this._regions.isCollapsed(index); + this._regions.setCollapsed(index, newCollapseState); accessor.changeDecorationOptions(editorDecorationId, this._decorationProvider.getDecorationOption(newCollapseState)); } } @@ -63,7 +63,7 @@ export class FoldingModel { this._updateEventEmitter.fire({ model: this, collapseStateChanged: regions }); } - public update(newRanges: FoldingRanges, blockedLineNumers: number[] = []): void { + public update(newRegions: FoldingRegions, blockedLineNumers: number[] = []): void { let newEditorDecorations = []; let isBlocked = (startLineNumber, endLineNumber) => { @@ -76,11 +76,11 @@ export class FoldingModel { }; let initRange = (index: number, isCollapsed: boolean) => { - let startLineNumber = newRanges.getStartLineNumber(index); - if (isCollapsed && isBlocked(startLineNumber, newRanges.getEndLineNumber(index))) { + let startLineNumber = newRegions.getStartLineNumber(index); + if (isCollapsed && isBlocked(startLineNumber, newRegions.getEndLineNumber(index))) { isCollapsed = false; } - newRanges.setCollapsed(index, isCollapsed); + newRegions.setCollapsed(index, isCollapsed); let maxColumn = this._textModel.getLineMaxColumn(startLineNumber); let decorationRange = { startLineNumber: startLineNumber, @@ -93,8 +93,8 @@ export class FoldingModel { let i = 0; let nextCollapsed = () => { - while (i < this._ranges.length) { - let isCollapsed = this._ranges.isCollapsed(i); + while (i < this._regions.length) { + let isCollapsed = this._regions.isCollapsed(i); i++; if (isCollapsed) { return i - 1; @@ -105,14 +105,14 @@ export class FoldingModel { let k = 0; let collapsedIndex = nextCollapsed(); - while (collapsedIndex !== -1 && k < newRanges.length) { + while (collapsedIndex !== -1 && k < newRegions.length) { // get the latest range let decRange = this._textModel.getDecorationRange(this._editorDecorationIds[collapsedIndex]); if (decRange) { let collapsedStartLineNumber = decRange.startLineNumber; if (this._textModel.getLineMaxColumn(collapsedStartLineNumber) === decRange.startColumn) { // test that the decoration is still at the end otherwise it got deleted - while (k < newRanges.length) { - let startLineNumber = newRanges.getStartLineNumber(k); + while (k < newRegions.length) { + let startLineNumber = newRegions.getStartLineNumber(k); if (collapsedStartLineNumber >= startLineNumber) { initRange(k, collapsedStartLineNumber === startLineNumber); k++; @@ -124,13 +124,13 @@ export class FoldingModel { } collapsedIndex = nextCollapsed(); } - while (k < newRanges.length) { + while (k < newRegions.length) { initRange(k, false); k++; } this._editorDecorationIds = this._decorationProvider.deltaDecorations(this._editorDecorationIds, newEditorDecorations); - this._ranges = newRanges; + this._regions = newRegions; this._isInitialized = true; this._updateEventEmitter.fire({ model: this }); } @@ -140,12 +140,12 @@ export class FoldingModel { */ public getMemento(): CollapseMemento { let collapsedRanges: ILineRange[] = []; - for (let i = 0; i < this._ranges.length; i++) { - if (this._ranges.isCollapsed(i)) { + for (let i = 0; i < this._regions.length; i++) { + if (this._regions.isCollapsed(i)) { let range = this._textModel.getDecorationRange(this._editorDecorationIds[i]); if (range) { let startLineNumber = range.startLineNumber; - let endLineNumber = range.endLineNumber + this._ranges.getEndLineNumber(i) - this._ranges.getStartLineNumber(i); + let endLineNumber = range.endLineNumber + this._regions.getEndLineNumber(i) - this._regions.getStartLineNumber(i); collapsedRanges.push({ startLineNumber, endLineNumber }); } } @@ -179,11 +179,11 @@ export class FoldingModel { getAllRegionsAtLine(lineNumber: number, filter?: (r: FoldingRegion, level: number) => boolean): FoldingRegion[] { let result: FoldingRegion[] = []; - if (this._ranges) { - let index = this._ranges.findRange(lineNumber); + if (this._regions) { + let index = this._regions.findRange(lineNumber); let level = 1; while (index >= 0) { - let current = this._ranges.toRegion(index); + let current = this._regions.toRegion(index); if (!filter || filter(current, level)) { result.push(current); } @@ -195,10 +195,10 @@ export class FoldingModel { } getRegionAtLine(lineNumber: number): FoldingRegion { - if (this._ranges) { - let index = this._ranges.findRange(lineNumber); + if (this._regions) { + let index = this._regions.findRange(lineNumber); if (index >= 0) { - return this._ranges.toRegion(index); + return this._regions.toRegion(index); } } return null; @@ -210,9 +210,9 @@ export class FoldingModel { let levelStack: FoldingRegion[] = trackLevel ? [] : null; let index = region ? region.regionIndex + 1 : 0; let endLineNumber = region ? region.endLineNumber : Number.MAX_VALUE; - for (let i = index, len = this._ranges.length; i < len; i++) { - let current = this._ranges.toRegion(i); - if (this._ranges.getStartLineNumber(i) < endLineNumber) { + for (let i = index, len = this._regions.length; i < len; i++) { + let current = this._regions.toRegion(i); + if (this._regions.getStartLineNumber(i) < endLineNumber) { if (trackLevel) { while (levelStack.length > 0 && !current.containedBy(levelStack[levelStack.length - 1])) { levelStack.pop(); @@ -296,13 +296,13 @@ export function setCollapseStateAtLevel(foldingModel: FoldingModel, foldLevel: n */ export function setCollapseStateForMatchingLines(foldingModel: FoldingModel, regExp: RegExp, doCollapse: boolean): void { let editorModel = foldingModel.textModel; - let ranges = foldingModel.ranges; + let regions = foldingModel.regions; let toToggle = []; - for (let i = ranges.length - 1; i >= 0; i--) { - if (doCollapse !== ranges.isCollapsed(i)) { - let startLineNumber = ranges.getStartLineNumber(i); + for (let i = regions.length - 1; i >= 0; i--) { + if (doCollapse !== regions.isCollapsed(i)) { + let startLineNumber = regions.getStartLineNumber(i); if (regExp.test(editorModel.getLineContent(startLineNumber))) { - toToggle.push(ranges.toRegion(i)); + toToggle.push(regions.toRegion(i)); } } } diff --git a/src/vs/editor/contrib/folding/foldingRanges.ts b/src/vs/editor/contrib/folding/foldingRanges.ts index 666e174f1b2..613fe4c169b 100644 --- a/src/vs/editor/contrib/folding/foldingRanges.ts +++ b/src/vs/editor/contrib/folding/foldingRanges.ts @@ -15,7 +15,7 @@ export const MAX_LINE_NUMBER = 0xFFFFFF; const MASK_INDENT = 0xFF000000; -export class FoldingRanges { +export class FoldingRegions { private _startIndexes: Uint32Array; private _endIndexes: Uint32Array; private _collapseStates: Uint32Array; @@ -138,7 +138,7 @@ export class FoldingRanges { export class FoldingRegion { - constructor(private ranges: FoldingRanges, private index: number) { + constructor(private ranges: FoldingRegions, private index: number) { } public get startLineNumber() { diff --git a/src/vs/editor/contrib/folding/hiddenRangeModel.ts b/src/vs/editor/contrib/folding/hiddenRangeModel.ts index fee0f5108c2..0acb3f59648 100644 --- a/src/vs/editor/contrib/folding/hiddenRangeModel.ts +++ b/src/vs/editor/contrib/folding/hiddenRangeModel.ts @@ -23,7 +23,7 @@ export class HiddenRangeModel { this._foldingModel = model; this._foldingModelListener = model.onDidChange(_ => this.updateHiddenRanges()); this._hiddenRanges = []; - if (model.ranges.length) { + if (model.regions.length) { this.updateHiddenRanges(); } } @@ -37,7 +37,7 @@ export class HiddenRangeModel { let lastCollapsedStart = Number.MAX_VALUE; let lastCollapsedEnd = -1; - let ranges = this._foldingModel.ranges; + let ranges = this._foldingModel.regions; for (; i < ranges.length; i++) { if (!ranges.isCollapsed(i)) { continue; diff --git a/src/vs/editor/contrib/folding/indentRangeProvider.ts b/src/vs/editor/contrib/folding/indentRangeProvider.ts index 04259b2c8b8..9ec730af0e2 100644 --- a/src/vs/editor/contrib/folding/indentRangeProvider.ts +++ b/src/vs/editor/contrib/folding/indentRangeProvider.ts @@ -7,25 +7,37 @@ import { ITextModel } from 'vs/editor/common/model'; import { FoldingMarkers } from 'vs/editor/common/modes/languageConfiguration'; -import { FoldingRanges, MAX_LINE_NUMBER } from 'vs/editor/contrib/folding/foldingRanges'; +import { FoldingRegions, MAX_LINE_NUMBER } from 'vs/editor/contrib/folding/foldingRanges'; import { TextModel } from 'vs/editor/common/model/textModel'; +import { RangeProvider } from './folding'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; const MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT = 5000; +export class IndentRangeProvider implements RangeProvider { + compute(editorModel: ITextModel): TPromise { + let foldingRules = LanguageConfigurationRegistry.getFoldingRules(editorModel.getLanguageIdentifier().id); + let offSide = foldingRules && foldingRules.offSide; + let markers = foldingRules && foldingRules.markers; + return TPromise.as(computeRanges(editorModel, offSide, markers)); + } +} + // public only for testing export class RangesCollector { private _startIndexes: number[]; private _endIndexes: number[]; private _indentOccurrences: number[]; private _length: number; - private _FoldingRangesLimit: number; + private _foldingRangesLimit: number; - constructor(FoldingRangesLimit: number) { + constructor(foldingRangesLimit: number) { this._startIndexes = []; this._endIndexes = []; this._indentOccurrences = []; this._length = 0; - this._FoldingRangesLimit = FoldingRangesLimit; + this._foldingRangesLimit = foldingRangesLimit; } public insertFirst(startLineNumber: number, endLineNumber: number, indent: number) { @@ -42,7 +54,7 @@ export class RangesCollector { } public toIndentRanges(model: ITextModel) { - if (this._length <= this._FoldingRangesLimit) { + if (this._length <= this._foldingRangesLimit) { // reverse and create arrays of the exact length let startIndexes = new Uint32Array(this._length); let endIndexes = new Uint32Array(this._length); @@ -50,14 +62,14 @@ export class RangesCollector { startIndexes[k] = this._startIndexes[i]; endIndexes[k] = this._endIndexes[i]; } - return new FoldingRanges(startIndexes, endIndexes); + return new FoldingRegions(startIndexes, endIndexes); } else { let entries = 0; let maxIndent = this._indentOccurrences.length; for (let i = 0; i < this._indentOccurrences.length; i++) { let n = this._indentOccurrences[i]; if (n) { - if (n + entries > this._FoldingRangesLimit) { + if (n + entries > this._foldingRangesLimit) { maxIndent = i; break; } @@ -78,7 +90,7 @@ export class RangesCollector { k++; } } - return new FoldingRanges(startIndexes, endIndexes); + return new FoldingRegions(startIndexes, endIndexes); } } @@ -87,9 +99,9 @@ export class RangesCollector { interface PreviousRegion { indent: number; line: number; marker: boolean; } -export function computeRanges(model: ITextModel, offSide: boolean, markers?: FoldingMarkers, FoldingRangesLimit = MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT): FoldingRanges { +export function computeRanges(model: ITextModel, offSide: boolean, markers?: FoldingMarkers, foldingRangesLimit = MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT): FoldingRegions { const tabSize = model.getOptions().tabSize; - let result = new RangesCollector(FoldingRangesLimit); + let result = new RangesCollector(foldingRangesLimit); let pattern = void 0; if (markers) { diff --git a/src/vs/editor/contrib/folding/syntaxRangeProvider.ts b/src/vs/editor/contrib/folding/syntaxRangeProvider.ts new file mode 100644 index 00000000000..345f08a12f3 --- /dev/null +++ b/src/vs/editor/contrib/folding/syntaxRangeProvider.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { FoldingProvider, IFoldingRange } from 'vs/editor/common/modes'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { asWinJsPromise } from 'vs/base/common/async'; +import { ITextModel } from 'vs/editor/common/model'; +import { RangeProvider } from './folding'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { MAX_LINE_NUMBER, FoldingRegions } from './foldingRanges'; + +const MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT = 5000; + +export interface IFoldingRangeData extends IFoldingRange { + rank: number; +} + +export class SyntaxRangeProvider implements RangeProvider { + + constructor(private providers: FoldingProvider[]) { + } + + compute(model: ITextModel): TPromise { + return collectSyntaxRanges(this.providers, model).then(ranges => { + return sanitizeRanges(ranges); + }); + } + +} + +function collectSyntaxRanges(providers: FoldingProvider[], model: ITextModel): TPromise { + const rangeData: IFoldingRangeData[] = []; + let promises = providers.map((provider, rank) => asWinJsPromise(token => provider.provideFoldingRanges(model, token)).then(list => { + if (list && Array.isArray(list.ranges)) { + for (let r of list.ranges) { + rangeData.push({ startLineNumber: r.startLineNumber, endLineNumber: r.endLineNumber, rank, type: r.type }); + } + } + }, onUnexpectedExternalError)); + + return TPromise.join(promises).then(() => { + return rangeData; + }); +} + +export class RangesCollector { + private _startIndexes: number[]; + private _endIndexes: number[]; + private _nestingLevels: number[]; + private _nestingLevelCounts: number[]; + private _length: number; + private _foldingRangesLimit: number; + + constructor(foldingRangesLimit: number) { + this._startIndexes = []; + this._endIndexes = []; + this._nestingLevels = []; + this._nestingLevelCounts = []; + this._length = 0; + this._foldingRangesLimit = foldingRangesLimit; + } + + public add(startLineNumber: number, endLineNumber: number, nestingLevel: number) { + if (startLineNumber > MAX_LINE_NUMBER || endLineNumber > MAX_LINE_NUMBER) { + return; + } + let index = this._length; + this._startIndexes[index] = startLineNumber; + this._endIndexes[index] = endLineNumber; + this._nestingLevels[index] = nestingLevel; + this._length++; + if (nestingLevel < 30) { + this._nestingLevelCounts[nestingLevel] = (this._nestingLevelCounts[nestingLevel] || 0) + 1; + } + } + + public toIndentRanges() { + if (this._length <= this._foldingRangesLimit) { + let startIndexes = new Uint32Array(this._length); + let endIndexes = new Uint32Array(this._length); + for (let i = 0; i < this._length; i++) { + startIndexes[i] = this._startIndexes[i]; + endIndexes[i] = this._endIndexes[i]; + } + return new FoldingRegions(startIndexes, endIndexes); + } else { + let entries = 0; + let maxLevel = this._nestingLevelCounts.length; + for (let i = 0; i < this._nestingLevelCounts.length; i++) { + let n = this._nestingLevelCounts[i]; + if (n) { + if (n + entries > this._foldingRangesLimit) { + maxLevel = i; + break; + } + entries += n; + } + } + let startIndexes = new Uint32Array(entries); + let endIndexes = new Uint32Array(entries); + for (let i = 0, k = 0; i < this._length; i++) { + let level = this._nestingLevels[i]; + if (level < maxLevel) { + startIndexes[k] = this._startIndexes[i]; + endIndexes[k] = this._endIndexes[i]; + k++; + } + } + return new FoldingRegions(startIndexes, endIndexes); + } + + } +} + +export function sanitizeRanges(rangeData: IFoldingRangeData[]): FoldingRegions { + + let sorted = rangeData.sort((d1, d2) => { + let diff = d1.startLineNumber - d2.startLineNumber; + if (diff === 0) { + diff = d1.rank - d2.rank; + } + return diff; + }); + let collector = new RangesCollector(MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT); + + let top: IFoldingRangeData = null; + let previous = []; + for (let entry of sorted) { + if (!top) { + top = entry; + collector.add(entry.startLineNumber, entry.endLineNumber, previous.length); + } else { + if (entry.startLineNumber > top.startLineNumber) { + if (entry.endLineNumber <= top.endLineNumber) { + previous.push(top); + top = entry; + collector.add(entry.startLineNumber, entry.endLineNumber, previous.length); + } else if (entry.startLineNumber > top.endLineNumber) { + do { + top = previous.pop(); + } while (top && entry.startLineNumber > top.endLineNumber); + previous.push(top); + top = entry; + collector.add(entry.startLineNumber, entry.endLineNumber, previous.length); + } + } + } + } + return collector.toIndentRanges(); +} \ No newline at end of file diff --git a/src/vs/editor/contrib/folding/test/foldingModel.test.ts b/src/vs/editor/contrib/folding/test/foldingModel.test.ts index 0b415eae211..a77fbf071c1 100644 --- a/src/vs/editor/contrib/folding/test/foldingModel.test.ts +++ b/src/vs/editor/contrib/folding/test/foldingModel.test.ts @@ -61,7 +61,7 @@ suite('Folding Model', () => { function assertFoldedRanges(foldingModel: FoldingModel, expectedRegions: ExpectedRegion[], message?: string) { let actualRanges = []; - let actual = foldingModel.ranges; + let actual = foldingModel.regions; for (let i = 0; i < actual.length; i++) { if (actual.isCollapsed(i)) { actualRanges.push(r(actual.getStartLineNumber(i), actual.getEndLineNumber(i))); @@ -72,7 +72,7 @@ suite('Folding Model', () => { function assertRanges(foldingModel: FoldingModel, expectedRegions: ExpectedRegion[], message?: string) { let actualRanges = []; - let actual = foldingModel.ranges; + let actual = foldingModel.regions; for (let i = 0; i < actual.length; i++) { actualRanges.push(r(actual.getStartLineNumber(i), actual.getEndLineNumber(i), actual.isCollapsed(i))); } diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index eded61cea6f..55ce345642e 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -389,6 +389,14 @@ export function registerColorProvider(languageId: string, provider: modes.Docume return modes.ColorProviderRegistry.register(languageId, provider); } +/** + * Register a folding provider + */ +/*export function registerFoldingProvider(languageId: string, provider: modes.FoldingProvider): IDisposable { + return modes.FoldingProviderRegistry.register(languageId, provider); +}*/ + + /** * Contains additional diagnostic information about the context in which * a [code action](#CodeActionProvider.provideCodeActions) is run. diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 0a1cc793efd..9165cba0dc6 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4047,6 +4047,9 @@ declare module monaco.languages { */ export function registerColorProvider(languageId: string, provider: DocumentColorProvider): IDisposable; + /** + * Register a folding provider + */ /** * Contains additional diagnostic information about the context in which * a [code action](#CodeActionProvider.provideCodeActions) is run. diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index a3be8f61888..5061c95a0c9 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -7,6 +7,63 @@ declare module 'vscode' { + export class FoldingRangeList { + + /** + * The folding ranges. + */ + ranges: FoldingRange[]; + + /** + * Creates mew folding range list. + * + * @param ranges The folding ranges + */ + constructor(ranges: FoldingRange[]); + } + + + export class FoldingRange { + + /** + * The start line number (0-based) + */ + startLine: number; + + /** + * The end line number (0-based) + */ + endLine: number; + + /** + * The actual color value for this color range. + */ + type?: FoldingRangeType | string; + + /** + * Creates a new folding range. + * + * @param startLineNumber The first line of the fold + * @param type The last line of the fold + */ + constructor(startLineNumber: number, endLineNumber: number, type?: FoldingRangeType); + } + + 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 enum FileErrorCodes { // /** // * Not owner. @@ -340,10 +397,27 @@ declare module 'vscode' { } export namespace languages { + + /** + * Register a folding provider. + * + * Multiple folding can be registered for a language. In that case providers are sorted + * by their [score](#languages.match) and the best-matching provider is used. Failure + * of the selected provider will cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider A folding provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerFoldingProvider(selector: DocumentSelector, provider: FoldingProvider): Disposable; + export interface RenameProvider2 extends RenameProvider { resolveInitialRenameValue?(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } } + export interface FoldingProvider { + provideFoldingRanges(document: TextDocument, token: CancellationToken): ProviderResult; + } /** * Represents the validation type of the Source Control input. diff --git a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts index 8d5bf82d505..3ca33d51e0a 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts @@ -346,6 +346,17 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha }); } + // --- folding + + $registerFoldingProvider(handle: number, selector: vscode.DocumentSelector): void { + const proxy = this._proxy; + this._registrations[handle] = modes.FoldingProviderRegistry.register(toLanguageSelector(selector), { + provideFoldingRanges: (model, token) => { + return wireCancellationToken(token, proxy.$provideFoldingRanges(handle, model.uri)); + } + }); + } + // --- configuration private static _reviveRegExp(regExp: ISerializedRegExp): RegExp { diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 466456f5bc3..863b1bfc5ab 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -292,6 +292,9 @@ export function createApiFactory( registerColorProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentColorProvider): vscode.Disposable { return extHostLanguageFeatures.registerColorProvider(selector, provider); }, + registerFoldingProvider: proposedApiFunction(extension, (selector: vscode.DocumentSelector, provider: vscode.FoldingProvider): vscode.Disposable => { + return extHostLanguageFeatures.registerFoldingProvider(selector, provider); + }), setLanguageConfiguration: (language: string, configuration: vscode.LanguageConfiguration): vscode.Disposable => { return extHostLanguageFeatures.setLanguageConfiguration(language, configuration); } @@ -634,7 +637,10 @@ export function createApiFactory( RelativePattern: extHostTypes.RelativePattern, FileChangeType: extHostTypes.FileChangeType, - FileType: extHostTypes.FileType + FileType: extHostTypes.FileType, + FoldingRangeList: extHostTypes.FoldingRangeList, + FoldingRange: extHostTypes.FoldingRange, + FoldingRangeType: extHostTypes.FoldingRangeType }; }; } diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index b4715e17ff2..2c694546d36 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -282,6 +282,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerSignatureHelpProvider(handle: number, selector: vscode.DocumentSelector, triggerCharacter: string[]): void; $registerDocumentLinkProvider(handle: number, selector: vscode.DocumentSelector): void; $registerDocumentColorProvider(handle: number, selector: vscode.DocumentSelector): void; + $registerFoldingProvider(handle: number, selector: vscode.DocumentSelector): void; $setLanguageConfiguration(handle: number, languageId: string, configuration: ISerializedLanguageConfiguration): void; } @@ -692,6 +693,7 @@ export interface ExtHostLanguageFeaturesShape { $resolveDocumentLink(handle: number, link: modes.ILink): TPromise; $provideDocumentColors(handle: number, resource: UriComponents): TPromise; $provideColorPresentations(handle: number, resource: UriComponents, colorInfo: IRawColorInfo): TPromise; + $provideFoldingRanges(handle: number, resource: UriComponents): TPromise; } export interface ExtHostQuickOpenShape { diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 56d191ee98b..8a270631e0b 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -804,10 +804,29 @@ class ColorProviderAdapter { } } +class FoldingProviderAdapter { + + constructor( + private _documents: ExtHostDocuments, + private _provider: vscode.FoldingProvider + ) { } + + provideFoldingRanges(resource: URI): TPromise { + const doc = this._documents.getDocumentData(resource).document; + return asWinJsPromise(token => this._provider.provideFoldingRanges(doc, token)).then(list => { + if (!Array.isArray(list.ranges)) { + return void 0; + } + return TypeConverters.FoldingRangeList.from(list); + }); + } +} + type Adapter = OutlineAdapter | CodeLensAdapter | DefinitionAdapter | HoverAdapter | DocumentHighlightAdapter | ReferenceAdapter | CodeActionAdapter | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter - | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter; + | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter + | ColorProviderAdapter | FoldingProviderAdapter; export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { @@ -1108,6 +1127,16 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._withAdapter(handle, ColorProviderAdapter, adapter => adapter.provideColorPresentations(URI.revive(resource), colorInfo)); } + registerFoldingProvider(selector: vscode.DocumentSelector, provider: vscode.FoldingProvider): vscode.Disposable { + const handle = this._addNewAdapter(new FoldingProviderAdapter(this._documents, provider)); + this._proxy.$registerFoldingProvider(handle, selector); + return this._createDisposable(handle); + } + + $provideFoldingRanges(handle: number, resource: UriComponents): TPromise { + return this._withAdapter(handle, FoldingProviderAdapter, adapter => adapter.provideFoldingRanges(URI.revive(resource))); + } + // --- configuration private static _serializeRegExp(regExp: RegExp): ISerializedRegExp { diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index 428d05ab635..c15538d7339 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -586,6 +586,14 @@ export namespace ProgressLocation { } } +export namespace FoldingRangeList { + export function from(rangeList: vscode.FoldingRangeList): modes.IFoldingRangeList { + return { + ranges: rangeList.ranges.map(r => ({ startLineNumber: r.startLine + 1, endLineNumber: r.endLine + 1 })) + }; + } +} + export function toTextEditorOptions(options?: vscode.TextDocumentShowOptions): ITextEditorOptions { if (options) { return { diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 2cac1f11910..41bf5746751 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -1659,3 +1659,46 @@ export enum FileType { } //#endregion + +//#region folding api + +export class FoldingRangeList { + + ranges: FoldingRange[]; + + constructor(ranges: FoldingRange[]) { + this.ranges = ranges; + } +} + +export class FoldingRange { + + startLine: number; + + endLine: number; + + type?: FoldingRangeType; + + constructor(startLine: number, endLine: number, type?: FoldingRangeType) { + this.startLine = startLine; + this.endLine = endLine; + this.type = type; + } +} + +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' +} + +//#endregion \ No newline at end of file