Skip to content

Commit

Permalink
Makes bracket pair guides aware of indentation. Implements #134267.
Browse files Browse the repository at this point in the history
  • Loading branch information
hediet committed Oct 11, 2021
1 parent 9bfabde commit 46e3f56
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 24 deletions.
21 changes: 21 additions & 0 deletions src/vs/editor/common/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1391,6 +1391,27 @@ export class BracketPair {
) { }
}

/**
* @internal
*/
export class BracketPairWithMinIndentation extends BracketPair {
constructor(
range: Range,
openingBracketRange: Range,
closingBracketRange: Range | undefined,
/**
* 0-based
*/
nestingLevel: number,
/**
* -1 if not requested, otherwise the size of the minimum indentation in the bracket pair in terms of visible columns.
*/
public readonly minVisibleColumnIndentation: number,
) {
super(range, openingBracketRange, closingBracketRange, nestingLevel);
}
}

/**
* @internal
*/
Expand Down
63 changes: 62 additions & 1 deletion src/vs/editor/common/model/bracketPairColorizer/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
*--------------------------------------------------------------------------------------------*/

import { SmallImmutableSet } from './smallImmutableSet';
import { lengthAdd, lengthZero, Length, lengthHash } from './length';
import { lengthAdd, lengthZero, Length, lengthHash, lengthGetLineCount, lengthToObj } from './length';
import { OpeningBracketId } from 'vs/editor/common/model/bracketPairColorizer/tokenizer';
import { ITextModel } from 'vs/editor/common/model';
import { CursorColumns } from 'vs/editor/common/controller/cursorColumns';

export const enum AstNodeKind {
Text = 0,
Expand Down Expand Up @@ -76,6 +78,8 @@ abstract class BaseAstNode {
* Creates a deep clone.
*/
public abstract deepClone(): AstNode;

public abstract computeMinIndentation(offset: Length, textModel: ITextModel): number;
}

/**
Expand Down Expand Up @@ -181,6 +185,10 @@ export class PairAstNode extends BaseAstNode {
this.missingOpeningBracketIds
);
}

public computeMinIndentation(offset: Length, textModel: ITextModel): number {
return this.child ? this.child.computeMinIndentation(lengthAdd(offset, this.openingBracket.length), textModel) : Number.MAX_SAFE_INTEGER;
}
}

export abstract class ListAstNode extends BaseAstNode {
Expand Down Expand Up @@ -238,6 +246,8 @@ export abstract class ListAstNode extends BaseAstNode {
return this._missingOpeningBracketIds;
}

private cachedMinIndentation: number = -1;

/**
* Use ListAstNode.create.
*/
Expand Down Expand Up @@ -319,6 +329,7 @@ export abstract class ListAstNode extends BaseAstNode {

this._length = length;
this._missingOpeningBracketIds = unopenedBrackets;
this.cachedMinIndentation = -1;
}

public flattenLists(): ListAstNode {
Expand All @@ -334,6 +345,25 @@ export abstract class ListAstNode extends BaseAstNode {
return ListAstNode.create(items);
}

public computeMinIndentation(offset: Length, textModel: ITextModel): number {
if (this.cachedMinIndentation !== -1) {
return this.cachedMinIndentation;
}

let minIndentation = Number.MAX_SAFE_INTEGER;
let childOffset = offset;
for (let i = 0; i < this.childrenLength; i++) {
const child = this.getChild(i);
if (child) {
minIndentation = Math.min(minIndentation, child.computeMinIndentation(childOffset, textModel));
childOffset = lengthAdd(childOffset, child.length);
}
}

this.cachedMinIndentation = minIndentation;
return minIndentation;
}

/**
* Creates a shallow clone that is mutable, or itself if it is already mutable.
*/
Expand Down Expand Up @@ -583,6 +613,29 @@ export class TextAstNode extends ImmutableLeafAstNode {
// Otherwise, long brackets might not be detected.
return !endLineDidChange;
}

public computeMinIndentation(offset: Length, textModel: ITextModel): number {
const start = lengthToObj(offset);
// Text ast nodes don't have partial indentation (ensured by the tokenizer).
// Thus, if this text node does not start at column 0, the first line cannot have any indentation at all.
const startLineNumber = (start.columnCount === 0 ? start.lineCount : start.lineCount + 1) + 1;
const endLineNumber = lengthGetLineCount(lengthAdd(offset, this.length)) + 1;

let result = Number.MAX_SAFE_INTEGER;

for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
const firstNonWsColumn = textModel.getLineFirstNonWhitespaceColumn(lineNumber);
const lineContent = textModel.getLineContent(lineNumber);
if (firstNonWsColumn === 0) {
continue;
}

const visibleColumn = CursorColumns.visibleColumnFromColumn(lineContent, firstNonWsColumn, textModel.getOptions().tabSize)!;
result = Math.min(result, visibleColumn);
}

return result;
}
}

