multicursor.ts 32.0 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

A
Alex Dima 已提交
7
import * as nls from 'vs/nls';
8
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
9 10
import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes';
import { RunOnceScheduler } from 'vs/base/common/async';
11
import { ScrollType, IEditorContribution } from 'vs/editor/common/editorCommon';
A
Alex Dima 已提交
12
import { FindMatch, TrackedRangeStickiness, OverviewRulerLane, ITextModel } from 'vs/editor/common/model';
13
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
14
import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction } from 'vs/editor/browser/editorExtensions';
15
import { Range } from 'vs/editor/common/core/range';
16
import { Selection } from 'vs/editor/common/core/selection';
17
import { CursorChangeReason, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
18
import { CursorMoveCommands } from 'vs/editor/common/controller/cursorMoveCommands';
19
import { RevealTarget } from 'vs/editor/common/controller/cursorCommon';
20 21
import { Constants } from 'vs/editor/common/core/uint';
import { DocumentHighlightProviderRegistry } from 'vs/editor/common/modes';
22
import { CommonFindController } from 'vs/editor/contrib/find/findController';
A
Alex Dima 已提交
23
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
24 25
import { overviewRulerSelectionHighlightForeground } from 'vs/platform/theme/common/colorRegistry';
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
26
import { INewFindReplaceState, FindOptionOverride } from 'vs/editor/contrib/find/findState';
27
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
28
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
29
import { MenuId } from 'vs/platform/actions/common/actions';
E
Erich Gamma 已提交
30

A
Alex Dima 已提交
31
export class InsertCursorAbove extends EditorAction {
M
mgquan@myseneca.ca 已提交
32

A
Alex Dima 已提交
33
	constructor() {
34 35 36 37 38 39
		super({
			id: 'editor.action.insertCursorAbove',
			label: nls.localize('mutlicursor.insertAbove', "Add Cursor Above"),
			alias: 'Add Cursor Above',
			precondition: null,
			kbOpts: {
40
				kbExpr: EditorContextKeys.editorTextFocus,
41 42 43 44
				primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.UpArrow,
				linux: {
					primary: KeyMod.Shift | KeyMod.Alt | KeyCode.UpArrow,
					secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow]
A
Alex Dima 已提交
45
				},
46
				weight: KeybindingWeight.EditorContrib
47 48 49 50 51 52
			},
			menubarOpts: {
				menuId: MenuId.MenubarSelectionMenu,
				group: '3_multi',
				title: nls.localize({ key: 'miInsertCursorAbove', comment: ['&& denotes a mnemonic'] }, "&&Add Cursor Above"),
				order: 2
A
Alex Dima 已提交
53
			}
54
		});
E
Erich Gamma 已提交
55
	}
A
Alex Dima 已提交
56

57
	public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void {
58
		const useLogicalLine = (args && args.logicalLine === true);
A
Alex Dima 已提交
59 60 61 62 63 64 65 66 67 68 69
		const cursors = editor._getCursors();
		const context = cursors.context;

		if (context.config.readOnly) {
			return;
		}

		context.model.pushStackElement();
		cursors.setStates(
			args.source,
			CursorChangeReason.Explicit,
70
			CursorMoveCommands.addCursorUp(context, cursors.getAll(), useLogicalLine)
A
Alex Dima 已提交
71
		);
72
		cursors.reveal(true, RevealTarget.TopMost, ScrollType.Smooth);
A
Alex Dima 已提交
73
	}
E
Erich Gamma 已提交
74 75
}

A
Alex Dima 已提交
76
export class InsertCursorBelow extends EditorAction {
M
mgquan@myseneca.ca 已提交
77

A
Alex Dima 已提交
78
	constructor() {
79 80 81 82 83 84
		super({
			id: 'editor.action.insertCursorBelow',
			label: nls.localize('mutlicursor.insertBelow', "Add Cursor Below"),
			alias: 'Add Cursor Below',
			precondition: null,
			kbOpts: {
85
				kbExpr: EditorContextKeys.editorTextFocus,
86 87 88 89
				primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.DownArrow,
				linux: {
					primary: KeyMod.Shift | KeyMod.Alt | KeyCode.DownArrow,
					secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow]
A
Alex Dima 已提交
90
				},
91
				weight: KeybindingWeight.EditorContrib
92 93 94 95 96 97
			},
			menubarOpts: {
				menuId: MenuId.MenubarSelectionMenu,
				group: '3_multi',
				title: nls.localize({ key: 'miInsertCursorBelow', comment: ['&& denotes a mnemonic'] }, "A&&dd Cursor Below"),
				order: 3
A
Alex Dima 已提交
98
			}
99
		});
E
Erich Gamma 已提交
100
	}
A
Alex Dima 已提交
101

