bracketMatching.ts 10.9 KB
Newer Older
1 2 3 4 5 6 7
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

'use strict';

8
import 'vs/css!./bracketMatching';
9 10 11 12 13
import * as nls from 'vs/nls';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Disposable } from 'vs/base/common/lifecycle';
import { Range } from 'vs/editor/common/core/range';
import { Position } from 'vs/editor/common/core/position';
14
import { Selection } from 'vs/editor/common/core/selection';
15
import { RunOnceScheduler } from 'vs/base/common/async';
16
import * as editorCommon from 'vs/editor/common/editorCommon';
T
Tomás Oliveira 已提交
17
import { EditorAction, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
18
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
F
francis-andrade 已提交
19 20
import { registerThemingParticipant, themeColorFromId } from 'vs/platform/theme/common/themeService';
import { editorBracketMatchBackground, editorBracketMatchBorder } from 'vs/editor/common/view/editorColorRegistry';
A
Alex Dima 已提交
21
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
22
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
A
Alex Dima 已提交
23
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
24
import { TrackedRangeStickiness, IModelDeltaDecoration, OverviewRulerLane } from 'vs/editor/common/model';
F
francis-andrade 已提交
25

A
Alex Dima 已提交
26
const overviewRulerBracketMatchForeground = registerColor('editorOverviewRuler.bracketMatchForeground', { dark: '#A0A0A0', light: '#A0A0A0', hc: '#A0A0A0' }, nls.localize('overviewRulerBracketMatchForeground', 'Overview ruler marker color for matching brackets.'));
F
francis-andrade 已提交
27

A
Afonso Pinto 已提交
28
class JumpToBracketAction extends EditorAction {
29 30 31 32 33 34 35
	constructor() {
		super({
			id: 'editor.action.jumpToBracket',
			label: nls.localize('smartSelect.jumpBracket', "Go to Bracket"),
			alias: 'Go to Bracket',
			precondition: null,
			kbOpts: {
36
				kbExpr: EditorContextKeys.textFocus,
37 38 39 40 41
				primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKSLASH
			}
		});
	}

42
	public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
43 44 45 46 47 48 49 50
		let controller = BracketMatchingController.get(editor);
		if (!controller) {
			return;
		}
		controller.jumpToBracket();
	}
}

A
Afonso Pinto 已提交
51 52 53 54 55 56
class SelectToBracketAction extends EditorAction {
	constructor() {
		super({
			id: 'editor.action.selectToBracket',
			label: nls.localize('smartSelect.selectToBracket', "Select to Bracket"),
			alias: 'Select to Bracket',
57
			precondition: null
A
Afonso Pinto 已提交
58 59 60 61 62 63 64 65 66 67 68 69
		});
	}

	public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
		let controller = BracketMatchingController.get(editor);
		if (!controller) {
			return;
		}
		controller.selectToBracket();
	}
}

70 71 72 73 74 75 76 77 78 79 80 81 82
type Brackets = [Range, Range];

class BracketsData {
	public readonly position: Position;
	public readonly brackets: Brackets;

	constructor(position: Position, brackets: Brackets) {
		this.position = position;
		this.brackets = brackets;
	}
}

export class BracketMatchingController extends Disposable implements editorCommon.IEditorContribution {
83
	private static readonly ID = 'editor.contrib.bracketMatchingController';
84

85
	public static get(editor: ICodeEditor): BracketMatchingController {
86 87 88
		return editor.getContribution<BracketMatchingController>(BracketMatchingController.ID);
	}

89
	private readonly _editor: ICodeEditor;
90 91 92 93

