[folding] fold regions, initial, preconfigured support. For #12146

This commit is contained in:
Martin Aeschlimann 2017-09-22 13:49:41 +02:00
parent a678f60100
commit 9e05d4b635
9 changed files with 341 additions and 135 deletions

View file

@ -33,5 +33,11 @@
"scopeName": "source.c.platform", "scopeName": "source.c.platform",
"path": "./syntaxes/Platform.tmLanguage" "path": "./syntaxes/Platform.tmLanguage"
}] }]
},
"folding": {
"markers": {
"start": "^\\s*#pragma\\s+region",
"end": "^\\s*#pragma\\s+endregion"
}
} }
} }

View file

@ -23,5 +23,11 @@
["<", ">"], ["<", ">"],
["'", "'"], ["'", "'"],
["\"", "\""] ["\"", "\""]
] ],
"folding": {
"markers": {
"start": "^\\s*#region",
"end": "^\\s*#endregion"
}
}
} }

View file

@ -24,5 +24,11 @@
["'", "'"], ["'", "'"],
["\"", "\""], ["\"", "\""],
["`", "`"] ["`", "`"]
] ],
"folding": {
"markers": {
"start": "^\\s*//\\s*#region",
"end": "^\\s*//\\s*#endregion"
}
}
} }

View file

@ -22,5 +22,11 @@
["(", ")"], ["(", ")"],
["\"", "\""], ["\"", "\""],
["'", "'"] ["'", "'"]
] ],
"folding": {
"markers": {
"start": "^\\s*#region",
"end": "^\\s*#endregion"
}
}
} }

View file

@ -24,5 +24,11 @@
["'", "'"], ["'", "'"],
["\"", "\""], ["\"", "\""],
["`", "`"] ["`", "`"]
] ],
"folding": {
"markers": {
"start": "^\\s*//\\s*#region",
"end": "^\\s*//\\s*#endregion"
}
}
} }

View file

@ -20,5 +20,11 @@
["(", ")"], ["(", ")"],
["\"", "\""], ["\"", "\""],
["<", ">"] ["<", ">"]
] ],
"folding": {
"markers": {
"start": "^\\s*#Region",
"end": "^\\s*#End Region"
}
}
} }

View file

@ -12,11 +12,13 @@ export class IndentRange {
startLineNumber: number; startLineNumber: number;
endLineNumber: number; endLineNumber: number;
indent: number; indent: number;
marker: boolean;
constructor(startLineNumber: number, endLineNumber: number, indent: number) { constructor(startLineNumber: number, endLineNumber: number, indent: number, marker?: boolean) {
this.startLineNumber = startLineNumber; this.startLineNumber = startLineNumber;
this.endLineNumber = endLineNumber; this.endLineNumber = endLineNumber;
this.indent = indent; this.indent = indent;
this.marker = marker;
} }
public static deepCloneArr(indentRanges: IndentRange[]): IndentRange[] { public static deepCloneArr(indentRanges: IndentRange[]): IndentRange[] {
@ -29,12 +31,27 @@ export class IndentRange {
} }
} }
export function computeRanges(model: ITextModel, offSide: boolean, minimumRangeSize: number = 1): IndentRange[] { export interface FoldMarkers {
start: string;
end: string;
indent?: number;
}
interface PreviousRegion { indent: number; line: number; marker: RegExp; };
export function computeRanges(model: ITextModel, offSide: boolean, markers?: FoldMarkers, minimumRangeSize: number = 1): IndentRange[] {
let result: IndentRange[] = []; let result: IndentRange[] = [];
let previousRegions: { indent: number, line: number }[] = []; let pattern = void 0;
previousRegions.push({ indent: -1, line: model.getLineCount() + 1 }); // sentinel, to make sure there's at least one entry let patternIndent = -1;
if (markers) {
pattern = new RegExp(`(${markers.start})|(?:${markers.end})`);
patternIndent = typeof markers.indent === 'number' ? markers.indent : -1;
}
let previousRegions: PreviousRegion[] = [];
previousRegions.push({ indent: -1, line: model.getLineCount() + 1, marker: null }); // sentinel, to make sure there's at least one entry
for (let line = model.getLineCount(); line > 0; line--) { for (let line = model.getLineCount(); line > 0; line--) {
let indent = model.getIndentLevel(line); let indent = model.getIndentLevel(line);
@ -46,26 +63,48 @@ export function computeRanges(model: ITextModel, offSide: boolean, minimumRangeS
} }
continue; // only whitespace continue; // only whitespace
} }
let m;
if (pattern && (patternIndent === -1 || patternIndent === indent) && (m = model.getLineContent(line).match(pattern))) {
// folding pattern match
if (m[1]) { // start pattern match
if (previous.indent >= 0 && !previous.marker) {
// discard all regions until the folding pattern
if (previous.indent > indent) { do {
// discard all regions with larger indent previousRegions.pop();
do { previous = previousRegions[previousRegions.length - 1];
previousRegions.pop(); } while (previous.indent >= 0 && !previous.marker);
previous = previousRegions[previousRegions.length - 1]; }
} while (previous.indent > indent); if (previous.marker) {
// new folding range from pattern, includes the end line
// new folding range result.push(new IndentRange(line, previous.line, indent, true));
let endLineNumber = previous.line - 1; previous.marker = null;
if (endLineNumber - line >= minimumRangeSize) { previous.indent = indent;
result.push(new IndentRange(line, endLineNumber, indent)); previous.line = line;
}
} else { // end pattern match
previousRegions.push({ indent: -2, line, marker: pattern });
}
} else {
if (previous.indent > indent) {
// discard all regions with larger indent
do {
previousRegions.pop();
previous = previousRegions[previousRegions.length - 1];
} while (previous.indent > indent);
// new folding range
let endLineNumber = previous.line - 1;
if (endLineNumber - line >= minimumRangeSize) {
result.push(new IndentRange(line, endLineNumber, indent));
}
}
if (previous.indent === indent) {
previous.line = line;
} else { // previous.indent < indent
// new region with a bigger indent
previousRegions.push({ indent, line, marker: null });
} }
}
if (previous.indent === indent) {
previous.line = line;
} else { // previous.indent < indent
// new region with a bigger indent
previousRegions.push({ indent, line });
} }
} }