102
	public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void {
103
		const useLogicalLine = (args && args.logicalLine === true);
A
Alex Dima 已提交
104 105 106 107 108 109 110 111 112 113 114
		const cursors = editor._getCursors();
		const context = cursors.context;

		if (context.config.readOnly) {
			return;
		}

		context.model.pushStackElement();
		cursors.setStates(
			args.source,
			CursorChangeReason.Explicit,
115
			CursorMoveCommands.addCursorDown(context, cursors.getAll(), useLogicalLine)
A
Alex Dima 已提交
116
		);
117
		cursors.reveal(true, RevealTarget.BottomMost, ScrollType.Smooth);
A
Alex Dima 已提交
118
	}
E
Erich Gamma 已提交
119 120
}

A
Alex Dima 已提交
121
class InsertCursorAtEndOfEachLineSelected extends EditorAction {
122

A
Alex Dima 已提交
123
	constructor() {
124 125
		super({
			id: 'editor.action.insertCursorAtEndOfEachLineSelected',
A
Alex Dima 已提交
126 127
			label: nls.localize('mutlicursor.insertAtEndOfEachLineSelected', "Add Cursors to Line Ends"),
			alias: 'Add Cursors to Line Ends',
128 129
			precondition: null,
			kbOpts: {
130
				kbExpr: EditorContextKeys.editorTextFocus,
A
Alex Dima 已提交
131
				primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_I,
132
				weight: KeybindingWeight.EditorContrib
133 134 135 136 137 138
			},
			menubarOpts: {
				menuId: MenuId.MenubarSelectionMenu,
				group: '3_multi',
				title: nls.localize({ key: 'miInsertCursorAtEndOfEachLineSelected', comment: ['&& denotes a mnemonic'] }, "Add C&&ursors to Line Ends"),
				order: 4
139 140
			}
		});
141 142
	}

A
Alex Dima 已提交
143
	private getCursorsForSelection(selection: Selection, model: ITextModel, result: Selection[]): void {
A
Alex Dima 已提交
144
		if (selection.isEmpty()) {
A
Alex Dima 已提交
145
			return;
A
Alex Dima 已提交
146 147
		}

A
Alex Dima 已提交
148 149
		for (let i = selection.startLineNumber; i < selection.endLineNumber; i++) {
			let currentLineMaxColumn = model.getLineMaxColumn(i);
A
Alex Dima 已提交
150
			result.push(new Selection(i, currentLineMaxColumn, i, currentLineMaxColumn));
A
Alex Dima 已提交
151 152
		}
		if (selection.endColumn > 1) {
A
Alex Dima 已提交
153
			result.push(new Selection(selection.endLineNumber, selection.endColumn, selection.endLineNumber, selection.endColumn));
154
		}
155 156
	}

157
	public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
A
Alex Dima 已提交
158 159 160 161
		const model = editor.getModel();
		const selections = editor.getSelections();
		let newSelections: Selection[] = [];
		selections.forEach((sel) => this.getCursorsForSelection(sel, model, newSelections));
162 163 164 165

		if (newSelections.length > 0) {
			editor.setSelections(newSelections);
		}
166 167
	}
}
168

169 170 171 172 173 174
export class MultiCursorSessionResult {
	constructor(
		public readonly selections: Selection[],
		public readonly revealRange: Range,
		public readonly revealScrollType: ScrollType
	) { }
175 176
}

