diff --git a/src/vs/editor/contrib/find/common/findController.ts b/src/vs/editor/contrib/find/common/findController.ts index 2c185f6f42d4e..4ead5ef999c38 100644 --- a/src/vs/editor/contrib/find/common/findController.ts +++ b/src/vs/editor/contrib/find/common/findController.ts @@ -6,26 +6,18 @@ import * as nls from 'vs/nls'; import { HistoryNavigator } from 'vs/base/common/history'; -import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; import { ContextKeyExpr, RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { Range } from 'vs/editor/common/core/range'; -import { Selection } from 'vs/editor/common/core/selection'; import * as strings from 'vs/base/common/strings'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { editorAction, commonEditorContribution, ServicesAccessor, EditorAction, EditorCommand, CommonEditorRegistry } from 'vs/editor/common/editorCommonExtensions'; +import { editorAction, ServicesAccessor, EditorAction, EditorCommand, CommonEditorRegistry } from 'vs/editor/common/editorCommonExtensions'; import { FIND_IDS, FindModelBoundToEditorModel, ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding, ToggleSearchScopeKeybinding, ShowPreviousFindTermKeybinding, ShowNextFindTermKeybinding } from 'vs/editor/contrib/find/common/findModel'; import { FindReplaceState, FindReplaceStateChangedEvent, INewFindReplaceState } from 'vs/editor/contrib/find/common/findState'; import { getSelectionSearchString } from 'vs/editor/contrib/find/common/find'; -import { DocumentHighlightProviderRegistry } from 'vs/editor/common/modes'; -import { RunOnceScheduler, Delayer } from 'vs/base/common/async'; -import { CursorChangeReason, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; +import { Delayer } from 'vs/base/common/async'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations'; -import { overviewRulerSelectionHighlightForeground } from 'vs/platform/theme/common/colorRegistry'; -import { themeColorFromId } from 'vs/platform/theme/common/themeService'; -import { Constants } from 'vs/editor/common/core/uint'; export const enum FindStartFocusAction { NoFocusChange, @@ -515,631 +507,6 @@ export class StartFindReplaceAction extends EditorAction { } } -export interface IMultiCursorFindInput { - changeFindSearchString: boolean; - allowMultiline: boolean; - highlightFindOptions: boolean; -} - -export interface IMultiCursorFindResult { - searchText: string; - matchCase: boolean; - wholeWord: boolean; - - currentMatch: Selection; -} - -function multiCursorFind(editor: editorCommon.ICommonCodeEditor, input: IMultiCursorFindInput): IMultiCursorFindResult { - let controller = CommonFindController.get(editor); - if (!controller) { - return null; - } - let state = controller.getState(); - let searchText: string; - let currentMatch: Selection; - - // In any case, if the find widget was ever opened, the options are taken from it - let wholeWord = state.wholeWord; - let matchCase = state.matchCase; - - // Find widget owns 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 - if (!editor.isFocused() && state.isRevealed && state.searchString.length > 0) { - // Find widget owns what is searched for - searchText = state.searchString; - } else { - // Selection owns what is searched for - let s = editor.getSelection(); - - if (s.startLineNumber !== s.endLineNumber && !input.allowMultiline) { - // multiline forbidden - return null; - } - - if (s.isEmpty()) { - // selection is empty => expand to current word - let 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'); - } - if (input.changeFindSearchString) { - controller.setSearchString(searchText); - } - } - - if (input.highlightFindOptions) { - controller.highlightFindOptions(); - } - - return { - searchText: searchText, - matchCase: matchCase, - wholeWord: wholeWord, - currentMatch: currentMatch - }; -} - -export abstract class SelectNextFindMatchAction extends EditorAction { - protected _getNextMatch(editor: editorCommon.ICommonCodeEditor): Selection { - let r = multiCursorFind(editor, { - changeFindSearchString: true, - allowMultiline: true, - highlightFindOptions: true - }); - if (!r) { - return null; - } - if (r.currentMatch) { - return r.currentMatch; - } - - let allSelections = editor.getSelections(); - let lastAddedSelection = allSelections[allSelections.length - 1]; - - let nextMatch = editor.getModel().findNextMatch(r.searchText, lastAddedSelection.getEndPosition(), false, r.matchCase, r.wholeWord ? editor.getConfiguration().wordSeparators : null, false); - - if (!nextMatch) { - return null; - } - - return new Selection(nextMatch.range.startLineNumber, nextMatch.range.startColumn, nextMatch.range.endLineNumber, nextMatch.range.endColumn); - } -} - -export abstract class SelectPreviousFindMatchAction extends EditorAction { - protected _getPreviousMatch(editor: editorCommon.ICommonCodeEditor): Selection { - let r = multiCursorFind(editor, { - changeFindSearchString: true, - allowMultiline: true, - highlightFindOptions: true - }); - if (!r) { - return null; - } - if (r.currentMatch) { - return r.currentMatch; - } - - let allSelections = editor.getSelections(); - let lastAddedSelection = allSelections[allSelections.length - 1]; - - let previousMatch = editor.getModel().findPreviousMatch(r.searchText, lastAddedSelection.getStartPosition(), false, r.matchCase, r.wholeWord ? editor.getConfiguration().wordSeparators : null, false); - - if (!previousMatch) { - return null; - } - - return new Selection(previousMatch.range.startLineNumber, previousMatch.range.startColumn, previousMatch.range.endLineNumber, previousMatch.range.endColumn); - } -} - -@editorAction -export class AddSelectionToNextFindMatchAction extends SelectNextFindMatchAction { - - constructor() { - super({ - id: FIND_IDS.AddSelectionToNextFindMatchAction, - label: nls.localize('addSelectionToNextFindMatch', "Add Selection To Next Find Match"), - alias: 'Add Selection To Next Find Match', - precondition: null, - kbOpts: { - kbExpr: EditorContextKeys.focus, - primary: KeyMod.CtrlCmd | KeyCode.KEY_D - } - }); - } - - public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void { - const allSelections = editor.getSelections(); - - // If there are mulitple cursors, handle the case where they do not all select the same text. - if (allSelections.length > 1) { - const model = editor.getModel(); - const controller = CommonFindController.get(editor); - if (!controller) { - return; - } - const findState = controller.getState(); - const caseSensitive = findState.matchCase; - - let selectionsContainSameText = true; - - let selectedText = model.getValueInRange(allSelections[0]); - if (!caseSensitive) { - selectedText = selectedText.toLowerCase(); - } - for (let i = 1, len = allSelections.length; i < len; i++) { - let selection = allSelections[i]; - if (selection.isEmpty()) { - selectionsContainSameText = false; - break; - } - - let thisSelectedText = model.getValueInRange(selection); - if (!caseSensitive) { - thisSelectedText = thisSelectedText.toLowerCase(); - } - if (selectedText !== thisSelectedText) { - selectionsContainSameText = false; - break; - } - } - - if (!selectionsContainSameText) { - let resultingSelections: Selection[] = []; - for (let i = 0, len = allSelections.length; i < len; i++) { - let selection = allSelections[i]; - if (selection.isEmpty()) { - let word = editor.getModel().getWordAtPosition(selection.getStartPosition()); - if (word) { - resultingSelections[i] = new Selection(selection.startLineNumber, word.startColumn, selection.startLineNumber, word.endColumn); - continue; - } - } - resultingSelections[i] = selection; - } - editor.setSelections(resultingSelections); - return; - } - } - - let nextMatch = this._getNextMatch(editor); - - if (!nextMatch) { - return; - } - - editor.setSelections(allSelections.concat(nextMatch)); - editor.revealRangeInCenterIfOutsideViewport(nextMatch, editorCommon.ScrollType.Smooth); - } -} - -@editorAction -export class AddSelectionToPreviousFindMatchAction extends SelectPreviousFindMatchAction { - - constructor() { - super({ - id: FIND_IDS.AddSelectionToPreviousFindMatchAction, - label: nls.localize('addSelectionToPreviousFindMatch', "Add Selection To Previous Find Match"), - alias: 'Add Selection To Previous Find Match', - precondition: null - }); - } - - public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void { - let previousMatch = this._getPreviousMatch(editor); - - if (!previousMatch) { - return; - } - - let allSelections = editor.getSelections(); - editor.setSelections(allSelections.concat(previousMatch)); - editor.revealRangeInCenterIfOutsideViewport(previousMatch, editorCommon.ScrollType.Smooth); - } -} - -@editorAction -export class MoveSelectionToNextFindMatchAction extends SelectNextFindMatchAction { - - constructor() { - super({ - id: FIND_IDS.MoveSelectionToNextFindMatchAction, - 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, - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_D) - } - }); - } - - public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void { - let nextMatch = this._getNextMatch(editor); - - if (!nextMatch) { - return; - } - - let allSelections = editor.getSelections(); - editor.setSelections(allSelections.slice(0, allSelections.length - 1).concat(nextMatch)); - editor.revealRangeInCenterIfOutsideViewport(nextMatch, editorCommon.ScrollType.Smooth); - } -} - -@editorAction -export class MoveSelectionToPreviousFindMatchAction extends SelectPreviousFindMatchAction { - - constructor() { - super({ - id: FIND_IDS.MoveSelectionToPreviousFindMatchAction, - label: nls.localize('moveSelectionToPreviousFindMatch', "Move Last Selection To Previous Find Match"), - alias: 'Move Last Selection To Previous Find Match', - precondition: null - }); - } - - public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void { - let previousMatch = this._getPreviousMatch(editor); - - if (!previousMatch) { - return; - } - - let allSelections = editor.getSelections(); - editor.setSelections(allSelections.slice(0, allSelections.length - 1).concat(previousMatch)); - editor.revealRangeInCenterIfOutsideViewport(previousMatch, editorCommon.ScrollType.Smooth); - } -} - -export abstract class AbstractSelectHighlightsAction extends EditorAction { - public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void { - const controller = CommonFindController.get(editor); - if (!controller) { - return null; - } - - let matches: editorCommon.FindMatch[] = null; - - const findState = controller.getState(); - if (findState.isRevealed && findState.isRegex && findState.searchString.length > 0) { - - matches = editor.getModel().findMatches(findState.searchString, true, findState.isRegex, findState.matchCase, findState.wholeWord ? editor.getConfiguration().wordSeparators : null, false, Constants.MAX_SAFE_SMALL_INTEGER); - - } else { - - const r = multiCursorFind(editor, { - changeFindSearchString: true, - allowMultiline: true, - highlightFindOptions: true - }); - if (!r) { - return; - } - - matches = editor.getModel().findMatches(r.searchText, true, false, r.matchCase, r.wholeWord ? editor.getConfiguration().wordSeparators : null, false, Constants.MAX_SAFE_SMALL_INTEGER); - } - - if (matches.length > 0) { - const editorSelection = editor.getSelection(); - for (let i = 0, len = matches.length; i < len; i++) { - const match = matches[i]; - let intersection = match.range.intersectRanges(editorSelection); - if (intersection) { - // bingo! - matches.splice(i, 1); - matches.unshift(match); - break; - } - } - editor.setSelections(matches.map(m => new Selection(m.range.startLineNumber, m.range.startColumn, m.range.endLineNumber, m.range.endColumn))); - } - } -} - -@editorAction -export class SelectHighlightsAction extends AbstractSelectHighlightsAction { - 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, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_L - } - }); - } -} - -@editorAction -export class CompatChangeAll extends AbstractSelectHighlightsAction { - constructor() { - super({ - id: 'editor.action.changeAll', - label: nls.localize('changeAll.label', "Change All Occurrences"), - alias: 'Change All Occurrences', - precondition: EditorContextKeys.writable, - kbOpts: { - kbExpr: EditorContextKeys.textFocus, - primary: KeyMod.CtrlCmd | KeyCode.F2 - }, - menuOpts: { - group: '1_modification', - order: 1.2 - } - }); - } -} - -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) { - 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 - ); - } -} - -@commonEditorContribution -export class SelectionHighlighter extends Disposable implements editorCommon.IEditorContribution { - private static ID = 'editor.contrib.selectionHighlighter'; - - private editor: editorCommon.ICommonCodeEditor; - private _isEnabled: boolean; - private decorations: string[]; - private updateSoon: RunOnceScheduler; - private state: SelectionHighlighterState; - - constructor(editor: editorCommon.ICommonCodeEditor) { - 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); - })); - this._register(CommonFindController.get(editor).getState().addChangeListener((e) => { - this._update(); - })); - } - - public getId(): string { - return SelectionHighlighter.ID; - } - - private _update(): void { - this._setState(SelectionHighlighter._createState(this._isEnabled, this.editor)); - } - - private static _createState(isEnabled: boolean, editor: editorCommon.ICommonCodeEditor): SelectionHighlighterState { - const model = editor.getModel(); - if (!model) { - return null; - } - - const config = editor.getConfiguration(); - - let lastWordUnderCursor: Selection = null; - if (!isEnabled) { - return null; - } - - const r = multiCursorFind(editor, { - changeFindSearchString: false, - allowMultiline: false, - highlightFindOptions: false - }); - if (!r) { - return null; - } - - 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; - } - - 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; - } - - const controller = CommonFindController.get(editor); - if (!controller) { - return null; - } - const findState = controller.getState(); - const caseSensitive = findState.matchCase; - - const selections = editor.getSelections(); - let firstSelectedText = model.getValueInRange(selections[0]); - if (!caseSensitive) { - firstSelectedText = firstSelectedText.toLowerCase(); - } - for (let i = 1; i < selections.length; i++) { - let selectedText = model.getValueInRange(selections[i]); - if (!caseSensitive) { - selectedText = selectedText.toLowerCase(); - } - if (firstSelectedText !== selectedText) { - // not all selections have the same text - return null; - } - } - - // 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) { - if (this.decorations.length > 0) { - this.decorations = this.editor.deltaDecorations(this.decorations, []); - } - return; - } - - const model = this.editor.getModel(); - 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 - matches.push(match); - 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); - } - - private static _SELECTION_HIGHLIGHT_OVERVIEW = ModelDecorationOptions.register({ - stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - className: 'selectionHighlight', - overviewRuler: { - color: themeColorFromId(overviewRulerSelectionHighlightForeground), - darkColor: themeColorFromId(overviewRulerSelectionHighlightForeground), - position: editorCommon.OverviewRulerLane.Center - } - }); - - private static _SELECTION_HIGHLIGHT = ModelDecorationOptions.register({ - stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - className: 'selectionHighlight', - }); - - public dispose(): void { - this._setState(null); - super.dispose(); - } -} @editorAction export class ShowNextFindTermAction extends MatchFindAction { diff --git a/src/vs/editor/contrib/find/common/findModel.ts b/src/vs/editor/contrib/find/common/findModel.ts index 573eba89a136b..1536e8c9541c3 100644 --- a/src/vs/editor/contrib/find/common/findModel.ts +++ b/src/vs/editor/contrib/find/common/findModel.ts @@ -50,10 +50,6 @@ export const FIND_IDS = { PreviousMatchFindAction: 'editor.action.previousMatchFindAction', NextSelectionMatchFindAction: 'editor.action.nextSelectionMatchFindAction', PreviousSelectionMatchFindAction: 'editor.action.previousSelectionMatchFindAction', - AddSelectionToNextFindMatchAction: 'editor.action.addSelectionToNextFindMatch', - AddSelectionToPreviousFindMatchAction: 'editor.action.addSelectionToPreviousFindMatch', - MoveSelectionToNextFindMatchAction: 'editor.action.moveSelectionToNextFindMatch', - MoveSelectionToPreviousFindMatchAction: 'editor.action.moveSelectionToPreviousFindMatch', StartFindReplaceAction: 'editor.action.startFindReplaceAction', CloseFindWidgetCommand: 'closeFindWidget', ToggleCaseSensitiveCommand: 'toggleFindCaseSensitive', diff --git a/src/vs/editor/contrib/find/common/findState.ts b/src/vs/editor/contrib/find/common/findState.ts index 0dccd531f7aae..000ee49bfd551 100644 --- a/src/vs/editor/contrib/find/common/findState.ts +++ b/src/vs/editor/contrib/find/common/findState.ts @@ -25,17 +25,36 @@ export interface FindReplaceStateChangedEvent { currentMatch: boolean; } +export const enum FindOptionOverride { + NotSet = 0, + True = 1, + False = 2 +} + export interface INewFindReplaceState { searchString?: string; replaceString?: string; isRevealed?: boolean; isReplaceRevealed?: boolean; isRegex?: boolean; + isRegexOverride?: FindOptionOverride; wholeWord?: boolean; + wholeWordOverride?: FindOptionOverride; matchCase?: boolean; + matchCaseOverride?: FindOptionOverride; searchScope?: Range; } +function effectiveOptionValue(override: FindOptionOverride, value: boolean): boolean { + if (override === FindOptionOverride.True) { + return true; + } + if (override === FindOptionOverride.False) { + return false; + } + return value; +} + export class FindReplaceState implements IDisposable { private static _CHANGED_EVENT = 'changed'; @@ -45,8 +64,11 @@ export class FindReplaceState implements IDisposable { private _isRevealed: boolean; private _isReplaceRevealed: boolean; private _isRegex: boolean; + private _isRegexOverride: FindOptionOverride; private _wholeWord: boolean; + private _wholeWordOverride: FindOptionOverride; private _matchCase: boolean; + private _matchCaseOverride: FindOptionOverride; private _searchScope: Range; private _matchesPosition: number; private _matchesCount: number; @@ -57,9 +79,9 @@ export class FindReplaceState implements IDisposable { public get replaceString(): string { return this._replaceString; } public get isRevealed(): boolean { return this._isRevealed; } public get isReplaceRevealed(): boolean { return this._isReplaceRevealed; } - public get isRegex(): boolean { return this._isRegex; } - public get wholeWord(): boolean { return this._wholeWord; } - public get matchCase(): boolean { return this._matchCase; } + public get isRegex(): boolean { return effectiveOptionValue(this._isRegexOverride, this._isRegex); } + public get wholeWord(): boolean { return effectiveOptionValue(this._wholeWordOverride, this._wholeWord); } + public get matchCase(): boolean { return effectiveOptionValue(this._matchCaseOverride, this._matchCase); } public get searchScope(): Range { return this._searchScope; } public get matchesPosition(): number { return this._matchesPosition; } public get matchesCount(): number { return this._matchesCount; } @@ -71,8 +93,11 @@ export class FindReplaceState implements IDisposable { this._isRevealed = false; this._isReplaceRevealed = false; this._isRegex = false; + this._isRegexOverride = FindOptionOverride.NotSet; this._wholeWord = false; + this._wholeWordOverride = FindOptionOverride.NotSet; this._matchCase = false; + this._matchCaseOverride = FindOptionOverride.NotSet; this._searchScope = null; this._matchesPosition = 0; this._matchesCount = 0; @@ -155,6 +180,10 @@ export class FindReplaceState implements IDisposable { }; let somethingChanged = false; + const oldEffectiveIsRegex = this.isRegex; + const oldEffectiveWholeWords = this.wholeWord; + const oldEffectiveMatchCase = this.matchCase; + if (typeof newState.searchString !== 'undefined') { if (this._searchString !== newState.searchString) { this._searchString = newState.searchString; @@ -184,25 +213,13 @@ export class FindReplaceState implements IDisposable { } } if (typeof newState.isRegex !== 'undefined') { - if (this._isRegex !== newState.isRegex) { - this._isRegex = newState.isRegex; - changeEvent.isRegex = true; - somethingChanged = true; - } + this._isRegex = newState.isRegex; } if (typeof newState.wholeWord !== 'undefined') { - if (this._wholeWord !== newState.wholeWord) { - this._wholeWord = newState.wholeWord; - changeEvent.wholeWord = true; - somethingChanged = true; - } + this._wholeWord = newState.wholeWord; } if (typeof newState.matchCase !== 'undefined') { - if (this._matchCase !== newState.matchCase) { - this._matchCase = newState.matchCase; - changeEvent.matchCase = true; - somethingChanged = true; - } + this._matchCase = newState.matchCase; } if (typeof newState.searchScope !== 'undefined') { if (!Range.equalsRange(this._searchScope, newState.searchScope)) { @@ -212,6 +229,24 @@ export class FindReplaceState implements IDisposable { } } + // Overrides get set when they explicitly come in and get reset anytime something else changes + this._isRegexOverride = (typeof newState.isRegexOverride !== 'undefined' ? newState.isRegexOverride : FindOptionOverride.NotSet); + this._wholeWordOverride = (typeof newState.wholeWordOverride !== 'undefined' ? newState.wholeWordOverride : FindOptionOverride.NotSet); + this._matchCaseOverride = (typeof newState.matchCaseOverride !== 'undefined' ? newState.matchCaseOverride : FindOptionOverride.NotSet); + + if (oldEffectiveIsRegex !== this.isRegex) { + somethingChanged = true; + changeEvent.isRegex = true; + } + if (oldEffectiveWholeWords !== this.wholeWord) { + somethingChanged = true; + changeEvent.wholeWord = true; + } + if (oldEffectiveMatchCase !== this.matchCase) { + somethingChanged = true; + changeEvent.matchCase = true; + } + if (somethingChanged) { this._eventEmitter.emit(FindReplaceState._CHANGED_EVENT, changeEvent); } diff --git a/src/vs/editor/contrib/find/test/common/findController.test.ts b/src/vs/editor/contrib/find/test/common/findController.test.ts index 20559f5713bb1..323f3f0ce428e 100644 --- a/src/vs/editor/contrib/find/test/common/findController.test.ts +++ b/src/vs/editor/contrib/find/test/common/findController.test.ts @@ -11,20 +11,16 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { Range } from 'vs/editor/common/core/range'; -import { EndOfLineSequence, ICommonCodeEditor, Handler } from 'vs/editor/common/editorCommon'; -import { - CommonFindController, FindStartFocusAction, IFindStartOptions, - NextMatchFindAction, StartFindAction, SelectHighlightsAction, - AddSelectionToNextFindMatchAction -} from 'vs/editor/contrib/find/common/findController'; -import { MockCodeEditor, withMockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor'; +import { ICommonCodeEditor } from 'vs/editor/common/editorCommon'; +import { CommonFindController, FindStartFocusAction, IFindStartOptions, NextMatchFindAction, StartFindAction } from 'vs/editor/contrib/find/common/findController'; +import { withMockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor'; import { HistoryNavigator } from 'vs/base/common/history'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { Delayer } from 'vs/base/common/async'; -class TestFindController extends CommonFindController { +export class TestFindController extends CommonFindController { public hasFocus: boolean; public delayUpdateHistory: boolean = false; @@ -188,60 +184,6 @@ suite('FindController', () => { }); }); - test('issue #8817: Cursor position changes when you cancel multicursor', () => { - withMockCodeEditor([ - 'var x = (3 * 5)', - 'var y = (3 * 5)', - 'var z = (3 * 5)', - ], { serviceCollection: serviceCollection }, (editor, cursor) => { - - let findController = editor.registerAndInstantiateContribution(TestFindController); - let selectHighlightsAction = new SelectHighlightsAction(); - - editor.setSelection(new Selection(2, 9, 2, 16)); - - selectHighlightsAction.run(null, editor); - assert.deepEqual(editor.getSelections().map(fromRange), [ - [2, 9, 2, 16], - [1, 9, 1, 16], - [3, 9, 3, 16], - ]); - - editor.trigger('test', 'removeSecondaryCursors', null); - - assert.deepEqual(fromRange(editor.getSelection()), [2, 9, 2, 16]); - - findController.dispose(); - }); - }); - - test('issue #5400: "Select All Occurrences of Find Match" does not select all if find uses regex', () => { - withMockCodeEditor([ - 'something', - 'someething', - 'someeething', - 'nothing' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { - - let findController = editor.registerAndInstantiateContribution(TestFindController); - let selectHighlightsAction = new SelectHighlightsAction(); - - editor.setSelection(new Selection(1, 1, 1, 1)); - findController.getState().change({ searchString: 'some+thing', isRegex: true, isRevealed: true }, false); - - selectHighlightsAction.run(null, editor); - assert.deepEqual(editor.getSelections().map(fromRange), [ - [1, 1, 1, 10], - [2, 1, 2, 11], - [3, 1, 3, 12], - ]); - - assert.equal(findController.getState().searchString, 'some+thing'); - - findController.dispose(); - }); - }); - test('issue #9043: Clear search scope when find widget is hidden', () => { withMockCodeEditor([ 'var x = (3 * 5)', @@ -380,119 +322,6 @@ suite('FindController', () => { }); }); - test('AddSelectionToNextFindMatchAction can work with multiline', () => { - withMockCodeEditor([ - '', - 'qwe', - 'rty', - '', - 'qwe', - '', - 'rty', - 'qwe', - 'rty' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { - - let findController = editor.registerAndInstantiateContribution(TestFindController); - let addSelectionToNextFindMatch = new AddSelectionToNextFindMatchAction(); - - editor.setSelection(new Selection(2, 1, 3, 4)); - - addSelectionToNextFindMatch.run(null, editor); - assert.deepEqual(editor.getSelections().map(fromRange), [ - [2, 1, 3, 4], - [8, 1, 9, 4] - ]); - - editor.trigger('test', 'removeSecondaryCursors', null); - - assert.deepEqual(fromRange(editor.getSelection()), [2, 1, 3, 4]); - - findController.dispose(); - }); - }); - - test('issue #6661: AddSelectionToNextFindMatchAction can work with touching ranges', () => { - withMockCodeEditor([ - 'abcabc', - 'abc', - 'abcabc', - ], { serviceCollection: serviceCollection }, (editor, cursor) => { - - let findController = editor.registerAndInstantiateContribution(TestFindController); - let addSelectionToNextFindMatch = new AddSelectionToNextFindMatchAction(); - - editor.setSelection(new Selection(1, 1, 1, 4)); - - addSelectionToNextFindMatch.run(null, editor); - assert.deepEqual(editor.getSelections().map(fromRange), [ - [1, 1, 1, 4], - [1, 4, 1, 7] - ]); - - addSelectionToNextFindMatch.run(null, editor); - addSelectionToNextFindMatch.run(null, editor); - addSelectionToNextFindMatch.run(null, editor); - assert.deepEqual(editor.getSelections().map(fromRange), [ - [1, 1, 1, 4], - [1, 4, 1, 7], - [2, 1, 2, 4], - [3, 1, 3, 4], - [3, 4, 3, 7] - ]); - - editor.trigger('test', Handler.Type, { text: 'z' }); - assert.deepEqual(editor.getSelections().map(fromRange), [ - [1, 2, 1, 2], - [1, 3, 1, 3], - [2, 2, 2, 2], - [3, 2, 3, 2], - [3, 3, 3, 3] - ]); - assert.equal(editor.getValue(), [ - 'zz', - 'z', - 'zz', - ].join('\n')); - - findController.dispose(); - }); - }); - - test('issue #23541: Multiline Ctrl+D does not work in CRLF files', () => { - withMockCodeEditor([ - '', - 'qwe', - 'rty', - '', - 'qwe', - '', - 'rty', - 'qwe', - 'rty' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { - - editor.getModel().setEOL(EndOfLineSequence.CRLF); - - let findController = editor.registerAndInstantiateContribution(TestFindController); - let addSelectionToNextFindMatch = new AddSelectionToNextFindMatchAction(); - - editor.setSelection(new Selection(2, 1, 3, 4)); - - addSelectionToNextFindMatch.run(null, editor); - assert.deepEqual(editor.getSelections().map(fromRange), [ - [2, 1, 3, 4], - [8, 1, 9, 4] - ]); - - editor.trigger('test', 'removeSecondaryCursors', null); - - assert.deepEqual(fromRange(editor.getSelection()), [2, 1, 3, 4]); - - findController.dispose(); - }); - }); - test('issue #18111: Regex replace with single space replaces with no space', () => { withMockCodeEditor([ 'HRESULT OnAmbientPropertyChange(DISPID dispid);' @@ -555,237 +384,6 @@ suite('FindController', () => { } return result; } - - function testAddSelectionToNextFindMatchAction(text: string[], callback: (editor: MockCodeEditor, action: AddSelectionToNextFindMatchAction, findController: TestFindController) => void): void { - withMockCodeEditor(text, { serviceCollection: serviceCollection }, (editor, cursor) => { - - let findController = editor.registerAndInstantiateContribution(TestFindController); - - let action = new AddSelectionToNextFindMatchAction(); - - callback(editor, action, findController); - - findController.dispose(); - }); - } - - test('AddSelectionToNextFindMatchAction starting with single collapsed selection', () => { - const text = [ - 'abc pizza', - 'abc house', - 'abc bar' - ]; - testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => { - editor.setSelections([ - new Selection(1, 2, 1, 2), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 4), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 4), - new Selection(2, 1, 2, 4), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 4), - new Selection(2, 1, 2, 4), - new Selection(3, 1, 3, 4), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 4), - new Selection(2, 1, 2, 4), - new Selection(3, 1, 3, 4), - ]); - }); - }); - - test('AddSelectionToNextFindMatchAction starting with two selections, one being collapsed 1)', () => { - const text = [ - 'abc pizza', - 'abc house', - 'abc bar' - ]; - testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => { - editor.setSelections([ - new Selection(1, 1, 1, 4), - new Selection(2, 2, 2, 2), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 4), - new Selection(2, 1, 2, 4), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 4), - new Selection(2, 1, 2, 4), - new Selection(3, 1, 3, 4), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 4), - new Selection(2, 1, 2, 4), - new Selection(3, 1, 3, 4), - ]); - }); - }); - - test('AddSelectionToNextFindMatchAction starting with two selections, one being collapsed 2)', () => { - const text = [ - 'abc pizza', - 'abc house', - 'abc bar' - ]; - testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => { - editor.setSelections([ - new Selection(1, 2, 1, 2), - new Selection(2, 1, 2, 4), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 4), - new Selection(2, 1, 2, 4), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 4), - new Selection(2, 1, 2, 4), - new Selection(3, 1, 3, 4), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 4), - new Selection(2, 1, 2, 4), - new Selection(3, 1, 3, 4), - ]); - }); - }); - - test('AddSelectionToNextFindMatchAction starting with all collapsed selections', () => { - const text = [ - 'abc pizza', - 'abc house', - 'abc bar' - ]; - testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => { - editor.setSelections([ - new Selection(1, 2, 1, 2), - new Selection(2, 2, 2, 2), - new Selection(3, 1, 3, 1), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 4), - new Selection(2, 1, 2, 4), - new Selection(3, 1, 3, 4), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 4), - new Selection(2, 1, 2, 4), - new Selection(3, 1, 3, 4), - ]); - }); - }); - - test('AddSelectionToNextFindMatchAction starting with all collapsed selections on different words', () => { - const text = [ - 'abc pizza', - 'abc house', - 'abc bar' - ]; - testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => { - editor.setSelections([ - new Selection(1, 6, 1, 6), - new Selection(2, 6, 2, 6), - new Selection(3, 6, 3, 6), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 5, 1, 10), - new Selection(2, 5, 2, 10), - new Selection(3, 5, 3, 8), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 5, 1, 10), - new Selection(2, 5, 2, 10), - new Selection(3, 5, 3, 8), - ]); - }); - }); - - test('issue #20651: AddSelectionToNextFindMatchAction case insensitive', () => { - const text = [ - 'test', - 'testte', - 'Test', - 'testte', - 'test' - ]; - testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => { - editor.setSelections([ - new Selection(1, 1, 1, 5), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 5), - new Selection(2, 1, 2, 5), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 5), - new Selection(2, 1, 2, 5), - new Selection(3, 1, 3, 5), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 5), - new Selection(2, 1, 2, 5), - new Selection(3, 1, 3, 5), - new Selection(4, 1, 4, 5), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 5), - new Selection(2, 1, 2, 5), - new Selection(3, 1, 3, 5), - new Selection(4, 1, 4, 5), - new Selection(5, 1, 5, 5), - ]); - - action.run(null, editor); - assert.deepEqual(editor.getSelections(), [ - new Selection(1, 1, 1, 5), - new Selection(2, 1, 2, 5), - new Selection(3, 1, 3, 5), - new Selection(4, 1, 4, 5), - new Selection(5, 1, 5, 5), - ]); - }); - }); }); suite('FindController query options persistence', () => { diff --git a/src/vs/editor/contrib/multicursor/common/multicursor.ts b/src/vs/editor/contrib/multicursor/common/multicursor.ts index 0f6b2fdec4126..a45a8226d0d17 100644 --- a/src/vs/editor/contrib/multicursor/common/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/common/multicursor.ts @@ -5,14 +5,24 @@ 'use strict'; import * as nls from 'vs/nls'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { ICommonCodeEditor, ScrollType } from 'vs/editor/common/editorCommon'; +import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { ICommonCodeEditor, ScrollType, IEditorContribution, FindMatch, TrackedRangeStickiness, OverviewRulerLane, IModel } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { editorAction, ServicesAccessor, EditorAction } from 'vs/editor/common/editorCommonExtensions'; +import { editorAction, commonEditorContribution, ServicesAccessor, EditorAction } from 'vs/editor/common/editorCommonExtensions'; +import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; +import { CursorChangeReason, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { CursorMoveCommands } from 'vs/editor/common/controller/cursorMoveCommands'; import { CursorState, RevealTarget } from 'vs/editor/common/controller/cursorCommon'; +import { Constants } from 'vs/editor/common/core/uint'; +import { DocumentHighlightProviderRegistry } from 'vs/editor/common/modes'; +import { CommonFindController } from 'vs/editor/contrib/find/common/findController'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations'; +import { overviewRulerSelectionHighlightForeground } from 'vs/platform/theme/common/colorRegistry'; +import { themeColorFromId } from 'vs/platform/theme/common/themeService'; +import { INewFindReplaceState, FindOptionOverride } from 'vs/editor/contrib/find/common/findState'; @editorAction export class InsertCursorAbove extends EditorAction { @@ -139,3 +149,764 @@ class InsertCursorAtEndOfEachLineSelected extends EditorAction { } } } + +export class MultiCursorSessionResult { + constructor( + public readonly selections: Selection[], + public readonly revealRange: Range, + public readonly revealScrollType: ScrollType + ) { } +} + +export class MultiCursorSession { + + public static create(editor: ICommonCodeEditor, findController: CommonFindController): MultiCursorSession { + const findState = findController.getState(); + + // 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 + if (!editor.isFocused() && findState.isRevealed && findState.searchString.length > 0) { + // Find widget owns what is searched for + return new MultiCursorSession(editor, findController, false, findState.searchString, findState.wholeWord, findState.matchCase, null); + } + + // 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; + } + + // Selection owns what is searched for + const s = editor.getSelection(); + + let searchText: string; + let currentMatch: Selection = null; + + 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'); + } + + return new MultiCursorSession(editor, findController, isDisconnectedFromFindController, searchText, wholeWord, matchCase, currentMatch); + } + + constructor( + private readonly _editor: ICommonCodeEditor, + public readonly findController: CommonFindController, + public readonly isDisconnectedFromFindController: boolean, + public readonly searchText: string, + public readonly wholeWord: boolean, + public readonly matchCase: boolean, + public currentMatch: Selection + ) { } + + 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); + } + + public moveSelectionToNextFindMatch(): MultiCursorSessionResult { + const nextMatch = this._getNextMatch(); + if (!nextMatch) { + return null; + } + + const allSelections = this._editor.getSelections(); + return new MultiCursorSessionResult(allSelections.slice(0, allSelections.length - 1).concat(nextMatch), nextMatch, ScrollType.Smooth); + } + + private _getNextMatch(): Selection { + if (this.currentMatch) { + const result = this.currentMatch; + this.currentMatch = null; + return result; + } + + this.findController.highlightFindOptions(); + + 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); + + if (!nextMatch) { + return null; + } + return new Selection(nextMatch.range.startLineNumber, nextMatch.range.startColumn, nextMatch.range.endLineNumber, nextMatch.range.endColumn); + } + + public addSelectionToPreviousFindMatch(): MultiCursorSessionResult { + const previousMatch = this._getPreviousMatch(); + if (!previousMatch) { + return null; + } + + const allSelections = this._editor.getSelections(); + return new MultiCursorSessionResult(allSelections.concat(previousMatch), previousMatch, ScrollType.Smooth); + } + + public moveSelectionToPreviousFindMatch(): MultiCursorSessionResult { + const previousMatch = this._getPreviousMatch(); + if (!previousMatch) { + return null; + } + + const allSelections = this._editor.getSelections(); + return new MultiCursorSessionResult(allSelections.slice(0, allSelections.length - 1).concat(previousMatch), previousMatch, ScrollType.Smooth); + } + + private _getPreviousMatch(): Selection { + if (this.currentMatch) { + const result = this.currentMatch; + this.currentMatch = null; + return result; + } + + this.findController.highlightFindOptions(); + + 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); + + if (!previousMatch) { + return null; + } + return new Selection(previousMatch.range.startLineNumber, previousMatch.range.startColumn, previousMatch.range.endLineNumber, previousMatch.range.endColumn); + } + + public selectAll(): FindMatch[] { + this.findController.highlightFindOptions(); + + return this._editor.getModel().findMatches(this.searchText, true, false, this.matchCase, this.wholeWord ? this._editor.getConfiguration().wordSeparators : null, false, Constants.MAX_SAFE_SMALL_INTEGER); + } +} + +@commonEditorContribution +export class MultiCursorSelectionController extends Disposable implements IEditorContribution { + + private static ID = 'editor.contrib.multiCursorController'; + + private readonly _editor: ICommonCodeEditor; + private _ignoreSelectionChange: boolean; + private _session: MultiCursorSession; + private _sessionDispose: IDisposable[]; + + public static get(editor: ICommonCodeEditor): MultiCursorSelectionController { + return editor.getContribution(MultiCursorSelectionController.ID); + } + + constructor(editor: ICommonCodeEditor) { + super(); + this._editor = editor; + this._ignoreSelectionChange = false; + this._session = null; + this._sessionDispose = []; + } + + public dispose(): void { + this._endSession(); + super.dispose(); + } + + public getId(): string { + return MultiCursorSelectionController.ID; + } + + private _beginSessionIfNeeded(findController: CommonFindController): void { + if (!this._session) { + // Create a new session + const session = MultiCursorSession.create(this._editor, findController); + if (!session) { + return; + } + + this._session = session; + + 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); + + this._sessionDispose = [ + this._editor.onDidChangeCursorSelection((e) => { + if (this._ignoreSelectionChange) { + return; + } + this._endSession(); + }), + this._editor.onDidBlurEditorText(() => { + this._endSession(); + }), + findController.getState().addChangeListener((e) => { + if (e.matchCase || e.wholeWord) { + this._endSession(); + } + }) + ]; + } + } + + private _endSession(): void { + this._sessionDispose = dispose(this._sessionDispose); + 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; + } + + private _setSelections(selections: Selection[]): void { + this._ignoreSelectionChange = true; + this._editor.setSelections(selections); + this._ignoreSelectionChange = false; + } + + private _expandEmptyToWord(model: IModel, selection: Selection): Selection { + 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); + } + + private _applySessionResult(result: MultiCursorSessionResult): void { + if (!result) { + return; + } + this._setSelections(result.selections); + if (result.revealRange) { + this._editor.revealRangeInCenterIfOutsideViewport(result.revealRange, result.revealScrollType); + } + } + + public getSession(findController: CommonFindController): MultiCursorSession { + return this._session; + } + + 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; + const selectionsContainSameText = modelRangesContainSameText(this._editor.getModel(), allSelections, matchCase); + 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()); + } + } + + public addSelectionToPreviousFindMatch(findController: CommonFindController): void { + this._beginSessionIfNeeded(findController); + if (this._session) { + this._applySessionResult(this._session.addSelectionToPreviousFindMatch()); + } + } + + public moveSelectionToNextFindMatch(findController: CommonFindController): void { + this._beginSessionIfNeeded(findController); + if (this._session) { + this._applySessionResult(this._session.moveSelectionToNextFindMatch()); + } + } + + public moveSelectionToPreviousFindMatch(findController: CommonFindController): void { + this._beginSessionIfNeeded(findController); + if (this._session) { + this._applySessionResult(this._session.addSelectionToPreviousFindMatch()); + } + } + + public selectAll(findController: CommonFindController): void { + let matches: FindMatch[] = null; + + const findState = findController.getState(); + + // 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 + if (!this._editor.isFocused() && findState.isRevealed && findState.searchString.length > 0 && findState.isRegex) { + + 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 { + + this._beginSessionIfNeeded(findController); + if (!this._session) { + return; + } + + matches = this._session.selectAll(); + } + + 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))); + } + } +} + +export abstract class MultiCursorSelectionControllerAction extends EditorAction { + + public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): void { + const multiCursorController = MultiCursorSelectionController.get(editor); + if (!multiCursorController) { + return; + } + const findController = CommonFindController.get(editor); + if (!findController) { + return null; + } + this._run(multiCursorController, findController); + } + + protected abstract _run(multiCursorController: MultiCursorSelectionController, findController: CommonFindController): void; +} + +@editorAction +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, + primary: KeyMod.CtrlCmd | KeyCode.KEY_D + } + }); + } + protected _run(multiCursorController: MultiCursorSelectionController, findController: CommonFindController): void { + multiCursorController.addSelectionToNextFindMatch(findController); + } +} + +@editorAction +export class AddSelectionToPreviousFindMatchAction extends MultiCursorSelectionControllerAction { + constructor() { + super({ + id: 'editor.action.addSelectionToPreviousFindMatch', + label: nls.localize('addSelectionToPreviousFindMatch', "Add Selection To Previous Find Match"), + alias: 'Add Selection To Previous Find Match', + precondition: null + }); + } + protected _run(multiCursorController: MultiCursorSelectionController, findController: CommonFindController): void { + multiCursorController.addSelectionToPreviousFindMatch(findController); + } +} + +@editorAction +export class MoveSelectionToNextFindMatchAction extends MultiCursorSelectionControllerAction { + 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, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_D) + } + }); + } + protected _run(multiCursorController: MultiCursorSelectionController, findController: CommonFindController): void { + multiCursorController.moveSelectionToNextFindMatch(findController); + } +} + +@editorAction +export class MoveSelectionToPreviousFindMatchAction extends MultiCursorSelectionControllerAction { + 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 + }); + } + protected _run(multiCursorController: MultiCursorSelectionController, findController: CommonFindController): void { + multiCursorController.moveSelectionToPreviousFindMatch(findController); + } +} + +@editorAction +export class SelectHighlightsAction extends MultiCursorSelectionControllerAction { + 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, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_L + } + }); + } + protected _run(multiCursorController: MultiCursorSelectionController, findController: CommonFindController): void { + multiCursorController.selectAll(findController); + } +} + +@editorAction +export class CompatChangeAll extends MultiCursorSelectionControllerAction { + constructor() { + super({ + id: 'editor.action.changeAll', + label: nls.localize('changeAll.label', "Change All Occurrences"), + alias: 'Change All Occurrences', + precondition: EditorContextKeys.writable, + kbOpts: { + kbExpr: EditorContextKeys.textFocus, + primary: KeyMod.CtrlCmd | KeyCode.F2 + }, + menuOpts: { + group: '1_modification', + order: 1.2 + } + }); + } + protected _run(multiCursorController: MultiCursorSelectionController, findController: CommonFindController): void { + multiCursorController.selectAll(findController); + } +} + +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) { + this.lastWordUnderCursor = lastWordUnderCursor; + 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 + ); + } +} + +@commonEditorContribution +export class SelectionHighlighter extends Disposable implements IEditorContribution { + private static ID = 'editor.contrib.selectionHighlighter'; + + private editor: ICommonCodeEditor; + private _isEnabled: boolean; + private decorations: string[]; + private updateSoon: RunOnceScheduler; + private state: SelectionHighlighterState; + + constructor(editor: ICommonCodeEditor) { + 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); + })); + this._register(CommonFindController.get(editor).getState().addChangeListener((e) => { + this._update(); + })); + } + + public getId(): string { + return SelectionHighlighter.ID; + } + + private _update(): void { + this._setState(SelectionHighlighter._createState(this._isEnabled, this.editor)); + } + + private static _createState(isEnabled: boolean, editor: ICommonCodeEditor): SelectionHighlighterState { + if (!isEnabled) { + return null; + } + const model = editor.getModel(); + if (!model) { + return null; + } + const s = editor.getSelection(); + if (s.startLineNumber !== s.endLineNumber) { + // multiline forbidden for perf reasons + return null; + } + const multiCursorController = MultiCursorSelectionController.get(editor); + if (!multiCursorController) { + return null; + } + const findController = CommonFindController.get(editor); + if (!findController) { + return null; + } + 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; + } + } + + r = MultiCursorSession.create(editor, findController); + } + if (!r) { + return null; + } + + let lastWordUnderCursor: Selection = null; + 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; + } + + const config = editor.getConfiguration(); + 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; + } + + // TODO: better handling of this case + const findState = findController.getState(); + 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) { + if (this.decorations.length > 0) { + this.decorations = this.editor.deltaDecorations(this.decorations, []); + } + return; + } + + const model = this.editor.getModel(); + 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 + matches.push(match); + 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); + } + + private static _SELECTION_HIGHLIGHT_OVERVIEW = ModelDecorationOptions.register({ + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + className: 'selectionHighlight', + overviewRuler: { + color: themeColorFromId(overviewRulerSelectionHighlightForeground), + darkColor: themeColorFromId(overviewRulerSelectionHighlightForeground), + position: OverviewRulerLane.Center + } + }); + + private static _SELECTION_HIGHLIGHT = ModelDecorationOptions.register({ + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + className: 'selectionHighlight', + }); + + public dispose(): void { + this._setState(null); + super.dispose(); + } +} + +function modelRangesContainSameText(model: IModel, ranges: Range[], matchCase: boolean): boolean { + 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; +} + +function getValueInRange(model: IModel, range: Range, toLowerCase: boolean): string { + const text = model.getValueInRange(range); + return (toLowerCase ? text.toLowerCase() : text); +} diff --git a/src/vs/editor/contrib/multicursor/test/common/multicursor.test.ts b/src/vs/editor/contrib/multicursor/test/common/multicursor.test.ts index 7824a0df71eaa..93c8377b9d06b 100644 --- a/src/vs/editor/contrib/multicursor/test/common/multicursor.test.ts +++ b/src/vs/editor/contrib/multicursor/test/common/multicursor.test.ts @@ -5,11 +5,14 @@ 'use strict'; import * as assert from 'assert'; -import { withMockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor'; +import { withMockCodeEditor, MockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor'; import { Selection } from 'vs/editor/common/core/selection'; -import { InsertCursorAbove, InsertCursorBelow } from 'vs/editor/contrib/multicursor/common/multicursor'; -import { Handler } from 'vs/editor/common/editorCommon'; - +import { Range } from 'vs/editor/common/core/range'; +import { InsertCursorAbove, InsertCursorBelow, MultiCursorSelectionController, SelectHighlightsAction, AddSelectionToNextFindMatchAction } from 'vs/editor/contrib/multicursor/common/multicursor'; +import { Handler, EndOfLineSequence } from 'vs/editor/common/editorCommon'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { CommonFindController } from 'vs/editor/contrib/find/common/findController'; suite('Multicursor', () => { @@ -42,3 +45,525 @@ suite('Multicursor', () => { }); }); + +function fromRange(rng: Range): number[] { + return [rng.startLineNumber, rng.startColumn, rng.endLineNumber, rng.endColumn]; +} + +suite('Multicursor selection', () => { + let queryState: { [key: string]: any; } = {}; + let serviceCollection = new ServiceCollection(); + serviceCollection.set(IStorageService, { + get: (key: string) => queryState[key], + getBoolean: (key: string) => !!queryState[key], + store: (key: string, value: any) => { queryState[key] = value; } + }); + + test('issue #8817: Cursor position changes when you cancel multicursor', () => { + withMockCodeEditor([ + 'var x = (3 * 5)', + 'var y = (3 * 5)', + 'var z = (3 * 5)', + ], { serviceCollection: serviceCollection }, (editor, cursor) => { + + let findController = editor.registerAndInstantiateContribution(CommonFindController); + let multiCursorSelectController = editor.registerAndInstantiateContribution(MultiCursorSelectionController); + let selectHighlightsAction = new SelectHighlightsAction(); + + editor.setSelection(new Selection(2, 9, 2, 16)); + + selectHighlightsAction.run(null, editor); + assert.deepEqual(editor.getSelections().map(fromRange), [ + [2, 9, 2, 16], + [1, 9, 1, 16], + [3, 9, 3, 16], + ]); + + editor.trigger('test', 'removeSecondaryCursors', null); + + assert.deepEqual(fromRange(editor.getSelection()), [2, 9, 2, 16]); + + multiCursorSelectController.dispose(); + findController.dispose(); + }); + }); + + test('issue #5400: "Select All Occurrences of Find Match" does not select all if find uses regex', () => { + withMockCodeEditor([ + 'something', + 'someething', + 'someeething', + 'nothing' + ], { serviceCollection: serviceCollection }, (editor, cursor) => { + + let findController = editor.registerAndInstantiateContribution(CommonFindController); + let multiCursorSelectController = editor.registerAndInstantiateContribution(MultiCursorSelectionController); + let selectHighlightsAction = new SelectHighlightsAction(); + + editor._isFocused = false; + + editor.setSelection(new Selection(1, 1, 1, 1)); + findController.getState().change({ searchString: 'some+thing', isRegex: true, isRevealed: true }, false); + + selectHighlightsAction.run(null, editor); + assert.deepEqual(editor.getSelections().map(fromRange), [ + [1, 1, 1, 10], + [2, 1, 2, 11], + [3, 1, 3, 12], + ]); + + assert.equal(findController.getState().searchString, 'some+thing'); + + multiCursorSelectController.dispose(); + findController.dispose(); + }); + }); + + test('AddSelectionToNextFindMatchAction can work with multiline', () => { + withMockCodeEditor([ + '', + 'qwe', + 'rty', + '', + 'qwe', + '', + 'rty', + 'qwe', + 'rty' + ], { serviceCollection: serviceCollection }, (editor, cursor) => { + + let findController = editor.registerAndInstantiateContribution(CommonFindController); + let multiCursorSelectController = editor.registerAndInstantiateContribution(MultiCursorSelectionController); + let addSelectionToNextFindMatch = new AddSelectionToNextFindMatchAction(); + + editor.setSelection(new Selection(2, 1, 3, 4)); + + addSelectionToNextFindMatch.run(null, editor); + assert.deepEqual(editor.getSelections().map(fromRange), [ + [2, 1, 3, 4], + [8, 1, 9, 4] + ]); + + editor.trigger('test', 'removeSecondaryCursors', null); + + assert.deepEqual(fromRange(editor.getSelection()), [2, 1, 3, 4]); + + multiCursorSelectController.dispose(); + findController.dispose(); + }); + }); + + test('issue #6661: AddSelectionToNextFindMatchAction can work with touching ranges', () => { + withMockCodeEditor([ + 'abcabc', + 'abc', + 'abcabc', + ], { serviceCollection: serviceCollection }, (editor, cursor) => { + + let findController = editor.registerAndInstantiateContribution(CommonFindController); + let multiCursorSelectController = editor.registerAndInstantiateContribution(MultiCursorSelectionController); + let addSelectionToNextFindMatch = new AddSelectionToNextFindMatchAction(); + + editor.setSelection(new Selection(1, 1, 1, 4)); + + addSelectionToNextFindMatch.run(null, editor); + assert.deepEqual(editor.getSelections().map(fromRange), [ + [1, 1, 1, 4], + [1, 4, 1, 7] + ]); + + addSelectionToNextFindMatch.run(null, editor); + addSelectionToNextFindMatch.run(null, editor); + addSelectionToNextFindMatch.run(null, editor); + assert.deepEqual(editor.getSelections().map(fromRange), [ + [1, 1, 1, 4], + [1, 4, 1, 7], + [2, 1, 2, 4], + [3, 1, 3, 4], + [3, 4, 3, 7] + ]); + + editor.trigger('test', Handler.Type, { text: 'z' }); + assert.deepEqual(editor.getSelections().map(fromRange), [ + [1, 2, 1, 2], + [1, 3, 1, 3], + [2, 2, 2, 2], + [3, 2, 3, 2], + [3, 3, 3, 3] + ]); + assert.equal(editor.getValue(), [ + 'zz', + 'z', + 'zz', + ].join('\n')); + + multiCursorSelectController.dispose(); + findController.dispose(); + }); + }); + + test('issue #23541: Multiline Ctrl+D does not work in CRLF files', () => { + withMockCodeEditor([ + '', + 'qwe', + 'rty', + '', + 'qwe', + '', + 'rty', + 'qwe', + 'rty' + ], { serviceCollection: serviceCollection }, (editor, cursor) => { + + editor.getModel().setEOL(EndOfLineSequence.CRLF); + + let findController = editor.registerAndInstantiateContribution(CommonFindController); + let multiCursorSelectController = editor.registerAndInstantiateContribution(MultiCursorSelectionController); + let addSelectionToNextFindMatch = new AddSelectionToNextFindMatchAction(); + + editor.setSelection(new Selection(2, 1, 3, 4)); + + addSelectionToNextFindMatch.run(null, editor); + assert.deepEqual(editor.getSelections().map(fromRange), [ + [2, 1, 3, 4], + [8, 1, 9, 4] + ]); + + editor.trigger('test', 'removeSecondaryCursors', null); + + assert.deepEqual(fromRange(editor.getSelection()), [2, 1, 3, 4]); + + multiCursorSelectController.dispose(); + findController.dispose(); + }); + }); + + function testMulticursor(text: string[], callback: (editor: MockCodeEditor, findController: CommonFindController) => void): void { + withMockCodeEditor(text, { serviceCollection: serviceCollection }, (editor, cursor) => { + let findController = editor.registerAndInstantiateContribution(CommonFindController); + let multiCursorSelectController = editor.registerAndInstantiateContribution(MultiCursorSelectionController); + + callback(editor, findController); + + multiCursorSelectController.dispose(); + findController.dispose(); + }); + } + + function testAddSelectionToNextFindMatchAction(text: string[], callback: (editor: MockCodeEditor, action: AddSelectionToNextFindMatchAction, findController: CommonFindController) => void): void { + testMulticursor(text, (editor, findController) => { + let action = new AddSelectionToNextFindMatchAction(); + callback(editor, action, findController); + }); + } + + test('AddSelectionToNextFindMatchAction starting with single collapsed selection', () => { + const text = [ + 'abc pizza', + 'abc house', + 'abc bar' + ]; + testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => { + editor.setSelections([ + new Selection(1, 2, 1, 2), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(2, 1, 2, 4), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(2, 1, 2, 4), + new Selection(3, 1, 3, 4), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(2, 1, 2, 4), + new Selection(3, 1, 3, 4), + ]); + }); + }); + + test('AddSelectionToNextFindMatchAction starting with two selections, one being collapsed 1)', () => { + const text = [ + 'abc pizza', + 'abc house', + 'abc bar' + ]; + testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => { + editor.setSelections([ + new Selection(1, 1, 1, 4), + new Selection(2, 2, 2, 2), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(2, 1, 2, 4), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(2, 1, 2, 4), + new Selection(3, 1, 3, 4), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(2, 1, 2, 4), + new Selection(3, 1, 3, 4), + ]); + }); + }); + + test('AddSelectionToNextFindMatchAction starting with two selections, one being collapsed 2)', () => { + const text = [ + 'abc pizza', + 'abc house', + 'abc bar' + ]; + testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => { + editor.setSelections([ + new Selection(1, 2, 1, 2), + new Selection(2, 1, 2, 4), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(2, 1, 2, 4), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(2, 1, 2, 4), + new Selection(3, 1, 3, 4), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(2, 1, 2, 4), + new Selection(3, 1, 3, 4), + ]); + }); + }); + + test('AddSelectionToNextFindMatchAction starting with all collapsed selections', () => { + const text = [ + 'abc pizza', + 'abc house', + 'abc bar' + ]; + testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => { + editor.setSelections([ + new Selection(1, 2, 1, 2), + new Selection(2, 2, 2, 2), + new Selection(3, 1, 3, 1), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(2, 1, 2, 4), + new Selection(3, 1, 3, 4), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(2, 1, 2, 4), + new Selection(3, 1, 3, 4), + ]); + }); + }); + + test('AddSelectionToNextFindMatchAction starting with all collapsed selections on different words', () => { + const text = [ + 'abc pizza', + 'abc house', + 'abc bar' + ]; + testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => { + editor.setSelections([ + new Selection(1, 6, 1, 6), + new Selection(2, 6, 2, 6), + new Selection(3, 6, 3, 6), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 5, 1, 10), + new Selection(2, 5, 2, 10), + new Selection(3, 5, 3, 8), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 5, 1, 10), + new Selection(2, 5, 2, 10), + new Selection(3, 5, 3, 8), + ]); + }); + }); + + test('issue #20651: AddSelectionToNextFindMatchAction case insensitive', () => { + const text = [ + 'test', + 'testte', + 'Test', + 'testte', + 'test' + ]; + testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => { + editor.setSelections([ + new Selection(1, 1, 1, 5), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 5), + new Selection(2, 1, 2, 5), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 5), + new Selection(2, 1, 2, 5), + new Selection(3, 1, 3, 5), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 5), + new Selection(2, 1, 2, 5), + new Selection(3, 1, 3, 5), + new Selection(4, 1, 4, 5), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 5), + new Selection(2, 1, 2, 5), + new Selection(3, 1, 3, 5), + new Selection(4, 1, 4, 5), + new Selection(5, 1, 5, 5), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 5), + new Selection(2, 1, 2, 5), + new Selection(3, 1, 3, 5), + new Selection(4, 1, 4, 5), + new Selection(5, 1, 5, 5), + ]); + }); + }); + + suite('Find state disassociation', () => { + + const text = [ + 'app', + 'apples', + 'whatsapp', + 'app', + 'App', + ' app' + ]; + + test('enters mode', () => { + testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => { + editor.setSelections([ + new Selection(1, 2, 1, 2), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(4, 1, 4, 4), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(4, 1, 4, 4), + new Selection(6, 2, 6, 5), + ]); + }); + }); + + test('leaves mode when selection changes', () => { + testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => { + editor.setSelections([ + new Selection(1, 2, 1, 2), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(4, 1, 4, 4), + ]); + + // change selection + editor.setSelections([ + new Selection(1, 1, 1, 4), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(2, 1, 2, 4), + ]); + }); + }); + + test('Select Highlights respects mode ', () => { + testMulticursor(text, (editor, findController) => { + let action = new SelectHighlightsAction(); + editor.setSelections([ + new Selection(1, 2, 1, 2), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(4, 1, 4, 4), + new Selection(6, 2, 6, 5), + ]); + + action.run(null, editor); + assert.deepEqual(editor.getSelections(), [ + new Selection(1, 1, 1, 4), + new Selection(4, 1, 4, 4), + new Selection(6, 2, 6, 5), + ]); + }); + }); + + }); +}); diff --git a/src/vs/editor/test/common/mocks/mockCodeEditor.ts b/src/vs/editor/test/common/mocks/mockCodeEditor.ts index afb99c73774ab..33621c3d209e2 100644 --- a/src/vs/editor/test/common/mocks/mockCodeEditor.ts +++ b/src/vs/editor/test/common/mocks/mockCodeEditor.ts @@ -18,6 +18,9 @@ import * as editorOptions from 'vs/editor/common/config/editorOptions'; import { IDisposable } from 'vs/base/common/lifecycle'; export class MockCodeEditor extends CommonCodeEditor { + + public _isFocused = true; + protected _createConfiguration(options: editorOptions.IEditorOptions): CommonEditorConfiguration { return new TestConfiguration(options); } @@ -25,7 +28,7 @@ export class MockCodeEditor extends CommonCodeEditor { public layout(dimension?: editorCommon.IDimension): void { } public focus(): void { } - public isFocused(): boolean { return true; } + public isFocused(): boolean { return this._isFocused; } public hasWidgetFocus(): boolean { return true; }; protected _enableEmptySelectionClipboard(): boolean { return false; } diff --git a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts index 4487502062941..a20434dd807d7 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesEditor.ts @@ -49,7 +49,7 @@ import { getCodeEditor } from 'vs/editor/common/services/codeEditorService'; import { IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; import { FoldingController } from 'vs/editor/contrib/folding/browser/folding'; import { FindController } from 'vs/editor/contrib/find/browser/find'; -import { SelectionHighlighter } from 'vs/editor/contrib/find/common/findController'; +import { SelectionHighlighter } from 'vs/editor/contrib/multicursor/common/multicursor'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { attachStylerCallback } from 'vs/platform/theme/common/styler';