[folding] add maxRanges to folding provider request

This commit is contained in:
Martin Aeschlimann 2018-03-13 17:47:25 +01:00
parent 3aea86db79
commit c65b881d16
19 changed files with 179 additions and 51 deletions

View file

@ -8,13 +8,13 @@ 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, FoldingRangeList, FoldingRange, workspace } from 'vscode';
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, RequestType, TextDocumentPositionParams, Disposable } from 'vscode-languageclient';
import { languages, ExtensionContext, IndentAction, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, FoldingRangeList, FoldingRange, workspace, FoldingContext } from 'vscode';
import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, RequestType, TextDocumentPositionParams, Disposable, CancellationToken } from 'vscode-languageclient';
import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared';
import { activateTagClosing } from './tagClosing';
import TelemetryReporter from 'vscode-extension-telemetry';
import { FoldingRangesRequest } from './protocol/foldingProvider.proposed';
import { FoldingRangesRequest, FoldingRangeRequestParam } from './protocol/foldingProvider.proposed';
namespace TagCloseRequest {
export const type: RequestType<TextDocumentPositionParams, string, any, any> = new RequestType('html/tag');
@ -172,12 +172,19 @@ export function activate(context: ExtensionContext) {
if (enable) {
if (!foldingProviderRegistration) {
foldingProviderRegistration = languages.registerFoldingProvider(documentSelector, {
provideFoldingRanges(document: TextDocument) {
return client.sendRequest(FoldingRangesRequest.type, { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document) }).then(res => {
provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken) {
const param: FoldingRangeRequestParam = {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
maxRanges: context.maxRanges
};
return client.sendRequest(FoldingRangesRequest.type, param, token).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;
}, error => {
client.logFailedRequest(FoldingRangesRequest.type, error);
return null;
});
}
});

View file

@ -82,6 +82,11 @@ export interface FoldingRangeRequestParam {
* The text document.
*/
textDocument: TextDocumentIdentifier;
/**
* The maximum number of ranges to provide
*/
maxRanges?: number;
}
export namespace FoldingRangesRequest {

View file

@ -82,6 +82,11 @@ export interface FoldingRangeRequestParam {
* The text document.
*/
textDocument: TextDocumentIdentifier;
/**
* The maximum number of ranges to provide
*/
maxRanges?: number;
}
export namespace FoldingRangesRequest {

View file

@ -8,11 +8,11 @@ import * as path from 'path';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { workspace, languages, ExtensionContext, extensions, Uri, LanguageConfiguration, TextDocument, FoldingRangeList, FoldingRange, Disposable } from 'vscode';
import { workspace, languages, ExtensionContext, extensions, Uri, LanguageConfiguration, TextDocument, FoldingRangeList, FoldingRange, Disposable, FoldingContext } from 'vscode';
import { LanguageClient, LanguageClientOptions, RequestType, ServerOptions, TransportKind, NotificationType, DidChangeConfigurationNotification, CancellationToken } from 'vscode-languageclient';
import TelemetryReporter from 'vscode-extension-telemetry';
import { FoldingRangesRequest } from './protocol/foldingProvider.proposed';
import { FoldingRangesRequest, FoldingRangeRequestParam } from './protocol/foldingProvider.proposed';
import { hash } from './utils/hash';
@ -154,8 +154,12 @@ export function activate(context: ExtensionContext) {
if (enable) {
if (!foldingProviderRegistration) {
foldingProviderRegistration = languages.registerFoldingProvider(documentSelector, {
provideFoldingRanges(document: TextDocument, token: CancellationToken) {
return client.sendRequest(FoldingRangesRequest.type, { textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document) }, token).then(res => {
provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken) {
const param: FoldingRangeRequestParam = {
textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(document),
maxRanges: context.maxRanges
};
return client.sendRequest(FoldingRangesRequest.type, param, token).then(res => {
if (res && Array.isArray(res.ranges)) {
return new FoldingRangeList(res.ranges.map(r => new FoldingRange(r.startLine, r.endLine, r.type)));
}

View file

@ -82,6 +82,11 @@ export interface FoldingRangeRequestParam {
* The text document.
*/
textDocument: TextDocumentIdentifier;
/**
* The maximum number of ranges to provide
*/
maxRanges?: number;
}
export namespace FoldingRangesRequest {

View file

@ -8,7 +8,8 @@
"request": "attach",
"port": 6004,
"sourceMaps": true,
"outDir": "${workspaceFolder}/out"
"outFiles": ["${workspaceFolder}/out/**/*js"],
"preLaunchTask": "npm: compile"
},
{
"name": "Unit Tests",
@ -26,7 +27,8 @@
"runtimeArgs": [],
"env": {},
"sourceMaps": true,
"outDir": "${workspaceFolder}/out"
"outFiles": ["${workspaceFolder}/out/**/*js"],
"preLaunchTask": "npm: compile"
}
]
}

View file

@ -1,9 +1,20 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "0.1.0",
"command": "npm",
"isShellCommand": true,
"showOutput": "silent",
"args": ["run", "watch"],
"isWatching": true,
"problemMatcher": "$tsc-watch"
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "compile",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

View file

@ -8,12 +8,19 @@ import { TextDocument, Position, CancellationToken } from 'vscode-languageserver
import { createScanner, SyntaxKind, ScanError } from 'jsonc-parser';
import { FoldingRangeType, FoldingRange, FoldingRangeList } from './protocol/foldingProvider.proposed';
export function getFoldingRegions(document: TextDocument, cancellationToken: CancellationToken | null) {
export function getFoldingRegions(document: TextDocument, maxRanges: number | undefined, cancellationToken: CancellationToken | null) {
let ranges: FoldingRange[] = [];
let nestingLevels: number[] = [];
let stack: FoldingRange[] = [];
let prevStart = -1;
let scanner = createScanner(document.getText(), false);
let token = scanner.scan();
function addRange(range: FoldingRange) {
ranges.push(range);
nestingLevels.push(stack.length);
}
while (token !== SyntaxKind.EOF) {
if (cancellationToken && cancellationToken.isCancellationRequested) {
return null;
@ -34,7 +41,7 @@ export function getFoldingRegions(document: TextDocument, cancellationToken: Can
let line = document.positionAt(scanner.getTokenOffset()).line;
if (range && line > range.startLine + 1 && prevStart !== range.startLine) {
range.endLine = line - 1;
ranges.push(range);
addRange(range);
prevStart = range.startLine;
}
}
@ -48,7 +55,7 @@ export function getFoldingRegions(document: TextDocument, cancellationToken: Can
scanner.setPosition(document.offsetAt(Position.create(startLine + 1, 0)));
} else {
if (startLine < endLine) {
ranges.push({ startLine, endLine, type: FoldingRangeType.Comment });
addRange({ startLine, endLine, type: FoldingRangeType.Comment });
prevStart = startLine;
}
}
@ -73,7 +80,7 @@ export function getFoldingRegions(document: TextDocument, cancellationToken: Can
stack.length = i;
if (line > range.startLine && prevStart !== range.startLine) {
range.endLine = line;
ranges.push(range);
addRange(range);
prevStart = range.startLine;
}
}
@ -85,5 +92,26 @@ export function getFoldingRegions(document: TextDocument, cancellationToken: Can
}
token = scanner.scan();
}
if (maxRanges && ranges.length > maxRanges) {
let counts: number[] = [];
for (let level of nestingLevels) {
if (level < 30) {
counts[level] = (counts[level] || 0) + 1;
}
}
let entries = 0;
let maxLevel = 0;
for (let i = 0; i < counts.length; i++) {
let n = counts[i];
if (n) {
if (n + entries > maxRanges) {
maxLevel = i;
break;
}
entries += n;
}
}
ranges = ranges.filter((r, index) => nestingLevels[index] < maxLevel);
}
return <FoldingRangeList>{ ranges };
}

View file

@ -365,7 +365,7 @@ connection.onRequest(FoldingRangesRequest.type, (params, token) => {
return runSafe(() => {
let document = documents.get(params.textDocument.uri);
if (document) {
return getFoldingRegions(document, token);
return getFoldingRegions(document, params.maxRanges, token);
}
return null;
}, null, `Error while computing folding ranges for ${params.textDocument.uri}`, token);

View file

@ -82,6 +82,11 @@ export interface FoldingRangeRequestParam {
* The text document.
*/
textDocument: TextDocumentIdentifier;
/**
* The maximum number of ranges to provide
*/
maxRanges?: number;
}
export namespace FoldingRangesRequest {

View file

@ -16,9 +16,9 @@ interface ExpectedIndentRange {
type?: string;
}
function assertRanges(lines: string[], expected: ExpectedIndentRange[]): void {
function assertRanges(lines: string[], expected: ExpectedIndentRange[], nRanges?: number): void {
let document = TextDocument.create('test://foo/bar.json', 'json', 1, lines.join('\n'));
let actual = getFoldingRegions(document, null)!.ranges;
let actual = getFoldingRegions(document, nRanges, null)!.ranges;
let actualRanges = [];
for (let i = 0; i < actual.length; i++) {
@ -34,27 +34,27 @@ function r(startLine: number, endLine: number, type?: string): ExpectedIndentRan
suite('Object Folding', () => {
test('Fold one level', () => {
let range = [
let input = [
/*0*/'{',
/*1*/'"foo":"bar"',
/*2*/'}'
];
assertRanges(range, [r(0, 1, 'object')]);
assertRanges(input, [r(0, 1, 'object')]);
});
test('Fold two level', () => {
let range = [
let input = [
/*0*/'[',
/*1*/'{',
/*2*/'"foo":"bar"',
/*3*/'}',
/*4*/']'
];
assertRanges(range, [r(0, 3, 'array'), r(1, 2, 'object')]);
assertRanges(input, [r(0, 3, 'array'), r(1, 2, 'object')]);
});
test('Fold Arrays', () => {
let range = [
let input = [
/*0*/'[',
/*1*/'[',
/*2*/'],[',
@ -62,11 +62,11 @@ suite('Object Folding', () => {
/*4*/']',
/*5*/']'
];
assertRanges(range, [r(0, 4, 'array'), r(2, 3, 'array')]);
assertRanges(input, [r(0, 4, 'array'), r(2, 3, 'array')]);
});
test('Filter start on same line', () => {
let range = [
let input = [
/*0*/'[[',
/*1*/'[',
/*2*/'],[',
@ -74,36 +74,71 @@ suite('Object Folding', () => {
/*4*/']',
/*5*/']]'
];
assertRanges(range, [r(0, 4, 'array'), r(2, 3, 'array')]);
assertRanges(input, [r(0, 4, 'array'), r(2, 3, 'array')]);
});
test('Fold commment', () => {
let range = [
let input = [
/*0*/'/*',
/*1*/' multi line',
/*2*/'*/',
];
assertRanges(range, [r(0, 2, 'comment')]);
assertRanges(input, [r(0, 2, 'comment')]);
});
test('Incomplete commment', () => {
let range = [
let input = [
/*0*/'/*',
/*1*/'{',
/*2*/'"foo":"bar"',
/*3*/'}',
];
assertRanges(range, [r(1, 2, 'object')]);
assertRanges(input, [r(1, 2, 'object')]);
});
test('Fold regions', () => {
let range = [
let input = [
/*0*/'// #region',
/*1*/'{',
/*2*/'}',
/*3*/'// #endregion',
];
assertRanges(range, [r(0, 3, 'region')]);
assertRanges(input, [r(0, 3, 'region')]);
});
test('Test limit', () => {
let input = [
/* 0*/'[',
/* 1*/' [',
/* 2*/' [',
/* 3*/' ',
/* 4*/' ],',
/* 5*/' [',
/* 6*/' [',
/* 7*/' ',
/* 8*/' ],',
/* 9*/' [',
/*10*/' ',
/*11*/' ],',
/*12*/' ],',
/*13*/' [',
/*14*/' ',
/*15*/' ],',
/*16*/' [',
/*17*/' ',
/*18*/' ]',
/*19*/' ]',
/*20*/']',
];
assertRanges(input, [r(0, 19, 'array'), r(1, 18, 'array'), r(2, 3, 'array'), r(5, 11, 'array'), r(6, 7, 'array'), r(9, 10, 'array'), r(13, 14, 'array'), r(16, 17, 'array')], void 0);
assertRanges(input, [r(0, 19, 'array'), r(1, 18, 'array'), r(2, 3, 'array'), r(5, 11, 'array'), r(6, 7, 'array'), r(9, 10, 'array'), r(13, 14, 'array'), r(16, 17, 'array')], 8);
assertRanges(input, [r(0, 19, 'array'), r(1, 18, 'array'), r(2, 3, 'array'), r(5, 11, 'array'), r(13, 14, 'array'), r(16, 17, 'array')], 7);
assertRanges(input, [r(0, 19, 'array'), r(1, 18, 'array'), r(2, 3, 'array'), r(5, 11, 'array'), r(13, 14, 'array'), r(16, 17, 'array')], 6);
assertRanges(input, [r(0, 19, 'array'), r(1, 18, 'array')], 5);
assertRanges(input, [r(0, 19, 'array'), r(1, 18, 'array')], 4);
assertRanges(input, [r(0, 19, 'array'), r(1, 18, 'array')], 3);
assertRanges(input, [r(0, 19, 'array'), r(1, 18, 'array')], 2);
assertRanges(input, [r(0, 19, 'array')], 1);
});
});

View file

@ -16,10 +16,14 @@ export default class MarkdownFoldingProvider implements vscode.FoldingProvider {
public async provideFoldingRanges(
document: vscode.TextDocument,
context: vscode.FoldingContext,
_token: vscode.CancellationToken
): Promise<vscode.FoldingRangeList> {
const tocProvider = new TableOfContentsProvider(this.engine, document);
const toc = await tocProvider.getToc();
let toc = await tocProvider.getToc();
if (context.maxRanges && toc.length > context.maxRanges) {
toc = toc.slice(0, context.maxRanges);
}
const foldingRanges = toc.map((entry, startIndex) => {
const start = entry.line;
@ -35,6 +39,7 @@ export default class MarkdownFoldingProvider implements vscode.FoldingProvider {
typeof end === 'number' ? end : document.lineCount - 1);
});
return new vscode.FoldingRangeList(foldingRanges);
}
}

View file

@ -35,6 +35,7 @@ export default class TypeScriptFoldingProvider implements vscode.FoldingProvider
async provideFoldingRanges(
document: vscode.TextDocument,
_: vscode.FoldingContext,
token: vscode.CancellationToken
): Promise<vscode.FoldingRangeList | undefined> {
if (!this.client.apiVersion.has270Features()) {

View file

@ -826,6 +826,13 @@ export interface DocumentColorProvider {
provideColorPresentations(model: model.ITextModel, colorInfo: IColorInformation, token: CancellationToken): IColorPresentation[] | Thenable<IColorPresentation[]>;
}
/**
* @internal
*/
export interface FoldingContext {
maxRanges?: number;
}
/**
* A provider of colors for editor models.
*/
@ -836,7 +843,7 @@ export interface FoldingProvider {
/**
* Provides the color ranges for a specific model.
*/
provideFoldingRanges(model: model.ITextModel, token: CancellationToken): IFoldingRangeList | Thenable<IFoldingRangeList>;
provideFoldingRanges(model: model.ITextModel, context: FoldingContext, token: CancellationToken): IFoldingRangeList | Thenable<IFoldingRangeList>;
}
/**
* @internal

View file

@ -5,7 +5,7 @@
'use strict';
import { FoldingProvider, IFoldingRange } from 'vs/editor/common/modes';
import { FoldingProvider, IFoldingRange, FoldingContext } from 'vs/editor/common/modes';
import { onUnexpectedExternalError } from 'vs/base/common/errors';
import { toThenable } from 'vs/base/common/async';
import { ITextModel } from 'vs/editor/common/model';
@ -20,6 +20,10 @@ export interface IFoldingRangeData extends IFoldingRange {
rank: number;
}
const foldingContext: FoldingContext = {
maxRanges: MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT
};
export class SyntaxRangeProvider implements RangeProvider {
constructor(private providers: FoldingProvider[]) {
@ -38,7 +42,7 @@ export class SyntaxRangeProvider implements RangeProvider {
}
function collectSyntaxRanges(providers: FoldingProvider[], model: ITextModel, cancellationToken: CancellationToken): Thenable<IFoldingRangeData[] | null> {
let promises = providers.map(provider => toThenable(provider.provideFoldingRanges(model, cancellationToken)));
let promises = providers.map(provider => toThenable(provider.provideFoldingRanges(model, foldingContext, cancellationToken)));
return TPromise.join(promises).then(lists => {
let rangeData: IFoldingRangeData[] = null;
if (cancellationToken.isCancellationRequested) {

View file

@ -398,11 +398,15 @@ declare module 'vscode' {
export function registerFoldingProvider(selector: DocumentSelector, provider: FoldingProvider): Disposable;
}
export interface FoldingContext {
maxRanges?: number;
}
export interface FoldingProvider {
/**
* Returns a list of folding ranges or null if the provider does not want to participate or was cancelled.
*/
provideFoldingRanges(document: TextDocument, token: CancellationToken): ProviderResult<FoldingRangeList>;
provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken): ProviderResult<FoldingRangeList>;
}
/**

View file

@ -351,8 +351,8 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
$registerFoldingProvider(handle: number, selector: vscode.DocumentSelector): void {
const proxy = this._proxy;
this._registrations[handle] = modes.FoldingProviderRegistry.register(toLanguageSelector(selector), <modes.FoldingProvider>{
provideFoldingRanges: (model, token) => {
return wireCancellationToken(token, proxy.$provideFoldingRanges(handle, model.uri));
provideFoldingRanges: (model, context, token) => {
return wireCancellationToken(token, proxy.$provideFoldingRanges(handle, model.uri, context));
}
});
}

View file

@ -701,7 +701,7 @@ export interface ExtHostLanguageFeaturesShape {
$resolveDocumentLink(handle: number, link: modes.ILink): TPromise<modes.ILink>;
$provideDocumentColors(handle: number, resource: UriComponents): TPromise<IRawColorInfo[]>;
$provideColorPresentations(handle: number, resource: UriComponents, colorInfo: IRawColorInfo): TPromise<modes.IColorPresentation[]>;
$provideFoldingRanges(handle: number, resource: UriComponents): TPromise<modes.IFoldingRangeList>;
$provideFoldingRanges(handle: number, resource: UriComponents, context: modes.FoldingContext): TPromise<modes.IFoldingRangeList>;
}
export interface ExtHostQuickOpenShape {

View file

@ -810,9 +810,9 @@ class FoldingProviderAdapter {
private _provider: vscode.FoldingProvider
) { }
provideFoldingRanges(resource: URI): TPromise<modes.IFoldingRangeList> {
provideFoldingRanges(resource: URI, context: modes.FoldingContext): TPromise<modes.IFoldingRangeList> {
const doc = this._documents.getDocumentData(resource).document;
return asWinJsPromise(token => this._provider.provideFoldingRanges(doc, token)).then(list => {
return asWinJsPromise(token => this._provider.provideFoldingRanges(doc, context, token)).then(list => {
if (!Array.isArray(list.ranges)) {
return void 0;
}
@ -1132,8 +1132,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
return this._createDisposable(handle);
}
$provideFoldingRanges(handle: number, resource: UriComponents): TPromise<modes.IFoldingRangeList> {
return this._withAdapter(handle, FoldingProviderAdapter, adapter => adapter.provideFoldingRanges(URI.revive(resource)));
$provideFoldingRanges(handle: number, resource: UriComponents, context: vscode.FoldingContext): TPromise<modes.IFoldingRangeList> {
return this._withAdapter(handle, FoldingProviderAdapter, adapter => adapter.provideFoldingRanges(URI.revive(resource), context));
}
// --- configuration