177
export class MultiCursorSession {
178

179
	public static create(editor: ICodeEditor, findController: CommonFindController): MultiCursorSession {
180
		const findState = findController.getState();
181

182 183 184 185
		// Find widget owns entirely what we search for if:
		//  - focus is not in the editor (i.e. it is in the find widget)
		//  - and the search widget is visible
		//  - and the search string is non-empty
A
Alex Dima 已提交
186
		if (!editor.hasTextFocus() && findState.isRevealed && findState.searchString.length > 0) {
187 188 189
			// Find widget owns what is searched for
			return new MultiCursorSession(editor, findController, false, findState.searchString, findState.wholeWord, findState.matchCase, null);
		}
190

191 192 193 194 195 196 197 198 199 200 201 202 203 204
		// Otherwise, the selection gives the search text, and the find widget gives the search settings
		// The exception is the find state disassociation case: when beginning with a single, collapsed selection
		let isDisconnectedFromFindController = false;
		let wholeWord: boolean;
		let matchCase: boolean;
		const selections = editor.getSelections();
		if (selections.length === 1 && selections[0].isEmpty()) {
			isDisconnectedFromFindController = true;
			wholeWord = true;
			matchCase = true;
		} else {
			wholeWord = findState.wholeWord;
			matchCase = findState.matchCase;
		}
205

206 207
		// Selection owns what is searched for
		const s = editor.getSelection();
208

209 210
		let searchText: string;
		let currentMatch: Selection = null;
211

212 213 214 215 216 217 218 219 220 221 222
		if (s.isEmpty()) {
			// selection is empty => expand to current word
			const word = editor.getModel().getWordAtPosition(s.getStartPosition());
			if (!word) {
				return null;
			}
			searchText = word.word;
			currentMatch = new Selection(s.startLineNumber, word.startColumn, s.startLineNumber, word.endColumn);
		} else {
			searchText = editor.getModel().getValueInRange(s).replace(/\r\n/g, '\n');
		}
223

224
		return new MultiCursorSession(editor, findController, isDisconnectedFromFindController, searchText, wholeWord, matchCase, currentMatch);
225 226
	}

227
	constructor(
228
		private readonly _editor: ICodeEditor,
A
Alex Dima 已提交
229 230
		public readonly findController: CommonFindController,
		public readonly isDisconnectedFromFindController: boolean,
231 232 233 234 235
		public readonly searchText: string,
		public readonly wholeWord: boolean,
		public readonly matchCase: boolean,
		public currentMatch: Selection
	) { }
236

237 238 239 240 241 242 243 244
	public addSelectionToNextFindMatch(): MultiCursorSessionResult {
		const nextMatch = this._getNextMatch();
		if (!nextMatch) {
			return null;
		}

		const allSelections = this._editor.getSelections();
		return new MultiCursorSessionResult(allSelections.concat(nextMatch), nextMatch, ScrollType.Smooth);
245 246
	}

247 248 249 250
	public moveSelectionToNextFindMatch(): MultiCursorSessionResult {
		const nextMatch = this._getNextMatch();
		if (!nextMatch) {
			return null;
251
		}
252 253 254

		const allSelections = this._editor.getSelections();
		return new MultiCursorSessionResult(allSelections.slice(0, allSelections.length - 1).concat(nextMatch), nextMatch, ScrollType.Smooth);
255 256
	}

257 258 259 260 261 262 263
	private _getNextMatch(): Selection {
		if (this.currentMatch) {
			const result = this.currentMatch;
			this.currentMatch = null;
			return result;
		}

A
Alex Dima 已提交
264
		this.findController.highlightFindOptions();
265

266 267 268
		const allSelections = this._editor.getSelections();
		const lastAddedSelection = allSelections[allSelections.length - 1];
		const nextMatch = this._editor.getModel().findNextMatch(this.searchText, lastAddedSelection.getEndPosition(), false, this.matchCase, this.wholeWord ? this._editor.getConfiguration().wordSeparators : null, false);
269

270 271
		if (!nextMatch) {
			return null;
272
		}
273
		return new Selection(nextMatch.range.startLineNumber, nextMatch.range.startColumn, nextMatch.range.endLineNumber, nextMatch.range.endColumn);
274 275
	}

276 277 278 279 280
	public addSelectionToPreviousFindMatch(): MultiCursorSessionResult {
		const previousMatch = this._getPreviousMatch();
		if (!previousMatch) {
			return null;
		}
281

282 283 284
		const allSelections = this._editor.getSelections();
		return new MultiCursorSessionResult(allSelections.concat(previousMatch), previousMatch, ScrollType.Smooth);
	}
285

286 287 288 289 290
	public moveSelectionToPreviousFindMatch(): MultiCursorSessionResult {
		const previousMatch = this._getPreviousMatch();
		if (!previousMatch) {
			return null;
		}
291

292 293 294
		const allSelections = this._editor.getSelections();
		return new MultiCursorSessionResult(allSelections.slice(0, allSelections.length - 1).concat(previousMatch), previousMatch, ScrollType.Smooth);
	}
295

296 297 298 299 300
	private _getPreviousMatch(): Selection {
		if (this.currentMatch) {
			const result = this.currentMatch;
			this.currentMatch = null;
			return result;
301 302
		}

A
Alex Dima 已提交
303
		this.findController.highlightFindOptions();
304

305 306 307
		const allSelections = this._editor.getSelections();
		const lastAddedSelection = allSelections[allSelections.length - 1];
		const previousMatch = this._editor.getModel().findPreviousMatch(this.searchText, lastAddedSelection.getStartPosition(), false, this.matchCase, this.wholeWord ? this._editor.getConfiguration().wordSeparators : null, false);
308

309
		if (!previousMatch) {
310 311
			return null;
		}
312 313 314 315
		return new Selection(previousMatch.range.startLineNumber, previousMatch.range.startColumn, previousMatch.range.endLineNumber, previousMatch.range.endColumn);
	}

	public selectAll(): FindMatch[] {
A
Alex Dima 已提交
316
		this.findController.highlightFindOptions();
317

318 319 320
		return this._editor.getModel().findMatches(this.searchText, true, false, this.matchCase, this.wholeWord ? this._editor.getConfiguration().wordSeparators : null, false, Constants.MAX_SAFE_SMALL_INTEGER);
	}
}
321

