diff --git a/src/vs/base/browser/ui/inputbox/inputBox.css b/src/vs/base/browser/ui/inputbox/inputBox.css
index 4edda336bb2..13fe2cd1d0f 100644
--- a/src/vs/base/browser/ui/inputbox/inputBox.css
+++ b/src/vs/base/browser/ui/inputbox/inputBox.css
@@ -106,10 +106,20 @@
/* Action bar support */
.monaco-inputbox .monaco-action-bar {
position: absolute;
- right: 0px;
+ right: 2px;
top: 4px;
}
+.monaco-inputbox .monaco-action-bar .action-item {
+ margin-left: 2px;
+}
+
+.monaco-inputbox .monaco-action-bar .action-item .icon {
+ background-repeat: no-repeat;
+ width: 16px;
+ height: 16px;
+}
+
/* Theming */
.monaco-inputbox.idle {
diff --git a/src/vs/workbench/parts/search/browser/media/expando-collapsed-dark.svg b/src/vs/workbench/parts/search/browser/media/expando-collapsed-dark.svg
new file mode 100644
index 00000000000..6f3abfce784
--- /dev/null
+++ b/src/vs/workbench/parts/search/browser/media/expando-collapsed-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/vs/workbench/parts/search/browser/media/expando-collapsed.svg b/src/vs/workbench/parts/search/browser/media/expando-collapsed.svg
new file mode 100644
index 00000000000..5dcb87c772c
--- /dev/null
+++ b/src/vs/workbench/parts/search/browser/media/expando-collapsed.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/vs/workbench/parts/search/browser/media/expando-expanded-dark.svg b/src/vs/workbench/parts/search/browser/media/expando-expanded-dark.svg
new file mode 100644
index 00000000000..22dfac04f15
--- /dev/null
+++ b/src/vs/workbench/parts/search/browser/media/expando-expanded-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/vs/workbench/parts/search/browser/media/expando-expanded.svg b/src/vs/workbench/parts/search/browser/media/expando-expanded.svg
new file mode 100644
index 00000000000..e55ccd923e5
--- /dev/null
+++ b/src/vs/workbench/parts/search/browser/media/expando-expanded.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/vs/workbench/parts/search/browser/media/replace-all-inverse.svg b/src/vs/workbench/parts/search/browser/media/replace-all-inverse.svg
new file mode 100644
index 00000000000..2744a6a4e81
--- /dev/null
+++ b/src/vs/workbench/parts/search/browser/media/replace-all-inverse.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/vs/workbench/parts/search/browser/media/replace-all.svg b/src/vs/workbench/parts/search/browser/media/replace-all.svg
new file mode 100644
index 00000000000..4c55b91d0e9
--- /dev/null
+++ b/src/vs/workbench/parts/search/browser/media/replace-all.svg
@@ -0,0 +1,11 @@
+
diff --git a/src/vs/workbench/parts/search/browser/media/replace-inverse.svg b/src/vs/workbench/parts/search/browser/media/replace-inverse.svg
new file mode 100644
index 00000000000..a15ad9b4af1
--- /dev/null
+++ b/src/vs/workbench/parts/search/browser/media/replace-inverse.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/vs/workbench/parts/search/browser/media/replace.svg b/src/vs/workbench/parts/search/browser/media/replace.svg
new file mode 100644
index 00000000000..14da77d2198
--- /dev/null
+++ b/src/vs/workbench/parts/search/browser/media/replace.svg
@@ -0,0 +1,13 @@
+
diff --git a/src/vs/workbench/parts/search/browser/media/searchviewlet.css b/src/vs/workbench/parts/search/browser/media/searchviewlet.css
index 5b827f09cd6..fca60f57ff7 100644
--- a/src/vs/workbench/parts/search/browser/media/searchviewlet.css
+++ b/src/vs/workbench/parts/search/browser/media/searchviewlet.css
@@ -3,20 +3,45 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
-.search-viewlet .highlight {
- color: black;
- background-color: rgba(234, 92, 0, 0.3);
+.search-viewlet .search-widgets-container {
+ margin: 6px 17px 0 2px;
}
-.search-viewlet .query-box {
- margin: 6px 17px 0 17px;
+.search-viewlet .search-widget .toggle-replace-button {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 16px;
+ height: 100%;
+
+ -webkit-box-sizing: border-box;
+ -o-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -ms-box-sizing: border-box;
+ box-sizing: border-box;
+
+ background-position: center center;
+ background-repeat: no-repeat;
+ cursor: pointer;
}
-.search-viewlet .query-box .monaco-findInput {
+.search-viewlet .search-widget .input-box {
+ margin-left: 17px;
+}
+
+.search-viewlet .search-widget .monaco-findInput {
display: inline-block;
vertical-align: middle;
}
+.search-viewlet .search-widget .replace-box {
+ margin-top: 6px;
+}
+
+.search-viewlet .search-widget .replace-box.disabled {
+ display: none;
+}
+
.search-viewlet .query-clear {
width: 20px;
height: 20px;
@@ -29,14 +54,13 @@
.search-viewlet .query-details {
min-height: 1em;
position: relative;
- margin: 0 17px 6px 17px;
- padding: 0 4px;
+ margin: 0 0 6px 17px;
}
.search-viewlet .query-details .more {
position: absolute;
- right: 0;
margin-right: 0.5em;
+ right: 0;
cursor: pointer;
width: 16px;
height: 13px;
@@ -163,6 +187,24 @@
background: url("action-remove.svg") center center no-repeat;
}
+.search-viewlet .action-replace {
+ background-image: url('replace.svg');
+}
+
+.search-viewlet .action-replace-all {
+ background-image: url('replace-all.svg');
+}
+
+.monaco-editor.hc-black .search-viewlet .action-replace,
+.monaco-editor.vs-dark .search-viewlet .action-replace {
+ background-image: url('replace-inverse.svg');
+}
+
+.monaco-editor.hc-black .search-viewlet .action-replace-all,
+.monaco-editor.vs-dark .search-viewlet .action-replace-all {
+ background-image: url('replace-all-inverse.svg');
+}
+
.search-viewlet .label {
font-style: italic;
}
@@ -248,8 +290,28 @@
box-sizing: border-box;
}
+
+.search-viewlet .replaceMatch {
+ background-color: rgba(234, 92, 0, 0.45);
+}
+
+.search-viewlet .removeMatch {
+ text-decoration: line-through;
+}
+
+.hc-black .monaco-workbench .search-viewlet .replaceMatch,
+.monaco-editor.hc-black .replaceMatch {
+ border-color: #F44336;
+}
+
/* Theming */
-.vs .search-viewlet .query-box,
+
+.search-viewlet .highlight {
+ color: black;
+ background-color: rgba(234, 92, 0, 0.3);
+}
+
+.vs .search-viewlet .input-box,
.vs .search-viewlet .file-types .monaco-inputbox {
background-color: white;
}
@@ -262,7 +324,15 @@
color: #FFF;
}
-.vs-dark .search-viewlet .query-box,
+.vs .search-viewlet .search-widget .toggle-replace-button.collapse {
+ background-image: url('expando-collapsed.svg');
+}
+
+.vs .search-viewlet .search-widget .toggle-replace-button.expand {
+ background-image: url('expando-expanded.svg');
+}
+
+.vs-dark .search-viewlet .input-box,
.vs-dark .search-viewlet .file-types .monaco-inputbox {
background-color: #3C3C3C;
}
@@ -287,8 +357,18 @@
padding: 0;
}
+.vs-dark .search-viewlet .search-widget .toggle-replace-button.expand,
+.hc-black .search-viewlet .search-widget .toggle-replace-button.expand {
+ background-image: url('expando-expanded-dark.svg');
+}
+
+.vs-dark .search-viewlet .search-widget .toggle-replace-button.collapse,
+.hc-black .search-viewlet .search-widget .toggle-replace-button.collapse {
+ background-image: url('expando-collapsed-dark.svg');
+}
+
/* High Contrast Theming */
-.hc-black .monaco-workbench .search-viewlet .query-box,
+.hc-black .monaco-workbench .search-viewlet .input-box,
.hc-black .monaco-workbench .search-viewlet .file-types .monaco-inputbox {
background-color: #000;
}
@@ -331,11 +411,11 @@
width: 16px;
}
-.hc-black .monaco-workbench .query-box {
+.hc-black .monaco-workbench .input-box {
border: 1px solid #6FC3DF;
}
-.hc-black .monaco-workbench .query-box .monaco-inputbox.idle {
+.hc-black .monaco-workbench .input-box .monaco-inputbox.idle {
border: none;
}
diff --git a/src/vs/workbench/parts/search/browser/replaceService.ts b/src/vs/workbench/parts/search/browser/replaceService.ts
new file mode 100644
index 00000000000..863240ef981
--- /dev/null
+++ b/src/vs/workbench/parts/search/browser/replaceService.ts
@@ -0,0 +1,53 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { TPromise } from 'vs/base/common/winjs.base';
+import { IReplaceService } from 'vs/workbench/parts/search/common/replace';
+import { IEditorService } from 'vs/platform/editor/common/editor';
+import { IEventService } from 'vs/platform/event/common/event';
+import { Match, FileMatch } from 'vs/workbench/parts/search/common/searchModel';
+import { BulkEdit, IResourceEdit, createBulkEdit } from 'vs/editor/common/services/bulkEdit';
+import { IProgressRunner } from 'vs/platform/progress/common/progress';
+
+export class ReplaceService implements IReplaceService {
+
+ public serviceId= IReplaceService;
+
+ constructor(@IEventService private eventService: IEventService, @IEditorService private editorService) {
+ }
+
+ public replace(match: Match, text: string): TPromise
+ public replace(files: FileMatch[], text: string, progress?: IProgressRunner): TPromise
+ public replace(arg: any, text: string, progress: IProgressRunner= null): TPromise {
+
+ let bulkEdit: BulkEdit = createBulkEdit(this.eventService, this.editorService, null);
+ bulkEdit.progress(progress);
+
+ if (arg instanceof Match) {
+ bulkEdit.add([this.createEdit(arg, text)]);
+ }
+
+ if (arg instanceof Array) {
+ arg.forEach(element => {
+ let fileMatch = element;
+ fileMatch.matches().forEach(match => {
+ bulkEdit.add([this.createEdit(match, text)]);
+ });
+ });
+ }
+
+ return bulkEdit.finish();
+ }
+
+ private createEdit(match: Match, text: string): IResourceEdit {
+ let fileMatch: FileMatch= match.parent();
+ let resourceEdit: IResourceEdit= {
+ resource: fileMatch.resource(),
+ range: match.range(),
+ newText: text
+ };
+ return resourceEdit;
+ }
+}
\ No newline at end of file
diff --git a/src/vs/workbench/parts/search/browser/search.contribution.ts b/src/vs/workbench/parts/search/browser/search.contribution.ts
index 2753d6131ed..942248d1ecd 100644
--- a/src/vs/workbench/parts/search/browser/search.contribution.ts
+++ b/src/vs/workbench/parts/search/browser/search.contribution.ts
@@ -27,6 +27,11 @@ import {IViewletService} from 'vs/workbench/services/viewlet/common/viewletServi
import {KeyMod, KeyCode} from 'vs/base/common/keyCodes';
import {OpenSearchViewletAction} from 'vs/workbench/parts/search/browser/searchActions';
import {VIEWLET_ID} from 'vs/workbench/parts/search/common/constants';
+import {registerSingleton} from 'vs/platform/instantiation/common/extensions';
+import { IReplaceService } from 'vs/workbench/parts/search/common/replace';
+import { ReplaceService } from 'vs/workbench/parts/search/browser/replaceService';
+
+registerSingleton(IReplaceService, ReplaceService);
KeybindingsRegistry.registerCommandDesc({
id: 'workbench.action.search.toggleQueryDetails',
@@ -185,4 +190,4 @@ configurationRegistry.registerConfiguration({
}
}
}
-});
+});
\ No newline at end of file
diff --git a/src/vs/workbench/parts/search/browser/searchActions.ts b/src/vs/workbench/parts/search/browser/searchActions.ts
index 2615d928b08..96108ac7a68 100644
--- a/src/vs/workbench/parts/search/browser/searchActions.ts
+++ b/src/vs/workbench/parts/search/browser/searchActions.ts
@@ -12,12 +12,13 @@ import { ToggleViewletAction } from 'vs/workbench/browser/viewlet';
import { IViewletService } from 'vs/workbench/services/viewlet/common/viewletService';
import { ITree } from 'vs/base/parts/tree/browser/tree';
import { SearchViewlet } from 'vs/workbench/parts/search/browser/searchViewlet';
-import { Match, FileMatch } from 'vs/workbench/parts/search/common/searchModel';
+import { SearchResult, Match, FileMatch, FileMatchOrMatch } from 'vs/workbench/parts/search/common/searchModel';
+import { IReplaceService } from 'vs/workbench/parts/search/common/replace';
import * as Constants from 'vs/workbench/parts/search/common/constants';
import { CollapseAllAction as TreeCollapseAction } from 'vs/base/parts/tree/browser/treeDefaults';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { OpenGlobalSettingsAction } from 'vs/workbench/browser/actions/openSettings';
-import {IWorkbenchEditorService} from 'vs/workbench/services/editor/common/editorService';
+import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
export class OpenSearchViewletAction extends ToggleViewletAction {
@@ -27,6 +28,7 @@ export class OpenSearchViewletAction extends ToggleViewletAction {
constructor(id: string, label: string, @IViewletService viewletService: IViewletService, @IWorkbenchEditorService editorService: IWorkbenchEditorService) {
super(id, label, Constants.VIEWLET_ID, viewletService, editorService);
}
+
}
export class FindInFolderAction extends Action {
@@ -88,82 +90,53 @@ export class ClearSearchResultsAction extends Action {
}
}
-export class SelectOrRemoveAction extends Action {
- private selectMode: boolean;
- private viewlet: SearchViewlet;
+export class RemoveAction extends Action {
- constructor(viewlet: SearchViewlet) {
- super('selectOrRemove');
-
- this.label = nls.localize('SelectOrRemoveAction.selectLabel', "Select");
- this.enabled = false;
- this.selectMode = true;
- this.viewlet = viewlet;
+ constructor(private viewer: ITree, private element: FileMatchOrMatch) {
+ super('remove', nls.localize('RemoveAction.label', "Remove"), 'action-remove');
}
public run(): TPromise {
- let result: TPromise;
-
- if (this.selectMode) {
- result = this.runAsSelect();
+ if (this.element instanceof FileMatch) {
+ let parent:SearchResult = this.element.parent();
+ parent.remove(this.element);
} else {
- result = this.runAsRemove();
+ let parent:FileMatch = this.element.parent();
+ parent.remove(this.element);
}
-
- this.selectMode = !this.selectMode;
- this.label = this.selectMode ? nls.localize('SelectOrRemoveAction.selectLabel', "Select") : nls.localize('SelectOrRemoveAction.removeLabel', "Remove");
-
- return result;
- }
-
- private runAsSelect(): TPromise {
- this.viewlet.getResults().addClass('select');
-
- return TPromise.as(null);
- }
-
- private runAsRemove(): TPromise {
- let elements: any[] = [];
- let tree: ITree = this.viewlet.getControl();
-
- tree.getInput().matches().forEach((fileMatch: FileMatch) => {
- fileMatch.matches().filter((lineMatch: Match) => {
- return (lineMatch).$checked;
- }).forEach((lineMatch: Match) => {
- lineMatch.parent().remove(lineMatch);
- elements.push(lineMatch.parent());
- });
- });
-
- this.viewlet.getResults().removeClass('select');
-
- if (elements.length > 0) {
- return tree.refreshAll(elements).then(() => {
- return tree.refresh();
- });
- }
-
- return TPromise.as(null);
+ return this.viewer.refresh(parent);
}
}
-export class RemoveAction extends Action {
+export class ReplaceAllAction extends Action {
- private viewer: ITree;
- private fileMatch: FileMatch;
-
- constructor(viewer: ITree, element: FileMatch) {
- super('remove', nls.localize('RemoveAction.label', "Remove"), 'action-remove');
-
- this.viewer = viewer;
- this.fileMatch = element;
+ constructor(private viewer: ITree, private fileMatch: FileMatch, private viewlet: SearchViewlet,
+ @IReplaceService private replaceService: IReplaceService) {
+ super('file-action-replace-all', nls.localize('file.replaceAll.label', "Replace All"), 'action-replace-all');
}
public run(): TPromise {
- let parent = this.fileMatch.parent();
- parent.remove(this.fileMatch);
+ return this.replaceService.replace([this.fileMatch], this.fileMatch.parent().replaceText).then(() => {
+ this.viewlet.open(this.fileMatch).done(() => {
+ new RemoveAction(this.viewer, this.fileMatch).run();
+ }, errors.onUnexpectedError);
+ });
+ }
+}
- return this.viewer.refresh(parent);
+export class ReplaceAction extends Action {
+
+ constructor(private viewer: ITree, private element: Match, private viewlet: SearchViewlet,
+ @IReplaceService private replaceService: IReplaceService) {
+ super('action-replace', nls.localize('match.replace.label', "Replace"), 'action-replace');
+ }
+
+ public run(): TPromise {
+ return this.replaceService.replace(this.element, this.element.parent().parent().replaceText).then(() => {
+ this.viewlet.open(this.element).done(() => {
+ new RemoveAction(this.viewer, this.element).run();
+ }, errors.onUnexpectedError);
+ });
}
}
diff --git a/src/vs/workbench/parts/search/browser/searchResultsView.ts b/src/vs/workbench/parts/search/browser/searchResultsView.ts
index 89be7678771..b617bc384e9 100644
--- a/src/vs/workbench/parts/search/browser/searchResultsView.ts
+++ b/src/vs/workbench/parts/search/browser/searchResultsView.ts
@@ -19,15 +19,14 @@ import { LeftRightWidget, IRenderer } from 'vs/base/browser/ui/leftRightWidget/l
import { ITree, IElementCallback, IDataSource, ISorter, IAccessibilityProvider, IFilter } from 'vs/base/parts/tree/browser/tree';
import {ClickBehavior, DefaultController} from 'vs/base/parts/tree/browser/treeDefaults';
import { ContributableActionProvider } from 'vs/workbench/browser/actionBarRegistry';
-import { Match, EmptyMatch, SearchResult, FileMatch} from 'vs/workbench/parts/search/common/searchModel';
+import { Match, EmptyMatch, SearchResult, FileMatch, FileMatchOrMatch } from 'vs/workbench/parts/search/common/searchModel';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { Range } from 'vs/editor/common/core/range';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { CommonKeybindings} from 'vs/base/common/keyCodes';
import { SearchViewlet } from 'vs/workbench/parts/search/browser/searchViewlet';
-import { RemoveAction } from 'vs/workbench/parts/search/browser/searchActions';
-
-export type FileMatchOrMatch = FileMatch | Match;
+import { RemoveAction, ReplaceAllAction, ReplaceAction } from 'vs/workbench/parts/search/browser/searchActions';
+import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export class SearchDataSource implements IDataSource {
@@ -87,14 +86,26 @@ export class SearchSorter implements ISorter {
class SearchActionProvider extends ContributableActionProvider {
+ constructor(private viewlet: SearchViewlet, @IInstantiationService private instantiationService: IInstantiationService) {
+ super();
+ }
+
public hasActions(tree: ITree, element: any): boolean {
- return element instanceof FileMatch || super.hasActions(tree, element);
+ return element instanceof FileMatch || (tree.getInput().isReplaceActive() || element instanceof Match) || super.hasActions(tree, element);
}
public getActions(tree: ITree, element: any): TPromise {
return super.getActions(tree, element).then(actions => {
if (element instanceof FileMatch) {
actions.unshift(new RemoveAction(tree, element));
+ if (tree.getInput().isReplaceActive() && element.count() > 0) {
+ actions.unshift(this.instantiationService.createInstance(ReplaceAllAction, tree, element, this.viewlet));
+ }
+ }
+ if (element instanceof Match && !(element instanceof EmptyMatch)) {
+ if (tree.getInput().isReplaceActive()) {
+ actions.unshift(this.instantiationService.createInstance(ReplaceAction, tree, element, this.viewlet), new RemoveAction(tree, element));
+ }
}
return actions;
@@ -104,9 +115,10 @@ class SearchActionProvider extends ContributableActionProvider {
export class SearchRenderer extends ActionsRenderer {
- constructor(actionRunner: IActionRunner, @IWorkspaceContextService private contextService: IWorkspaceContextService) {
+ constructor(actionRunner: IActionRunner, viewlet: SearchViewlet, @IWorkspaceContextService private contextService: IWorkspaceContextService,
+ @IInstantiationService private instantiationService: IInstantiationService) {
super({
- actionProvider: new SearchActionProvider(),
+ actionProvider: instantiationService.createInstance(SearchActionProvider, viewlet),
actionRunner: actionRunner
});
}
@@ -159,8 +171,21 @@ export class SearchRenderer extends ActionsRenderer {
elements.push('');
elements.push(strings.escape(preview.before));
- elements.push('');
- elements.push(strings.escape(preview.inside));
+
+ let input= tree.getInput();
+ if (input.isReplaceActive()) {
+ let replaceValue= input.replaceText;
+ if (replaceValue) {
+ elements.push('');
+ elements.push(strings.escape(replaceValue));
+ } else {
+ elements.push('');
+ elements.push(strings.escape(preview.inside));
+ }
+ } else {
+ elements.push('');
+ elements.push(strings.escape(preview.inside));
+ }
elements.push('');
elements.push(strings.escape(preview.after));
elements.push('');
@@ -220,13 +245,22 @@ export class SearchController extends DefaultController {
private onDelete(tree: ITree, event: IKeyboardEvent): boolean {
let result = false;
let element = tree.getFocus();
- if (element instanceof FileMatch) {
+ if (element instanceof FileMatch ||
+ (element instanceof Match && tree.getInput().isReplaceActive() && !(element instanceof EmptyMatch))) {
new RemoveAction(tree, element).run().done(null, errors.onUnexpectedError);
result = true;
}
return result;
}
+
+ protected onUp(tree: ITree, event: IKeyboardEvent): boolean {
+ if (tree.getNavigator().first() === tree.getFocus()) {
+ this.viewlet.moveFocusFromResults();
+ return true;
+ }
+ return super.onUp(tree, event);
+ }
}
export class SearchFilter implements IFilter {
diff --git a/src/vs/workbench/parts/search/browser/searchViewlet.ts b/src/vs/workbench/parts/search/browser/searchViewlet.ts
index 2ca4ef03896..6665f91abed 100644
--- a/src/vs/workbench/parts/search/browser/searchViewlet.ts
+++ b/src/vs/workbench/parts/search/browser/searchViewlet.ts
@@ -8,6 +8,7 @@
import 'vs/css!./media/searchviewlet';
import nls = require('vs/nls');
import {TPromise, PPromise} from 'vs/base/common/winjs.base';
+import {Delayer} from 'vs/base/common/async';
import {EditorType} from 'vs/editor/common/editorCommon';
import lifecycle = require('vs/base/common/lifecycle');
import errors = require('vs/base/common/errors');
@@ -18,10 +19,10 @@ import URI from 'vs/base/common/uri';
import strings = require('vs/base/common/strings');
import dom = require('vs/base/browser/dom');
import {IAction, Action} from 'vs/base/common/actions';
-import {StandardKeyboardEvent, IKeyboardEvent} from 'vs/base/browser/keyboardEvent';
+import {StandardKeyboardEvent} from 'vs/base/browser/keyboardEvent';
import timer = require('vs/base/common/timer');
import {Dimension, Builder, $} from 'vs/base/browser/builder';
-import {FindInput, IFindInputOptions} from 'vs/base/browser/ui/findinput/findInput';
+import { FindInput } from 'vs/base/browser/ui/findinput/findInput';
import {ITree} from 'vs/base/parts/tree/browser/tree';
import {Tree} from 'vs/base/parts/tree/browser/treeImpl';
import {Scope} from 'vs/workbench/common/memento';
@@ -31,7 +32,7 @@ import {IEditorGroupService} from 'vs/workbench/services/group/common/groupServi
import {getOutOfWorkspaceEditorResources} from 'vs/workbench/common/editor';
import {FileChangeType, FileChangesEvent, EventType as FileEventType} from 'vs/platform/files/common/files';
import {Viewlet} from 'vs/workbench/browser/viewlet';
-import {Match, EmptyMatch, SearchResult} from 'vs/workbench/parts/search/common/searchModel';
+import {Match, EmptyMatch, FileMatch, SearchResult, FileMatchOrMatch} from 'vs/workbench/parts/search/common/searchModel';
import {getExcludes, QueryBuilder} from 'vs/workbench/parts/search/common/searchQuery';
import {VIEWLET_ID} from 'vs/workbench/parts/search/common/constants';
import {MessageType, InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
@@ -51,27 +52,31 @@ import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry';
import {KeyCode, CommonKeybindings} from 'vs/base/common/keyCodes';
import { PatternInputWidget } from 'vs/workbench/parts/search/browser/patternInputWidget';
import { SearchRenderer, SearchDataSource, SearchSorter, SearchController, SearchAccessibilityProvider, SearchFilter } from 'vs/workbench/parts/search/browser/searchResultsView';
-import { RefreshAction, SelectOrRemoveAction, CollapseAllAction, ClearSearchResultsAction, ConfigureGlobalExclusionsAction } from 'vs/workbench/parts/search/browser/searchActions';
+import { SearchWidget } from 'vs/workbench/parts/search/browser/searchWidget';
+import { RefreshAction, CollapseAllAction, ClearSearchResultsAction, ConfigureGlobalExclusionsAction } from 'vs/workbench/parts/search/browser/searchActions';
+import { IReplaceService } from 'vs/workbench/parts/search/common/replace';
export class SearchViewlet extends Viewlet {
- private static MAX_TEXT_RESULTS = 2048;
-
private isDisposed: boolean;
+ private toDispose: lifecycle.IDisposable[];
+
private currentRequest: PPromise;
+ private delayedRefresh: Delayer;
private loading: boolean;
private queryBuilder: QueryBuilder;
private viewModel: SearchResult;
private callOnModelChange: lifecycle.IDisposable[];
+ private replacingAll:boolean= false;
private viewletVisible: IKeybindingContextKey;
private actionRegistry: { [key: string]: Action; };
private tree: ITree;
private viewletSettings: any;
private domNode: Builder;
- private queryBox: HTMLElement;
private messages: Builder;
- private findInput: FindInput;
+ private searchWidgetsContainer: Builder;
+ private searchWidget: SearchWidget;
private size: Dimension;
private queryDetails: HTMLElement;
private inputPatternExclusions: PatternInputWidget;
@@ -93,10 +98,13 @@ export class SearchViewlet extends Viewlet {
@IConfigurationService private configurationService: IConfigurationService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@ISearchService private searchService: ISearchService,
- @IKeybindingService keybindingService: IKeybindingService
+ @IKeybindingService keybindingService: IKeybindingService,
+ @IReplaceService private replaceService: IReplaceService
) {
super(VIEWLET_ID, telemetryService);
+ this.toDispose = [];
+ this.delayedRefresh = new Delayer(200);
this.viewletVisible = keybindingService.createKey('searchViewletVisible', true);
this.callOnModelChange = [];
@@ -119,16 +127,6 @@ export class SearchViewlet extends Viewlet {
public create(parent: Builder): TPromise {
super.create(parent);
- let filePatterns = this.viewletSettings['query.filePatterns'] || '';
- let patternExclusions = this.viewletSettings['query.folderExclusions'] || '';
- let exclusionsUsePattern = this.viewletSettings['query.exclusionsUsePattern'];
- let includesUsePattern = this.viewletSettings['query.includesUsePattern'];
- let patternIncludes = this.viewletSettings['query.folderIncludes'] || '';
- let contentPattern = this.viewletSettings['query.contentPattern'] || '';
- let isRegex = this.viewletSettings['query.regex'] === true;
- let isWholeWords = this.viewletSettings['query.wholeWords'] === true;
- let isCaseSensitive = this.viewletSettings['query.caseSensitive'] === true;
-
let builder: Builder;
this.domNode = parent.div({
'class': 'search-viewlet'
@@ -136,63 +134,26 @@ export class SearchViewlet extends Viewlet {
builder = div;
});
- let onStandardKeyUp = (keyboardEvent: IKeyboardEvent) => {
- if (keyboardEvent.keyCode === KeyCode.Enter) {
+ builder.div({'class': ['search-widgets-container']}, (div) => {
+ this.searchWidgetsContainer= div;
+ });
+ this.createSearchWidget(this.searchWidgetsContainer);
+
+ let filePatterns = this.viewletSettings['query.filePatterns'] || '';
+ let patternExclusions = this.viewletSettings['query.folderExclusions'] || '';
+ let exclusionsUsePattern = this.viewletSettings['query.exclusionsUsePattern'];
+ let includesUsePattern = this.viewletSettings['query.includesUsePattern'];
+ let patternIncludes = this.viewletSettings['query.folderIncludes'] || '';
+
+ let onKeyUp = (e: KeyboardEvent) => {
+ if (e.keyCode === KeyCode.Enter) {
this.onQueryChanged(true);
- } else if (keyboardEvent.keyCode === KeyCode.Escape) {
+ } else if (e.keyCode === KeyCode.Escape) {
this.cancelSearch();
}
};
- let onKeyUp = (e: KeyboardEvent) => {
- onStandardKeyUp(new StandardKeyboardEvent(e));
- };
-
- this.queryBox = builder.div({ 'class': 'query-box' }, (div) => {
- let options: IFindInputOptions = {
- label: nls.localize('label.Search', 'Search: Type Search Term and press Enter to search or Escape to cancel'),
- validation: (value: string) => {
- if (value.length === 0) {
- return null;
- }
- if (!this.findInput.getRegex()) {
- return null;
- }
- let regExp: RegExp;
- try {
- regExp = new RegExp(value);
- } catch (e) {
- return { content: e.message };
- }
- if (strings.regExpLeadsToEndlessLoop(regExp)) {
- return { content: nls.localize('regexp.validationFailure', "Expression matches everything") };
- }
- },
- placeholder: nls.localize('findPlaceHolder', "Press Enter to Search, ESC to Cancel")
- };
-
- this.findInput = new FindInput(div.getHTMLElement(), this.contextViewService, options);
- this.findInput.onKeyUp(onStandardKeyUp);
- this.findInput.onKeyDown((keyboardEvent: IKeyboardEvent) => {
- if (keyboardEvent.keyCode === KeyCode.DownArrow) {
- dom.EventHelper.stop(keyboardEvent);
- if (this.showsFileTypes()) {
- this.toggleFileTypes(true, true);
- } else {
- this.selectTreeIfNotSelected(keyboardEvent);
- }
- }
- });
- this.findInput.onDidOptionChange((viaKeyboard) => {
- this.onQueryChanged(true, viaKeyboard);
- });
- this.findInput.setValue(contentPattern);
- this.findInput.setRegex(isRegex);
- this.findInput.setCaseSensitive(isCaseSensitive);
- this.findInput.setWholeWords(isWholeWords);
- }).style({ position: 'relative' }).getHTMLElement();
-
- this.queryDetails = builder.div({ 'class': ['query-details', 'separator'] }, (builder) => {
+ this.queryDetails = this.searchWidgetsContainer.div({ 'class': ['query-details'] }, (builder) => {
builder.div({ 'class': 'more', 'tabindex': 0, 'role': 'button', 'title': nls.localize('moreSearch', "Toggle Search Details") })
.on(dom.EventType.CLICK, (e) => {
dom.EventHelper.stop(e);
@@ -224,8 +185,7 @@ export class SearchViewlet extends Viewlet {
let keyboardEvent = new StandardKeyboardEvent(e);
if (keyboardEvent.equals(CommonKeybindings.UP_ARROW)) {
dom.EventHelper.stop(e);
- this.findInput.focus();
- this.findInput.select();
+ this.searchWidget.focus(true, true);
} else if (keyboardEvent.equals(CommonKeybindings.DOWN_ARROW)) {
dom.EventHelper.stop(e);
this.inputPatternExclusions.focus();
@@ -258,7 +218,7 @@ export class SearchViewlet extends Viewlet {
this.inputPatternIncludes.select();
} else if (keyboardEvent.equals(CommonKeybindings.DOWN_ARROW)) {
dom.EventHelper.stop(e);
- this.selectTreeIfNotSelected(keyboardEvent);
+ this.selectTreeIfNotSelected();
}
}).on(FindInput.OPTION_CHANGE, (e) => {
this.onQueryChanged(false);
@@ -280,13 +240,104 @@ export class SearchViewlet extends Viewlet {
}).hide();
}).getHTMLElement();
- this.messages = builder.div({ 'class': 'messages' }).hide().clone();
+ this.messages = this.searchWidgetsContainer.div({ 'class': 'messages' }).hide().clone();
+ this.createSearchResultsView(builder);
+
+ this.actionRegistry = {};
+ let actions: Action[] = [new CollapseAllAction(this), new RefreshAction(this), new ClearSearchResultsAction(this)];
+ actions.forEach((action) => {
+ this.actionRegistry[action.id] = action;
+ });
+
+ if (filePatterns !== '' || patternExclusions !== '' || patternIncludes !== '') {
+ this.toggleFileTypes(true, true, true);
+ }
+
+ this.updateGlobalPatternExclusions(this.configurationService.getConfiguration());
+
+ return TPromise.as(null);
+ }
+
+ private createSearchWidget(builder: Builder): void {
+ let contentPattern = this.viewletSettings['query.contentPattern'] || '';
+ let isRegex = this.viewletSettings['query.regex'] === true;
+ let isWholeWords = this.viewletSettings['query.wholeWords'] === true;
+ let isCaseSensitive = this.viewletSettings['query.caseSensitive'] === true;
+
+ this.searchWidget= new SearchWidget(builder, this.contextViewService, {
+ value: contentPattern,
+ isRegex: isRegex,
+ isCaseSensitive: isCaseSensitive,
+ isWholeWords: isWholeWords
+ }, this.instantiationService);
+
+ this.searchWidget.onSearchSubmit((refresh) => this.onQueryChanged(refresh));
+ this.searchWidget.onSearchCancel(() => this.cancelSearch());
+ this.searchWidget.searchInput.onDidOptionChange((viaKeyboard) => this.onQueryChanged(true, viaKeyboard));
+
+ this.searchWidget.onReplaceToggled((state) => this.layout(this.size));
+ this.searchWidget.onReplaceStateChange((state) => {
+ if (this.viewModel) {
+ this.viewModel.replaceText= this.searchWidget.getReplaceValue();
+ }
+ this.tree.refresh();
+ });
+ this.searchWidget.onReplaceValueChanged((value) => {
+ if (this.viewModel) {
+ this.viewModel.replaceText= this.searchWidget.getReplaceValue();
+ }
+ this.tree.refresh();
+ });
+
+ this.searchWidget.onKeyDownArrow(() => {
+ if (this.showsFileTypes()) {
+ this.toggleFileTypes(true, this.showsFileTypes());
+ } else {
+ this.selectTreeIfNotSelected();
+ }
+ });
+ this.searchWidget.onReplaceAll(() => this.replaceAll());
+ }
+
+ private replaceAll(): void {
+ let progressRunner= this.progressService.show(100);
+
+ let occurrences= this.viewModel.count();
+ let fileCount= this.viewModel.fileCount();
+ let afterReplaceAllMessage= nls.localize('replaceAll.message', "Replaced {0} occurrences across {1} files.", occurrences, fileCount);
+ let isDone:boolean= false;
+
+ let confirmation= {
+ title: nls.localize('replaceAll.confirmation.title', "Replace All"),
+ message: nls.localize('replaceAll.confirmation.message', "Replace {0} occurrences across {1} files?", occurrences, fileCount),
+ primaryButton: nls.localize('replaceAll.confirm.button', "Replace")
+ };
+
+ if (this.messageService.confirm(confirmation)) {
+ this.replacingAll= true;
+ this.replaceService.replace(this.viewModel.matches(), this.viewModel.replaceText, progressRunner).then(() => {
+ isDone= true;
+ this.replacingAll= false;
+ setTimeout(() => {
+ progressRunner.done();
+ this.showEmptyStage();
+ this.showMessage(afterReplaceAllMessage);
+ }, 200);
+ }, () => this.replacingAll= false);
+ }
+ }
+
+ private showMessage(text: string): Builder {
+ return this.messages.empty().show().asContainer().div({ 'class': 'message', text: text });
+ }
+
+ private createSearchResultsView(builder: Builder): void {
builder.div({ 'class': 'results' }, (div) => {
this.results = div;
let dataSource = new SearchDataSource();
- let renderer = this.instantiationService.createInstance(SearchRenderer, this.getActionRunner());
+ let renderer = this.instantiationService.createInstance(SearchRenderer, this.getActionRunner(), this);
this.tree = new Tree(div.getHTMLElement(), {
dataSource: dataSource,
@@ -323,20 +374,6 @@ export class SearchViewlet extends Viewlet {
this.onFocus(element, !focusEditor, sideBySide, doubleClick);
}));
});
-
- this.actionRegistry = {};
- let actions: Action[] = [new CollapseAllAction(this), new RefreshAction(this), new ClearSearchResultsAction(this), new SelectOrRemoveAction(this)];
- actions.forEach((action) => {
- this.actionRegistry[action.id] = action;
- });
-
- if (filePatterns !== '' || patternExclusions !== '' || patternIncludes !== '') {
- this.toggleFileTypes(true, true, true);
- }
-
- this.updateGlobalPatternExclusions(this.configurationService.getConfiguration());
-
- return TPromise.as(null);
}
private updateGlobalPatternExclusions(configuration: ISearchConfiguration): void {
@@ -393,13 +430,19 @@ export class SearchViewlet extends Viewlet {
public focus(): void {
super.focus();
- let selectedText = this.getSelectionFromEditor();
+ let selectedText = this.getSearchTextFromEditor();
if (selectedText) {
- this.findInput.setValue(selectedText);
+ this.searchWidget.searchInput.setValue(selectedText);
}
+ this.searchWidget.focus();
+ }
- this.findInput.focus();
- this.findInput.select();
+ public moveFocusFromResults(): void {
+ if (this.showsFileTypes()) {
+ this.toggleFileTypes(true, true, false, true);
+ } else {
+ this.searchWidget.focus(true, true);
+ }
}
private reLayout(): void {
@@ -407,15 +450,13 @@ export class SearchViewlet extends Viewlet {
return;
}
- this.findInput.setWidth(this.size.width - 34 /* container margin */);
+ this.searchWidget.setWidth(this.size.width - 34 /* container margin */);
- this.inputPatternExclusions.setWidth(this.size.width - 42 /* container margin */);
- this.inputPatternIncludes.setWidth(this.size.width - 42 /* container margin */);
- this.inputPatternGlobalExclusions.width = this.size.width - 42 /* container margin */ - 24 /* actions */;
+ this.inputPatternExclusions.setWidth(this.size.width - 36 /* container margin */);
+ this.inputPatternIncludes.setWidth(this.size.width - 36 /* container margin */);
+ this.inputPatternGlobalExclusions.width = this.size.width - 36 /* container margin */ - 24 /* actions */;
- let queryBoxHeight = dom.getTotalHeight(this.queryBox);
- let queryDetailsHeight = dom.getTotalHeight(this.queryDetails);
- let searchResultContainerSize = this.size.height - queryBoxHeight - queryDetailsHeight;
+ let searchResultContainerSize = this.size.height - dom.getTotalHeight(this.searchWidgetsContainer.getContainer()) - 6 /** container margin top */;
this.results.style({ height: searchResultContainerSize + 'px' });
this.tree.layout(searchResultContainerSize);
@@ -433,7 +474,7 @@ export class SearchViewlet extends Viewlet {
public clearSearchResults(): void {
this.disposeModel();
this.showEmptyStage();
- this.findInput.clear();
+ this.searchWidget.clear();
if (this.currentRequest) {
this.currentRequest.cancel();
this.currentRequest = null;
@@ -442,8 +483,7 @@ export class SearchViewlet extends Viewlet {
public cancelSearch(): boolean {
if (this.currentRequest) {
- this.findInput.focus();
- this.findInput.select();
+ this.searchWidget.focus();
this.currentRequest.cancel();
this.currentRequest = null;
@@ -454,7 +494,7 @@ export class SearchViewlet extends Viewlet {
return false;
}
- private selectTreeIfNotSelected(keyboardEvent: IKeyboardEvent): void {
+ private selectTreeIfNotSelected(): void {
if (this.tree.getInput()) {
this.tree.DOMFocus();
let selection = this.tree.getSelection();
@@ -464,21 +504,21 @@ export class SearchViewlet extends Viewlet {
}
}
- private getSelectionFromEditor(): string {
+ private getSearchTextFromEditor(): string {
if (!this.editorService.getActiveEditor()) {
return null;
}
- let editor: any = this.editorService.getActiveEditor().getControl();
- if (!editor || !isFunction(editor.getEditorType) || editor.getEditorType() !== EditorType.ICodeEditor) { // Substitute for (editor instanceof ICodeEditor)
+ let editorControl: any = this.editorService.getActiveEditor().getControl();
+ if (!editorControl || !isFunction(editorControl.getEditorType) || editorControl.getEditorType() !== EditorType.ICodeEditor) { // Substitute for (editor instanceof ICodeEditor)
return null;
}
- let range = editor.getSelection();
+ let range = editorControl.getSelection();
if (range && !range.isEmpty() && range.startLineNumber === range.endLineNumber) {
- let r = editor.getModel().getLineContent(range.startLineNumber);
- r = r.substring(range.startColumn - 1, range.endColumn - 1);
- return r;
+ let searchText = editorControl.getModel().getLineContent(range.startLineNumber);
+ searchText = searchText.substring(range.startColumn - 1, range.endColumn - 1);
+ return searchText;
}
return null;
@@ -488,7 +528,7 @@ export class SearchViewlet extends Viewlet {
return dom.hasClass(this.queryDetails, 'more');
}
- public toggleFileTypes(moveFocus?: boolean, show?: boolean, skipLayout?: boolean): void {
+ public toggleFileTypes(moveFocus?: boolean, show?: boolean, skipLayout?: boolean, reverse?: boolean): void {
let cls = 'more';
show = typeof show === 'undefined' ? !dom.hasClass(this.queryDetails, cls) : Boolean(show);
skipLayout = Boolean(skipLayout);
@@ -496,14 +536,18 @@ export class SearchViewlet extends Viewlet {
if (show) {
dom.addClass(this.queryDetails, cls);
if (moveFocus) {
- this.inputPatternIncludes.focus();
- this.inputPatternIncludes.select();
+ if (reverse) {
+ this.inputPatternExclusions.focus();
+ this.inputPatternExclusions.select();
+ } else {
+ this.inputPatternIncludes.focus();
+ this.inputPatternIncludes.select();
+ }
}
} else {
dom.removeClass(this.queryDetails, cls);
if (moveFocus) {
- this.findInput.focus();
- this.findInput.select();
+ this.searchWidget.focus();
}
}
@@ -521,15 +565,15 @@ export class SearchViewlet extends Viewlet {
if (workspaceRelativePath) {
this.inputPatternIncludes.setIsGlobPattern(false);
this.inputPatternIncludes.setValue(workspaceRelativePath);
- this.findInput.focus();
+ this.searchWidget.focus(false);
}
}
public onQueryChanged(rerunQuery: boolean, preserveFocus?: boolean): void {
- let isRegex = this.findInput.getRegex();
- let isWholeWords = this.findInput.getWholeWords();
- let isCaseSensitive = this.findInput.getCaseSensitive();
- let contentPattern = this.findInput.getValue();
+ let isRegex = this.searchWidget.searchInput.getRegex();
+ let isWholeWords = this.searchWidget.searchInput.getWholeWords();
+ let isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive();
+ let contentPattern = this.searchWidget.searchInput.getValue();
let patternExcludes = this.inputPatternExclusions.getValue().trim();
let exclusionsUsePattern = this.inputPatternExclusions.isGlobPattern();
let patternIncludes = this.inputPatternIncludes.getValue().trim();
@@ -581,14 +625,14 @@ export class SearchViewlet extends Viewlet {
folderResources: this.contextService.getWorkspace() ? [this.contextService.getWorkspace().resource] : [],
extraFileResources: getOutOfWorkspaceEditorResources(this.editorGroupService, this.contextService),
excludePattern: excludes,
- includePattern: includes,
- maxResults: SearchViewlet.MAX_TEXT_RESULTS,
+ includePattern: includes
+ // maxResults: SearchViewlet.MAX_TEXT_RESULTS,
};
this.onQueryTriggered(this.queryBuilder.text(content, options), patternExcludes, patternIncludes);
if (!preserveFocus) {
- this.findInput.focus(); // focus back to input field
+ this.searchWidget.focus(false); // focus back to input field
}
}
@@ -607,7 +651,7 @@ export class SearchViewlet extends Viewlet {
let progressWorked = 0;
this.loading = true;
- this.findInput.clearMessage();
+ this.searchWidget.searchInput.clearMessage();
this.disposeModel();
this.showEmptyStage();
@@ -657,6 +701,7 @@ export class SearchViewlet extends Viewlet {
this.viewModel.append(completed.results);
}
}
+ this.viewModel.replaceText= this.searchWidget.getReplaceValue();
this.tree.refresh().then(() => {
autoExpand(true);
@@ -667,12 +712,11 @@ export class SearchViewlet extends Viewlet {
this.telemetryService.publicLog('searchResultsShown', { count: this.viewModel.count(), fileCount: this.viewModel.fileCount() });
this.actionRegistry['refresh'].enabled = true;
- this.actionRegistry['selectOrRemove'].enabled = hasResults;
this.actionRegistry['vs.tree.collapse'].enabled = hasResults;
this.actionRegistry['clearSearchResults'].enabled = hasResults;
if (completed && completed.limitHit) {
- this.findInput.showMessage({
+ this.searchWidget.searchInput.showMessage({
content: nls.localize('searchMaxResultsWarning', "The result set only contains a subset of all matches. Please be more specific in your search to narrow down the results."),
type: MessageType.WARNING
});
@@ -700,7 +744,7 @@ export class SearchViewlet extends Viewlet {
this.tree.onHidden();
this.results.hide();
- let div = this.messages.empty().show().asContainer().div({ 'class': 'message', text: message });
+ let div = this.showMessage(message);
if (!completed) {
$(div).a({
@@ -784,7 +828,11 @@ export class SearchViewlet extends Viewlet {
this.viewModel = this.instantiationService.createInstance(SearchResult, query.contentPattern);
this.tree.setInput(this.viewModel).then(() => {
autoExpand(false);
- this.callOnModelChange.push(this.viewModel.addListener2('changed', (e: any) => this.tree.refresh(e, true)));
+ this.callOnModelChange.push(this.viewModel.addListener2('changed', (e: any) => {
+ if (this.replacingAll) {
+ this.delayedRefresh.trigger(() => this.tree.refresh(e, true));
+ }
+ }));
}).done(null, errors.onUnexpectedError);
}
@@ -840,7 +888,6 @@ export class SearchViewlet extends Viewlet {
// disable 'result'-actions
this.actionRegistry['refresh'].enabled = false;
- this.actionRegistry['selectOrRemove'].enabled = false;
this.actionRegistry['vs.tree.collapse'].enabled = false;
this.actionRegistry['clearSearchResults'].enabled = false;
@@ -858,16 +905,50 @@ export class SearchViewlet extends Viewlet {
this.telemetryService.publicLog('searchResultChosen');
+ return this.open(lineMatch, preserveFocus, sideBySide, pinned);
+ }
+
+ public open(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): TPromise {
+ let selection= this.getSelectionFrom(element);
+ let resource= element instanceof Match ? element.parent().resource() : (element).resource();
return this.editorService.openEditor({
- resource: lineMatch.parent().resource(),
+ resource: resource,
options: {
preserveFocus,
pinned,
- selection: lineMatch instanceof EmptyMatch ? void 0 : lineMatch.range()
+ selection: selection
}
}, sideBySide);
}
+ private getSelectionFrom(element: FileMatchOrMatch): any {
+ if (element instanceof EmptyMatch) {
+ return void 0;
+ }
+
+ let match: Match= null;
+ if (element instanceof Match) {
+ match= element;
+ }
+ if (element instanceof FileMatch && element.count() > 0) {
+ match= element.matches()[element.matches.length - 1];
+ }
+ if (match) {
+ let range= match.range();
+ if (this.viewModel.isReplaceActive()) {
+ let replaceText= this.viewModel.replaceText;
+ return {
+ startLineNumber: range.startLineNumber,
+ startColumn: range.startColumn + replaceText.length,
+ endLineNumber: range.startLineNumber,
+ endColumn: range.startColumn + replaceText.length
+ };
+ }
+ return range;
+ }
+ return void 0;
+ }
+
private onUntitledFileSaved(e: UntitledEditorEvent): void {
if (!this.viewModel) {
return;
@@ -906,10 +987,13 @@ export class SearchViewlet extends Viewlet {
public dispose(): void {
this.isDisposed = true;
+ this.toDispose = lifecycle.dispose(this.toDispose);
+
if (this.tree) {
this.tree.dispose();
}
+ this.searchWidget.dispose();
this.inputPatternIncludes.dispose();
this.inputPatternExclusions.dispose();
@@ -926,4 +1010,4 @@ export class SearchViewlet extends Viewlet {
}
this.callOnModelChange = lifecycle.dispose(this.callOnModelChange);
}
-}
+}
\ No newline at end of file
diff --git a/src/vs/workbench/parts/search/browser/searchWidget.ts b/src/vs/workbench/parts/search/browser/searchWidget.ts
new file mode 100644
index 00000000000..6f4f6045bbc
--- /dev/null
+++ b/src/vs/workbench/parts/search/browser/searchWidget.ts
@@ -0,0 +1,257 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import nls = require('vs/nls');
+import strings = require('vs/base/common/strings');
+import dom = require('vs/base/browser/dom');
+import { TPromise } from 'vs/base/common/winjs.base';
+import { Widget } from 'vs/base/browser/ui/widget';
+import { Action } from 'vs/base/common/actions';
+import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput';
+import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
+import { Button } from 'vs/base/browser/ui/button/button';
+import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
+import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
+import { KeyCode } from 'vs/base/common/keyCodes';
+import Event, { Emitter } from 'vs/base/common/event';
+import { Builder } from 'vs/base/browser/builder';
+import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
+
+export interface ISearchWidgetOptions {
+ value?:string;
+ isRegex?:boolean;
+ isCaseSensitive?:boolean;
+ isWholeWords?:boolean;
+}
+
+export class SearchWidget extends Widget {
+
+ public static REPLACE_PLACE_HOLD= nls.localize('search.replace.placeHolder', "Replace");
+
+ public domNode: HTMLElement;
+ public searchInput: FindInput;
+ private replaceInput: InputBox;
+
+ private replaceInputContainer: HTMLElement;
+ private toggleReplaceButton: Button;
+ private replaceAllAction: Action;
+
+ private _onSearchSubmit = this._register(new Emitter());
+ public onSearchSubmit: Event = this._onSearchSubmit.event;
+
+ private _onSearchCancel = this._register(new Emitter());
+ public onSearchCancel: Event = this._onSearchCancel.event;
+
+ private _onReplaceToggled = this._register(new Emitter());
+ public onReplaceToggled: Event = this._onReplaceToggled.event;
+
+ private _onReplaceState = this._register(new Emitter());
+ public onReplaceStateChange: Event = this._onReplaceState.event;
+
+ private _onReplaceValueChanged = this._register(new Emitter());
+ public onReplaceValueChanged: Event = this._onReplaceValueChanged.event;
+
+ private _onKeyDownArrow = this._register(new Emitter());
+ public onKeyDownArrow: Event = this._onKeyDownArrow.event;
+
+ private _onReplaceAll = this._register(new Emitter());
+ public onReplaceAll: Event = this._onReplaceAll.event;
+
+ constructor(container: Builder, private contextViewService: IContextViewService, options: ISearchWidgetOptions= Object.create(null),
+ @IInstantiationService private instantiationService: IInstantiationService) {
+ super();
+ this.render(container, options);
+ }
+
+ public focus(select:boolean= true, focusReplace: boolean= false):void {
+ if (this.searchInput.inputBox.hasFocus() || this.replaceInput.hasFocus()) {
+ return;
+ }
+
+ if (focusReplace && this.isReplaceShown()) {
+ this.replaceInput.focus();
+ if (select) {
+ this.replaceInput.select();
+ }
+ } else {
+ this.searchInput.focus();
+ if (select) {
+ this.searchInput.select();
+ }
+ }
+ }
+
+ public setWidth(width: number) {
+ this.searchInput.setWidth(width - 2);
+ this.replaceInput.width= width - 28;
+ }
+
+ public clear() {
+ this.searchInput.clear();
+ this.replaceInput.value= '';
+ }
+
+ public isReplaceActive(): boolean {
+ return this.isReplaceShown() && this.replaceAllAction.enabled;
+ }
+
+ public isReplaceShown(): boolean {
+ return !dom.hasClass(this.replaceInputContainer, 'disabled');
+ }
+
+ public getReplaceValue():string {
+ return this.isReplaceActive() ? this.replaceInput.value : null;
+ }
+
+ private render(container: Builder, options: ISearchWidgetOptions): void {
+ this.domNode = container.div({ 'class': 'search-widget' }).style({ position: 'relative' }).getHTMLElement();
+ this.renderToggleReplaceButton(this.domNode);
+
+ this.renderSearchInput(this.domNode, options);
+ this.renderReplaceInput(this.domNode);
+ }
+
+ private renderToggleReplaceButton(parent: HTMLElement): void {
+ this.toggleReplaceButton= this._register(new Button(parent));
+ this.toggleReplaceButton.icon= 'toggle-replace-button collapse';
+ this.toggleReplaceButton.addListener2('click', () => this.onToggleReplaceButton());
+ this.toggleReplaceButton.getElement().title= nls.localize('search.replace.toggle.button.title', "Toggle Replace");
+ }
+
+ private renderSearchInput(parent: HTMLElement, options: ISearchWidgetOptions): void {
+ let inputOptions: IFindInputOptions = {
+ label: nls.localize('label.Search', 'Search: Type Search Term and press Enter to search or Escape to cancel'),
+ validation: (value: string) => this.validatSearchInput(value),
+ placeholder: nls.localize('search.placeHolder', "Search")
+ };
+
+ let searchInputContainer= dom.append(parent, dom.emmet('.search-box.input-box'));
+ this.searchInput = this._register(new FindInput(searchInputContainer, this.contextViewService, inputOptions));
+ this.searchInput.onKeyUp((keyboardEvent: IKeyboardEvent) => this.onSearchInputKeyUp(keyboardEvent));
+ this.searchInput.onKeyDown((keyboardEvent: IKeyboardEvent) => this.onSearchInputKeyDown(keyboardEvent));
+ this.searchInput.setValue(options.value || '');
+ this.searchInput.setRegex(!!options.isRegex);
+ this.searchInput.setCaseSensitive(!!options.isCaseSensitive);
+ this.searchInput.setWholeWords(!!options.isWholeWords);
+ this._register(dom.addDisposableListener(this.searchInput.inputBox.inputElement, dom.EventType.FOCUS, () => this.updateReplaceActionState()));
+ this._register(dom.addDisposableListener(this.searchInput.inputBox.inputElement, dom.EventType.BLUR, () => this.updateReplaceActionState()));
+ }
+
+ private renderReplaceInput(parent: HTMLElement): void {
+ this.replaceAllAction = new Action('action-replace-all', nls.localize('file.replaceAll.label', "Replace All"), 'action-replace-all', false, () => {
+ this._onReplaceAll.fire();
+ return TPromise.as(null);
+ });
+ this.replaceInputContainer= dom.append(parent, dom.emmet('.replace-box.input-box.disabled'));
+ this.replaceInput = this._register(new InputBox(this.replaceInputContainer, this.contextViewService, {
+ placeholder: SearchWidget.REPLACE_PLACE_HOLD,
+ actions: [this.replaceAllAction]
+ }));
+ this.onkeydown(this.replaceInput.inputElement, (keyboardEvent) => this.onReplaceInputKeyDown(keyboardEvent));
+ this.onkeyup(this.replaceInput.inputElement, (keyboardEvent) => this.onReplaceInputKeyUp(keyboardEvent));
+ this.replaceInput.onDidChange(() => this._onReplaceValueChanged.fire());
+ }
+
+ private onToggleReplaceButton():void {
+ dom.toggleClass(this.replaceInputContainer, 'disabled');
+ dom.toggleClass(this.toggleReplaceButton.getElement(), 'collapse');
+ dom.toggleClass(this.toggleReplaceButton.getElement(), 'expand');
+ this._onReplaceToggled.fire();
+ this.updateReplaceActionState();
+ }
+
+ private updateReplaceActionState():boolean {
+ let enabled= this.isReplaceShown() && !this.searchInput.inputBox.hasFocus();
+ if (this.replaceAllAction.enabled !== enabled) {
+ this.replaceAllAction.enabled= enabled;
+ this._onReplaceState.fire();
+ return true;
+ }
+ return false;
+ }
+
+ private validatSearchInput(value: string): any {
+ if (value.length === 0) {
+ return null;
+ }
+ if (!this.searchInput.getRegex()) {
+ return null;
+ }
+ let regExp: RegExp;
+ try {
+ regExp = new RegExp(value);
+ } catch (e) {
+ return { content: e.message };
+ }
+ if (strings.regExpLeadsToEndlessLoop(regExp)) {
+ return { content: nls.localize('regexp.validationFailure', "Expression matches everything") };
+ }
+ }
+
+ private onSearchInputKeyUp(keyboardEvent: IKeyboardEvent) {
+ switch (keyboardEvent.keyCode) {
+ case KeyCode.Enter:
+ this.submitSearch();
+ return;
+ case KeyCode.Escape:
+ this._onSearchCancel.fire();
+ return;
+ default:
+ return;
+ }
+ }
+
+ private onSearchInputKeyDown(keyboardEvent: IKeyboardEvent) {
+ switch (keyboardEvent.keyCode) {
+ case KeyCode.DownArrow:
+ if (this.isReplaceShown()) {
+ this.replaceInput.focus();
+ keyboardEvent.stopPropagation();
+ } else {
+ this._onKeyDownArrow.fire();
+ }
+ return;
+ default:
+ return;
+ }
+ }
+
+ private onReplaceInputKeyUp(keyboardEvent: IKeyboardEvent) {
+ switch (keyboardEvent.keyCode) {
+ case KeyCode.Enter:
+ this.submitSearch();
+ return;
+ case KeyCode.Escape:
+ this.onToggleReplaceButton();
+ this.searchInput.focus();
+ return;
+ default:
+ return;
+ }
+ }
+
+ private onReplaceInputKeyDown(keyboardEvent: IKeyboardEvent) {
+ switch (keyboardEvent.keyCode) {
+ case KeyCode.UpArrow:
+ this.searchInput.focus();
+ return;
+ case KeyCode.DownArrow:
+ this._onKeyDownArrow.fire();
+ return;
+ default:
+ return;
+ }
+ }
+
+ private submitSearch(refresh: boolean= true): void {
+ if (this.searchInput.getValue()) {
+ this._onSearchSubmit.fire(refresh);
+ }
+ }
+
+ public dispose(): void {
+ super.dispose();
+ }
+}
\ No newline at end of file
diff --git a/src/vs/workbench/parts/search/common/replace.ts b/src/vs/workbench/parts/search/common/replace.ts
new file mode 100644
index 00000000000..e757d0150f4
--- /dev/null
+++ b/src/vs/workbench/parts/search/common/replace.ts
@@ -0,0 +1,27 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { TPromise } from 'vs/base/common/winjs.base';
+import { Match, FileMatch } from 'vs/workbench/parts/search/common/searchModel';
+import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
+import { IProgressRunner } from 'vs/platform/progress/common/progress';
+
+export var IReplaceService = createDecorator('replaceService');
+
+export interface IReplaceService {
+
+ serviceId : ServiceIdentifier;
+
+ /**
+ * Replace the match with the given text.
+ */
+ replace(match: Match, text: string): TPromise;
+
+ /**
+ * Replace all the matches in the given file matches with provided text.
+ * You can also pass the progress runner to update the progress of replacing.
+ */
+ replace(files: FileMatch[], text: string, progress?: IProgressRunner): TPromise;
+}
diff --git a/src/vs/workbench/parts/search/common/searchModel.ts b/src/vs/workbench/parts/search/common/searchModel.ts
index f246e33c4be..995094d2cd9 100644
--- a/src/vs/workbench/parts/search/common/searchModel.ts
+++ b/src/vs/workbench/parts/search/common/searchModel.ts
@@ -123,6 +123,8 @@ export class FileMatch implements lifecycle.IDisposable {
}
}
+export type FileMatchOrMatch = FileMatch | Match;
+
export class LiveFileMatch extends FileMatch implements lifecycle.IDisposable {
private static DecorationOption: IModelDecorationOptions = {
@@ -199,12 +201,21 @@ export class LiveFileMatch extends FileMatch implements lifecycle.IDisposable {
private _isTextModelDisposed(): boolean {
return !this._model || (this._model).isDisposed();
}
+
+ public remove(match: Match): void {
+ super.remove(match);
+ if (this.count() === 0) {
+ this.add(new EmptyMatch(this));
+ }
+ }
+
}
export class SearchResult extends EventEmitter {
private _modelService: IModelService;
private _query: Search.IPatternInfo;
+ private _replace: string= null;
private _disposables: lifecycle.IDisposable[] = [];
private _matches: { [key: string]: FileMatch; } = Object.create(null);
@@ -221,6 +232,26 @@ export class SearchResult extends EventEmitter {
}
}
+ /**
+ * Return true if replace is enabled otherwise false
+ */
+ public isReplaceActive():boolean {
+ return this.replaceText !== null && this.replaceText !== void 0;
+ }
+
+ /**
+ * Returns the text to replace.
+ * Can be null if replace is not enabled. Use replace() before.
+ * Can be empty.
+ */
+ public get replaceText(): string {
+ return this._replace;
+ }
+
+ public set replaceText(replace: string) {
+ this._replace= replace;
+ }
+
private _onModelAdded(model: IModel): void {
let resource = model.uri,
fileMatch = this._matches[resource.toString()];