diff --git a/src/vs/base/browser/ui/findinput/findInput.ts b/src/vs/base/browser/ui/findinput/findInput.ts index e05585ac8df..380e3e65702 100644 --- a/src/vs/base/browser/ui/findinput/findInput.ts +++ b/src/vs/base/browser/ui/findinput/findInput.ts @@ -48,8 +48,7 @@ export class FindInput { private caseSensitive:Checkbox.Checkbox; public domNode: HTMLElement; public validationNode: Builder.Builder; - private inputNode:HTMLInputElement; - private inputBox:InputBox.InputBox; + public inputBox:InputBox.InputBox; constructor(parent:HTMLElement, contextViewProvider: ContextView.IContextViewProvider, options?:IOptions) { this.contextViewProvider = contextViewProvider; @@ -64,7 +63,6 @@ export class FindInput { this.wholeWords = null; this.caseSensitive = null; this.domNode = null; - this.inputNode = null; this.inputBox = null; this.validationNode = null; diff --git a/src/vs/editor/contrib/find/browser/find.ts b/src/vs/editor/contrib/find/browser/find.ts index 2065cf48786..3512c181514 100644 --- a/src/vs/editor/contrib/find/browser/find.ts +++ b/src/vs/editor/contrib/find/browser/find.ts @@ -4,18 +4,16 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import * as nls from 'vs/nls'; import {TPromise} from 'vs/base/common/winjs.base'; import {EditorBrowserRegistry} from 'vs/editor/browser/editorBrowserExtensions'; import {CommonEditorRegistry, ContextKey, EditorActionDescriptor} from 'vs/editor/common/editorCommonExtensions'; import {EditorAction, Behaviour} from 'vs/editor/common/editorAction'; -import FindWidget = require('./findWidget'); -import FindModel = require('vs/editor/contrib/find/common/findModel'); -import nls = require('vs/nls'); -import EventEmitter = require('vs/base/common/eventEmitter'); -import EditorBrowser = require('vs/editor/browser/editorBrowser'); -import Lifecycle = require('vs/base/common/lifecycle'); -import config = require('vs/editor/common/config/config'); -import EditorCommon = require('vs/editor/common/editorCommon'); +import {IFindController, FindWidget} from 'vs/editor/contrib/find/browser/findWidget'; +import {FindModelBoundToEditorModel, FindIds} from 'vs/editor/contrib/find/common/findModel'; +import * as EditorBrowser from 'vs/editor/browser/editorBrowser'; +import {IDisposable, disposeAll} from 'vs/base/common/lifecycle'; +import * as EditorCommon from 'vs/editor/common/editorCommon'; import {Selection} from 'vs/editor/common/core/selection'; import {IKeybindingService, IKeybindingContextKey, IKeybindings} from 'vs/platform/keybinding/common/keybindingService'; import {IContextViewService} from 'vs/platform/contextview/browser/contextView'; @@ -23,6 +21,7 @@ import {INullService} from 'vs/platform/instantiation/common/instantiation'; import {KeyMod, KeyCode} from 'vs/base/common/keyCodes'; import {Range} from 'vs/editor/common/core/range'; import {OccurrencesRegistry} from 'vs/editor/contrib/wordHighlighter/common/wordHighlighter'; +import {INewFindReplaceState, FindReplaceStateChangedEvent, FindReplaceState} from 'vs/editor/contrib/find/common/findState'; enum FindStartFocusAction { NoFocusChange, @@ -43,47 +42,40 @@ const CONTEXT_FIND_WIDGET_VISIBLE = 'findWidgetVisible'; /** * The Find controller will survive an editor.setModel(..) call */ -class FindController implements EditorCommon.IEditorContribution, FindWidget.IFindController { +class FindController implements EditorCommon.IEditorContribution, IFindController { static ID = 'editor.contrib.findController'; - private static _STATE_CHANGED_EVENT = 'stateChanged'; - - private editor:EditorBrowser.ICodeEditor; + private _editor: EditorBrowser.ICodeEditor; + private _toDispose: IDisposable[]; private _findWidgetVisible: IKeybindingContextKey; - private model:FindModel.FindModelBoundToEditorModel; - private widget:FindWidget.FindWidget; - private widgetIsVisible:boolean; - private widgetListeners:Lifecycle.IDisposable[]; - - private editorListeners:EventEmitter.ListenerUnbind[]; - private lastState:FindModel.IFindState; - private _eventEmitter: EventEmitter.EventEmitter; + private _state: FindReplaceState; + private _widget: FindWidget; + private _model: FindModelBoundToEditorModel; static getFindController(editor:EditorCommon.ICommonCodeEditor): FindController { return editor.getContribution(FindController.ID); } constructor(editor:EditorBrowser.ICodeEditor, @IContextViewService contextViewService: IContextViewService, @IKeybindingService keybindingService: IKeybindingService) { + this._editor = editor; + this._toDispose = []; this._findWidgetVisible = keybindingService.createKey(CONTEXT_FIND_WIDGET_VISIBLE, false); - this.editor = editor; - this.model = null; - this.widgetIsVisible = false; - this.lastState = null; + this._state = new FindReplaceState(); + this._toDispose.push(this._state); + this._toDispose.push(this._state.addChangeListener((e) => this._onStateChanged(e))) - this.widget = new FindWidget.FindWidget(this.editor, this, contextViewService, keybindingService); + this._widget = new FindWidget(this._editor, this, this._state, contextViewService, keybindingService); + this._toDispose.push(this._widget); - this.widgetListeners = []; - this.widgetListeners.push(this.widget.addUserInputEventListener((e) => this.onWidgetUserInput(e))); - this.widgetListeners.push(this.widget.addClosedEventListener(() => this.onWidgetClosed())); + this._model = null; - this.editorListeners = []; - this.editorListeners.push(this.editor.addListener(EditorCommon.EventType.ModelChanged, () => { - let shouldRestartFind = (this.editor.getModel() && this.lastState && this.widgetIsVisible); + this._toDispose.push(this._editor.addListener2(EditorCommon.EventType.ModelChanged, () => { + let shouldRestartFind = (this._editor.getModel() && this._state.isRevealed); - this.disposeBindingAndModel(); + this.disposeModel(); if (shouldRestartFind) { this._start({ @@ -95,130 +87,109 @@ class FindController implements EditorCommon.IEditorContribution, FindWidget.IFi }); } })); - this.editorListeners.push(this.editor.addListener(EditorCommon.EventType.Disposed, () => { - this.editorListeners.forEach((element:EventEmitter.ListenerUnbind) => { - element(); - }); - this.editorListeners = []; - })); + } - this._eventEmitter = new EventEmitter.EventEmitter([FindController._STATE_CHANGED_EVENT]); + public dispose(): void { + this.disposeModel(); + this._toDispose = disposeAll(this._toDispose); + } + + private disposeModel(): void { + if (this._model) { + this._model.dispose(); + this._model = null; + } } public getId(): string { return FindController.ID; } - public dispose(): void { - this.widgetListeners = Lifecycle.disposeAll(this.widgetListeners); - this.widget.dispose(); - this.disposeBindingAndModel(); - this._eventEmitter.dispose(); - } - - public onStateChanged(listener:()=>void): Lifecycle.IDisposable { - return this._eventEmitter.addListener2(FindController._STATE_CHANGED_EVENT, listener); - } - - private disposeBindingAndModel(): void { - this._findWidgetVisible.reset(); - this.widget.setModel(null); - if (this.model) { - this.model.dispose(); - this.model = null; + private _onStateChanged(e:FindReplaceStateChangedEvent): void { + if (e.isRevealed) { + if (this._state.isRevealed) { + this._findWidgetVisible.set(true); + } else { + this._findWidgetVisible.reset(); + this.disposeModel(); + } } } + public getState(): FindReplaceState { + return this._state; + } + public closeFindWidget(): void { - this.widgetIsVisible = false; - this.disposeBindingAndModel(); - this.editor.focus(); + this._state.change({ isRevealed: false }, false); + this._editor.focus(); } public toggleCaseSensitive(): void { - this.widget.toggleCaseSensitive(); + this._state.change({ matchCase: !this._state.matchCase }, false); } public toggleWholeWords(): void { - this.widget.toggleWholeWords(); + this._state.change({ wholeWord: !this._state.wholeWord }, false); } public toggleRegex(): void { - this.widget.toggleRegex(); - } - - private onWidgetClosed(): void { - this.widgetIsVisible = false; - this.disposeBindingAndModel(); - } - - public getFindState(): FindModel.IFindState { - return this.lastState; + this._state.change({ isRegex: !this._state.isRegex }, false); } public setSearchString(searchString:string): void { - this.widget.setSearchString(searchString); - } - - private onWidgetUserInput(e:FindWidget.IUserInputEvent): void { - this.lastState = this.widget.getState(); - if (this.model) { - this.model.recomputeMatches(this.lastState, e.jumpToNextMatch); - } - this._eventEmitter.emit(FindController._STATE_CHANGED_EVENT); + this._state.change({ searchString: searchString }, false); } private _start(opts:IFindStartOptions): void { - if (!this.editor.getModel()) { + this.disposeModel(); + + if (!this._editor.getModel()) { // cannot do anything with an editor that doesn't have a model... return; } - if (!this.model) { - this.model = new FindModel.FindModelBoundToEditorModel(this.editor); - this.widget.setModel(this.model); - } - - this._findWidgetVisible.set(true); - - // Get a default state if none existed before - this.lastState = this.lastState || this.widget.getState(); + let stateChanges: INewFindReplaceState = { + isRevealed: true + }; // Consider editor selection and overwrite the state with it - let selection = this.editor.getSelection(); + let selection = this._editor.getSelection(); if (opts.seedSearchStringFromSelection) { if (selection.startLineNumber === selection.endLineNumber) { if (selection.isEmpty()) { - let wordAtPosition = this.editor.getModel().getWordAtPosition(selection.getStartPosition()); + let wordAtPosition = this._editor.getModel().getWordAtPosition(selection.getStartPosition()); if (wordAtPosition) { - this.lastState.searchString = wordAtPosition.word; + stateChanges.searchString = wordAtPosition.word; } } else { - this.lastState.searchString = this.editor.getModel().getValueInRange(selection); + stateChanges.searchString = this._editor.getModel().getValueInRange(selection); } } } - let searchScope:EditorCommon.IEditorRange = null; + stateChanges.searchScope = null; if (opts.seedSearchScopeFromSelection && selection.startLineNumber < selection.endLineNumber) { // Take search scope into account only if it is more than one line. - searchScope = selection; + stateChanges.searchScope = selection; } // Overwrite isReplaceRevealed if (opts.forceRevealReplace) { - this.lastState.isReplaceRevealed = true; + stateChanges.isReplaceRevealed = true; } - // Start searching - this.model.start(this.lastState, searchScope, opts.shouldAnimate); - this.widgetIsVisible = true; + this._state.change(stateChanges, false); + + if (!this._model) { + this._model = new FindModelBoundToEditorModel(this._editor, this._state); + } if (opts.shouldFocus === FindStartFocusAction.FocusReplaceInput) { - this.widget.focusReplaceInput(); + this._widget.focusReplaceInput(); } else if (opts.shouldFocus === FindStartFocusAction.FocusFindInput) { - this.widget.focusFindInput(); + this._widget.focusFindInput(); } } @@ -232,45 +203,33 @@ class FindController implements EditorCommon.IEditorContribution, FindWidget.IFi }); } - public next(): boolean { - if (this.model) { - this.model.next(); + public moveToNextMatch(): boolean { + if (this._model) { + this._model.moveToNextMatch(); return true; } return false; } - public prev(): boolean { - if (this.model) { - this.model.prev(); + public moveToPrevMatch(): boolean { + if (this._model) { + this._model.moveToPrevMatch(); return true; } return false; } - public enableSelectionFind(): void { - if (this.model) { - this.model.setFindScope(this.editor.getSelection()); - } - } - - public disableSelectionFind(): void { - if (this.model) { - this.model.setFindScope(null); - } - } - public replace(): boolean { - if (this.model) { - this.model.replace(); + if (this._model) { + this._model.replace(); return true; } return false; } public replaceAll(): boolean { - if (this.model) { - this.model.replaceAll(); + if (this._model) { + this._model.replaceAll(); return true; } return false; @@ -298,9 +257,9 @@ export class NextMatchFindAction extends EditorAction { public run(): TPromise { let controller = FindController.getFindController(this.editor); - if (!controller.next()) { + if (!controller.moveToNextMatch()) { controller.startFromAction(false); - controller.next(); + controller.moveToNextMatch(); } return TPromise.as(true); } @@ -314,9 +273,9 @@ export class PreviousMatchFindAction extends EditorAction { public run(): TPromise { let controller = FindController.getFindController(this.editor); - if (!controller.prev()) { + if (!controller.moveToPrevMatch()) { controller.startFromAction(false); - controller.prev(); + controller.moveToPrevMatch(); } return TPromise.as(true); } @@ -346,25 +305,20 @@ export interface IMultiCursorFindResult { export function multiCursorFind(editor:EditorCommon.ICommonCodeEditor, changeFindSearchString:boolean): IMultiCursorFindResult { let controller = FindController.getFindController(editor); - let state = controller.getFindState(); + let state = controller.getState(); let searchText: string, - isRegex = false, - wholeWord = false, - matchCase = false, nextMatch: EditorCommon.IEditorSelection; // In any case, if the find widget was ever opened, the options are taken from it - if (state) { - isRegex = state.properties.isRegex; - wholeWord = state.properties.wholeWord; - matchCase = state.properties.matchCase; - } + let isRegex = state.isRegex; + 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 && state.searchString.length > 0) { + if (!editor.isFocused() && state.isRevealed && state.searchString.length > 0) { // Find widget owns what is searched for searchText = state.searchString; } else { @@ -474,7 +428,6 @@ class MoveSelectionToNextFindMatchAction extends SelectNextFindMatchAction { } class SelectHighlightsAction extends EditorAction { - static ID = 'editor.action.selectHighlights'; static COMPAT_ID = 'editor.action.changeAll'; @@ -506,12 +459,11 @@ class SelectHighlightsAction extends EditorAction { } export class SelectionHighlighter implements EditorCommon.IEditorContribution { - static ID = 'editor.contrib.selectionHighlighter'; - private editor:EditorCommon.ICommonCodeEditor; - private decorations:string[]; - private toDispose:Lifecycle.IDisposable[]; + private editor: EditorCommon.ICommonCodeEditor; + private decorations: string[]; + private toDispose: IDisposable[]; constructor(editor:EditorCommon.ICommonCodeEditor, @INullService ns) { this.editor = editor; @@ -522,7 +474,7 @@ export class SelectionHighlighter implements EditorCommon.IEditorContribution { this.toDispose.push(editor.addListener2(EditorCommon.EventType.ModelChanged, (e) => { this.removeDecorations(); })); - this.toDispose.push(FindController.getFindController(editor).onStateChanged(() => this._update())); + this.toDispose.push(FindController.getFindController(editor).getState().addChangeListener((e) => this._update())); } public getId(): string { @@ -613,7 +565,7 @@ export class SelectionHighlighter implements EditorCommon.IEditorContribution { public dispose(): void { this.removeDecorations(); - this.toDispose = Lifecycle.disposeAll(this.toDispose); + this.toDispose = disposeAll(this.toDispose); } } @@ -629,22 +581,22 @@ CommonEditorRegistry.registerEditorAction(new EditorActionDescriptor(SelectHighl })); // register actions -CommonEditorRegistry.registerEditorAction(new EditorActionDescriptor(StartFindAction, FindModel.START_FIND_ACTION_ID, nls.localize('startFindAction',"Find"), { +CommonEditorRegistry.registerEditorAction(new EditorActionDescriptor(StartFindAction, FindIds.START_FIND_ACTION_ID, nls.localize('startFindAction',"Find"), { context: ContextKey.None, primary: KeyMod.CtrlCmd | KeyCode.KEY_F, secondary: [KeyMod.CtrlCmd | KeyCode.F3] })); -CommonEditorRegistry.registerEditorAction(new EditorActionDescriptor(NextMatchFindAction, FindModel.NEXT_MATCH_FIND_ACTION_ID, nls.localize('findNextMatchAction', "Find Next"), { +CommonEditorRegistry.registerEditorAction(new EditorActionDescriptor(NextMatchFindAction, FindIds.NEXT_MATCH_FIND_ACTION_ID, nls.localize('findNextMatchAction', "Find Next"), { context: ContextKey.EditorFocus, primary: KeyCode.F3, mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, secondary: [KeyCode.F3] } })); -CommonEditorRegistry.registerEditorAction(new EditorActionDescriptor(PreviousMatchFindAction, FindModel.PREVIOUS_MATCH_FIND_ACTION_ID, nls.localize('findPreviousMatchAction', "Find Previous"), { +CommonEditorRegistry.registerEditorAction(new EditorActionDescriptor(PreviousMatchFindAction, FindIds.PREVIOUS_MATCH_FIND_ACTION_ID, nls.localize('findPreviousMatchAction', "Find Previous"), { context: ContextKey.EditorFocus, primary: KeyMod.Shift | KeyCode.F3, mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3] } })); -CommonEditorRegistry.registerEditorAction(new EditorActionDescriptor(StartFindReplaceAction, FindModel.START_FIND_REPLACE_ACTION_ID, nls.localize('startReplace', "Replace"), { +CommonEditorRegistry.registerEditorAction(new EditorActionDescriptor(StartFindReplaceAction, FindIds.START_FIND_REPLACE_ACTION_ID, nls.localize('startReplace', "Replace"), { context: ContextKey.None, primary: KeyMod.CtrlCmd | KeyCode.KEY_H, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_F } @@ -666,18 +618,18 @@ function registerFindCommand(id:string, callback:(controller:FindController)=>vo }); } -registerFindCommand(FindModel.CLOSE_FIND_WIDGET_COMMAND_ID, x => x.closeFindWidget(), { +registerFindCommand(FindIds.CLOSE_FIND_WIDGET_COMMAND_ID, x => x.closeFindWidget(), { primary: KeyCode.Escape }, CONTEXT_FIND_WIDGET_VISIBLE); -registerFindCommand(FindModel.TOGGLE_CASE_SENSITIVE_COMMAND_ID, x => x.toggleCaseSensitive(), { +registerFindCommand(FindIds.TOGGLE_CASE_SENSITIVE_COMMAND_ID, x => x.toggleCaseSensitive(), { primary: KeyMod.Alt | KeyCode.KEY_C, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C } }); -registerFindCommand(FindModel.TOGGLE_WHOLE_WORD_COMMAND_ID, x => x.toggleWholeWords(), { +registerFindCommand(FindIds.TOGGLE_WHOLE_WORD_COMMAND_ID, x => x.toggleWholeWords(), { primary: KeyMod.Alt | KeyCode.KEY_W, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_W } }); -registerFindCommand(FindModel.TOGGLE_REGEX_COMMAND_ID, x => x.toggleRegex(), { +registerFindCommand(FindIds.TOGGLE_REGEX_COMMAND_ID, x => x.toggleRegex(), { primary: KeyMod.Alt | KeyCode.KEY_R, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_R } }); diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index e8b6422971c..584d5bb3563 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -6,29 +6,23 @@ 'use strict'; import 'vs/css!./findWidget'; -import nls = require('vs/nls'); -import Errors = require('vs/base/common/errors'); -import EventEmitter = require('vs/base/common/eventEmitter'); -import DomUtils = require('vs/base/browser/dom'); -import ContextView = require('vs/base/browser/ui/contextview/contextview'); -import Keyboard = require('vs/base/browser/keyboardEvent'); -import InputBox = require('vs/base/browser/ui/inputbox/inputBox'); -import Findinput = require('vs/base/browser/ui/findinput/findInput'); -import EditorBrowser = require('vs/editor/browser/editorBrowser'); -import EditorCommon = require('vs/editor/common/editorCommon'); -import FindModel = require('vs/editor/contrib/find/common/findModel'); -import Lifecycle = require('vs/base/common/lifecycle'); + +import * as nls from 'vs/nls'; +import * as Errors from 'vs/base/common/errors'; +import * as DomUtils from 'vs/base/browser/dom'; +import {IContextViewProvider} from 'vs/base/browser/ui/contextview/contextview'; +import {StandardKeyboardEvent} from 'vs/base/browser/keyboardEvent'; +import {InputBox, IMessage as InputBoxMessage} from 'vs/base/browser/ui/inputbox/inputBox'; +import {FindInput} from 'vs/base/browser/ui/findinput/findInput'; +import * as EditorBrowser from 'vs/editor/browser/editorBrowser'; +import * as EditorCommon from 'vs/editor/common/editorCommon'; +import {FindIds} from 'vs/editor/contrib/find/common/findModel'; +import {disposeAll, IDisposable} from 'vs/base/common/lifecycle'; import {CommonKeybindings} from 'vs/base/common/keyCodes'; import {IKeybindingService} from 'vs/platform/keybinding/common/keybindingService'; -import {Keybinding} from 'vs/base/common/keyCodes'; - -export interface IUserInputEvent { - jumpToNextMatch: boolean; -} +import {INewFindReplaceState, FindReplaceStateChangedEvent, FindReplaceState} from 'vs/editor/contrib/find/common/findState'; export interface IFindController { - enableSelectionFind(): void; - disableSelectionFind(): void; replace(): void; replaceAll(): void; } @@ -45,56 +39,49 @@ const NLS_REPLACE_BTN_LABEL = nls.localize('label.replaceButton', "Replace"); const NLS_REPLACE_ALL_BTN_LABEL = nls.localize('label.replaceAllButton', "Replace All"); const NLS_TOGGLE_REPLACE_MODE_BTN_LABEL = nls.localize('label.toggleReplaceButton', "Toggle Replace mode"); -export class FindWidget extends EventEmitter.EventEmitter implements EditorBrowser.IOverlayWidget { - - private static _USER_CLOSED_EVENT = 'close'; - private static _USER_INPUT_EVENT = 'userInputEvent'; +export class FindWidget implements EditorBrowser.IOverlayWidget { private static ID = 'editor.contrib.findWidget'; private static PART_WIDTH = 275; private static FIND_INPUT_AREA_WIDTH = FindWidget.PART_WIDTH - 54; private static REPLACE_INPUT_AREA_WIDTH = FindWidget.FIND_INPUT_AREA_WIDTH; - private _codeEditor:EditorBrowser.ICodeEditor; + private _codeEditor: EditorBrowser.ICodeEditor; + private _state: FindReplaceState; private _controller: IFindController; - private _contextViewProvider:ContextView.IContextViewProvider; + private _contextViewProvider: IContextViewProvider; private _keybindingService: IKeybindingService; - private _domNode:HTMLElement; - private _findInput:Findinput.FindInput; - private _replaceInputBox:InputBox.InputBox; + private _domNode: HTMLElement; + private _findInput: FindInput; + private _replaceInputBox: InputBox; - private _toggleReplaceBtn:SimpleButton; - private _prevBtn:SimpleButton; - private _nextBtn:SimpleButton; - private _toggleSelectionFind:Checkbox; - private _closeBtn:SimpleButton; - private _replaceBtn:SimpleButton; - private _replaceAllBtn:SimpleButton; + private _toggleReplaceBtn: SimpleButton; + private _prevBtn: SimpleButton; + private _nextBtn: SimpleButton; + private _toggleSelectionFind: Checkbox; + private _closeBtn: SimpleButton; + private _replaceBtn: SimpleButton; + private _replaceAllBtn: SimpleButton; - private _isReplaceEnabled:boolean; - private _isVisible:boolean; - private _isReplaceVisible:boolean; + private _isReplaceEnabled: boolean; + private _isVisible: boolean; + private _isReplaceVisible: boolean; - private _toDispose:Lifecycle.IDisposable[]; + private _toDispose: IDisposable[]; - private _model:FindModel.FindModelBoundToEditorModel; - private _modelListenersToDispose:Lifecycle.IDisposable[]; - - private focusTracker:DomUtils.IFocusTracker; + private focusTracker: DomUtils.IFocusTracker; constructor( - codeEditor:EditorBrowser.ICodeEditor, - controller:IFindController, - contextViewProvider:ContextView.IContextViewProvider, - keybindingService:IKeybindingService + codeEditor: EditorBrowser.ICodeEditor, + controller: IFindController, + state: FindReplaceState, + contextViewProvider: IContextViewProvider, + keybindingService: IKeybindingService ) { - super([ - FindWidget._USER_INPUT_EVENT, - FindWidget._USER_CLOSED_EVENT, - ]); this._codeEditor = codeEditor; this._controller = controller; + this._state = state; this._contextViewProvider = contextViewProvider; this._keybindingService = keybindingService; @@ -103,30 +90,34 @@ export class FindWidget extends EventEmitter.EventEmitter implements EditorBrows this._isReplaceEnabled = false; this._toDispose = []; - this._model = null; - this._modelListenersToDispose = []; + this._toDispose.push(this._state.addChangeListener((e) => this._onStateChanged(e))); this._buildDomNode(); - this.focusTracker = DomUtils.trackFocus(this._domNode); + this.focusTracker = DomUtils.trackFocus(this._findInput.inputBox.inputElement); + this.focusTracker.addFocusListener(() => this._reseedFindScope()); + this._toDispose.push(this.focusTracker); - this._codeEditor.addOverlayWidget(this); - - this.focusTracker.addFocusListener(() => { - var selection = this._codeEditor.getSelection(); - if (selection.startLineNumber !== selection.endLineNumber) { - // Search in selection - this._controller.enableSelectionFind(); + this._toDispose.push({ + dispose: () => { + this._findInput.destroy(); } }); + this._toDispose.push(this._replaceInputBox); + + this._codeEditor.addOverlayWidget(this); } public dispose(): void { - this.focusTracker.dispose(); - this._removeModel(); - this._findInput.destroy(); - this._replaceInputBox.dispose(); - this._toDispose = Lifecycle.disposeAll(this._toDispose); + this._toDispose = disposeAll(this._toDispose); + } + + private _reseedFindScope(): void { + let selection = this._codeEditor.getSelection(); + if (selection.startLineNumber !== selection.endLineNumber) { + // Reseed find scope + this._state.change({ searchScope: selection }, true); + } } // ----- IOverlayWidget API @@ -148,62 +139,60 @@ export class FindWidget extends EventEmitter.EventEmitter implements EditorBrows return null; } - public setSearchString(searchString:string): void { - this._findInput.setValue(searchString); - this._emitUserInputEvent(false); - } + // ----- React to state changes - private _setState(state:FindModel.IFindState, selectionFindEnabled:boolean): void { - this._findInput.setValue(state.searchString); - this._findInput.setCaseSensitive(state.properties.matchCase); - this._findInput.setWholeWords(state.properties.wholeWord); - this._findInput.setRegex(state.properties.isRegex); - this._toggleSelectionFind.checkbox.disabled = !selectionFindEnabled; - this._toggleSelectionFind.checkbox.checked = selectionFindEnabled; + private _onStateChanged(e:FindReplaceStateChangedEvent): void { + if (e.searchString) { + this._findInput.setValue(this._state.searchString); - this._replaceInputBox.value = state.replaceString; - if (state.isReplaceRevealed) { - this._enableReplace(false); - } else { - this._disableReplace(false); + let findInputIsNonEmpty = (this._state.searchString.length > 0); + this._prevBtn.setEnabled(findInputIsNonEmpty); + this._nextBtn.setEnabled(findInputIsNonEmpty); + this._replaceBtn.setEnabled(findInputIsNonEmpty); + this._replaceAllBtn.setEnabled(findInputIsNonEmpty); + } + if (e.replaceString) { + this._replaceInputBox.value = this._state.replaceString; + } + if (e.isRevealed) { + if (this._state.isRevealed) { + this._reveal(true); + } else { + this._hide(true); + } + } + if (e.isReplaceRevealed) { + if (this._state.isReplaceRevealed) { + this._enableReplace(); + } else { + this._disableReplace(); + } + } + if (e.isRegex) { + this._findInput.setRegex(this._state.isRegex); + } + if (e.wholeWord) { + this._findInput.setWholeWords(this._state.wholeWord); + } + if (e.matchCase) { + this._findInput.setCaseSensitive(this._state.matchCase); + } + if (e.searchScope) { + if (this._state.searchScope) { + this._toggleSelectionFind.checkbox.checked = true; + } else { + this._toggleSelectionFind.checkbox.checked = false; + } + this._updateToggleSelectionFindButton(); + } + if (e.searchString || e.matchesCount) { + let showRedOutline = (this._state.searchString.length > 0 && this._state.matchesCount === 0); + DomUtils.toggleClass(this._domNode, 'no-results', showRedOutline); } - this._onFindValueChange(); } // ----- Public - public getState(): FindModel.IFindState { - var result:FindModel.IFindState = { - searchString: this._findInput.getValue(), - replaceString: this._replaceInputBox.value, - properties: { - isRegex: this._findInput.getRegex(), - wholeWord: this._findInput.getWholeWords(), - matchCase: this._findInput.getCaseSensitive() - }, - isReplaceRevealed: this._isReplaceEnabled - }; - return result; - } - - public setModel(newFindModel:FindModel.FindModelBoundToEditorModel): void { - this._removeModel(); - if (newFindModel) { - // We have a new model! :) - this._model = newFindModel; - this._modelListenersToDispose.push(this._model.addStartEventListener((e:FindModel.IFindStartEvent) => { - this._reveal(e.shouldAnimate); - this._setState(e.state, e.selectionFindEnabled); - })); - this._modelListenersToDispose.push(this._model.addMatchesUpdatedEventListener((e:FindModel.IFindMatchesEvent) => { - DomUtils.toggleClass(this._domNode, 'no-results', this._findInput.getValue() !== '' && e.count === 0); - })); - } else { - // No model :( - this._hide(false); - } - } - public focusFindInput(): void { this._findInput.select(); // Edge browser requires focus() in addition to select() @@ -216,14 +205,7 @@ export class FindWidget extends EventEmitter.EventEmitter implements EditorBrows this._replaceInputBox.focus(); } - private _removeModel(): void { - if (this._model !== null) { - this._modelListenersToDispose = Lifecycle.disposeAll(this._modelListenersToDispose); - this._model = null; - } - } - - private _enableReplace(sendEvent:boolean): void { + private _enableReplace(): void { this._isReplaceEnabled = true; if (!this._codeEditor.getConfiguration().readOnly && !this._isReplaceVisible) { this._replaceInputBox.enable(); @@ -233,12 +215,9 @@ export class FindWidget extends EventEmitter.EventEmitter implements EditorBrows this._toggleReplaceBtn.toggleClass('expand', true); this._toggleReplaceBtn.setExpanded(true); } - if (sendEvent) { - this._emitUserInputEvent(false); - } } - private _disableReplace(sendEvent:boolean): void { + private _disableReplace(): void { this._isReplaceEnabled = false; if (this._isReplaceVisible) { this._replaceInputBox.disable(); @@ -248,22 +227,17 @@ export class FindWidget extends EventEmitter.EventEmitter implements EditorBrows this._toggleReplaceBtn.setExpanded(false); this._isReplaceVisible = false; } - if (sendEvent) { - this._emitUserInputEvent(false); - } } - // ----- initialization - private _onFindInputKeyDown(e:DomUtils.IKeyboardEvent): void { - var handled = false; + let handled = false; if (e.equals(CommonKeybindings.ENTER)) { - this._codeEditor.getAction(FindModel.NEXT_MATCH_FIND_ACTION_ID).run().done(null, Errors.onUnexpectedError); + this._codeEditor.getAction(FindIds.NEXT_MATCH_FIND_ACTION_ID).run().done(null, Errors.onUnexpectedError); handled = true; } else if (e.equals(CommonKeybindings.SHIFT_ENTER)) { - this._codeEditor.getAction(FindModel.PREVIOUS_MATCH_FIND_ACTION_ID).run().done(null, Errors.onUnexpectedError); + this._codeEditor.getAction(FindIds.PREVIOUS_MATCH_FIND_ACTION_ID).run().done(null, Errors.onUnexpectedError); handled = true; } else if (e.equals(CommonKeybindings.TAB)) { if (this._isReplaceVisible) { @@ -280,16 +254,16 @@ export class FindWidget extends EventEmitter.EventEmitter implements EditorBrows if (handled) { e.preventDefault(); } else { + // getValue() is not updated right away setTimeout(() => { - this._onFindValueChange(); - this._emitUserInputEvent(true); + this._state.change({ searchString: this._findInput.getValue() }, true); }, 10); } } private _onReplaceInputKeyDown(e:DomUtils.IKeyboardEvent): void { - var handled = false; + let handled = false; if (e.equals(CommonKeybindings.ENTER)) { this._controller.replace(); @@ -309,19 +283,12 @@ export class FindWidget extends EventEmitter.EventEmitter implements EditorBrows e.preventDefault(); } else { setTimeout(() => { - this._emitUserInputEvent(true); + this._state.change({ replaceString: this._replaceInputBox.value }, false); }, 10); } } - private _onFindValueChange(): void { - var findInputIsNonEmpty = (this._findInput.getValue().length > 0); - - this._prevBtn.setEnabled(findInputIsNonEmpty); - this._nextBtn.setEnabled(findInputIsNonEmpty); - this._replaceBtn.setEnabled(findInputIsNonEmpty); - this._replaceAllBtn.setEnabled(findInputIsNonEmpty); - } + // ----- initialization private _keybindingLabelFor(actionId:string): string { let keybindings = this._keybindingService.lookupKeybindings(actionId); @@ -333,14 +300,14 @@ export class FindWidget extends EventEmitter.EventEmitter implements EditorBrows private _buildFindPart(): HTMLElement { // Find input - this._findInput = new Findinput.FindInput(null, this._contextViewProvider, { + this._findInput = new FindInput(null, this._contextViewProvider, { width: FindWidget.FIND_INPUT_AREA_WIDTH, label: NLS_FIND_INPUT_LABEL, placeholder: NLS_FIND_INPUT_PLACEHOLDER, - appendCaseSensitiveLabel: this._keybindingLabelFor(FindModel.TOGGLE_CASE_SENSITIVE_COMMAND_ID), - appendWholeWordsLabel: this._keybindingLabelFor(FindModel.TOGGLE_WHOLE_WORD_COMMAND_ID), - appendRegexLabel: this._keybindingLabelFor(FindModel.TOGGLE_REGEX_COMMAND_ID), - validation: (value:string): InputBox.IMessage => { + appendCaseSensitiveLabel: this._keybindingLabelFor(FindIds.TOGGLE_CASE_SENSITIVE_COMMAND_ID), + appendWholeWordsLabel: this._keybindingLabelFor(FindIds.TOGGLE_WHOLE_WORD_COMMAND_ID), + appendRegexLabel: this._keybindingLabelFor(FindIds.TOGGLE_REGEX_COMMAND_ID), + validation: (value:string): InputBoxMessage => { if (value.length === 0) { return null; } @@ -355,32 +322,40 @@ export class FindWidget extends EventEmitter.EventEmitter implements EditorBrows } } }).on('keydown', (browserEvent:KeyboardEvent) => { - this._onFindInputKeyDown(new Keyboard.StandardKeyboardEvent(browserEvent)); - }).on(Findinput.FindInput.OPTION_CHANGE, () => { - this._emitUserInputEvent(true); + this._onFindInputKeyDown(new StandardKeyboardEvent(browserEvent)); + }).on(FindInput.OPTION_CHANGE, () => { + this._state.change({ + isRegex: this._findInput.getRegex(), + wholeWord: this._findInput.getWholeWords(), + matchCase: this._findInput.getCaseSensitive() + }, true); }); this._findInput.disable(); // Previous button - this._prevBtn = new SimpleButton( - NLS_PREVIOUS_MATCH_BTN_LABEL + this._keybindingLabelFor(FindModel.PREVIOUS_MATCH_FIND_ACTION_ID), - 'previous' - ).onTrigger(() => { - this._codeEditor.getAction(FindModel.PREVIOUS_MATCH_FIND_ACTION_ID).run().done(null, Errors.onUnexpectedError); + this._prevBtn = new SimpleButton({ + label: NLS_PREVIOUS_MATCH_BTN_LABEL + this._keybindingLabelFor(FindIds.PREVIOUS_MATCH_FIND_ACTION_ID), + className: 'previous', + onTrigger: () => { + this._codeEditor.getAction(FindIds.PREVIOUS_MATCH_FIND_ACTION_ID).run().done(null, Errors.onUnexpectedError); + }, + onKeyDown: (e) => {} }); this._toDispose.push(this._prevBtn); // Next button - this._nextBtn = new SimpleButton( - NLS_NEXT_MATCH_BTN_LABEL + this._keybindingLabelFor(FindModel.NEXT_MATCH_FIND_ACTION_ID), - 'next' - ).onTrigger(() => { - this._codeEditor.getAction(FindModel.NEXT_MATCH_FIND_ACTION_ID).run().done(null, Errors.onUnexpectedError); + this._nextBtn = new SimpleButton({ + label: NLS_NEXT_MATCH_BTN_LABEL + this._keybindingLabelFor(FindIds.NEXT_MATCH_FIND_ACTION_ID), + className: 'next', + onTrigger: () => { + this._codeEditor.getAction(FindIds.NEXT_MATCH_FIND_ACTION_ID).run().done(null, Errors.onUnexpectedError); + }, + onKeyDown: (e) => {} }); this._toDispose.push(this._nextBtn); - var findPart = document.createElement('div'); + let findPart = document.createElement('div'); findPart.className = 'find-part'; findPart.appendChild(this._findInput.domNode); findPart.appendChild(this._prevBtn.domNode); @@ -391,29 +366,28 @@ export class FindWidget extends EventEmitter.EventEmitter implements EditorBrows this._toggleSelectionFind.disable(); this._toDispose.push(DomUtils.addStandardDisposableListener(this._toggleSelectionFind.checkbox, 'change', (e) => { if (this._toggleSelectionFind.checkbox.checked) { - this._controller.enableSelectionFind(); + this._reseedFindScope(); } else { - this._controller.disableSelectionFind(); - this._updateToggleSelectionFindButton(); + this._state.change({ searchScope: null }, true); } })); - this._toDispose.push(this._toggleSelectionFind); this._codeEditor.addListener(EditorCommon.EventType.CursorSelectionChanged, () => { this._updateToggleSelectionFindButton(); }); // Close button - this._closeBtn = new SimpleButton( - NLS_CLOSE_BTN_LABEL + this._keybindingLabelFor(FindModel.CLOSE_FIND_WIDGET_COMMAND_ID), - 'close-fw' - ).onTrigger(() => { - this._hide(true); - this._emitClosedEvent(); - }).onKeyDown((e) => { - if (this._isReplaceVisible) { - this._replaceBtn.focus(); - e.preventDefault(); + this._closeBtn = new SimpleButton({ + label: NLS_CLOSE_BTN_LABEL + this._keybindingLabelFor(FindIds.CLOSE_FIND_WIDGET_COMMAND_ID), + className: 'close-fw', + onTrigger: () => { + this._state.change({ isRevealed: false }, false); + }, + onKeyDown: (e) => { + if (this._isReplaceVisible) { + this._replaceBtn.focus(); + e.preventDefault(); + } } }); this._toDispose.push(this._closeBtn); @@ -433,7 +407,7 @@ export class FindWidget extends EventEmitter.EventEmitter implements EditorBrows } if (!this._toggleSelectionFind.checkbox.checked) { - var selection = this._codeEditor.getSelection(); + let selection = this._codeEditor.getSelection(); if (selection.startLineNumber === selection.endLineNumber) { this._toggleSelectionFind.disable(); @@ -445,10 +419,10 @@ export class FindWidget extends EventEmitter.EventEmitter implements EditorBrows private _buildReplacePart(): HTMLElement { // Replace input - var replaceInput = document.createElement('div'); + let replaceInput = document.createElement('div'); replaceInput.className = 'replace-input'; replaceInput.style.width = FindWidget.REPLACE_INPUT_AREA_WIDTH + 'px'; - this._replaceInputBox = new InputBox.InputBox(replaceInput, null, { + this._replaceInputBox = new InputBox(replaceInput, null, { ariaLabel: NLS_REPLACE_INPUT_LABEL, placeholder: NLS_REPLACE_INPUT_PLACEHOLDER }); @@ -457,24 +431,28 @@ export class FindWidget extends EventEmitter.EventEmitter implements EditorBrows this._replaceInputBox.disable(); // Replace one button - this._replaceBtn = new SimpleButton( - NLS_REPLACE_BTN_LABEL, - 'replace' - ).onTrigger(() => { - this._controller.replace(); + this._replaceBtn = new SimpleButton({ + label: NLS_REPLACE_BTN_LABEL, + className: 'replace', + onTrigger: () => { + this._controller.replace(); + }, + onKeyDown: (e) => {} }); this._toDispose.push(this._replaceBtn); // Replace all button - this._replaceAllBtn = new SimpleButton( - NLS_REPLACE_ALL_BTN_LABEL, - 'replace-all' - ).onTrigger(() => { - this._controller.replaceAll(); + this._replaceAllBtn = new SimpleButton({ + label: NLS_REPLACE_ALL_BTN_LABEL, + className: 'replace-all', + onTrigger: () => { + this._controller.replaceAll(); + }, + onKeyDown: (e) => {} }); this._toDispose.push(this._replaceAllBtn); - var replacePart = document.createElement('div'); + let replacePart = document.createElement('div'); replacePart.className = 'replace-part'; replacePart.appendChild(replaceInput); replacePart.appendChild(this._replaceBtn.domNode); @@ -485,21 +463,19 @@ export class FindWidget extends EventEmitter.EventEmitter implements EditorBrows private _buildDomNode(): void { // Find part - var findPart = this._buildFindPart(); + let findPart = this._buildFindPart(); // Replace part - var replacePart = this._buildReplacePart(); + let replacePart = this._buildReplacePart(); // Toggle replace button - this._toggleReplaceBtn = new SimpleButton( - NLS_TOGGLE_REPLACE_MODE_BTN_LABEL, - 'toggle left' - ).onTrigger(() => { - if (this._isReplaceVisible) { - this._disableReplace(true); - } else { - this._enableReplace(true); - } + this._toggleReplaceBtn = new SimpleButton({ + label: NLS_TOGGLE_REPLACE_MODE_BTN_LABEL, + className: 'toggle left', + onTrigger: () => { + this._state.change({ isReplaceRevealed: !this._isReplaceVisible }, true); + }, + onKeyDown: (e) => {} }); this._toggleReplaceBtn.toggleClass('expand', this._isReplaceVisible); this._toggleReplaceBtn.toggleClass('collapse', !this._isReplaceVisible); @@ -532,14 +508,18 @@ export class FindWidget extends EventEmitter.EventEmitter implements EditorBrows this._toggleSelectionFind.enable(); this._closeBtn.setEnabled(true); - this._onFindValueChange(); + let findInputIsNonEmpty = (this._state.searchString.length > 0); + this._prevBtn.setEnabled(findInputIsNonEmpty); + this._nextBtn.setEnabled(findInputIsNonEmpty); + this._replaceBtn.setEnabled(findInputIsNonEmpty); + this._replaceAllBtn.setEnabled(findInputIsNonEmpty); this._isVisible = true; - window.setTimeout(() => { + setTimeout(() => { DomUtils.addClass(this._domNode, 'visible'); if (!animate) { DomUtils.addClass(this._domNode, 'noanimation'); - window.setTimeout(() => { + setTimeout(() => { DomUtils.removeClass(this._domNode, 'noanimation'); }, 200); } @@ -568,48 +548,15 @@ export class FindWidget extends EventEmitter.EventEmitter implements EditorBrows this._codeEditor.layoutOverlayWidget(this); } } - - public addUserInputEventListener(callback:(e:IUserInputEvent)=>void): Lifecycle.IDisposable { - return this.addListener2(FindWidget._USER_INPUT_EVENT, callback); - } - - private _emitUserInputEvent(jumpToNextMatch:boolean): void { - var e:IUserInputEvent = { - jumpToNextMatch: jumpToNextMatch - }; - this.emit(FindWidget._USER_INPUT_EVENT, e); - } - - public addClosedEventListener(callback:()=>void): Lifecycle.IDisposable { - return this.addListener2(FindWidget._USER_CLOSED_EVENT, callback); - } - - private _emitClosedEvent(): void { - this.emit(FindWidget._USER_CLOSED_EVENT); - } - - public toggleCaseSensitive(): void { - this._findInput.setCaseSensitive(!this._findInput.getCaseSensitive()); - this._emitUserInputEvent(false); - } - - public toggleWholeWords(): void { - this._findInput.setWholeWords(!this._findInput.getWholeWords()); - this._emitUserInputEvent(false); - } - - public toggleRegex(): void { - this._findInput.setRegex(!this._findInput.getRegex()); - this._emitUserInputEvent(false); - } } -export class Checkbox implements Lifecycle.IDisposable { +export class Checkbox { - private static COUNTER = 0; - private _domNode:HTMLElement; - private _checkbox:HTMLInputElement; - private label:HTMLLabelElement; + private static _COUNTER = 0; + + private _domNode: HTMLElement; + private _checkbox: HTMLInputElement; + private _label: HTMLLabelElement; constructor(parent: HTMLElement, title: string) { this._domNode = document.createElement('div'); @@ -619,15 +566,15 @@ export class Checkbox implements Lifecycle.IDisposable { this._checkbox = document.createElement('input'); this._checkbox.type = 'checkbox'; this._checkbox.className = 'checkbox'; - this._checkbox.id = 'checkbox-' + Checkbox.COUNTER++; + this._checkbox.id = 'checkbox-' + Checkbox._COUNTER++; - this.label = document.createElement('label'); - this.label.className = 'label'; + this._label = document.createElement('label'); + this._label.className = 'label'; // Connect the label and the checkbox. Checkbox will get checked when the label recieves a click. - this.label.htmlFor = this._checkbox.id; + this._label.htmlFor = this._checkbox.id; this._domNode.appendChild(this._checkbox); - this._domNode.appendChild(this.label); + this._domNode.appendChild(this._label); parent.appendChild(this._domNode); } @@ -651,50 +598,48 @@ export class Checkbox implements Lifecycle.IDisposable { public disable(): void { this._checkbox.disabled = true; } - - public dispose(): void { - this._domNode = null; - this._checkbox = null; - this.label = null; - } } -class SimpleButton implements Lifecycle.IDisposable { +interface ISimpleButtonOpts { + label: string; + className: string; + onTrigger: ()=>void; + onKeyDown: (e:DomUtils.IKeyboardEvent)=>void; +} - private _onTrigger:()=>void; - private _onKeyDown:(e:DomUtils.IKeyboardEvent)=>void; - private _domNode:HTMLElement; - private _toDispose:Lifecycle.IDisposable[]; +class SimpleButton implements IDisposable { - constructor(label:string, className:string) { + private _opts: ISimpleButtonOpts; + private _domNode: HTMLElement; + private _toDispose: IDisposable[]; - this._onTrigger = null; - this._onKeyDown = null; + constructor(opts:ISimpleButtonOpts) { + this._opts = opts; this._domNode = document.createElement('div'); - this._domNode.title = label; + this._domNode.title = this._opts.label; this._domNode.tabIndex = -1; - this._domNode.className = 'button ' + className; + this._domNode.className = 'button ' + this._opts.className; this._domNode.setAttribute('role', 'button'); - this._domNode.setAttribute('aria-label', label); + this._domNode.setAttribute('aria-label', this._opts.label); this._toDispose = []; this._toDispose.push(DomUtils.addStandardDisposableListener(this._domNode, 'click', (e) => { - this._invokeOnTrigger(); + this._opts.onTrigger(); e.preventDefault(); })); this._toDispose.push(DomUtils.addStandardDisposableListener(this._domNode, 'keydown', (e) => { if (e.equals(CommonKeybindings.SPACE) || e.equals(CommonKeybindings.ENTER)) { - this._invokeOnTrigger(); + this._opts.onTrigger(); e.preventDefault(); return; } - this._invokeOnKeyDown(e); + this._opts.onKeyDown(e); })); } public dispose(): void { - this._toDispose = Lifecycle.disposeAll(this._toDispose); + this._toDispose = disposeAll(this._toDispose); } public get domNode(): HTMLElement { @@ -718,26 +663,4 @@ class SimpleButton implements Lifecycle.IDisposable { public toggleClass(className:string, shouldHaveIt:boolean): void { DomUtils.toggleClass(this._domNode, className, shouldHaveIt); } - - public onTrigger(onTrigger:()=>void): SimpleButton { - this._onTrigger = onTrigger; - return this; - } - - public onKeyDown(onKeyDown:(e:DomUtils.IKeyboardEvent)=>void): SimpleButton { - this._onKeyDown = onKeyDown; - return this; - } - - private _invokeOnTrigger(): void { - if (this._onTrigger) { - this._onTrigger(); - } - } - - private _invokeOnKeyDown(e:DomUtils.IKeyboardEvent): void { - if (this._onKeyDown) { - this._onKeyDown(e); - } - } } diff --git a/src/vs/editor/contrib/find/common/findDecorations.ts b/src/vs/editor/contrib/find/common/findDecorations.ts new file mode 100644 index 00000000000..7dd035d73c0 --- /dev/null +++ b/src/vs/editor/contrib/find/common/findDecorations.ts @@ -0,0 +1,221 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import EditorCommon = require('vs/editor/common/editorCommon'); +import Strings = require('vs/base/common/strings'); +import Events = require('vs/base/common/eventEmitter'); +import ReplaceAllCommand = require('./replaceAllCommand'); +import Lifecycle = require('vs/base/common/lifecycle'); +import Schedulers = require('vs/base/common/async'); +import {Range} from 'vs/editor/common/core/range'; +import {Position} from 'vs/editor/common/core/position'; +import {ReplaceCommand} from 'vs/editor/common/commands/replaceCommand'; + +export class FindDecorations implements Lifecycle.IDisposable { + private editor:EditorCommon.ICommonCodeEditor; + + private decorations:string[]; + private decorationIndex:number; + private findScopeDecorationId:string; + private highlightedDecorationId:string; + private startPosition:EditorCommon.IEditorPosition; + + constructor(editor:EditorCommon.ICommonCodeEditor) { + this.editor = editor; + this.decorations = []; + this.decorationIndex = 0; + this.findScopeDecorationId = null; + this.highlightedDecorationId = null; + this.startPosition = this.editor.getPosition(); + } + + public dispose(): void { + this.editor.deltaDecorations(this._allDecorations(), []); + + this.editor = null; + this.decorations = []; + this.decorationIndex = 0; + this.findScopeDecorationId = null; + this.highlightedDecorationId = null; + this.startPosition = null; + } + + public reset(): void { + this.decorations = []; + this.decorationIndex = -1; + this.findScopeDecorationId = null; + this.highlightedDecorationId = null; + } + + public getFindScope(): EditorCommon.IEditorRange { + if (this.findScopeDecorationId) { + return this.editor.getModel().getDecorationRange(this.findScopeDecorationId); + } + return null; + } + + public setStartPosition(newStartPosition:EditorCommon.IEditorPosition): void { + this.startPosition = newStartPosition; + this._setDecorationIndex(-1, false); + } + + public hasMatches(): boolean { + return (this.decorations.length > 0); + } + + public getCurrentIndexRange(): EditorCommon.IEditorRange { + if (this.decorationIndex >= 0 && this.decorationIndex < this.decorations.length) { + return this.editor.getModel().getDecorationRange(this.decorations[this.decorationIndex]); + } + return null; + } + + public setIndexToFirstAfterStartPosition(): void { + this._setDecorationIndex(this.indexAfterPosition(this.startPosition), false); + } + + public moveToFirstAfterStartPosition(): void { + this._setDecorationIndex(this.indexAfterPosition(this.startPosition), true); + } + + public movePrev(): void { + if (!this.hasMatches()) { + this._revealFindScope(); + return; + } + if (this.decorationIndex === -1) { + this._setDecorationIndex(this.previousIndex(this.indexAfterPosition(this.startPosition)), true); + } else { + this._setDecorationIndex(this.previousIndex(this.decorationIndex), true); + } + } + + public moveNext(): void { + if (!this.hasMatches()) { + this._revealFindScope(); + return; + } + if (this.decorationIndex === -1) { + this._setDecorationIndex(this.indexAfterPosition(this.startPosition), true); + } else { + this._setDecorationIndex(this.nextIndex(this.decorationIndex), true); + } + } + + private _revealFindScope(): void { + let findScope = this.getFindScope(); + if (findScope) { + // Reveal the selection so user is reminded that 'selection find' is on. + this.editor.revealRangeInCenterIfOutsideViewport(findScope); + } + } + + private _setDecorationIndex(newIndex:number, moveCursor:boolean): void { + this.decorationIndex = newIndex; + this.editor.changeDecorations((changeAccessor: EditorCommon.IModelDecorationsChangeAccessor) => { + if (this.highlightedDecorationId !== null) { + changeAccessor.changeDecorationOptions(this.highlightedDecorationId, FindDecorations.createFindMatchDecorationOptions(false)); + this.highlightedDecorationId = null; + } + if (moveCursor && this.decorationIndex >= 0 && this.decorationIndex < this.decorations.length) { + this.highlightedDecorationId = this.decorations[this.decorationIndex]; + changeAccessor.changeDecorationOptions(this.highlightedDecorationId, FindDecorations.createFindMatchDecorationOptions(true)); + } + }); + if (moveCursor && this.decorationIndex >= 0 && this.decorationIndex < this.decorations.length) { + let range = this.editor.getModel().getDecorationRange(this.decorations[this.decorationIndex]); + this.editor.setSelection(range); + } + } + + public set(matches:EditorCommon.IEditorRange[], findScope:EditorCommon.IEditorRange): void { + let newDecorations: EditorCommon.IModelDeltaDecoration[] = matches.map((match) => { + return { + range: match, + options: FindDecorations.createFindMatchDecorationOptions(false) + }; + }); + if (findScope) { + newDecorations.unshift({ + range: findScope, + options: FindDecorations.createFindScopeDecorationOptions() + }); + } + let tmpDecorations = this.editor.deltaDecorations(this._allDecorations(), newDecorations); + + if (findScope) { + this.findScopeDecorationId = tmpDecorations.shift(); + } else { + this.findScopeDecorationId = null; + } + this.decorations = tmpDecorations; + this.decorationIndex = -1; + this.highlightedDecorationId = null; + } + + private _allDecorations(): string[] { + let result:string[] = []; + result = result.concat(this.decorations); + if (this.findScopeDecorationId) { + result.push(this.findScopeDecorationId); + } + return result; + } + + private indexAfterPosition(position:EditorCommon.IEditorPosition): number { + if (this.decorations.length === 0) { + return 0; + } + for (let i = 0, len = this.decorations.length; i < len; i++) { + let decorationId = this.decorations[i]; + let r = this.editor.getModel().getDecorationRange(decorationId); + if (!r || r.startLineNumber < position.lineNumber) { + continue; + } + if (r.startLineNumber > position.lineNumber) { + return i; + } + if (r.startColumn < position.column) { + continue; + } + return i; + } + return 0; + } + + private previousIndex(index:number): number { + if (this.decorations.length > 0) { + return (index - 1 + this.decorations.length) % this.decorations.length; + } + return 0; + } + + private nextIndex(index:number): number { + if (this.decorations.length > 0) { + return (index + 1) % this.decorations.length; + } + return 0; + } + + private static createFindMatchDecorationOptions(isCurrent:boolean): EditorCommon.IModelDecorationOptions { + return { + stickiness: EditorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + className: isCurrent ? 'currentFindMatch' : 'findMatch', + overviewRuler: { + color: 'rgba(246, 185, 77, 0.7)', + darkColor: 'rgba(246, 185, 77, 0.7)', + position: EditorCommon.OverviewRulerLane.Center + } + }; + } + + private static createFindScopeDecorationOptions(): EditorCommon.IModelDecorationOptions { + return { + className: 'findScope', + isWholeLine: true + }; + } +} diff --git a/src/vs/editor/contrib/find/common/findModel.ts b/src/vs/editor/contrib/find/common/findModel.ts index 27d105759b2..73d7f156000 100644 --- a/src/vs/editor/contrib/find/common/findModel.ts +++ b/src/vs/editor/contrib/find/common/findModel.ts @@ -12,340 +12,140 @@ import Schedulers = require('vs/base/common/async'); import {Range} from 'vs/editor/common/core/range'; import {Position} from 'vs/editor/common/core/position'; import {ReplaceCommand} from 'vs/editor/common/commands/replaceCommand'; +import {FindDecorations} from './findDecorations'; +import {FindReplaceStateChangedEvent, FindReplaceState} from './findState'; -export const START_FIND_ACTION_ID = 'actions.find'; -export const NEXT_MATCH_FIND_ACTION_ID = 'editor.action.nextMatchFindAction'; -export const PREVIOUS_MATCH_FIND_ACTION_ID = 'editor.action.previousMatchFindAction'; -export const START_FIND_REPLACE_ACTION_ID = 'editor.action.startFindReplaceAction'; -export const CLOSE_FIND_WIDGET_COMMAND_ID = 'closeFindWidget'; -export const TOGGLE_CASE_SENSITIVE_COMMAND_ID = 'toggleFindCaseSensitive'; -export const TOGGLE_WHOLE_WORD_COMMAND_ID = 'toggleFindWholeWord'; -export const TOGGLE_REGEX_COMMAND_ID = 'toggleFindRegex'; - -export interface IFindMatchesEvent { - position: number; - count: number; +export const FindIds = { + START_FIND_ACTION_ID: 'actions.find', + NEXT_MATCH_FIND_ACTION_ID: 'editor.action.nextMatchFindAction', + PREVIOUS_MATCH_FIND_ACTION_ID: 'editor.action.previousMatchFindAction', + START_FIND_REPLACE_ACTION_ID: 'editor.action.startFindReplaceAction', + CLOSE_FIND_WIDGET_COMMAND_ID: 'closeFindWidget', + TOGGLE_CASE_SENSITIVE_COMMAND_ID: 'toggleFindCaseSensitive', + TOGGLE_WHOLE_WORD_COMMAND_ID: 'toggleFindWholeWord', + TOGGLE_REGEX_COMMAND_ID: 'toggleFindRegex' } -export interface IFindProperties { - isRegex: boolean; - wholeWord: boolean; - matchCase: boolean; -} - -export interface IFindState { - searchString: string; - replaceString: string; - properties: IFindProperties; - isReplaceRevealed: boolean; -} - -export interface IFindStartEvent { - state: IFindState; - selectionFindEnabled: boolean; - shouldAnimate: boolean; -} - -export class FindModelBoundToEditorModel extends Events.EventEmitter { - - private static _START_EVENT = 'start'; - private static _MATCHES_UPDATED_EVENT = 'matches'; +export class FindModelBoundToEditorModel { private editor:EditorCommon.ICommonCodeEditor; - private startPosition:EditorCommon.IEditorPosition; - private searchString:string; - private replaceString:string; - private searchOnlyEditableRange:boolean; - private decorations:string[]; - private decorationIndex:number; - private findScopeDecorationId:string; - private highlightedDecorationId:string; - private listenersToRemove:Events.ListenerUnbind[]; + private _state:FindReplaceState; + private _toDispose:Lifecycle.IDisposable[]; + private _decorations: FindDecorations; + private _ignoreModelContentChanged:boolean; + private updateDecorationsScheduler:Schedulers.RunOnceScheduler; - private didReplace:boolean; - private isRegex:boolean; - private matchCase:boolean; - private wholeWord:boolean; - - constructor(editor:EditorCommon.ICommonCodeEditor) { - super([ - FindModelBoundToEditorModel._MATCHES_UPDATED_EVENT, - FindModelBoundToEditorModel._START_EVENT - ]); + constructor(editor:EditorCommon.ICommonCodeEditor, state:FindReplaceState) { this.editor = editor; - this.startPosition = null; - this.searchString = ''; - this.replaceString = ''; - this.searchOnlyEditableRange = false; - this.decorations = []; - this.decorationIndex = 0; - this.findScopeDecorationId = null; - this.highlightedDecorationId = null; - this.listenersToRemove = []; - this.didReplace = false; + this._state = state; + this._toDispose = []; - this.isRegex = false; - this.matchCase = false; - this.wholeWord = false; + this._decorations = new FindDecorations(editor); + this._toDispose.push(this._decorations); - this.updateDecorationsScheduler = new Schedulers.RunOnceScheduler(() => { - this.updateDecorations(false, false, null); - }, 100); + this.updateDecorationsScheduler = new Schedulers.RunOnceScheduler(() => this.research(false), 100); + this._toDispose.push(this.updateDecorationsScheduler); - this.listenersToRemove.push(this.editor.addListener(EditorCommon.EventType.CursorPositionChanged, (e:EditorCommon.ICursorPositionChangedEvent) => { + this._toDispose.push(this.editor.addListener2(EditorCommon.EventType.CursorPositionChanged, (e:EditorCommon.ICursorPositionChangedEvent) => { if (e.reason === 'explicit' || e.reason === 'undo' || e.reason === 'redo') { - if (this.highlightedDecorationId !== null) { - this.editor.changeDecorations((changeAccessor: EditorCommon.IModelDecorationsChangeAccessor) => { - changeAccessor.changeDecorationOptions(this.highlightedDecorationId, this.createFindMatchDecorationOptions(false)); - this.highlightedDecorationId = null; - }); - } - this.startPosition = this.editor.getPosition(); - this.decorationIndex = -1; + this._decorations.setStartPosition(this.editor.getPosition()); } })); - this.listenersToRemove.push(this.editor.addListener(EditorCommon.EventType.ModelContentChanged, (e:EditorCommon.IModelContentChangedEvent) => { + this._ignoreModelContentChanged = false; + this._toDispose.push(this.editor.addListener2(EditorCommon.EventType.ModelContentChanged, (e:EditorCommon.IModelContentChangedEvent) => { + if (this._ignoreModelContentChanged) { + return; + } if (e.changeType === EditorCommon.EventType.ModelContentChangedFlush) { // a model.setValue() was called - this.decorations = []; - this.decorationIndex = -1; - this.findScopeDecorationId = null; - this.highlightedDecorationId = null; + this._decorations.reset(); } - this.startPosition = this.editor.getPosition(); + this._decorations.setStartPosition(this.editor.getPosition()); this.updateDecorationsScheduler.schedule(); })); + + this._toDispose.push(this._state.addChangeListener((e) => this._onStateChanged(e))); + + this.research(false, this._state.searchScope); } - private removeOldDecorations(changeAccessor:EditorCommon.IModelDecorationsChangeAccessor, removeFindScopeDecoration:boolean): void { - let toRemove: string[] = []; - var i:number, len:number; - for (i = 0, len = this.decorations.length; i < len; i++) { - toRemove.push(this.decorations[i]); - } - this.decorations = []; - - if (removeFindScopeDecoration && this.hasFindScope()) { - toRemove.push(this.findScopeDecorationId); - this.findScopeDecorationId = null; - } - - changeAccessor.deltaDecorations(toRemove, []); + public dispose(): void { + this._toDispose = Lifecycle.disposeAll(this._toDispose); } - private createFindMatchDecorationOptions(isCurrent:boolean): EditorCommon.IModelDecorationOptions { - return { - stickiness: EditorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - className: isCurrent ? 'currentFindMatch' : 'findMatch', - overviewRuler: { - color: 'rgba(246, 185, 77, 0.7)', - darkColor: 'rgba(246, 185, 77, 0.7)', - position: EditorCommon.OverviewRulerLane.Center + private _onStateChanged(e:FindReplaceStateChangedEvent): void { + if (e.searchString || e.isReplaceRevealed || e.isRegex || e.wholeWord || e.matchCase || e.searchScope) { + if (e.searchScope) { + this.research(e.moveCursor, this._state.searchScope); + } else { + this.research(e.moveCursor); } - }; - } - - private createFindScopeDecorationOptions(): EditorCommon.IModelDecorationOptions { - return { - className: 'findScope', - isWholeLine: true - }; - } - - private addMatchesDecorations(changeAccessor:EditorCommon.IModelDecorationsChangeAccessor, matches:EditorCommon.IEditorRange[]): void { - var newDecorations: EditorCommon.IModelDeltaDecoration[] = []; - - var i:number, len:number; - for (i = 0, len = matches.length; i < len; i++) { - newDecorations[i] = { - range: matches[i], - options: this.createFindMatchDecorationOptions(false) - }; } - - this.decorations = changeAccessor.deltaDecorations([], newDecorations); } - private _getSearchRange(): EditorCommon.IEditorRange { - var searchRange:EditorCommon.IEditorRange; + private static _getSearchRange(model:EditorCommon.IModel, searchOnlyEditableRange:boolean, findScope:EditorCommon.IEditorRange): EditorCommon.IEditorRange { + let searchRange:EditorCommon.IEditorRange; - if (this.searchOnlyEditableRange) { - searchRange = this.editor.getModel().getEditableRange(); + if (searchOnlyEditableRange) { + searchRange = model.getEditableRange(); } else { - searchRange = this.editor.getModel().getFullModelRange(); + searchRange = model.getFullModelRange(); } - if (this.hasFindScope()) { - // If we have set now or before a find scope, use it for computing the search range - searchRange = searchRange.intersectRanges(this.editor.getModel().getDecorationRange(this.findScopeDecorationId)); + // If we have set now or before a find scope, use it for computing the search range + if (findScope) { + searchRange = searchRange.intersectRanges(findScope); } + return searchRange; } - private updateDecorations(jumpToNextMatch:boolean, resetFindScopeDecoration:boolean, newFindScope:EditorCommon.IEditorRange): void { - if (this.didReplace) { - this.next(); - } - - this.editor.changeDecorations((changeAccessor:EditorCommon.IModelDecorationsChangeAccessor) => { - this.removeOldDecorations(changeAccessor, resetFindScopeDecoration); - - if (resetFindScopeDecoration && newFindScope) { - // Add a decoration to track the find scope - let decorations = changeAccessor.deltaDecorations([], [{ - range: newFindScope, - options: this.createFindScopeDecorationOptions() - }]); - this.findScopeDecorationId = decorations[0]; - } - - this.addMatchesDecorations(changeAccessor, this._findMatches()); - }); - this.highlightedDecorationId = null; - - this.decorationIndex = this.indexAfterPosition(this.startPosition); - - if (!this.didReplace && !jumpToNextMatch) { - this.decorationIndex = this.previousIndex(this.decorationIndex); - } else if (this.decorations.length > 0) { - this.setSelectionToDecoration(this.decorations[this.decorationIndex]); - } - - var e:IFindMatchesEvent = { - position: this.decorations.length > 0 ? (this.decorationIndex+1) : 0, - count: this.decorations.length - }; - - this._emitMatchesUpdatedEvent(e); - - this.didReplace = false; - } - - - /** - * Updates selection find scope. - * Selection find scope just gets removed if passed findScope is null. - * Selection find scope does not take columns into account. - */ - public setFindScope(findScope:EditorCommon.IEditorRange): void { - if (findScope === null) { - this.updateDecorations(false, true, findScope); + private research(moveCursor:boolean, newFindScope?:EditorCommon.IEditorRange): void { + let findScope: EditorCommon.IEditorRange = null; + if (typeof newFindScope !== 'undefined') { + findScope = newFindScope; } else { - this.updateDecorations(false, true, new Range(findScope.startLineNumber, 1, findScope.endLineNumber, this.editor.getModel().getLineMaxColumn(findScope.endLineNumber))); + findScope = this._decorations.getFindScope(); + } + + let findMatches = this._findMatches(findScope); + this._decorations.set(findMatches, findScope); + + this._state.change({ matchesCount: findMatches.length }, false); + + if (moveCursor) { + this._decorations.moveToFirstAfterStartPosition(); } } - public recomputeMatches(newFindData:IFindState, jumpToNextMatch:boolean): void { - var somethingChanged = false; - if (this.isRegex !== newFindData.properties.isRegex) { - this.isRegex = newFindData.properties.isRegex; - somethingChanged = true; - } - if (this.matchCase !== newFindData.properties.matchCase) { - this.matchCase = newFindData.properties.matchCase; - somethingChanged = true; - } - if (this.wholeWord !== newFindData.properties.wholeWord) { - this.wholeWord = newFindData.properties.wholeWord; - somethingChanged = true; - } - if (newFindData.searchString !== this.searchString) { - this.searchString = newFindData.searchString; - somethingChanged = true; - } - this.replaceString = newFindData.replaceString; - if (newFindData.isReplaceRevealed !== this.searchOnlyEditableRange) { - this.searchOnlyEditableRange = newFindData.isReplaceRevealed; - somethingChanged = true; - } - - if (somethingChanged) { - this.updateDecorations(jumpToNextMatch, false, null); - } + public moveToPrevMatch(): void { + this._decorations.movePrev(); } - public start(newFindData:IFindState, findScope:EditorCommon.IEditorRange, shouldAnimate:boolean): void { - this.startPosition = this.editor.getPosition(); - - this.isRegex = newFindData.properties.isRegex; - this.matchCase = newFindData.properties.matchCase; - this.wholeWord = newFindData.properties.wholeWord; - this.searchString = newFindData.searchString; - this.replaceString = newFindData.replaceString; - this.searchOnlyEditableRange = newFindData.isReplaceRevealed; - - this.setFindScope(findScope); - this.decorationIndex = this.previousIndex(this.indexAfterPosition(this.startPosition)); - var e:IFindStartEvent = { - state: newFindData, - selectionFindEnabled: this.hasFindScope(), - shouldAnimate: shouldAnimate - }; - this._emitStartEvent(e); - } - - public prev(): void { - if (this.decorations.length > 0) { - if (this.decorationIndex === -1) { - this.decorationIndex = this.indexAfterPosition(this.startPosition); - } - this.decorationIndex = this.previousIndex(this.decorationIndex); - this.setSelectionToDecoration(this.decorations[this.decorationIndex]); - } else if (this.hasFindScope()) { - // Reveal the selection so user is reminded that 'selection find' is on. - this.editor.revealRangeInCenterIfOutsideViewport(this.editor.getModel().getDecorationRange(this.findScopeDecorationId)); - } - } - - public next(): void { - if (this.decorations.length > 0) { - if (this.decorationIndex === -1) { - this.decorationIndex = this.indexAfterPosition(this.startPosition); - } else { - this.decorationIndex = this.nextIndex(this.decorationIndex); - } - this.setSelectionToDecoration(this.decorations[this.decorationIndex]); - } else if (this.hasFindScope()) { - // Reveal the selection so user is reminded that 'selection find' is on. - this.editor.revealRangeInCenterIfOutsideViewport(this.editor.getModel().getDecorationRange(this.findScopeDecorationId)); - } - } - - private setSelectionToDecoration(decorationId:string): void { - this.editor.changeDecorations((changeAccessor: EditorCommon.IModelDecorationsChangeAccessor) => { - if (this.highlightedDecorationId !== null) { - changeAccessor.changeDecorationOptions(this.highlightedDecorationId, this.createFindMatchDecorationOptions(false)); - } - changeAccessor.changeDecorationOptions(decorationId, this.createFindMatchDecorationOptions(true)); - this.highlightedDecorationId = decorationId; - }); - var decorationRange = this.editor.getModel().getDecorationRange(decorationId); - if (Range.isIRange(decorationRange)) { - this.editor.setSelection(decorationRange); - this.editor.revealRangeInCenterIfOutsideViewport(decorationRange); - } + public moveToNextMatch(): void { + this._decorations.moveNext(); } private getReplaceString(matchedString:string): string { - if (!this.isRegex) { - return this.replaceString; + if (!this._state.isRegex) { + return this._state.replaceString; } - let regexp = Strings.createRegExp(this.searchString, this.isRegex, this.matchCase, this.wholeWord); + let regexp = Strings.createRegExp(this._state.searchString, this._state.isRegex, this._state.matchCase, this._state.wholeWord); // Parse the replace string to support that \t or \n mean the right thing - let parsedReplaceString = parseReplaceString(this.replaceString); + let parsedReplaceString = parseReplaceString(this._state.replaceString); return matchedString.replace(regexp, parsedReplaceString); } public replace(): void { - if (this.decorations.length === 0) { + if (!this._decorations.hasMatches()) { return; } - var model = this.editor.getModel(); - var currentDecorationRange = model.getDecorationRange(this.decorations[this.decorationIndex]); - var selection = this.editor.getSelection(); + let model = this.editor.getModel(); + let currentDecorationRange = this._decorations.getCurrentIndexRange(); + let selection = this.editor.getSelection(); if (currentDecorationRange !== null && selection.startColumn === currentDecorationRange.startColumn && @@ -353,117 +153,55 @@ export class FindModelBoundToEditorModel extends Events.EventEmitter { selection.startLineNumber === currentDecorationRange.startLineNumber && selection.endLineNumber === currentDecorationRange.endLineNumber) { - var matchedString = model.getValueInRange(selection); - var replaceString = this.getReplaceString(matchedString); + let matchedString = model.getValueInRange(selection); + let replaceString = this.getReplaceString(matchedString); - var command = new ReplaceCommand(selection, replaceString); - this.editor.executeCommand('replace', command); + let command = new ReplaceCommand(selection, replaceString); - this.startPosition = new Position(selection.startLineNumber, selection.startColumn + replaceString.length); - this.decorationIndex = -1; - this.didReplace = true; + this._executeEditorCommand('replace', command); + + this._decorations.setStartPosition(new Position(selection.startLineNumber, selection.startColumn + replaceString.length)); + this.research(true); } else { - this.next(); + this.moveToNextMatch(); } } - private _findMatches(limitResultCount?:number): EditorCommon.IEditorRange[] { - return this.editor.getModel().findMatches(this.searchString, this._getSearchRange(), this.isRegex, this.matchCase, this.wholeWord, limitResultCount); + private _findMatches(findScope: EditorCommon.IEditorRange, limitResultCount?:number): EditorCommon.IEditorRange[] { + let searchRange = FindModelBoundToEditorModel._getSearchRange(this.editor.getModel(), this._state.isReplaceRevealed, findScope); + return this.editor.getModel().findMatches(this._state.searchString, searchRange, this._state.isRegex, this._state.matchCase, this._state.wholeWord, limitResultCount); } public replaceAll(): void { - if (this.decorations.length === 0) { + if (!this._decorations.hasMatches()) { return; } let model = this.editor.getModel(); + let findScope = this._decorations.getFindScope(); // Get all the ranges (even more than the highlighted ones) - let ranges = this._findMatches(Number.MAX_VALUE); + let ranges = this._findMatches(findScope, Number.MAX_VALUE); - // Remove all decorations - this.editor.changeDecorations((changeAccessor:EditorCommon.IModelDecorationsChangeAccessor) => { - this.removeOldDecorations(changeAccessor, false); - }); + this._decorations.set([], findScope); - var replaceStrings:string[] = []; - for (var i = 0, len = ranges.length; i < len; i++) { + let replaceStrings:string[] = []; + for (let i = 0, len = ranges.length; i < len; i++) { replaceStrings.push(this.getReplaceString(model.getValueInRange(ranges[i]))); } - var command = new ReplaceAllCommand.ReplaceAllCommand(ranges, replaceStrings); - this.editor.executeCommand('replaceAll', command); + let command = new ReplaceAllCommand.ReplaceAllCommand(ranges, replaceStrings); + this._executeEditorCommand('replaceAll', command); } - public dispose(): void { - super.dispose(); - this.updateDecorationsScheduler.dispose(); - this.listenersToRemove.forEach((element) => { - element(); - }); - this.listenersToRemove = []; - if (this.editor.getModel()) { - this.editor.changeDecorations((changeAccessor:EditorCommon.IModelDecorationsChangeAccessor) => { - this.removeOldDecorations(changeAccessor, true); - }); + private _executeEditorCommand(source:string, command:EditorCommon.ICommand): void { + try { + this._ignoreModelContentChanged = true; + this.editor.executeCommand(source, command); + } finally { + this._ignoreModelContentChanged = false; } } - - public hasFindScope(): boolean { - return !!this.findScopeDecorationId; - } - - private previousIndex(index:number): number { - if (this.decorations.length > 0) { - return (index - 1 + this.decorations.length) % this.decorations.length; - } - return 0; - } - - private nextIndex(index:number): number { - if (this.decorations.length > 0) { - return (index + 1) % this.decorations.length; - } - return 0; - } - - private indexAfterPosition(position:EditorCommon.IEditorPosition): number { - if (this.decorations.length === 0) { - return 0; - } - for (var i = 0, len = this.decorations.length; i < len; i++) { - var decorationId = this.decorations[i]; - var r = this.editor.getModel().getDecorationRange(decorationId); - if (!r || r.startLineNumber < position.lineNumber) { - continue; - } - if (r.startLineNumber > position.lineNumber) { - return i; - } - if (r.startColumn < position.column) { - continue; - } - return i; - } - return 0; - } - - public addStartEventListener(callback:(e:IFindStartEvent)=>void): Lifecycle.IDisposable { - return this.addListener2(FindModelBoundToEditorModel._START_EVENT, callback); - } - - private _emitStartEvent(e:IFindStartEvent): void { - this.emit(FindModelBoundToEditorModel._START_EVENT, e); - } - - public addMatchesUpdatedEventListener(callback:(e:IFindMatchesEvent)=>void): Lifecycle.IDisposable { - return this.addListener2(FindModelBoundToEditorModel._MATCHES_UPDATED_EVENT, callback); - } - - private _emitMatchesUpdatedEvent(e:IFindMatchesEvent): void { - this.emit(FindModelBoundToEditorModel._MATCHES_UPDATED_EVENT, e); - } - } const BACKSLASH_CHAR_CODE = '\\'.charCodeAt(0); diff --git a/src/vs/editor/contrib/find/common/findState.ts b/src/vs/editor/contrib/find/common/findState.ts new file mode 100644 index 00000000000..11452b825ec --- /dev/null +++ b/src/vs/editor/contrib/find/common/findState.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as EditorCommon from 'vs/editor/common/editorCommon'; +import {EventEmitter} from 'vs/base/common/eventEmitter'; +import {IDisposable} from 'vs/base/common/lifecycle'; +import {Range} from 'vs/editor/common/core/range'; + +export interface FindReplaceStateChangedEvent { + moveCursor: boolean; + + searchString: boolean; + replaceString: boolean; + isRevealed: boolean; + isReplaceRevealed: boolean; + isRegex: boolean; + wholeWord: boolean; + matchCase: boolean; + searchScope: boolean; + matchesCount: boolean; +} + +export interface INewFindReplaceState { + searchString?: string; + replaceString?: string; + isRevealed?: boolean; + isReplaceRevealed?: boolean; + isRegex?: boolean; + wholeWord?: boolean; + matchCase?: boolean; + searchScope?: EditorCommon.IEditorRange; + matchesCount?: number; +} + +export class FindReplaceState implements IDisposable { + + private static _CHANGED_EVENT = 'changed'; + + private _searchString: string; + private _replaceString: string; + private _isRevealed: boolean; + private _isReplaceRevealed: boolean; + private _isRegex: boolean; + private _wholeWord: boolean; + private _matchCase: boolean; + private _searchScope: EditorCommon.IEditorRange; + private _matchesCount: number; + private _eventEmitter: EventEmitter; + + public get searchString(): string { return this._searchString; } + 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 searchScope(): EditorCommon.IEditorRange { return this._searchScope; } + public get matchesCount(): number { return this._matchesCount; } + + constructor() { + this._searchString = ''; + this._replaceString = ''; + this._isRevealed = false; + this._isReplaceRevealed = false; + this._isRegex = false; + this._wholeWord = false; + this._matchCase = false; + this._searchScope = null; + this._matchesCount = 0; + this._eventEmitter = new EventEmitter(); + } + + public dispose(): void { + this._eventEmitter.dispose(); + } + + public addChangeListener(listener:(e:FindReplaceStateChangedEvent)=>void): IDisposable { + return this._eventEmitter.addListener2(FindReplaceState._CHANGED_EVENT, listener); + } + + public change(newState:INewFindReplaceState, moveCursor:boolean): void { + let changeEvent:FindReplaceStateChangedEvent = { + moveCursor: moveCursor, + searchString: false, + replaceString: false, + isRevealed: false, + isReplaceRevealed: false, + isRegex: false, + wholeWord: false, + matchCase: false, + searchScope: false, + matchesCount: false + }; + let somethingChanged = false; + + if (typeof newState.searchString !== 'undefined') { + if (this._searchString !== newState.searchString) { + this._searchString = newState.searchString; + changeEvent.searchString = true; + somethingChanged = true; + } + } + if (typeof newState.replaceString !== 'undefined') { + if (this._replaceString !== newState.replaceString) { + this._replaceString = newState.replaceString; + changeEvent.replaceString = true; + somethingChanged = true; + } + } + if (typeof newState.isRevealed !== 'undefined') { + if (this._isRevealed !== newState.isRevealed) { + this._isRevealed = newState.isRevealed; + changeEvent.isRevealed = true; + somethingChanged = true; + } + } + if (typeof newState.isReplaceRevealed !== 'undefined') { + if (this._isReplaceRevealed !== newState.isReplaceRevealed) { + this._isReplaceRevealed = newState.isReplaceRevealed; + changeEvent.isReplaceRevealed = true; + somethingChanged = true; + } + } + if (typeof newState.isRegex !== 'undefined') { + if (this._isRegex !== newState.isRegex) { + this._isRegex = newState.isRegex; + changeEvent.isRegex = true; + somethingChanged = true; + } + } + if (typeof newState.wholeWord !== 'undefined') { + if (this._wholeWord !== newState.wholeWord) { + this._wholeWord = newState.wholeWord; + changeEvent.wholeWord = true; + somethingChanged = true; + } + } + if (typeof newState.matchCase !== 'undefined') { + if (this._matchCase !== newState.matchCase) { + this._matchCase = newState.matchCase; + changeEvent.matchCase = true; + somethingChanged = true; + } + } + if (typeof newState.searchScope !== 'undefined') { + if (!Range.equalsRange(this._searchScope, newState.searchScope)) { + this._searchScope = newState.searchScope; + changeEvent.searchScope = true; + somethingChanged = true; + } + } + if (typeof newState.matchesCount !== 'undefined') { + if (this._matchesCount !== newState.matchesCount) { + this._matchesCount = newState.matchesCount; + changeEvent.matchesCount = true; + somethingChanged = true; + } + } + + if (somethingChanged) { + this._eventEmitter.emit(FindReplaceState._CHANGED_EVENT, changeEvent); + } + } +}