322
export class MultiCursorSelectionController extends Disposable implements IEditorContribution {
323

324
	private static readonly ID = 'editor.contrib.multiCursorController';
325

326
	private readonly _editor: ICodeEditor;
327 328 329 330
	private _ignoreSelectionChange: boolean;
	private _session: MultiCursorSession;
	private _sessionDispose: IDisposable[];

331
	public static get(editor: ICodeEditor): MultiCursorSelectionController {
332
		return editor.getContribution<MultiCursorSelectionController>(MultiCursorSelectionController.ID);
333 334
	}

335
	constructor(editor: ICodeEditor) {
336 337 338 339 340 341 342 343 344 345 346
		super();
		this._editor = editor;
		this._ignoreSelectionChange = false;
		this._session = null;
		this._sessionDispose = [];
	}

	public dispose(): void {
		this._endSession();
		super.dispose();
	}
347

348 349 350
	public getId(): string {
		return MultiCursorSelectionController.ID;
	}
351

352 353 354 355 356 357 358
	private _beginSessionIfNeeded(findController: CommonFindController): void {
		if (!this._session) {
			// Create a new session
			const session = MultiCursorSession.create(this._editor, findController);
			if (!session) {
				return;
			}
359

360
			this._session = session;
A
Alex Dima 已提交
361 362 363 364 365 366 367 368 369

			const newState: INewFindReplaceState = { searchString: this._session.searchText };
			if (this._session.isDisconnectedFromFindController) {
				newState.wholeWordOverride = FindOptionOverride.True;
				newState.matchCaseOverride = FindOptionOverride.True;
				newState.isRegexOverride = FindOptionOverride.False;
			}
			findController.getState().change(newState, false);

370 371 372 373 374 375 376 377 378
			this._sessionDispose = [
				this._editor.onDidChangeCursorSelection((e) => {
					if (this._ignoreSelectionChange) {
						return;
					}
					this._endSession();
				}),
				this._editor.onDidBlurEditorText(() => {
					this._endSession();
A
Alex Dima 已提交
379
				}),
R
rebornix 已提交
380
				findController.getState().onFindReplaceStateChange((e) => {
A
Alex Dima 已提交
381 382 383
					if (e.matchCase || e.wholeWord) {
						this._endSession();
					}
384 385
				})
			];
386
		}
387
	}
388

389 390
	private _endSession(): void {
		this._sessionDispose = dispose(this._sessionDispose);
A
Alex Dima 已提交
391 392 393 394 395 396 397 398 399
		if (this._session && this._session.isDisconnectedFromFindController) {
			const newState: INewFindReplaceState = {
				wholeWordOverride: FindOptionOverride.NotSet,
				matchCaseOverride: FindOptionOverride.NotSet,
				isRegexOverride: FindOptionOverride.NotSet,
			};
			this._session.findController.getState().change(newState, false);
		}
		this._session = null;
400 401 402 403 404 405
	}

	private _setSelections(selections: Selection[]): void {
		this._ignoreSelectionChange = true;
		this._editor.setSelections(selections);
		this._ignoreSelectionChange = false;
406 407
	}

A
Alex Dima 已提交
408
	private _expandEmptyToWord(model: ITextModel, selection: Selection): Selection {
409 410 411 412 413 414 415 416 417
		if (!selection.isEmpty()) {
			return selection;
		}
		const word = model.getWordAtPosition(selection.getStartPosition());
		if (!word) {
			return selection;
		}
		return new Selection(selection.startLineNumber, word.startColumn, selection.startLineNumber, word.endColumn);
	}
418

419 420
	private _applySessionResult(result: MultiCursorSessionResult): void {
		if (!result) {
421 422
			return;
		}
423 424 425 426 427
		this._setSelections(result.selections);
		if (result.revealRange) {
			this._editor.revealRangeInCenterIfOutsideViewport(result.revealRange, result.revealScrollType);
		}
	}
428

429
	public getSession(findController: CommonFindController): MultiCursorSession {
A
Alex Dima 已提交
430
		return this._session;
431
	}
432

433 434 435 436 437 438 439
	public addSelectionToNextFindMatch(findController: CommonFindController): void {
		if (!this._session) {
			// If there are multiple cursors, handle the case where they do not all select the same text.
			const allSelections = this._editor.getSelections();
			if (allSelections.length > 1) {
				const findState = findController.getState();
				const matchCase = findState.matchCase;
A
Alex Dima 已提交
440
				const selectionsContainSameText = modelRangesContainSameText(this._editor.getModel(), allSelections, matchCase);
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
				if (!selectionsContainSameText) {
					const model = this._editor.getModel();
					let resultingSelections: Selection[] = [];
					for (let i = 0, len = allSelections.length; i < len; i++) {
						resultingSelections[i] = this._expandEmptyToWord(model, allSelections[i]);
					}
					this._editor.setSelections(resultingSelections);
					return;
				}
			}
		}
		this._beginSessionIfNeeded(findController);
		if (this._session) {
			this._applySessionResult(this._session.addSelectionToNextFindMatch());
		}
456 457
	}

458
	public addSelectionToPreviousFindMatch(findController: CommonFindController): void {
459 460 461
		this._beginSessionIfNeeded(findController);
		if (this._session) {
			this._applySessionResult(this._session.addSelectionToPreviousFindMatch());
462 463 464
		}
	}

465
	public moveSelectionToNextFindMatch(findController: CommonFindController): void {
466 467 468
		this._beginSessionIfNeeded(findController);
		if (this._session) {
			this._applySessionResult(this._session.moveSelectionToNextFindMatch());
469 470 471
		}
	}

472
	public moveSelectionToPreviousFindMatch(findController: CommonFindController): void {
473 474
		this._beginSessionIfNeeded(findController);
		if (this._session) {
475
			this._applySessionResult(this._session.moveSelectionToPreviousFindMatch());
476
		}
477 478 479 480 481 482
	}