	private _lastBracketsData: BracketsData[];
	private _lastVersionId: number;
	private _decorations: string[];
94
	private _updateBracketsSoon: RunOnceScheduler;
95
	private _matchBrackets: boolean;
96

97
	constructor(
98
		editor: ICodeEditor
99
	) {
100 101 102 103 104
		super();
		this._editor = editor;
		this._lastBracketsData = [];
		this._lastVersionId = 0;
		this._decorations = [];
105
		this._updateBracketsSoon = this._register(new RunOnceScheduler(() => this._updateBrackets(), 50));
106
		this._matchBrackets = this._editor.getConfiguration().contribInfo.matchBrackets;
107

108
		this._updateBracketsSoon.schedule();
109 110 111 112 113 114 115 116 117 118
		this._register(editor.onDidChangeCursorPosition((e) => {

			if (!this._matchBrackets) {
				// Early exit if nothing needs to be done!
				// Leave some form of early exit check here if you wish to continue being a cursor position change listener ;)
				return;
			}

			this._updateBracketsSoon.schedule();
		}));
119
		this._register(editor.onDidChangeModel((e) => { this._decorations = []; this._updateBracketsSoon.schedule(); }));
120 121 122 123
		this._register(editor.onDidChangeModelLanguageConfiguration((e) => {
			this._lastBracketsData = [];
			this._updateBracketsSoon.schedule();
		}));
124 125 126 127 128 129 130 131
		this._register(editor.onDidChangeConfiguration((e) => {
			this._matchBrackets = this._editor.getConfiguration().contribInfo.matchBrackets;
			if (!this._matchBrackets && this._decorations.length > 0) {
				// Remove existing decorations if bracket matching is off
				this._decorations = this._editor.deltaDecorations(this._decorations, []);
			}
			this._updateBracketsSoon.schedule();
		}));
132 133 134 135 136 137 138 139 140 141 142 143
	}

	public getId(): string {
		return BracketMatchingController.ID;
	}

	public jumpToBracket(): void {
		const model = this._editor.getModel();
		if (!model) {
			return;
		}

144 145
		let newSelections = this._editor.getSelections().map(selection => {
			const position = selection.getStartPosition();
146

147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
			// find matching brackets if position is on a bracket
			const brackets = model.matchBracket(position);
			let newCursorPosition: Position = null;
			if (brackets) {
				if (brackets[0].containsPosition(position)) {
					newCursorPosition = brackets[1].getStartPosition();
				} else if (brackets[1].containsPosition(position)) {
					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();
				}
			}
163

164 165 166 167 168
			if (newCursorPosition) {
				return new Selection(newCursorPosition.lineNumber, newCursorPosition.column, newCursorPosition.lineNumber, newCursorPosition.column);
			}
			return new Selection(position.lineNumber, position.column, position.lineNumber, position.column);
		});
169

170
		this._editor.setSelections(newSelections);
171
		this._editor.revealRange(newSelections[0]);
172 173
	}

A
Afonso Pinto 已提交
174
	public selectToBracket(): void {
D
DavidPortoUP 已提交
175 176 177 178
		const model = this._editor.getModel();
		if (!model) {
			return;
		}
A
Afonso Pinto 已提交
179 180 181 182
		const selection = this._editor.getSelection();
		if (!selection.isEmpty()) {
			return;
		}
D
DavidPortoUP 已提交
183

A
Afonso Pinto 已提交
184
		const position = selection.getStartPosition();
D
DavidPortoUP 已提交
185

A
Afonso Pinto 已提交
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
		let brackets = model.matchBracket(position);

		let openBracket: Position = null;
		let closeBracket: Position = null;

		if (!brackets) {
			const nextBracket = model.findNextBracket(position);
			if (nextBracket && nextBracket.range) {
				brackets = model.matchBracket(nextBracket.range.getStartPosition());
			}
		}

		if (brackets) {
			if (brackets[0].startLineNumber === brackets[1].startLineNumber) {
				openBracket = brackets[1].startColumn < brackets[0].startColumn ?
					brackets[1].getStartPosition() : brackets[0].getStartPosition();
				closeBracket = brackets[1].startColumn < brackets[0].startColumn ?
					brackets[0].getEndPosition() : brackets[1].getEndPosition();
			} else {
				openBracket = brackets[1].startLineNumber < brackets[0].startLineNumber ?
					brackets[1].getStartPosition() : brackets[0].getStartPosition();
				closeBracket = brackets[1].startLineNumber < brackets[0].startLineNumber ?
					brackets[0].getEndPosition() : brackets[1].getEndPosition();
D
DavidPortoUP 已提交
209 210
			}
		}
A
Afonso Pinto 已提交
211

D
DavidPortoUP 已提交
212
		if (openBracket && closeBracket) {
A
Afonso Pinto 已提交
213
			this._editor.setSelection(new Range(openBracket.lineNumber, openBracket.column, closeBracket.lineNumber, closeBracket.column));
D
DavidPortoUP 已提交
214 215 216 217
		}
	}


218
	private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({
219
		stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
F
francis-andrade 已提交
220 221
		className: 'bracket-match',
		overviewRuler: {
A
Alex Dima 已提交
222 223
			color: themeColorFromId(overviewRulerBracketMatchForeground),
			darkColor: themeColorFromId(overviewRulerBracketMatchForeground),
224
			position: OverviewRulerLane.Center
F
francis-andrade 已提交
225
		}
226
	});
227 228

