From 26b7ed5245bc5d77db2948c357b1abca48e0bfde Mon Sep 17 00:00:00 2001 From: Alexandru Dima Date: Thu, 17 Oct 2019 12:25:07 +0200 Subject: [PATCH] Fixes #1772: Have bracket actions go to enclosing brackets when not on a bracket --- src/vs/editor/common/model.ts | 7 ++ src/vs/editor/common/model/textModel.ts | 112 ++++++++++++++++++ .../common/modes/supports/richEditBrackets.ts | 5 +- .../bracketMatching/bracketMatching.ts | 23 ++-- .../test/bracketMatching.test.ts | 25 ++++ 5 files changed, 164 insertions(+), 8 deletions(-) diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index a2a1df544df..c1ce6deaf8e 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -881,6 +881,13 @@ export interface ITextModel { */ findNextBracket(position: IPosition): IFoundBracket | null; + /** + * Find the enclosing brackets that contain `position`. + * @param position The position at which to start the search. + * @internal + */ + findEnclosingBrackets(position: IPosition): [Range, Range] | null; + /** * Given a `position`, if the position is on top or near a bracket, * find the matching bracket of that bracket and return the ranges of both brackets. diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index ad761e34230..d7b84cb84d0 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -2317,6 +2317,118 @@ export class TextModel extends Disposable implements model.ITextModel { return null; } + public findEnclosingBrackets(_position: IPosition): [Range, Range] | null { + const position = this.validatePosition(_position); + const lineCount = this.getLineCount(); + + let counts: number[] = []; + const resetCounts = (modeBrackets: RichEditBrackets | null) => { + counts = []; + for (let i = 0, len = modeBrackets ? modeBrackets.brackets.length : 0; i < len; i++) { + counts[i] = 0; + } + }; + const searchInRange = (modeBrackets: RichEditBrackets, lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): [Range, Range] | null => { + while (true) { + const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (!r) { + break; + } + + const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase(); + const bracket = modeBrackets.textIsBracket[hitText]; + if (bracket) { + if (bracket.isOpen(hitText)) { + counts[bracket.index]++; + } else if (bracket.isClose(hitText)) { + counts[bracket.index]--; + } + + if (counts[bracket.index] === -1) { + return this._matchFoundBracket(r, bracket, false); + } + } + + searchStartOffset = r.endColumn - 1; + } + return null; + }; + + let languageId: LanguageId = -1; + let modeBrackets: RichEditBrackets | null = null; + for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) { + const lineTokens = this._getLineTokens(lineNumber); + const tokenCount = lineTokens.getCount(); + const lineText = this._buffer.getLineContent(lineNumber); + + let tokenIndex = 0; + let searchStartOffset = 0; + let searchEndOffset = 0; + if (lineNumber === position.lineNumber) { + tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1); + searchStartOffset = position.column - 1; + searchEndOffset = position.column - 1; + const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); + if (languageId !== tokenLanguageId) { + languageId = tokenLanguageId; + modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId); + resetCounts(modeBrackets); + } + } + + let prevSearchInToken = true; + for (; tokenIndex < tokenCount; tokenIndex++) { + const tokenLanguageId = lineTokens.getLanguageId(tokenIndex); + + if (languageId !== tokenLanguageId) { + // language id change! + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return r; + } + prevSearchInToken = false; + } + languageId = tokenLanguageId; + modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId); + resetCounts(modeBrackets); + } + + const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex))); + if (searchInToken) { + // this token should be searched + if (prevSearchInToken) { + // the previous token should be searched, simply extend searchEndOffset + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } else { + // the previous token should not be searched + searchStartOffset = lineTokens.getStartOffset(tokenIndex); + searchEndOffset = lineTokens.getEndOffset(tokenIndex); + } + } else { + // this token should not be searched + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return r; + } + } + } + + prevSearchInToken = searchInToken; + } + + if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) { + const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset); + if (r) { + return r; + } + } + } + + return null; + } + private _toFoundBracket(modeBrackets: RichEditBrackets, r: Range): model.IFoundBracket | null { if (!r) { return null; diff --git a/src/vs/editor/common/modes/supports/richEditBrackets.ts b/src/vs/editor/common/modes/supports/richEditBrackets.ts index 49c7cdcbc54..ae10537c82e 100644 --- a/src/vs/editor/common/modes/supports/richEditBrackets.ts +++ b/src/vs/editor/common/modes/supports/richEditBrackets.ts @@ -17,6 +17,7 @@ export class RichEditBracket { _richEditBracketBrand: void; readonly languageIdentifier: LanguageIdentifier; + readonly index: number; readonly open: string[]; readonly close: string[]; readonly forwardRegex: RegExp; @@ -24,8 +25,9 @@ export class RichEditBracket { private readonly _openSet: Set; private readonly _closeSet: Set; - constructor(languageIdentifier: LanguageIdentifier, open: string[], close: string[], forwardRegex: RegExp, reversedRegex: RegExp) { + constructor(languageIdentifier: LanguageIdentifier, index: number, open: string[], close: string[], forwardRegex: RegExp, reversedRegex: RegExp) { this.languageIdentifier = languageIdentifier; + this.index = index; this.open = open; this.close = close; this.forwardRegex = forwardRegex; @@ -125,6 +127,7 @@ export class RichEditBrackets { this.brackets = brackets.map((b, index) => { return new RichEditBracket( languageIdentifier, + index, b.open, b.close, getRegexForBracketPair(b.open, b.close, brackets, index), diff --git a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts index 9f21abbf9cc..c3a6bf743f8 100644 --- a/src/vs/editor/contrib/bracketMatching/bracketMatching.ts +++ b/src/vs/editor/contrib/bracketMatching/bracketMatching.ts @@ -159,10 +159,16 @@ export class BracketMatchingController extends Disposable implements editorCommo newCursorPosition = brackets[0].getStartPosition(); } } else { - // find the next bracket if the position isn't on a matching bracket - const nextBracket = model.findNextBracket(position); - if (nextBracket && nextBracket.range) { - newCursorPosition = nextBracket.range.getStartPosition(); + // find the enclosing brackets if the position isn't on a matching bracket + const enclosingBrackets = model.findEnclosingBrackets(position); + if (enclosingBrackets) { + newCursorPosition = enclosingBrackets[0].getStartPosition(); + } else { + // no enclosing brackets, try the very first next bracket + const nextBracket = model.findNextBracket(position); + if (nextBracket && nextBracket.range) { + newCursorPosition = nextBracket.range.getStartPosition(); + } } } @@ -192,9 +198,12 @@ export class BracketMatchingController extends Disposable implements editorCommo let closeBracket: Position | null = null; if (!brackets) { - const nextBracket = model.findNextBracket(position); - if (nextBracket && nextBracket.range) { - brackets = model.matchBracket(nextBracket.range.getStartPosition()); + brackets = model.findEnclosingBrackets(position); + if (!brackets) { + const nextBracket = model.findNextBracket(position); + if (nextBracket && nextBracket.range) { + brackets = model.matchBracket(nextBracket.range.getStartPosition()); + } } } diff --git a/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts b/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts index 26567b958fe..f01bd101eea 100644 --- a/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts +++ b/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts @@ -143,6 +143,31 @@ suite('bracket matching', () => { mode.dispose(); }); + test('issue #1772: jump to enclosing brackets', () => { + const text = [ + 'const x = {', + ' something: [0, 1, 2],', + ' another: true,', + ' somethingmore: [0, 2, 4]', + '};', + ].join('\n'); + const mode = new BracketMode(); + const model = TextModel.createFromString(text, undefined, mode.getLanguageIdentifier()); + + withTestCodeEditor(null, { model: model }, (editor, cursor) => { + const bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController); + + editor.setPosition(new Position(3, 5)); + bracketMatchingController.jumpToBracket(); + assert.deepEqual(editor.getSelection(), new Selection(5, 1, 5, 1)); + + bracketMatchingController.dispose(); + }); + + model.dispose(); + mode.dispose(); + }); + test('issue #45369: Select to Bracket with multicursor', () => { let mode = new BracketMode(); let model = TextModel.createFromString('{ } { } { }', undefined, mode.getLanguageIdentifier()); -- GitLab