	public selectAll(findController: CommonFindController): void {
		let matches: FindMatch[] = null;

		const findState = findController.getState();
483 484 485 486 487 488

		// Special case: find widget owns entirely what we search for if:
		// - focus is not in the editor (i.e. it is in the find widget)
		// - and the search widget is visible
		// - and the search string is non-empty
		// - and we're searching for a regex
489
		if (findState.isRevealed && findState.searchString.length > 0 && findState.isRegex) {
490 491 492 493 494

			matches = this._editor.getModel().findMatches(findState.searchString, true, findState.isRegex, findState.matchCase, findState.wholeWord ? this._editor.getConfiguration().wordSeparators : null, false, Constants.MAX_SAFE_SMALL_INTEGER);

		} else {

495 496
			this._beginSessionIfNeeded(findController);
			if (!this._session) {
497 498 499
				return;
			}

500
			matches = this._session.selectAll();
501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518
		}

		if (matches.length > 0) {
			const editorSelection = this._editor.getSelection();
			// Have the primary cursor remain the one where the action was invoked
			for (let i = 0, len = matches.length; i < len; i++) {
				const match = matches[i];
				const intersection = match.range.intersectRanges(editorSelection);
				if (intersection) {
					// bingo!
					matches[i] = matches[0];
					matches[0] = match;
					break;
				}
			}

			this._setSelections(matches.map(m => new Selection(m.range.startLineNumber, m.range.startColumn, m.range.endLineNumber, m.range.endColumn)));
		}
519 520 521 522 523
	}
}

export abstract class MultiCursorSelectionControllerAction extends EditorAction {

524
	public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
525 526
		const multiCursorController = MultiCursorSelectionController.get(editor);
		if (!multiCursorController) {
527 528
			return;
		}
529 530 531 532 533
		const findController = CommonFindController.get(editor);
		if (!findController) {
			return null;
		}
		this._run(multiCursorController, findController);
534 535
	}

536
	protected abstract _run(multiCursorController: MultiCursorSelectionController, findController: CommonFindController): void;
537 538 539 540 541 542 543 544 545 546 547
}

export class AddSelectionToNextFindMatchAction extends MultiCursorSelectionControllerAction {
	constructor() {
		super({
			id: 'editor.action.addSelectionToNextFindMatch',
			label: nls.localize('addSelectionToNextFindMatch', "Add Selection To Next Find Match"),
			alias: 'Add Selection To Next Find Match',
			precondition: null,
			kbOpts: {
				kbExpr: EditorContextKeys.focus,
A
Alex Dima 已提交
548
				primary: KeyMod.CtrlCmd | KeyCode.KEY_D,
549
				weight: KeybindingWeight.EditorContrib
550 551 552 553 554 555
			},
			menubarOpts: {
				menuId: MenuId.MenubarSelectionMenu,
				group: '3_multi',
				title: nls.localize({ key: 'miAddSelectionToNextFindMatch', comment: ['&& denotes a mnemonic'] }, "Add &&Next Occurrence"),
				order: 5
556 557 558
			}
		});
	}
559 560
	protected _run(multiCursorController: MultiCursorSelectionController, findController: CommonFindController): void {
		multiCursorController.addSelectionToNextFindMatch(findController);
561 562 563
	}
}