	private _updateBrackets(): void {
229 230 231
		if (!this._matchBrackets) {
			return;
		}
232 233
		this._recomputeBrackets();

234
		let newDecorations: IModelDeltaDecoration[] = [], newDecorationsLen = 0;
235 236
		for (let i = 0, len = this._lastBracketsData.length; i < len; i++) {
			let brackets = this._lastBracketsData[i].brackets;
237
			if (brackets) {
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
				newDecorations[newDecorationsLen++] = { range: brackets[0], options: BracketMatchingController._DECORATION_OPTIONS };
				newDecorations[newDecorationsLen++] = { range: brackets[1], options: BracketMatchingController._DECORATION_OPTIONS };
			}
		}

		this._decorations = this._editor.deltaDecorations(this._decorations, newDecorations);
	}

	private _recomputeBrackets(): void {
		const model = this._editor.getModel();
		if (!model) {
			// no model => no brackets!
			this._lastBracketsData = [];
			this._lastVersionId = 0;
			return;
		}

		const versionId = model.getVersionId();
		let previousData: BracketsData[] = [];
		if (this._lastVersionId === versionId) {
			// use the previous data only if the model is at the same version id
			previousData = this._lastBracketsData;
		}

		const selections = this._editor.getSelections();

		let positions: Position[] = [], positionsLen = 0;
		for (let i = 0, len = selections.length; i < len; i++) {
			let selection = selections[i];

			if (selection.isEmpty()) {
				// will bracket match a cursor only if the selection is collapsed
				positions[positionsLen++] = selection.getStartPosition();
			}
		}

		// sort positions for `previousData` cache hits
		if (positions.length > 1) {
			positions.sort(Position.compare);
		}

		let newData: BracketsData[] = [], newDataLen = 0;
		let previousIndex = 0, previousLen = previousData.length;
		for (let i = 0, len = positions.length; i < len; i++) {
			let position = positions[i];

			while (previousIndex < previousLen && previousData[previousIndex].position.isBefore(position)) {
				previousIndex++;
			}

			if (previousIndex < previousLen && previousData[previousIndex].position.equals(position)) {
				newData[newDataLen++] = previousData[previousIndex];
			} else {
				let brackets = model.matchBracket(position);
				newData[newDataLen++] = new BracketsData(position, brackets);
			}
		}

		this._lastBracketsData = newData;
		this._lastVersionId = versionId;
	}
}
300

301
registerEditorContribution(BracketMatchingController);
A
Afonso Pinto 已提交
302 303
registerEditorAction(SelectToBracketAction);
registerEditorAction(JumpToBracketAction);
304 305 306 307 308 309 310 311 312
registerThemingParticipant((theme, collector) => {
	let bracketMatchBackground = theme.getColor(editorBracketMatchBackground);
	if (bracketMatchBackground) {
		collector.addRule(`.monaco-editor .bracket-match { background-color: ${bracketMatchBackground}; }`);
	}
	let bracketMatchBorder = theme.getColor(editorBracketMatchBorder);
	if (bracketMatchBorder) {
		collector.addRule(`.monaco-editor .bracket-match { border: 1px solid ${bracketMatchBorder}; }`);
	}
313
});