export class BracketAstNode extends ImmutableLeafAstNode {
Expand Down Expand Up @@ -621,6 +674,10 @@ export class BracketAstNode extends ImmutableLeafAstNode {
// Their parent may be reused.
return false;
}

public computeMinIndentation(offset: Length, textModel: ITextModel): number {
return Number.MAX_SAFE_INTEGER;
}
}

export class InvalidBracketAstNode extends ImmutableLeafAstNode {
Expand All @@ -641,4 +698,8 @@ export class InvalidBracketAstNode extends ImmutableLeafAstNode {
) {
return !openedBracketIds.intersects(this.missingOpeningBracketIds);
}

public computeMinIndentation(offset: Length, textModel: ITextModel): number {
return Number.MAX_SAFE_INTEGER;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Color } from 'vs/base/common/color';
import { Emitter } from 'vs/base/common/event';
import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable } from 'vs/base/common/lifecycle';
import { Range } from 'vs/editor/common/core/range';
import { BracketPair, BracketPairColorizationOptions, IModelDecoration } from 'vs/editor/common/model';
import { BracketPair, BracketPairColorizationOptions, BracketPairWithMinIndentation, IModelDecoration, ITextModel } from 'vs/editor/common/model';
import { DenseKeyProvider } from 'vs/editor/common/model/bracketPairColorizer/smallImmutableSet';
import { DecorationProvider } from 'vs/editor/common/model/decorationProvider';
import { BackgroundTokenizationState, TextModel } from 'vs/editor/common/model/textModel';
Expand All @@ -31,6 +31,12 @@ export interface IBracketPairs {
* The result is sorted by the start position.
*/
getBracketPairsInRange(range: Range): BracketPair[];

/**
* Gets all bracket pairs that intersect the given position.
* The result is sorted by the start position.
*/
getBracketPairsInRangeWithMinIndentation(range: Range): BracketPairWithMinIndentation[];
}

export class BracketPairColorizer extends Disposable implements DecorationProvider, IBracketPairs {
Expand Down Expand Up @@ -123,7 +129,13 @@ export class BracketPairColorizer extends Disposable implements DecorationProvid
getBracketPairsInRange(range: Range): BracketPair[] {
this.bracketsRequested = true;
this.updateCache();
return this.cache.value?.object.getBracketPairsInRange(range) || [];
return this.cache.value?.object.getBracketPairsInRange(range, false) || [];
}

getBracketPairsInRangeWithMinIndentation(range: Range): BracketPairWithMinIndentation[] {
this.bracketsRequested = true;
this.updateCache();
return this.cache.value?.object.getBracketPairsInRange(range, true) || [];
}
}

Expand All @@ -134,7 +146,7 @@ function createDisposableRef<T>(object: T, disposable?: IDisposable): IReference
};
}