564
export class AddSelectionToPreviousFindMatchAction extends MultiCursorSelectionControllerAction {
565 566 567 568 569
	constructor() {
		super({
			id: 'editor.action.addSelectionToPreviousFindMatch',
			label: nls.localize('addSelectionToPreviousFindMatch', "Add Selection To Previous Find Match"),
			alias: 'Add Selection To Previous Find Match',
570 571 572 573 574 575 576
			precondition: null,
			menubarOpts: {
				menuId: MenuId.MenubarSelectionMenu,
				group: '3_multi',
				title: nls.localize({ key: 'miAddSelectionToPreviousFindMatch', comment: ['&& denotes a mnemonic'] }, "Add P&&revious Occurrence"),
				order: 6
			}
577 578
		});
	}
579 580
	protected _run(multiCursorController: MultiCursorSelectionController, findController: CommonFindController): void {
		multiCursorController.addSelectionToPreviousFindMatch(findController);
581 582 583
	}
}

584
export class MoveSelectionToNextFindMatchAction extends MultiCursorSelectionControllerAction {
585 586 587 588 589 590 591 592
	constructor() {
		super({
			id: 'editor.action.moveSelectionToNextFindMatch',
			label: nls.localize('moveSelectionToNextFindMatch', "Move Last Selection To Next Find Match"),
			alias: 'Move Last Selection To Next Find Match',
			precondition: null,
			kbOpts: {
				kbExpr: EditorContextKeys.focus,
A
Alex Dima 已提交
593
				primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_D),
594
				weight: KeybindingWeight.EditorContrib
595 596 597
			}
		});
	}
598 599
	protected _run(multiCursorController: MultiCursorSelectionController, findController: CommonFindController): void {
		multiCursorController.moveSelectionToNextFindMatch(findController);
600 601 602
	}
}

603
export class MoveSelectionToPreviousFindMatchAction extends MultiCursorSelectionControllerAction {
604 605 606 607 608 609 610 611
	constructor() {
		super({
			id: 'editor.action.moveSelectionToPreviousFindMatch',
			label: nls.localize('moveSelectionToPreviousFindMatch', "Move Last Selection To Previous Find Match"),
			alias: 'Move Last Selection To Previous Find Match',
			precondition: null
		});
	}
612 613
	protected _run(multiCursorController: MultiCursorSelectionController, findController: CommonFindController): void {
		multiCursorController.moveSelectionToPreviousFindMatch(findController);
614 615 616
	}
}

617
export class SelectHighlightsAction extends MultiCursorSelectionControllerAction {
618 619 620 621 622 623 624 625
	constructor() {
		super({
			id: 'editor.action.selectHighlights',
			label: nls.localize('selectAllOccurrencesOfFindMatch', "Select All Occurrences of Find Match"),
			alias: 'Select All Occurrences of Find Match',
			precondition: null,
			kbOpts: {
				kbExpr: EditorContextKeys.focus,
A
Alex Dima 已提交
626
				primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_L,
627
				weight: KeybindingWeight.EditorContrib
628 629 630 631 632 633
			},
			menubarOpts: {
				menuId: MenuId.MenubarSelectionMenu,
				group: '3_multi',
				title: nls.localize({ key: 'miSelectHighlights', comment: ['&& denotes a mnemonic'] }, "Select All &&Occurrences"),
				order: 7
634 635 636
			}
		});
	}
637 638 639
	protected _run(multiCursorController: MultiCursorSelectionController, findController: CommonFindController): void {
		multiCursorController.selectAll(findController);
	}
640 641
}

642
export class CompatChangeAll extends MultiCursorSelectionControllerAction {
643 644 645 646 647 648 649
	constructor() {
		super({
			id: 'editor.action.changeAll',
			label: nls.localize('changeAll.label', "Change All Occurrences"),
			alias: 'Change All Occurrences',
			precondition: EditorContextKeys.writable,
			kbOpts: {
650
				kbExpr: EditorContextKeys.editorTextFocus,
A
Alex Dima 已提交
651
				primary: KeyMod.CtrlCmd | KeyCode.F2,
652
				weight: KeybindingWeight.EditorContrib
653 654 655 656 657 658 659
			},
			menuOpts: {
				group: '1_modification',
				order: 1.2
			}
		});
	}
660 661 662
	protected _run(multiCursorController: MultiCursorSelectionController, findController: CommonFindController): void {
		multiCursorController.selectAll(findController);
	}
663 664 665 666 667 668 669 670 671
}

class SelectionHighlighterState {
	public readonly lastWordUnderCursor: Selection;
	public readonly searchText: string;
	public readonly matchCase: boolean;
	public readonly wordSeparators: string;

	constructor(lastWordUnderCursor: Selection, searchText: string, matchCase: boolean, wordSeparators: string) {
672
		this.lastWordUnderCursor = lastWordUnderCursor;
673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696
		this.searchText = searchText;
		this.matchCase = matchCase;
		this.wordSeparators = wordSeparators;
	}