View file

@ -845,6 +845,8 @@ export class TextModelWithTokens extends TextModel implements editorCommon.IToke
if (!this._indentRanges) { if (!this._indentRanges) {
let foldingRules = LanguageConfigurationRegistry.getFoldingRules(this._languageIdentifier.id); let foldingRules = LanguageConfigurationRegistry.getFoldingRules(this._languageIdentifier.id);
let offSide = foldingRules && foldingRules.offSide; let offSide = foldingRules && foldingRules.offSide;
let markers = foldingRules && foldingRules['markers'];
this._indentRanges = computeRanges(this, offSide, markers);
} }
return this._indentRanges; return this._indentRanges;
} }

View file

@ -7,137 +7,266 @@
import * as assert from 'assert'; import * as assert from 'assert';
import { Model } from 'vs/editor/common/model/model'; import { Model } from 'vs/editor/common/model/model';
import { computeRanges } from 'vs/editor/common/model/indentRanges'; import { computeRanges, FoldMarkers } from 'vs/editor/common/model/indentRanges';
export interface IndentRange { export interface IndentRange {
startLineNumber: number; startLineNumber: number;
endLineNumber: number; endLineNumber: number;
indent: number; indent: number;
marker: boolean;
} }
suite('Indentation Folding', () => { function assertRanges(lines: string[], expected: IndentRange[], offside: boolean, markers?: FoldMarkers): void {
function assertRanges(lines: string[], expected: IndentRange[], offside): void { let model = Model.createFromString(lines.join('\n'));
let model = Model.createFromString(lines.join('\n')); let actual = computeRanges(model, offside, markers);
let actual = computeRanges(model, offside); actual.sort((r1, r2) => r1.startLineNumber - r2.startLineNumber);
actual.sort((r1, r2) => r1.startLineNumber - r2.startLineNumber); assert.deepEqual(actual, expected);
assert.deepEqual(actual, expected); model.dispose();
model.dispose(); }
}
function r(startLineNumber: number, endLineNumber: number, indent: number): IndentRange { function r(startLineNumber: number, endLineNumber: number, indent: number, marker?: boolean): IndentRange {
return { startLineNumber, endLineNumber, indent }; return { startLineNumber, endLineNumber, indent, marker };
} }
test('Fold one level', () => { // suite('Indentation Folding', () => {
let range = [
'A',
' A',
' A',
' A'
];
assertRanges(range, [r(1, 4, 0)], true);
assertRanges(range, [r(1, 4, 0)], false);
});
test('Fold two levels', () => { // test('Fold one level', () => {
let range = [ // let range = [
'A', // 'A',
' A', // ' A',
' A', // ' A',
' A', // ' A'
' A' // ];
]; // assertRanges(range, [r(1, 4, 0)], true);
assertRanges(range, [r(1, 5, 0), r(3, 5, 2)], true); // assertRanges(range, [r(1, 4, 0)], false);
assertRanges(range, [r(1, 5, 0), r(3, 5, 2)], false); // });
});
test('Fold three levels', () => { // test('Fold two levels', () => {
let range = [ // let range = [
'A', // 'A',
' A', // ' A',
' A', // ' A',
' A', // ' A',
'A' // ' A'
]; // ];
assertRanges(range, [r(1, 4, 0), r(2, 4, 2), r(3, 4, 4)], true); // assertRanges(range, [r(1, 5, 0), r(3, 5, 2)], true);
assertRanges(range, [r(1, 4, 0), r(2, 4, 2), r(3, 4, 4)], false); // assertRanges(range, [r(1, 5, 0), r(3, 5, 2)], false);
}); // });
test('Fold decreasing indent', () => { // test('Fold three levels', () => {
let range = [ // let range = [
' A', // 'A',
' A', // ' A',
'A' // ' A',
]; // ' A',
assertRanges(range, [], true); // 'A'
assertRanges(range, [], false); // ];
}); // assertRanges(range, [r(1, 4, 0), r(2, 4, 2), r(3, 4, 4)], true);
// assertRanges(range, [r(1, 4, 0), r(2, 4, 2), r(3, 4, 4)], false);
// });
test('Fold Java', () => { // test('Fold decreasing indent', () => {
// let range = [
// ' A',
// ' A',
// 'A'
// ];
// assertRanges(range, [], true);
// assertRanges(range, [], false);
// });
// test('Fold Java', () => {
// assertRanges([
// /* 1*/ 'class A {',
// /* 2*/ ' void foo() {',
// /* 3*/ ' console.log();',
// /* 4*/ ' console.log();',
// /* 5*/ ' }',
// /* 6*/ '',
// /* 7*/ ' void bar() {',
// /* 8*/ ' console.log();',
// /* 9*/ ' }',
// /*10*/ '}',
// /*11*/ 'interface B {',
// /*12*/ ' void bar();',
// /*13*/ '}',
// ], [r(1, 9, 0), r(2, 4, 2), r(7, 8, 2), r(11, 12, 0)], false);
// });
// test('Fold Javadoc', () => {
// assertRanges([
// /* 1*/ '/**',
// /* 2*/ ' * Comment',
// /* 3*/ ' */',
// /* 4*/ 'class A {',
// /* 5*/ ' void foo() {',
// /* 6*/ ' }',
// /* 7*/ '}',
// ], [r(1, 3, 0), r(4, 6, 0)], false);
// });
// test('Fold Whitespace Java', () => {
// assertRanges([
// /* 1*/ 'class A {',
// /* 2*/ '',
// /* 3*/ ' void foo() {',
// /* 4*/ ' ',
// /* 5*/ ' return 0;',
// /* 6*/ ' }',
// /* 7*/ ' ',
// /* 8*/ '}',
// ], [r(1, 7, 0), r(3, 5, 2)], false);
// });
// test('Fold Whitespace Python', () => {
// assertRanges([
// /* 1*/ 'def a:',
// /* 2*/ ' pass',
// /* 3*/ ' ',
// /* 4*/ ' def b:',
// /* 5*/ ' pass',
// /* 6*/ ' ',
// /* 7*/ ' ',
// /* 8*/ 'def c: # since there was a deintent here'
// ], [r(1, 5, 0), r(4, 5, 2)], true);
// });
// test('Fold Tabs', () => {
// assertRanges([
// /* 1*/ 'class A {',
// /* 2*/ '\t\t',
// /* 3*/ '\tvoid foo() {',
// /* 4*/ '\t \t//hello',
// /* 5*/ '\t return 0;',
// /* 6*/ ' \t}',
// /* 7*/ ' ',
// /* 8*/ '}',
// ], [r(1, 7, 0), r(3, 5, 4)], false);
// });
// });
let foldPattern: FoldMarkers = {
start: '^\\s*#region',
end: '^\\s*#endregion'
};
suite('Folding with regions', () => {
test('Inside region, indented', () => {
assertRanges([ assertRanges([
/* 1*/ 'class A {', /* 1*/ 'class A {',
/* 2*/ ' void foo() {', /* 2*/ ' #region',
/* 3*/ ' console.log();',
/* 4*/ ' console.log();',
/* 5*/ ' }',
/* 6*/ '',
/* 7*/ ' void bar() {',
/* 8*/ ' console.log();',
/* 9*/ ' }',
/*10*/ '}',
/*11*/ 'interface B {',
/*12*/ ' void bar();',
/*13*/ '}',
], [r(1, 9, 0), r(2, 4, 2), r(7, 8, 2), r(11, 12, 0)], false);
});
test('Fold Javadoc', () => {
assertRanges([
/* 1*/ '/**',
/* 2*/ ' * Comment',
/* 3*/ ' */',
/* 4*/ 'class A {',
/* 5*/ ' void foo() {',
/* 6*/ ' }',
/* 7*/ '}',
], [r(1, 3, 0), r(4, 6, 0)], false);
});
test('Fold Whitespace Java', () => {
assertRanges([
/* 1*/ 'class A {',
/* 2*/ '',
/* 3*/ ' void foo() {', /* 3*/ ' void foo() {',
/* 4*/ ' ', /* 4*/ ' ',
/* 5*/ ' return 0;', /* 5*/ ' return 0;',
/* 6*/ ' }', /* 6*/ ' }',
/* 7*/ ' ', /* 7*/ ' #endregion',
/* 8*/ '}', /* 8*/ '}',
], [r(1, 7, 0), r(3, 5, 2)], false); ], [r(1, 7, 0), r(2, 7, 2, true), r(3, 5, 2)], false, foldPattern);
}); });
test('Inside region, not indented', () => {
test('Fold Whitespace Python', () => {
assertRanges([ assertRanges([
/* 1*/ 'def a:', /* 1*/ 'var x;',
/* 2*/ ' pass', /* 2*/ '#region',
/* 3*/ ' ', /* 3*/ 'void foo() {',
/* 4*/ ' def b:', /* 4*/ ' ',
/* 5*/ ' pass', /* 5*/ ' return 0;',
/* 6*/ ' ', /* 6*/ ' }',
/* 7*/ ' ', /* 7*/ '#endregion',
/* 8*/ 'def c: # since there was a deintent here' /* 8*/ '',
], [r(1, 5, 0), r(4, 5, 2)], true); ], [r(2, 7, 0, true), r(3, 6, 0)], false, foldPattern);
}); });
test('Empty Regions', () => {
test('Fold Tabs', () => { assertRanges([
/* 1*/ 'var x;',
/* 2*/ '#region',
/* 3*/ '#endregion',
/* 4*/ '#region',
/* 5*/ '',
/* 6*/ '#endregion',
/* 7*/ 'var y;',
], [r(2, 3, 0, true), r(4, 6, 0, true)], false, foldPattern);
});
test('Nested Regions', () => {
assertRanges([
/* 1*/ 'var x;',
/* 2*/ '#region',
/* 3*/ '#region',
/* 4*/ '',
/* 5*/ '#endregion',
/* 6*/ '#endregion',
/* 7*/ 'var y;',
], [r(2, 6, 0, true), r(3, 5, 0, true)], false, foldPattern);
});
test('Nested Regions 2', () => {
assertRanges([ assertRanges([
/* 1*/ 'class A {', /* 1*/ 'class A {',
/* 2*/ '\t\t', /* 2*/ ' #region',
/* 3*/ '\tvoid foo() {', /* 3*/ '',
/* 4*/ '\t \t//hello', /* 4*/ ' #region',
/* 5*/ '\t return 0;', /* 5*/ '',
/* 6*/ ' \t}', /* 6*/ ' #endregion',
/* 7*/ ' ', /* 7*/ ' // comment',
/* 8*/ '}', /* 8*/ ' #endregion',
], [r(1, 7, 0), r(3, 5, 4)], false); /* 9*/ '}',
], [r(1, 8, 0), r(2, 8, 2, true), r(4, 6, 2, true)], false, foldPattern);
}); });
}); test('Incomplete Regions', () => {
assertRanges([
/* 1*/ 'class A {',
/* 2*/ '#region',
/* 3*/ ' // comment',
/* 4*/ '}',
], [], false, foldPattern);
});
test('Incomplete Regions', () => {
assertRanges([
/* 1*/ '',
/* 2*/ '#region',
/* 3*/ '#region',
/* 4*/ '#region',
/* 5*/ ' // comment',
/* 6*/ '#endregion',
/* 7*/ '#endregion',
/* 8*/ ' // hello',
], [r(3, 7, 0, true), r(4, 6, 0, true)], false, foldPattern);
});
test('Indented region before', () => {
assertRanges([
/* 1*/ 'if (x)',
/* 2*/ ' return;',
/* 3*/ '',
/* 4*/ '#region',
/* 5*/ ' // comment',
/* 6*/ '#endregion',
], [r(1, 3, 0), r(4, 6, 0, true)], false, foldPattern);
});
test('Indented region before 2', () => {
assertRanges([
/* 1*/ 'if (x)',
/* 2*/ ' log();',
/* 3*/ '',
/* 4*/ ' #region',
/* 5*/ ' // comment',
/* 6*/ ' #endregion',
], [r(1, 6, 0), r(2, 6, 2), r(4, 6, 4, true)], false, foldPattern);
});
test('Indented region in-between', () => {
assertRanges([
/* 1*/ '#region',
/* 2*/ ' // comment',
/* 3*/ ' if (x)',
/* 4*/ ' return;',
/* 5*/ '',
/* 6*/ '#endregion',
], [r(1, 6, 0, true), r(3, 5, 2)], false, foldPattern);
});
test('Indented region after', () => {
assertRanges([
/* 1*/ '#region',
/* 2*/ ' // comment',
/* 3*/ '',
/* 4*/ '#endregion',
/* 5*/ ' if (x)',
/* 6*/ ' return;',
], [r(1, 4, 0, true), r(5, 6, 2)], false, foldPattern);
});
});