class BracketPairColorizerImpl extends Disposable implements DecorationProvider, IBracketPairs {
class BracketPairColorizerImpl extends Disposable implements DecorationProvider {
private readonly didChangeDecorationsEmitter = new Emitter<void>();
private readonly colorProvider = new ColorProvider();

Expand Down Expand Up @@ -258,14 +270,15 @@ class BracketPairColorizerImpl extends Disposable implements DecorationProvider,
return this.getDecorationsInRange(new Range(1, 1, this.textModel.getLineCount(), 1), ownerId, filterOutValidation);
}

getBracketPairsInRange(range: Range): BracketPair[] {
const result = new Array<BracketPair>();
getBracketPairsInRange(range: Range, includeMinIndentation: boolean): BracketPairWithMinIndentation[] {
const result = new Array<BracketPairWithMinIndentation>();

const startLength = positionToLength(range.getStartPosition());
const endLength = positionToLength(range.getEndPosition());

const node = this.initialAstWithoutTokens || this.astWithTokens!;
collectBracketPairs(node, lengthZero, node.length, startLength, endLength, result);
const context = new CollectBracketPairsContext(result, includeMinIndentation, this.textModel);
collectBracketPairs(node, lengthZero, node.length, startLength, endLength, context);

return result;
}
Expand Down Expand Up @@ -318,16 +331,31 @@ function collectBrackets(node: AstNode, nodeOffsetStart: Length, nodeOffsetEnd:
}
}

function collectBracketPairs(node: AstNode, nodeOffset: Length, nodeOffsetEnd: Length, startOffset: Length, endOffset: Length, result: BracketPair[], level: number = 0) {
class CollectBracketPairsContext {
constructor(
public readonly result: BracketPairWithMinIndentation[],
public readonly includeMinIndentation: boolean,
public readonly textModel: ITextModel,
) {
}
}

function collectBracketPairs(node: AstNode, nodeOffset: Length, nodeOffsetEnd: Length, startOffset: Length, endOffset: Length, context: CollectBracketPairsContext, level: number = 0) {
if (node.kind === AstNodeKind.Pair) {
const openingBracketEnd = lengthAdd(nodeOffset, node.openingBracket.length);
result.push(new BracketPair(
let minIndentation = -1;
if (context.includeMinIndentation) {
minIndentation = node.computeMinIndentation(nodeOffset, context.textModel);
}

context.result.push(new BracketPairWithMinIndentation(
lengthsToRange(nodeOffset, nodeOffsetEnd),
lengthsToRange(nodeOffset, openingBracketEnd),
node.closingBracket
? lengthsToRange(lengthAdd(openingBracketEnd, node.child?.length || lengthZero), nodeOffsetEnd)
: undefined,
level
level,
minIndentation
));
level++;
}
Expand All @@ -338,7 +366,7 @@ function collectBracketPairs(node: AstNode, nodeOffset: Length, nodeOffsetEnd: L
curOffset = lengthAdd(curOffset, child.length);

if (lengthLessThanEqual(childOffset, endOffset) && lengthLessThanEqual(startOffset, curOffset)) {
collectBracketPairs(child, childOffset, curOffset, startOffset, endOffset, result, level);
collectBracketPairs(child, childOffset, curOffset, startOffset, endOffset, context, level);
}
}
}
Expand Down
22 changes: 20 additions & 2 deletions src/vs/editor/common/model/bracketPairColorizer/tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ class NonPeekableTextBufferTokenizer {
// limits the length of text tokens.
// If text tokens get too long, incremental updates will be slow
let lengthHeuristic = 0;
while (lengthHeuristic < 1000) {
while (true) {
const lineTokens = this.lineTokens!;
const tokenCount = lineTokens.getCount();

Expand Down Expand Up @@ -237,10 +237,27 @@ class NonPeekableTextBufferTokenizer {
this.line = this.lineTokens.getLineContent();
this.lineCharOffset = 0;

lengthHeuristic++;
lengthHeuristic += 33; // max 1000/33 = 30 lines
// This limits the amount of work to recompute min-indentation

if (lengthHeuristic > 1000) {
// only break (automatically) at the end of line.
break;
}
}

if (lengthHeuristic > 1500) {
// Eventually break regardless of the line length so that
// very long lines do not cause bad performance.
// This effective limits max indentation to 500, as
// indentation is not computed across multiple text nodes.
break;
}
}

// If a token contains some proper indentation, it also contains \n{INDENTATION+}(?!{INDENTATION}),
// unless the line is too long.
// Thus, the min indentation of the document is the minimum min indentation of every text node.
const length = lengthDiff(startLineIdx, startLineCharOffset, this.lineIdx, this.lineCharOffset);
return new Token(length, TokenKind.Text, -1, SmallImmutableSet.getEmpty(), new TextAstNode(length));
}
Expand Down Expand Up @@ -286,6 +303,7 @@ export class FastTokenizer implements Tokenizer {

if (regexp) {
regexp.lastIndex = 0;
// If a token contains indentation, it also contains \n{INDENTATION+}(?!{INDENTATION})
while ((match = regexp.exec(text)) !== null) {
const curOffset = match.index;
const value = match[0];
Expand Down
28 changes: 17 additions & 11 deletions src/vs/editor/common/model/textModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3085,7 +3085,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
options: model.BracketGuideOptions
): model.IndentGuide[][] {
const result: model.IndentGuide[][] = [];
const bracketPairs = this._bracketPairColorizer.getBracketPairsInRange(
const bracketPairs = this._bracketPairColorizer.getBracketPairsInRangeWithMinIndentation(
new Range(
startLineNumber,
1,
Expand Down Expand Up @@ -3148,7 +3148,8 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
}
const guideVisibleColumn = Math.min(
this.getVisibleColumnFromPosition(pair.openingBracketRange.getStartPosition()),
this.getVisibleColumnFromPosition(pair.closingBracketRange?.getStartPosition() ?? pair.range.getEndPosition())
this.getVisibleColumnFromPosition(pair.closingBracketRange?.getStartPosition() ?? pair.range.getEndPosition()),
pair.minVisibleColumnIndentation + 1
);
let renderHorizontalEndLineAtTheBottom = false;
if (pair.closingBracketRange) {
Expand Down Expand Up @@ -3205,6 +3206,20 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
// Going backwards, so the last guide potentially replaces others
for (let i = activeGuides.length - 1; i >= 0; i--) {
const line = activeGuides[i];

const isActive = options.highlightActive && activeBracketPairRange &&
line.bracketPair.range.equalsRange(activeBracketPairRange);

const className =
colorProvider.getInlineClassNameOfLevel(line.nestingLevel) +
(isActive ? ' ' + colorProvider.activeClassName : '');

if (isActive || options.includeInactive) {
if (line.renderHorizontalEndLineAtTheBottom && line.end.lineNumber === lineNumber + 1) {
nextGuides.push(new model.IndentGuide(line.guideVisibleColumn, className, null));
}
}

if (line.end.lineNumber <= lineNumber
|| line.start.lineNumber >= lineNumber) {
continue;
Expand All @@ -3215,18 +3230,9 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
}
lastVisibleColumnCount = line.guideVisibleColumn;

const isActive = options.highlightActive && activeBracketPairRange &&
line.bracketPair.range.equalsRange(activeBracketPairRange);

const className =
colorProvider.getInlineClassNameOfLevel(line.nestingLevel) +
(isActive ? ' ' + colorProvider.activeClassName : '');

if (isActive || options.includeInactive) {
guides.push(new model.IndentGuide(line.guideVisibleColumn, className, null));
if (line.renderHorizontalEndLineAtTheBottom && line.end.lineNumber === lineNumber + 1) {
nextGuides.push(new model.IndentGuide(line.guideVisibleColumn, className, null));
}
}
}

Expand Down

0 comments on commit 46e3f56

Please sign in to comment.