	/**
	 * Everything equals except for `lastWordUnderCursor`
	 */
	public static softEquals(a: SelectionHighlighterState, b: SelectionHighlighterState): boolean {
		if (!a && !b) {
			return true;
		}
		if (!a || !b) {
			return false;
		}
		return (
			a.searchText === b.searchText
			&& a.matchCase === b.matchCase
			&& a.wordSeparators === b.wordSeparators
		);
	}
}

export class SelectionHighlighter extends Disposable implements IEditorContribution {
697
	private static readonly ID = 'editor.contrib.selectionHighlighter';
698

699
	private editor: ICodeEditor;
700 701 702 703 704
	private _isEnabled: boolean;
	private decorations: string[];
	private updateSoon: RunOnceScheduler;
	private state: SelectionHighlighterState;

705
	constructor(editor: ICodeEditor) {
706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741
		super();
		this.editor = editor;
		this._isEnabled = editor.getConfiguration().contribInfo.selectionHighlight;
		this.decorations = [];
		this.updateSoon = this._register(new RunOnceScheduler(() => this._update(), 300));
		this.state = null;

		this._register(editor.onDidChangeConfiguration((e) => {
			this._isEnabled = editor.getConfiguration().contribInfo.selectionHighlight;
		}));
		this._register(editor.onDidChangeCursorSelection((e: ICursorSelectionChangedEvent) => {

			if (!this._isEnabled) {
				// 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;
			}

			if (e.selection.isEmpty()) {
				if (e.reason === CursorChangeReason.Explicit) {
					if (this.state && (!this.state.lastWordUnderCursor || !this.state.lastWordUnderCursor.containsPosition(e.selection.getStartPosition()))) {
						// no longer valid
						this._setState(null);
					}
					this.updateSoon.schedule();
				} else {
					this._setState(null);

				}
			} else {
				this._update();
			}
		}));
		this._register(editor.onDidChangeModel((e) => {
			this._setState(null);
		}));
R
rebornix 已提交
742
		this._register(CommonFindController.get(editor).getState().onFindReplaceStateChange((e) => {
743 744 745 746 747 748 749 750 751 752 753 754
			this._update();
		}));
	}

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

	private _update(): void {
		this._setState(SelectionHighlighter._createState(this._isEnabled, this.editor));
	}

755
	private static _createState(isEnabled: boolean, editor: ICodeEditor): SelectionHighlighterState {
756 757 758
		if (!isEnabled) {
			return null;
		}
759 760 761 762
		const model = editor.getModel();
		if (!model) {
			return null;
		}
763 764 765 766 767 768 769
		const s = editor.getSelection();
		if (s.startLineNumber !== s.endLineNumber) {
			// multiline forbidden for perf reasons
			return null;
		}
		const multiCursorController = MultiCursorSelectionController.get(editor);
		if (!multiCursorController) {
770 771
			return null;
		}
772 773 774 775
		const findController = CommonFindController.get(editor);
		if (!findController) {
			return null;
		}
A
Alex Dima 已提交
776 777 778 779 780 781 782 783 784 785 786
		let r = multiCursorController.getSession(findController);
		if (!r) {
			const allSelections = editor.getSelections();
			if (allSelections.length > 1) {
				const findState = findController.getState();
				const matchCase = findState.matchCase;
				const selectionsContainSameText = modelRangesContainSameText(editor.getModel(), allSelections, matchCase);
				if (!selectionsContainSameText) {
					return null;
				}
			}
787

A
Alex Dima 已提交
788 789
			r = MultiCursorSession.create(editor, findController);
		}
790 791 792 793
		if (!r) {
			return null;
		}

794
		let lastWordUnderCursor: Selection = null;
795 796 797 798 799 800 801 802
		const hasFindOccurrences = DocumentHighlightProviderRegistry.has(model);
		if (r.currentMatch) {
			// This is an empty selection
			if (hasFindOccurrences) {
				// Do not interfere with semantic word highlighting in the no selection case
				return null;
			}

803
			const config = editor.getConfiguration();
804 805 806 807 808 809 810 811 812 813 814 815 816 817 818
			if (!config.contribInfo.occurrencesHighlight) {
				return null;
			}

			lastWordUnderCursor = r.currentMatch;
		}
		if (/^[ \t]+$/.test(r.searchText)) {
			// whitespace only selection
			return null;
		}
		if (r.searchText.length > 200) {
			// very long selection
			return null;
		}

819 820
		// TODO: better handling of this case
		const findState = findController.getState();
821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850
		const caseSensitive = findState.matchCase;

		// Return early if the find widget shows the exact same matches
		if (findState.isRevealed) {
			let findStateSearchString = findState.searchString;
			if (!caseSensitive) {
				findStateSearchString = findStateSearchString.toLowerCase();
			}

			let mySearchString = r.searchText;
			if (!caseSensitive) {
				mySearchString = mySearchString.toLowerCase();
			}

			if (findStateSearchString === mySearchString && r.matchCase === findState.matchCase && r.wholeWord === findState.wholeWord && !findState.isRegex) {
				return null;
			}
		}

		return new SelectionHighlighterState(lastWordUnderCursor, r.searchText, r.matchCase, r.wholeWord ? editor.getConfiguration().wordSeparators : null);
	}

