Skip to content

Commit 26b7ed5

Browse files
committed
Fixes microsoft#1772: Have bracket actions go to enclosing brackets when not on a bracket
1 parent 157a3ac commit 26b7ed5

5 files changed

Lines changed: 164 additions & 8 deletions

File tree

src/vs/editor/common/model.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,13 @@ export interface ITextModel {
881881
*/
882882
findNextBracket(position: IPosition): IFoundBracket | null;
883883

884+
/**
885+
* Find the enclosing brackets that contain `position`.
886+
* @param position The position at which to start the search.
887+
* @internal
888+
*/
889+
findEnclosingBrackets(position: IPosition): [Range, Range] | null;
890+
884891
/**
885892
* Given a `position`, if the position is on top or near a bracket,
886893
* find the matching bracket of that bracket and return the ranges of both brackets.

src/vs/editor/common/model/textModel.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2317,6 +2317,118 @@ export class TextModel extends Disposable implements model.ITextModel {
23172317
return null;
23182318
}
23192319

2320+
public findEnclosingBrackets(_position: IPosition): [Range, Range] | null {
2321+
const position = this.validatePosition(_position);
2322+
const lineCount = this.getLineCount();
2323+
2324+
let counts: number[] = [];
2325+
const resetCounts = (modeBrackets: RichEditBrackets | null) => {
2326+
counts = [];
2327+
for (let i = 0, len = modeBrackets ? modeBrackets.brackets.length : 0; i < len; i++) {
2328+
counts[i] = 0;
2329+
}
2330+
};
2331+
const searchInRange = (modeBrackets: RichEditBrackets, lineNumber: number, lineText: string, searchStartOffset: number, searchEndOffset: number): [Range, Range] | null => {
2332+
while (true) {
2333+
const r = BracketsUtils.findNextBracketInRange(modeBrackets.forwardRegex, lineNumber, lineText, searchStartOffset, searchEndOffset);
2334+
if (!r) {
2335+
break;
2336+
}
2337+
2338+
const hitText = lineText.substring(r.startColumn - 1, r.endColumn - 1).toLowerCase();
2339+
const bracket = modeBrackets.textIsBracket[hitText];
2340+
if (bracket) {
2341+
if (bracket.isOpen(hitText)) {
2342+
counts[bracket.index]++;
2343+
} else if (bracket.isClose(hitText)) {
2344+
counts[bracket.index]--;
2345+
}
2346+
2347+
if (counts[bracket.index] === -1) {
2348+
return this._matchFoundBracket(r, bracket, false);
2349+
}
2350+
}
2351+
2352+
searchStartOffset = r.endColumn - 1;
2353+
}
2354+
return null;
2355+
};
2356+
2357+
let languageId: LanguageId = -1;
2358+
let modeBrackets: RichEditBrackets | null = null;
2359+
for (let lineNumber = position.lineNumber; lineNumber <= lineCount; lineNumber++) {
2360+
const lineTokens = this._getLineTokens(lineNumber);
2361+
const tokenCount = lineTokens.getCount();
2362+
const lineText = this._buffer.getLineContent(lineNumber);
2363+
2364+
let tokenIndex = 0;
2365+
let searchStartOffset = 0;
2366+
let searchEndOffset = 0;
2367+
if (lineNumber === position.lineNumber) {
2368+
tokenIndex = lineTokens.findTokenIndexAtOffset(position.column - 1);
2369+
searchStartOffset = position.column - 1;
2370+
searchEndOffset = position.column - 1;
2371+
const tokenLanguageId = lineTokens.getLanguageId(tokenIndex);
2372+
if (languageId !== tokenLanguageId) {
2373+
languageId = tokenLanguageId;
2374+
modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId);
2375+
resetCounts(modeBrackets);
2376+
}
2377+
}
2378+
2379+
let prevSearchInToken = true;
2380+
for (; tokenIndex < tokenCount; tokenIndex++) {
2381+
const tokenLanguageId = lineTokens.getLanguageId(tokenIndex);
2382+
2383+
if (languageId !== tokenLanguageId) {
2384+
// language id change!
2385+
if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) {
2386+
const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset);
2387+
if (r) {
2388+
return r;
2389+
}
2390+
prevSearchInToken = false;
2391+
}
2392+
languageId = tokenLanguageId;
2393+
modeBrackets = LanguageConfigurationRegistry.getBracketsSupport(languageId);
2394+
resetCounts(modeBrackets);
2395+
}
2396+
2397+
const searchInToken = (!!modeBrackets && !ignoreBracketsInToken(lineTokens.getStandardTokenType(tokenIndex)));
2398+
if (searchInToken) {
2399+
// this token should be searched
2400+
if (prevSearchInToken) {
2401+
// the previous token should be searched, simply extend searchEndOffset
2402+
searchEndOffset = lineTokens.getEndOffset(tokenIndex);
2403+
} else {
2404+
// the previous token should not be searched
2405+
searchStartOffset = lineTokens.getStartOffset(tokenIndex);
2406+
searchEndOffset = lineTokens.getEndOffset(tokenIndex);
2407+
}
2408+
} else {
2409+
// this token should not be searched
2410+
if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) {
2411+
const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset);
2412+
if (r) {
2413+
return r;
2414+
}
2415+
}
2416+
}
2417+
2418+
prevSearchInToken = searchInToken;
2419+
}
2420+
2421+
if (modeBrackets && prevSearchInToken && searchStartOffset !== searchEndOffset) {
2422+
const r = searchInRange(modeBrackets, lineNumber, lineText, searchStartOffset, searchEndOffset);
2423+
if (r) {
2424+
return r;
2425+
}
2426+
}
2427+
}
2428+
2429+
return null;
2430+
}
2431+
23202432
private _toFoundBracket(modeBrackets: RichEditBrackets, r: Range): model.IFoundBracket | null {
23212433
if (!r) {
23222434
return null;

src/vs/editor/common/modes/supports/richEditBrackets.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,17 @@ export class RichEditBracket {
1717
_richEditBracketBrand: void;
1818

1919
readonly languageIdentifier: LanguageIdentifier;
20+
readonly index: number;
2021
readonly open: string[];
2122
readonly close: string[];
2223
readonly forwardRegex: RegExp;
2324
readonly reversedRegex: RegExp;
2425
private readonly _openSet: Set<string>;
2526
private readonly _closeSet: Set<string>;
2627

27-
constructor(languageIdentifier: LanguageIdentifier, open: string[], close: string[], forwardRegex: RegExp, reversedRegex: RegExp) {
28+
constructor(languageIdentifier: LanguageIdentifier, index: number, open: string[], close: string[], forwardRegex: RegExp, reversedRegex: RegExp) {
2829
this.languageIdentifier = languageIdentifier;
30+
this.index = index;
2931
this.open = open;
3032
this.close = close;
3133
this.forwardRegex = forwardRegex;
@@ -125,6 +127,7 @@ export class RichEditBrackets {
125127
this.brackets = brackets.map((b, index) => {
126128
return new RichEditBracket(
127129
languageIdentifier,
130+
index,
128131
b.open,
129132
b.close,
130133
getRegexForBracketPair(b.open, b.close, brackets, index),

src/vs/editor/contrib/bracketMatching/bracketMatching.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,16 @@ export class BracketMatchingController extends Disposable implements editorCommo
159159
newCursorPosition = brackets[0].getStartPosition();
160160
}
161161
} else {
162-
// find the next bracket if the position isn't on a matching bracket
163-
const nextBracket = model.findNextBracket(position);
164-
if (nextBracket && nextBracket.range) {
165-
newCursorPosition = nextBracket.range.getStartPosition();
162+
// find the enclosing brackets if the position isn't on a matching bracket
163+
const enclosingBrackets = model.findEnclosingBrackets(position);
164+
if (enclosingBrackets) {
165+
newCursorPosition = enclosingBrackets[0].getStartPosition();
166+
} else {
167+
// no enclosing brackets, try the very first next bracket
168+
const nextBracket = model.findNextBracket(position);
169+
if (nextBracket && nextBracket.range) {
170+
newCursorPosition = nextBracket.range.getStartPosition();
171+
}
166172
}
167173
}
168174

@@ -192,9 +198,12 @@ export class BracketMatchingController extends Disposable implements editorCommo
192198
let closeBracket: Position | null = null;
193199

194200
if (!brackets) {
195-
const nextBracket = model.findNextBracket(position);
196-
if (nextBracket && nextBracket.range) {
197-
brackets = model.matchBracket(nextBracket.range.getStartPosition());
201+
brackets = model.findEnclosingBrackets(position);
202+
if (!brackets) {
203+
const nextBracket = model.findNextBracket(position);
204+
if (nextBracket && nextBracket.range) {
205+
brackets = model.matchBracket(nextBracket.range.getStartPosition());
206+
}
198207
}
199208
}
200209

src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,31 @@ suite('bracket matching', () => {
143143
mode.dispose();
144144
});
145145

146+
test('issue #1772: jump to enclosing brackets', () => {
147+
const text = [
148+
'const x = {',
149+
' something: [0, 1, 2],',
150+
' another: true,',
151+
' somethingmore: [0, 2, 4]',
152+
'};',
153+
].join('\n');
154+
const mode = new BracketMode();
155+
const model = TextModel.createFromString(text, undefined, mode.getLanguageIdentifier());
156+
157+
withTestCodeEditor(null, { model: model }, (editor, cursor) => {
158+
const bracketMatchingController = editor.registerAndInstantiateContribution<BracketMatchingController>(BracketMatchingController.ID, BracketMatchingController);
159+
160+
editor.setPosition(new Position(3, 5));
161+
bracketMatchingController.jumpToBracket();
162+
assert.deepEqual(editor.getSelection(), new Selection(5, 1, 5, 1));
163+
164+
bracketMatchingController.dispose();
165+
});
166+
167+
model.dispose();
168+
mode.dispose();
169+
});
170+
146171
test('issue #45369: Select to Bracket with multicursor', () => {
147172
let mode = new BracketMode();
148173
let model = TextModel.createFromString('{ } { } { }', undefined, mode.getLanguageIdentifier());

0 commit comments

Comments
 (0)