	private _setState(state: SelectionHighlighterState): void {
		if (SelectionHighlighterState.softEquals(this.state, state)) {
			this.state = state;
			return;
		}
		this.state = state;

		if (!this.state) {
A
Alex Dima 已提交
851
			this.decorations = this.editor.deltaDecorations(this.decorations, []);
852 853 854 855
			return;
		}

		const model = this.editor.getModel();
856 857 858 859 860
		if (model.isTooLargeForTokenization()) {
			// the file is too large, so searching word under cursor in the whole document takes is blocking the UI.
			return;
		}

861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881
		const hasFindOccurrences = DocumentHighlightProviderRegistry.has(model);

		let allMatches = model.findMatches(this.state.searchText, true, false, this.state.matchCase, this.state.wordSeparators, false).map(m => m.range);
		allMatches.sort(Range.compareRangesUsingStarts);

		let selections = this.editor.getSelections();
		selections.sort(Range.compareRangesUsingStarts);

		// do not overlap with selection (issue #64 and #512)
		let matches: Range[] = [];
		for (let i = 0, j = 0, len = allMatches.length, lenJ = selections.length; i < len;) {
			const match = allMatches[i];

			if (j >= lenJ) {
				// finished all editor selections
				matches.push(match);
				i++;
			} else {
				const cmp = Range.compareRangesUsingStarts(match, selections[j]);
				if (cmp < 0) {
					// match is before sel
A
Alex Dima 已提交
882
					if (selections[j].isEmpty() || !Range.areIntersecting(match, selections[j])) {
883 884
						matches.push(match);
					}
885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907
					i++;
				} else if (cmp > 0) {
					// sel is before match
					j++;
				} else {
					// sel is equal to match
					i++;
					j++;
				}
			}
		}

		const decorations = matches.map(r => {
			return {
				range: r,
				// Show in overviewRuler only if model has no semantic highlighting
				options: (hasFindOccurrences ? SelectionHighlighter._SELECTION_HIGHLIGHT : SelectionHighlighter._SELECTION_HIGHLIGHT_OVERVIEW)
			};
		});

		this.decorations = this.editor.deltaDecorations(this.decorations, decorations);
	}

908
	private static readonly _SELECTION_HIGHLIGHT_OVERVIEW = ModelDecorationOptions.register({
909 910 911 912 913 914 915 916 917
		stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
		className: 'selectionHighlight',
		overviewRuler: {
			color: themeColorFromId(overviewRulerSelectionHighlightForeground),
			darkColor: themeColorFromId(overviewRulerSelectionHighlightForeground),
			position: OverviewRulerLane.Center
		}
	});

918
	private static readonly _SELECTION_HIGHLIGHT = ModelDecorationOptions.register({
919 920 921 922 923 924 925 926 927
		stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
		className: 'selectionHighlight',
	});

	public dispose(): void {
		this._setState(null);
		super.dispose();
	}
}
A
Alex Dima 已提交
928

A
Alex Dima 已提交
929
function modelRangesContainSameText(model: ITextModel, ranges: Range[], matchCase: boolean): boolean {
A
Alex Dima 已提交
930 931 932 933 934 935 936 937 938 939 940 941 942 943
	const selectedText = getValueInRange(model, ranges[0], !matchCase);
	for (let i = 1, len = ranges.length; i < len; i++) {
		const range = ranges[i];
		if (range.isEmpty()) {
			return false;
		}
		const thisSelectedText = getValueInRange(model, range, !matchCase);
		if (selectedText !== thisSelectedText) {
			return false;
		}
	}
	return true;
}

A
Alex Dima 已提交
944
function getValueInRange(model: ITextModel, range: Range, toLowerCase: boolean): string {
A
Alex Dima 已提交
945 946 947
	const text = model.getValueInRange(range);
	return (toLowerCase ? text.toLowerCase() : text);
}
948

949 950
registerEditorContribution(MultiCursorSelectionController);
registerEditorContribution(SelectionHighlighter);
951

952 953 954 955 956 957 958 959 960
registerEditorAction(InsertCursorAbove);
registerEditorAction(InsertCursorBelow);
registerEditorAction(InsertCursorAtEndOfEachLineSelected);
registerEditorAction(AddSelectionToNextFindMatchAction);
registerEditorAction(AddSelectionToPreviousFindMatchAction);
registerEditorAction(MoveSelectionToNextFindMatchAction);
registerEditorAction(MoveSelectionToPreviousFindMatchAction);
registerEditorAction(SelectHighlightsAction);
registerEditorAction(CompatChangeAll);