diff --git a/lib/find-options.coffee b/lib/find-options.coffee
index ee9feaca..f40aa8c3 100644
--- a/lib/find-options.coffee
+++ b/lib/find-options.coffee
@@ -4,6 +4,7 @@ _ = require 'underscore-plus'
Params = [
'findPattern'
'replacePattern'
+ 'paths'
'pathsPattern'
'useRegex'
'wholeWord'
@@ -20,6 +21,7 @@ class FindOptions
@findPattern = ''
@replacePattern = state.replacePattern ? ''
+ @paths = state.paths ? []
@pathsPattern = state.pathsPattern ? ''
@useRegex = state.useRegex ? atom.config.get('find-and-replace.useRegex') ? false
@caseSensitive = state.caseSensitive ? atom.config.get('find-and-replace.caseSensitive') ? false
diff --git a/lib/find.coffee b/lib/find.coffee
index efe7d746..9c547c95 100644
--- a/lib/find.coffee
+++ b/lib/find.coffee
@@ -6,6 +6,7 @@ FindOptions = require './find-options'
BufferSearch = require './buffer-search'
FileIcons = require './file-icons'
FindView = require './find-view'
+OpenFilesFindView = require './open-files-find-view'
ProjectFindView = require './project-find-view'
ResultsModel = require './project/results-model'
ResultsPaneView = require './project/results-pane'
@@ -35,39 +36,48 @@ module.exports =
else
@findModel.setEditor(null)
- @subscriptions.add atom.commands.add '.find-and-replace, .project-find', 'window:focus-next-pane', ->
+ @subscriptions.add atom.commands.add '.find-and-replace, .open-files-find, .project-find', 'window:focus-next-pane', ->
atom.views.getView(atom.workspace).focus()
+ @subscriptions.add atom.commands.add 'atom-workspace', 'open-files-find:show', =>
+ @createViews()
+ showPanel @openFilesFindPanel, => @openFilesFindView.focusFindElement()
+
+ @subscriptions.add atom.commands.add 'atom-workspace', 'open-files-find:toggle', =>
+ @createViews()
+ togglePanel @openFilesFindPanel, => @openFilesFindView.focusFindElement()
+
@subscriptions.add atom.commands.add 'atom-workspace', 'project-find:show', =>
@createViews()
- showPanel @projectFindPanel, @findPanel, => @projectFindView.focusFindElement()
+ showPanel @projectFindPanel, => @projectFindView.focusFindElement()
@subscriptions.add atom.commands.add 'atom-workspace', 'project-find:toggle', =>
@createViews()
- togglePanel @projectFindPanel, @findPanel, => @projectFindView.focusFindElement()
+ togglePanel @projectFindPanel, => @projectFindView.focusFindElement()
@subscriptions.add atom.commands.add 'atom-workspace', 'project-find:show-in-current-directory', ({target}) =>
@createViews()
@findPanel.hide()
+ @openFilesFindPanel.hide()
@projectFindPanel.show()
@projectFindView.focusFindElement()
@projectFindView.findInCurrentlySelectedDirectory(target)
@subscriptions.add atom.commands.add 'atom-workspace', 'find-and-replace:use-selection-as-find-pattern', =>
- return if @projectFindPanel?.isVisible() or @findPanel?.isVisible()
+ return if @openFilesFindPanel?.isVisible() or @projectFindPanel?.isVisible() or @findPanel?.isVisible()
@createViews()
@subscriptions.add atom.commands.add 'atom-workspace', 'find-and-replace:toggle', =>
@createViews()
- togglePanel @findPanel, @projectFindPanel, => @findView.focusFindEditor()
+ togglePanel @findPanel, => @findView.focusFindEditor()
@subscriptions.add atom.commands.add 'atom-workspace', 'find-and-replace:show', =>
@createViews()
- showPanel @findPanel, @projectFindPanel, => @findView.focusFindEditor()
+ showPanel @findPanel, => @findView.focusFindEditor()
@subscriptions.add atom.commands.add 'atom-workspace', 'find-and-replace:show-replace', =>
@createViews()
- showPanel @findPanel, @projectFindPanel, => @findView.focusReplaceEditor()
+ showPanel @findPanel, => @findView.focusReplaceEditor()
@subscriptions.add atom.commands.add 'atom-workspace', 'find-and-replace:clear-history', =>
@findHistory.clear()
@@ -78,6 +88,7 @@ module.exports =
isMiniEditor = target.tagName is 'ATOM-TEXT-EDITOR' and target.hasAttribute('mini')
unless isMiniEditor
@findPanel?.hide()
+ @openFilesFindPanel?.hide()
@projectFindPanel?.hide()
@subscriptions.add atom.commands.add 'atom-workspace',
@@ -93,13 +104,13 @@ module.exports =
@selectNextObjects.set(editor, selectNext)
selectNext
- showPanel = (panelToShow, panelToHide, postShowAction) ->
- panelToHide.hide()
+ showPanel = (panelToShow, postShowAction) =>
+ @panels.map (p) => p.hide() unless p is panelToShow
panelToShow.show()
postShowAction?()
- togglePanel = (panelToToggle, panelToHide, postToggleAction) ->
- panelToHide.hide()
+ togglePanel = (panelToToggle, postToggleAction) =>
+ @panels.map (p) => p.hide() unless p is panelToToggle
if panelToToggle.isVisible()
panelToToggle.hide()
@@ -159,13 +170,16 @@ module.exports =
options = {findBuffer, replaceBuffer, pathsBuffer, findHistoryCycler, replaceHistoryCycler, pathsHistoryCycler}
@findView = new FindView(@findModel, options)
-
+ @openFilesFindView = new OpenFilesFindView(@resultsModel, options)
@projectFindView = new ProjectFindView(@resultsModel, options)
@findPanel = atom.workspace.addBottomPanel(item: @findView, visible: false, className: 'tool-panel panel-bottom')
+ @openFilesFindPanel = atom.workspace.addBottomPanel(item: @openFilesFindView, visible: false, className: 'tool-panel panel-bottom')
@projectFindPanel = atom.workspace.addBottomPanel(item: @projectFindView, visible: false, className: 'tool-panel panel-bottom')
+ @panels = [@findPanel, @openFilesFindPanel, @projectFindPanel]
@findView.setPanel(@findPanel)
+ @openFilesFindView.setPanel(@openFilesFindPanel)
@projectFindView.setPanel(@projectFindPanel)
# HACK: Soooo, we need to get the model to the pane view whenever it is
@@ -191,6 +205,11 @@ module.exports =
@findModel?.destroy()
@findModel = null
+ @openFilesFindPanel?.destroy()
+ @openFilesFindPanel = null
+ @openFilesFindView?.destroy()
+ @openFilesFindView = null
+
@projectFindPanel?.destroy()
@projectFindPanel = null
@projectFindView?.destroy()
diff --git a/lib/open-files-find-view.js b/lib/open-files-find-view.js
new file mode 100644
index 00000000..e7294151
--- /dev/null
+++ b/lib/open-files-find-view.js
@@ -0,0 +1,508 @@
+const fs = require('fs-plus');
+const path = require('path');
+const _ = require('underscore-plus');
+const { TextEditor, Disposable, CompositeDisposable } = require('atom');
+const etch = require('etch');
+const Util = require('./project/util');
+const ResultsModel = require('./project/results-model');
+const ResultsPaneView = require('./project/results-pane');
+const $ = etch.dom;
+
+module.exports =
+class OpenFilesFindView {
+ constructor(model, {findBuffer, replaceBuffer, findHistoryCycler, replaceHistoryCycler}) {
+ this.model = model
+ this.findBuffer = findBuffer
+ this.replaceBuffer = replaceBuffer
+ this.findHistoryCycler = findHistoryCycler;
+ this.replaceHistoryCycler = replaceHistoryCycler;
+ this.subscriptions = new CompositeDisposable()
+
+ etch.initialize(this)
+
+ this.handleEvents();
+
+ this.findHistoryCycler.addEditorElement(this.findEditor.element);
+ this.replaceHistoryCycler.addEditorElement(this.replaceEditor.element);
+
+ this.onlyRunIfChanged = true;
+
+ this.clearMessages();
+ this.updateOptionViews();
+ }
+
+ update() {}
+
+ render() {
+ return (
+ $.div({tabIndex: -1, className: 'open-files-find padded'},
+ $.header({className: 'header'},
+ $.span({ref: 'closeButton', className: 'header-item close-button pull-right'},
+ $.i({className: "icon icon-x clickable"})
+ ),
+ $.span({ref: 'descriptionLabel', className: 'header-item description'}),
+ $.span({className: 'header-item options-label pull-right'},
+ $.span({}, 'Finding with Options: '),
+ $.span({ref: 'optionsLabel', className: 'options'}),
+ $.span({className: 'btn-group btn-toggle btn-group-options'},
+ $.button({ref: 'regexOptionButton', className: 'btn option-regex'},
+ $.svg({className: "icon", innerHTML: ``})
+ ),
+ $.button({ref: 'caseOptionButton', className: 'btn option-case-sensitive'},
+ $.svg({className: "icon", innerHTML: ``})
+ ),
+ $.button({ref: 'wholeWordOptionButton', className: 'btn option-whole-word'},
+ $.svg({className: "icon", innerHTML:``})
+ )
+ )
+ )
+ ),
+
+ $.section({ref: 'replacmentInfoBlock', className: 'input-block'},
+ $.progress({ref: 'replacementProgress', className: 'inline-block'}),
+ $.span({ref: 'replacmentInfo', className: 'inline-block'}, 'Replaced 2 files of 10 files')
+ ),
+
+ $.section({className: 'input-block find-container'},
+ $.div({className: 'input-block-item input-block-item--flex editor-container'},
+ etch.dom(TextEditor, {
+ ref: 'findEditor',
+ mini: true,
+ placeholderText: 'Find in open files',
+ buffer: this.findBuffer
+ })
+ ),
+ $.div({className: 'input-block-item'},
+ $.div({className: 'btn-group btn-group-find'},
+ $.button({ref: 'findAllButton', className: 'btn'}, 'Find All')
+ )
+ )
+ ),
+
+ $.section({className: 'input-block replace-container'},
+ $.div({className: 'input-block-item input-block-item--flex editor-container'},
+ etch.dom(TextEditor, {
+ ref: 'replaceEditor',
+ mini: true,
+ placeholderText: 'Replace in open files',
+ buffer: this.replaceBuffer
+ })
+ ),
+ $.div({className: 'input-block-item'},
+ $.div({className: 'btn-group btn-group-replace-all'},
+ $.button({ref: 'replaceAllButton', className: 'btn disabled'}, 'Replace All')
+ )
+ )
+ )
+ )
+ );
+ }
+
+ get findEditor() { return this.refs.findEditor }
+ get replaceEditor() { return this.refs.replaceEditor }
+
+ destroy() {
+ if (this.subscriptions) this.subscriptions.dispose();
+ if (this.tooltipSubscriptions) this.tooltipSubscriptions.dispose();
+ }
+
+ setPanel(panel) {
+ this.panel = panel;
+ this.subscriptions.add(this.panel.onDidChangeVisible(visible => {
+ if (visible) {
+ this.didShow();
+ } else {
+ this.didHide();
+ }
+ }));
+ }
+
+ didShow() {
+ atom.views.getView(atom.workspace).classList.add('find-visible');
+ if (this.tooltipSubscriptions != null) { return; }
+
+ this.updateReplaceAllButtonEnablement();
+
+ this.subscriptions.add(atom.workspace.onDidDestroyPaneItem(({ item }) => {
+ if (this.model.getFindOptions().openFiles && item && item.constructor.name === 'TextEditor'
+ && atom.workspace.getActivePaneItem().constructor.name === 'ResultsPaneView') {
+ this.search({onlyRunIfActive: true, openFiles: true});
+ }
+ }));
+
+ // this.subscriptions.add(atom.workspace.onDidAddTextEditor(() => {
+ // if (this.model.getFindOptions().openFiles) {
+ // this.search({onlyRunIfActive: true, openFiles: true});
+ // }
+ // }));
+
+ this.tooltipSubscriptions = new CompositeDisposable(
+ atom.tooltips.add(this.refs.closeButton, {
+ title: 'Close Panel Esc',
+ html: true
+ }),
+
+ atom.tooltips.add(this.refs.regexOptionButton, {
+ title: "Use Regex",
+ keyBindingCommand: 'open-files-find:toggle-regex-option',
+ keyBindingTarget: this.findEditor.element
+ }),
+
+ atom.tooltips.add(this.refs.caseOptionButton, {
+ title: "Match Case",
+ keyBindingCommand: 'open-files-find:toggle-case-option',
+ keyBindingTarget: this.findEditor.element
+ }),
+
+ atom.tooltips.add(this.refs.wholeWordOptionButton, {
+ title: "Whole Word",
+ keyBindingCommand: 'open-files-find:toggle-whole-word-option',
+ keyBindingTarget: this.findEditor.element
+ }),
+
+ atom.tooltips.add(this.refs.findAllButton, {
+ title: "Find All",
+ keyBindingCommand: 'find-and-replace:search',
+ keyBindingTarget: this.findEditor.element
+ })
+ );
+ }
+
+ didHide() {
+ this.hideAllTooltips();
+ let workspaceElement = atom.views.getView(atom.workspace);
+ workspaceElement.focus();
+ workspaceElement.classList.remove('find-visible');
+ }
+
+ hideAllTooltips() {
+ this.tooltipSubscriptions.dispose();
+ this.tooltipSubscriptions = null;
+ }
+
+ handleEvents() {
+ this.subscriptions.add(atom.commands.add('atom-workspace', {
+ 'find-and-replace:use-selection-as-find-pattern': () => this.setSelectionAsFindPattern()
+ }));
+
+ this.subscriptions.add(atom.commands.add(this.element, {
+ 'find-and-replace:focus-next': () => this.focusNextElement(1),
+ 'find-and-replace:focus-previous': () => this.focusNextElement(-1),
+ 'core:confirm': () => this.confirm(),
+ 'core:close': () => this.panel && this.panel.hide(),
+ 'core:cancel': () => this.panel && this.panel.hide(),
+ 'open-files-find:confirm': () => this.confirm(),
+ 'open-files-find:toggle-regex-option': () => this.toggleRegexOption(),
+ 'open-files-find:toggle-case-option': () => this.toggleCaseOption(),
+ 'open-files-find:toggle-whole-word-option': () => this.toggleWholeWordOption(),
+ 'open-files-find:replace-all': () => this.replaceAll()
+ }));
+
+ let updateInterfaceForSearching = () => {
+ this.setInfoMessage('Searching...');
+ };
+
+ let updateInterfaceForResults = results => {
+ if (results.matchCount === 0 && results.findPattern === '') {
+ this.clearMessages();
+ } else {
+ this.generateResultsMessage(results);
+ }
+ this.updateReplaceAllButtonEnablement(results);
+ };
+
+ let resetInterface = () => {
+ this.clearMessages();
+ this.updateReplaceAllButtonEnablement(null);
+ };
+
+ let afterSearch = () => {
+ if (atom.config.get('find-and-replace.closeFindPanelAfterSearch')) {
+ this.panel && this.panel.hide();
+ }
+ }
+
+ let searchFinished = results => {
+ afterSearch();
+ updateInterfaceForResults(results);
+ };
+
+ this.subscriptions.add(this.model.onDidClear(resetInterface));
+ this.subscriptions.add(this.model.onDidClearReplacementState(updateInterfaceForResults));
+ this.subscriptions.add(this.model.onDidStartSearching(updateInterfaceForSearching));
+ this.subscriptions.add(this.model.onDidNoopSearch(afterSearch));
+ this.subscriptions.add(this.model.onDidFinishSearching(updateInterfaceForResults));
+ this.subscriptions.add(this.model.getFindOptions().onDidChange(this.updateOptionViews.bind(this)));
+
+ this.element.addEventListener('focus', () => this.findEditor.element.focus());
+ this.refs.closeButton.addEventListener('click', () => this.panel && this.panel.hide());
+ this.refs.regexOptionButton.addEventListener('click', () => this.toggleRegexOption());
+ this.refs.caseOptionButton.addEventListener('click', () => this.toggleCaseOption());
+ this.refs.wholeWordOptionButton.addEventListener('click', () => this.toggleWholeWordOption());
+ this.refs.replaceAllButton.addEventListener('click', () => this.replaceAll());
+ this.refs.findAllButton.addEventListener('click', () => this.search());
+
+ const focusCallback = () => this.onlyRunIfChanged = false;
+ window.addEventListener('focus', focusCallback);
+ this.subscriptions.add(new Disposable(() => window.removeEventListener('focus', focusCallback)))
+
+ this.findEditor.getBuffer().onDidChange(() => {
+ this.updateReplaceAllButtonEnablement(this.model.getResultsSummary());
+ });
+ this.handleEventsForReplace();
+ }
+
+ handleEventsForReplace() {
+ this.replaceEditor.getBuffer().onDidChange(() => this.model.clearReplacementState());
+ this.replaceEditor.onDidStopChanging(() => this.model.getFindOptions().set({replacePattern: this.replaceEditor.getText()}));
+ this.replacementsMade = 0;
+ this.subscriptions.add(this.model.onDidStartReplacing(promise => {
+ this.replacementsMade = 0;
+ this.refs.replacmentInfoBlock.style.display = '';
+ this.refs.replacementProgress.removeAttribute('value');
+ }));
+
+ this.subscriptions.add(this.model.onDidReplacePath(result => {
+ this.replacementsMade++;
+ this.refs.replacementProgress.value = this.replacementsMade / this.model.getPathCount();
+ this.refs.replacmentInfo.textContent = `Replaced ${this.replacementsMade} of ${_.pluralize(this.model.getPathCount(), 'open file')}`;
+ }));
+
+ this.subscriptions.add(this.model.onDidFinishReplacing(result => this.onFinishedReplacing(result)));
+ }
+
+ focusNextElement(direction) {
+ const elements = [
+ this.findEditor.element,
+ this.replaceEditor.element
+ ];
+
+ let focusedIndex = elements.findIndex(el => el.hasFocus()) + direction;
+ if (focusedIndex >= elements.length) focusedIndex = 0;
+ if (focusedIndex < 0) focusedIndex = elements.length - 1;
+
+ elements[focusedIndex].focus();
+ elements[focusedIndex].getModel().selectAll();
+ }
+
+ focusFindElement() {
+ const activeEditor = atom.workspace.getActiveTextEditor();
+ let selectedText = activeEditor && activeEditor.getSelectedText()
+ if (selectedText && selectedText.indexOf('\n') < 0) {
+ if (this.model.getFindOptions().useRegex) {
+ selectedText = Util.escapeRegex(selectedText);
+ }
+ this.findEditor.setText(selectedText);
+ }
+ this.findEditor.getElement().focus();
+ this.findEditor.selectAll();
+ }
+
+ confirm() {
+ if (this.findEditor.getText().length === 0) {
+ this.model.clear();
+ return;
+ }
+
+ this.findHistoryCycler.store();
+ this.replaceHistoryCycler.store();
+
+ let searchPromise = this.search({onlyRunIfChanged: this.onlyRunIfChanged});
+ this.onlyRunIfChanged = true;
+ return searchPromise;
+ }
+
+ getOpenFilePaths() {
+ return atom.workspace.getTextEditors().map(editor => editor.getPath());
+ }
+
+ search(options) {
+ // We always want to set the options passed in, even if we dont end up doing the search
+ if (options == null) { options = {}; }
+ this.model.getFindOptions().set(options);
+
+ let findPattern = this.findEditor.getText();
+ let replacePattern = this.replaceEditor.getText();
+ let paths = this.getOpenFilePaths();
+
+ let {onlyRunIfActive, onlyRunIfChanged} = options;
+ if ((onlyRunIfActive && !this.model.active) || !findPattern) return Promise.resolve();
+
+ return this.showResultPane().then(() => {
+ try {
+ return this.model.searchPaths(findPattern, paths, replacePattern, options);
+ } catch (e) {
+ this.setErrorMessage(e.message);
+ }
+ });
+ }
+
+ replaceAll() {
+ if (!this.model.matchCount) {
+ atom.beep();
+ return;
+ }
+
+ const findPattern = this.model.getLastFindPattern();
+ const currentPattern = this.findEditor.getText();
+ if (findPattern && findPattern !== currentPattern) {
+ atom.confirm({
+ message: `The searched pattern '${findPattern}' was changed to '${currentPattern}'`,
+ detailedMessage: `Please run the search with the new pattern '${currentPattern}' before running a replace-all`,
+ buttons: ['OK']
+ });
+ return;
+ }
+
+ return this.showResultPane().then(() => {
+ const replacePattern = this.replaceEditor.getText();
+
+ // TODO: What happens when there are no open text editors?
+
+ const message = `This will replace '${findPattern}' with '${replacePattern}' ${_.pluralize(this.model.matchCount, 'time')} in ${_.pluralize(this.model.pathCount, 'open file')}`;
+ const buttonChosen = atom.confirm({
+ message: 'Are you sure you want to replace all?',
+ detailedMessage: message,
+ buttons: ['OK', 'Cancel']
+ });
+
+ if (buttonChosen === 0) {
+ this.clearMessages();
+ return this.model.replacePaths(replacePattern, this.model.getPaths());
+ }
+ });
+ }
+
+ showResultPane() {
+ let options = {searchAllPanes: true};
+ let openDirection = atom.config.get('find-and-replace.projectSearchResultsPaneSplitDirection');
+ if (openDirection !== 'none') { options.split = openDirection; }
+ return atom.workspace.open(ResultsPaneView.URI, options);
+ }
+
+ onFinishedReplacing(results) {
+ if (!results.replacedPathCount) atom.beep();
+ this.refs.replacmentInfoBlock.style.display = 'none';
+ }
+
+ generateResultsMessage(results) {
+ let message = Util.getSearchResultsMessage(results);
+ if (results.replacedPathCount != null) { message = Util.getReplacementResultsMessage(results); }
+ this.setInfoMessage(message);
+ }
+
+ clearMessages() {
+ this.element.classList.remove('has-results', 'has-no-results');
+ this.setInfoMessage('Find in Open Files');
+ this.refs.replacmentInfoBlock.style.display = 'none';
+ }
+
+ setInfoMessage(infoMessage) {
+ this.refs.descriptionLabel.innerHTML = infoMessage;
+ this.refs.descriptionLabel.classList.remove('text-error');
+ }
+
+ setErrorMessage(errorMessage) {
+ this.refs.descriptionLabel.innerHTML = errorMessage;
+ this.refs.descriptionLabel.classList.add('text-error');
+ }
+
+ updateReplaceAllButtonEnablement(results) {
+ const canReplace = results &&
+ results.matchCount &&
+ results.findPattern == this.findEditor.getText();
+ if (canReplace && !this.refs.replaceAllButton.classList.contains('disabled')) return;
+
+ if (this.replaceTooltipSubscriptions) this.replaceTooltipSubscriptions.dispose();
+ this.replaceTooltipSubscriptions = new CompositeDisposable;
+
+ if (canReplace) {
+ this.refs.replaceAllButton.classList.remove('disabled');
+ this.replaceTooltipSubscriptions.add(atom.tooltips.add(this.refs.replaceAllButton, {
+ title: "Replace All",
+ keyBindingCommand: 'open-files-find:replace-all',
+ keyBindingTarget: this.replaceEditor.element
+ }));
+ } else {
+ this.refs.replaceAllButton.classList.add('disabled');
+ this.replaceTooltipSubscriptions.add(atom.tooltips.add(this.refs.replaceAllButton, {
+ title: "Replace All [run a search to enable]"}
+ ));
+ }
+ }
+
+ setSelectionAsFindPattern() {
+ const editor = atom.workspace.getCenter().getActivePaneItem();
+ if (editor && editor.getSelectedText) {
+ let pattern = editor.getSelectedText() || editor.getWordUnderCursor();
+ if (this.model.getFindOptions().useRegex) {
+ pattern = Util.escapeRegex(pattern);
+ }
+ if (pattern) {
+ this.findEditor.setText(pattern);
+ }
+ }
+ }
+
+ updateOptionViews() {
+ this.updateOptionButtons();
+ this.updateOptionsLabel();
+ this.updateSyntaxHighlighting();
+ }
+
+ updateSyntaxHighlighting() {
+ if (this.model.getFindOptions().useRegex) {
+ this.findEditor.setGrammar(atom.grammars.grammarForScopeName('source.js.regexp'));
+ return this.replaceEditor.setGrammar(atom.grammars.grammarForScopeName('source.js.regexp.replacement'));
+ } else {
+ this.findEditor.setGrammar(atom.grammars.nullGrammar);
+ return this.replaceEditor.setGrammar(atom.grammars.nullGrammar);
+ }
+ }
+
+ updateOptionsLabel() {
+ const label = [];
+
+ if (this.model.getFindOptions().useRegex) {
+ label.push('Regex');
+ }
+
+ if (this.model.getFindOptions().caseSensitive) {
+ label.push('Case Sensitive');
+ } else {
+ label.push('Case Insensitive');
+ }
+
+ if (this.model.getFindOptions().wholeWord) {
+ label.push('Whole Word');
+ }
+
+ this.refs.optionsLabel.textContent = label.join(', ');
+ }
+
+ updateOptionButtons() {
+ this.setOptionButtonState(this.refs.regexOptionButton, this.model.getFindOptions().useRegex);
+ this.setOptionButtonState(this.refs.caseOptionButton, this.model.getFindOptions().caseSensitive);
+ this.setOptionButtonState(this.refs.wholeWordOptionButton, this.model.getFindOptions().wholeWord);
+ }
+
+ setOptionButtonState(optionButton, selected) {
+ if (selected) {
+ optionButton.classList.add('selected');
+ } else {
+ optionButton.classList.remove('selected');
+ }
+ }
+
+ toggleRegexOption() {
+ this.search({onlyRunIfActive: true, useRegex: !this.model.getFindOptions().useRegex});
+ }
+
+ toggleCaseOption() {
+ this.search({onlyRunIfActive: true, caseSensitive: !this.model.getFindOptions().caseSensitive});
+ }
+
+ toggleWholeWordOption() {
+ this.search({onlyRunIfActive: true, wholeWord: !this.model.getFindOptions().wholeWord});
+ }
+};
diff --git a/lib/project/results-model.js b/lib/project/results-model.js
index 6cc66eb9..c7479f01 100644
--- a/lib/project/results-model.js
+++ b/lib/project/results-model.js
@@ -1,5 +1,8 @@
const _ = require('underscore-plus')
const {Emitter, TextEditor, Range} = require('atom')
+const {PathReplacer, PathSearcher} = require('scandal')
+const replacer = new PathReplacer()
+const searcher = new PathSearcher()
const escapeHelper = require('../escape-helper')
class Result {
@@ -196,6 +199,78 @@ module.exports = class ResultsModel {
})
}
+ shouldRerunSearchPaths (findPattern, paths, replacePattern, options) {
+ if (options == null) { options = {} }
+ const {onlyRunIfChanged} = options
+ return !(onlyRunIfChanged && (findPattern != null) &&
+ (findPattern === this.lastFindPattern) && (paths === this.lastPaths))
+ }
+
+ searchPaths (findPattern, paths, replacePattern, options) {
+ if (options == null) { options = {} }
+ if (!this.shouldRerunSearchPaths(findPattern, paths, replacePattern, options)) {
+ this.emitter.emit('did-noop-search')
+ return Promise.resolve()
+ }
+
+ const {keepReplacementState} = options
+ if (keepReplacementState) {
+ this.clearSearchState()
+ } else {
+ this.clear()
+ }
+
+ this.lastFindPattern = findPattern
+ this.lastPaths = paths
+ this.findOptions.set(_.extend({findPattern, replacePattern, paths}, options))
+ this.regex = this.findOptions.getFindPatternRegex()
+
+ this.active = true
+
+ const leadingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountBefore')
+ const trailingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountAfter')
+
+ searcher.on('results-found', result => {
+ this.setResult(result.filePath, Result.create(result))
+ })
+
+ /* build a cancellable promise like workspace.scan does internally so that it
+ looks identical to #search to outsiders looking in */
+ const isCancelled = false
+ this.inProgressSearchPromise = new Promise((resolve, reject) => {
+ if (isCancelled) { resolve('cancelled') }
+ searcher.searchPaths(this.regex, paths, (results, errors) => {
+ if (errors) {
+ errors.forEach(e => {
+ this.searchErrors = this.searchErrors || []
+ this.searchErrors.push(e)
+ this.emitter.emit('did-error-for-path', e)
+ })
+ reject()
+ } else {
+ this.emitter.emit('did-search-paths', paths.length)
+ resolve(null)
+ }
+ })
+ })
+
+ this.inProgressSearchPromise.cancel = () => {
+ isCancelled = true
+ }
+ // this.inProgressSearchPromise.done = () => {}
+
+ this.emitter.emit('did-start-searching', this.inProgressSearchPromise)
+
+ return this.inProgressSearchPromise.then(message => {
+ if (message === 'cancelled') {
+ this.emitter.emit('did-cancel-searching')
+ } else {
+ this.inProgressSearchPromise = null
+ this.emitter.emit('did-finish-searching', this.getResultsSummary())
+ }
+ })
+ }
+
replace (pathsPattern, replacePattern, replacementPaths) {
if (!this.findOptions.findPattern || (this.regex == null)) { return }
@@ -229,6 +304,39 @@ module.exports = class ResultsModel {
}).catch(e => console.error(e.stack))
}
+ replacePaths (replacePattern, replacementPaths) {
+ if (!this.findOptions.findPattern || (this.regex == null)) { return }
+
+ this.findOptions.set({replacePattern})
+
+ if (this.findOptions.useRegex) { replacePattern = escapeHelper.unescapeEscapeSequence(replacePattern) }
+
+ this.active = false // not active until the search is finished
+ this.replacedPathCount = 0
+ this.replacementCount = 0
+
+ const promise = pathReplacer.replacePaths(this.regex, replacePattern, replacementPaths, (result, error) => {
+ if (result) {
+ if (result.replacements) {
+ this.replacedPathCount++
+ this.replacementCount += result.replacements
+ }
+ this.emitter.emit('did-replace-path', result)
+ } else {
+ if (this.replacementErrors == null) { this.replacementErrors = [] }
+ this.replacementErrors.push(error)
+ this.emitter.emit('did-error-for-path', error)
+ }
+ })
+
+ this.emitter.emit('did-start-replacing', promise)
+ return promise.then(() => {
+ this.emitter.emit('did-finish-replacing', this.getResultsSummary())
+ return this.searchPaths(this.findOptions.findPattern, this.findOptions.paths,
+ this.findOptions.replacePattern, {keepReplacementState: true})
+ }).catch(e => console.error(e.stack))
+ }
+
setActive (isActive) {
if ((isActive && this.findOptions.findPattern) || !isActive) {
this.active = isActive
diff --git a/lib/project/results-pane.js b/lib/project/results-pane.js
index 598ab9ee..572fee53 100644
--- a/lib/project/results-pane.js
+++ b/lib/project/results-pane.js
@@ -203,7 +203,7 @@ class ResultsPaneView {
}
getTitle() {
- return 'Project Find Results';
+ return 'Project search results';
}
getIconName() {
@@ -334,4 +334,4 @@ class ResultsPaneView {
}
}
-module.exports.URI = "atom://find-and-replace/project-results";
+module.exports.URI = "atom://find-and-replace/results";
diff --git a/menus/find-and-replace.cson b/menus/find-and-replace.cson
index e459ed2e..caaaac6b 100644
--- a/menus/find-and-replace.cson
+++ b/menus/find-and-replace.cson
@@ -10,6 +10,9 @@
{ 'label': 'Find in Project', 'command': 'project-find:show'}
{ 'label': 'Toggle Find in Project', 'command': 'project-find:toggle'}
{ 'type': 'separator' }
+ { 'label': 'Find in Open Files', 'command': 'open-files-find:show'}
+ { 'label': 'Toggle Find in Open Files', 'command': 'open-files-find:toggle'}
+ { 'type': 'separator' }
{ 'label': 'Find All', 'command': 'find-and-replace:find-all'}
{ 'label': 'Find Next', 'command': 'find-and-replace:find-next'}
{ 'label': 'Find Previous', 'command': 'find-and-replace:find-previous'}
diff --git a/package.json b/package.json
index 6d81ccc2..266faed7 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,8 @@
"license": "MIT",
"activationCommands": {
"atom-workspace": [
+ "open-files-find:show",
+ "open-files-find:toggle",
"project-find:show",
"project-find:toggle",
"project-find:show-in-current-directory",
@@ -33,6 +35,7 @@
"element-resize-detector": "^1.1.10",
"etch": "0.9.3",
"fs-plus": "^3.0.0",
+ "scandal": "^3.1.0",
"temp": "^0.8.3",
"underscore-plus": "1.x"
},
diff --git a/spec/open-files-find-view-spec.js b/spec/open-files-find-view-spec.js
new file mode 100644
index 00000000..df55bb39
--- /dev/null
+++ b/spec/open-files-find-view-spec.js
@@ -0,0 +1,1392 @@
+/** @babel */
+
+const os = require('os');
+const path = require('path');
+const temp = require('temp');
+const fs = require('fs-plus');
+const {TextBuffer} = require('atom');
+const {PathReplacer, PathSearcher} = require('scandal');
+const ResultsPaneView = require('../lib/project/results-pane');
+const etch = require('etch');
+const {beforeEach, it, fit, ffit, fffit, conditionPromise} = require('./async-spec-helpers')
+
+describe('OpenFilesFindView', () => {
+ const {stoppedChangingDelay} = TextBuffer.prototype;
+ let activationPromise, searchPromise, editor, editorElement, findView,
+ openFilesFindView, pathSearcher, workspaceElement;
+
+ function getAtomPanel() {
+ return workspaceElement.querySelector('.open-files-find').parentNode;
+ }
+
+ function getExistingResultsPane() {
+ const pane = atom.workspace.paneForURI(ResultsPaneView.URI);
+ if (pane) {
+
+ // Allow element-resize-detector to perform batched measurements
+ advanceClock(1);
+
+ return pane.itemForURI(ResultsPaneView.URI);
+ }
+ }
+
+ function getResultsView() {
+ return getExistingResultsPane().refs.resultsView;
+ }
+
+ beforeEach(async () => {
+ pathSearcher = new PathSearcher();
+ workspaceElement = atom.views.getView(atom.workspace);
+ atom.config.set('core.excludeVcsIgnoredPaths', false);
+ atom.project.setPaths([path.join(__dirname, 'fixtures')]);
+ await atom.workspace.open(path.join(__dirname, 'fixtures', 'one-long-line.coffee'));
+ await atom.workspace.open(path.join(__dirname, 'fixtures', 'sample.js'));
+ jasmine.attachToDOM(workspaceElement);
+
+ activationPromise = atom.packages.activatePackage("find-and-replace").then(function({mainModule}) {
+ mainModule.createViews();
+ ({findView, openFilesFindView} = mainModule);
+ const spy = spyOn(openFilesFindView, 'search').andCallFake((options) => {
+ return searchPromise = spy.originalValue.call(openFilesFindView, options);
+ });
+ });
+ });
+
+ describe("when open-files-find:show is triggered", () => {
+ it("attaches openFilesFindView to the root view", async () => {
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ await activationPromise;
+
+ openFilesFindView.findEditor.setText('items');
+ expect(getAtomPanel()).toBeVisible();
+ expect(openFilesFindView.findEditor.getSelectedBufferRange()).toEqual([[0, 0], [0, 5]]);
+ });
+
+ describe("with an open buffer", () => {
+ beforeEach(async () => {
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ await activationPromise;
+ openFilesFindView.findEditor.setText('');
+ editor = await atom.workspace.open('sample.js');
+ });
+
+ it("populates the findEditor with selection when there is a selection", () => {
+ editor.setSelectedBufferRange([[2, 8], [2, 13]]);
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ expect(getAtomPanel()).toBeVisible();
+ expect(openFilesFindView.findEditor.getText()).toBe('items');
+
+ editor.setSelectedBufferRange([[2, 14], [2, 20]]);
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ expect(getAtomPanel()).toBeVisible();
+ expect(openFilesFindView.findEditor.getText()).toBe('length');
+ });
+
+ it("populates the findEditor with the previous selection when there is no selection", () => {
+ editor.setSelectedBufferRange([[2, 14], [2, 20]]);
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ expect(getAtomPanel()).toBeVisible();
+ expect(openFilesFindView.findEditor.getText()).toBe('length');
+
+ editor.setSelectedBufferRange([[2, 30], [2, 30]]);
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ expect(getAtomPanel()).toBeVisible();
+ expect(openFilesFindView.findEditor.getText()).toBe('length');
+ });
+
+ it("places selected text into the find editor and escapes it when Regex is enabled", () => {
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:toggle-regex-option');
+ editor.setSelectedBufferRange([[6, 6], [6, 65]]);
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ expect(openFilesFindView.findEditor.getText()).toBe('current < pivot \\? left\\.push\\(current\\) : right\\.push\\(current\\);');
+ });
+ });
+
+ describe("when the openFilesFindView is already attached", () => {
+ beforeEach(async () => {
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ await activationPromise;
+
+ openFilesFindView.findEditor.setText('items');
+ openFilesFindView.findEditor.setSelectedBufferRange([[0, 0], [0, 0]]);
+ });
+
+ it("focuses the find editor and selects all the text", () => {
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ expect(openFilesFindView.findEditor.getElement()).toHaveFocus();
+ expect(openFilesFindView.findEditor.getSelectedText()).toBe("items");
+ });
+ });
+
+ it("honors config settings for find options", async () => {
+ atom.config.set('find-and-replace.useRegex', true);
+ atom.config.set('find-and-replace.caseSensitive', true);
+ atom.config.set('find-and-replace.wholeWord', true);
+
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ await activationPromise;
+
+ expect(openFilesFindView.refs.caseOptionButton).toHaveClass('selected');
+ expect(openFilesFindView.refs.regexOptionButton).toHaveClass('selected');
+ expect(openFilesFindView.refs.wholeWordOptionButton).toHaveClass('selected');
+ });
+ });
+
+ describe("when open-files-find:toggle is triggered", () => {
+ it("toggles the visibility of the OpenFilesFindView", async () => {
+ atom.commands.dispatch(workspaceElement, 'open-files-find:toggle');
+ await activationPromise;
+
+ expect(getAtomPanel()).toBeVisible();
+ atom.commands.dispatch(workspaceElement, 'open-files-find:toggle');
+ expect(getAtomPanel()).not.toBeVisible();
+ });
+ });
+
+ describe("finding", () => {
+ beforeEach(async () => {
+ editor = await atom.workspace.open('sample.js');
+ editorElement = atom.views.getView(editor);
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ await activationPromise;
+ workspaceElement.style.height = '800px'
+ });
+
+ describe("when the find string contains an escaped char", () => {
+ beforeEach(async () => {
+ let projectPath = temp.mkdirSync("atom");
+ fs.writeFileSync(path.join(projectPath, "tabs.txt"), "\t\n\\\t\n\\\\t");
+ await atom.workspace.open(path.join(projectPath, "tabs.txt"));
+ atom.project.setPaths([projectPath]);
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ });
+
+ describe("when regex seach is enabled", () => {
+ it("finds a literal tab character", async () => {
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:toggle-regex-option');
+ openFilesFindView.findEditor.setText('\\t');
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ const resultsView = getResultsView();
+ await resultsView.heightInvalidationPromise
+ expect(resultsView.element).toBeVisible();
+ expect(resultsView.refs.listView.element.querySelectorAll(".search-result")).toHaveLength(2);
+ })
+ });
+
+ describe("when regex seach is disabled", () => {
+ it("finds the escape char", async () => {
+ openFilesFindView.findEditor.setText('\\t');
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ const resultsView = getResultsView();
+ await resultsView.heightInvalidationPromise
+ expect(resultsView.element).toBeVisible();
+ expect(resultsView.refs.listView.element.querySelectorAll(".search-result")).toHaveLength(1);
+ });
+
+ it("finds a backslash", async () => {
+ openFilesFindView.findEditor.setText('\\');
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ const resultsView = getResultsView();
+ await resultsView.heightInvalidationPromise
+ expect(resultsView.element).toBeVisible();
+ expect(resultsView.refs.listView.element.querySelectorAll(".search-result")).toHaveLength(3);
+ });
+
+ it("doesn't insert a escaped char if there are multiple backslashs in front of the char", async () => {
+ openFilesFindView.findEditor.setText('\\\\t');
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ const resultsView = getResultsView();
+ await resultsView.heightInvalidationPromise
+ expect(resultsView.element).toBeVisible();
+ expect(resultsView.refs.listView.element.querySelectorAll(".search-result")).toHaveLength(1);
+ });
+ });
+ });
+
+ describe("when core:cancel is triggered", () => {
+ it("detaches from the root view", () => {
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ openFilesFindView.element.focus();
+ atom.commands.dispatch(document.activeElement, 'core:cancel');
+ expect(getAtomPanel()).not.toBeVisible();
+ });
+ });
+
+ describe("when close option is true", () => {
+ beforeEach(() => {
+ atom.config.set('find-and-replace.closeFindPanelAfterSearch', true);
+ })
+
+ it("closes the panel after search", async () => {
+ openFilesFindView.findEditor.setText('something');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ expect(getAtomPanel()).not.toBeVisible();
+ });
+
+ it("leaves the panel open after an empty search", async () => {
+ openFilesFindView.findEditor.setText('');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ expect(getAtomPanel()).toBeVisible();
+ });
+
+ it("closes the panel after a no-op search", async () => {
+ openFilesFindView.findEditor.setText('something');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ await activationPromise;
+
+ expect(getAtomPanel()).toBeVisible();
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ expect(getAtomPanel()).not.toBeVisible();
+ });
+
+ it("does not close the panel after the replacement text is altered", async () => {
+ openFilesFindView.replaceEditor.setText('something else');
+
+ expect(getAtomPanel()).toBeVisible();
+ });
+ });
+
+ describe("splitting into a second pane", () => {
+ beforeEach(() => {
+ workspaceElement.style.height = '1000px';
+ atom.commands.dispatch(editorElement, 'open-files-find:show');
+ });
+
+ it("splits when option is right", async () => {
+ const initialPane = atom.workspace.getActivePane();
+ atom.config.set('find-and-replace.projectSearchResultsPaneSplitDirection', 'right');
+ openFilesFindView.findEditor.setText('items');
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ expect(atom.workspace.getActivePane()).not.toBe(initialPane);
+ });
+
+ it("splits when option is bottom", async () => {
+ const initialPane = atom.workspace.getActivePane();
+ atom.config.set('find-and-replace.projectSearchResultsPaneSplitDirection', 'down');
+ openFilesFindView.findEditor.setText('items');
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ expect(atom.workspace.getActivePane()).not.toBe(initialPane);
+ });
+
+ it("does not split when option is false", async () => {
+ const initialPane = atom.workspace.getActivePane();
+ openFilesFindView.findEditor.setText('items');
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ expect(atom.workspace.getActivePane()).toBe(initialPane);
+ });
+
+ it("can be duplicated on the right", async () => {
+ atom.config.set('find-and-replace.projectSearchResultsPaneSplitDirection', 'right');
+ openFilesFindView.findEditor.setText('items');
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ const resultsPaneView1 = atom.views.getView(getExistingResultsPane());
+ const pane1 = atom.workspace.getActivePane();
+ const resultsView1 = pane1.getItems()[0].refs.resultsView
+ pane1.splitRight({copyActiveItem: true});
+
+ const pane2 = atom.workspace.getActivePane();
+ const resultsView2 = pane2.getItems()[0].refs.resultsView
+ const resultsPaneView2 = atom.views.getView(pane2.itemForURI(ResultsPaneView.URI));
+ expect(pane1).not.toBe(pane2);
+ expect(resultsPaneView1).not.toBe(resultsPaneView2);
+ simulateResizeEvent(resultsView2.element);
+
+ const {length: resultCount} = resultsPaneView1.querySelectorAll('.search-result');
+ expect(resultCount).toBeGreaterThan(0);
+ expect(resultsPaneView2.querySelectorAll('.search-result')).toHaveLength(resultCount);
+ expect(resultsPaneView2.querySelector('.preview-count').innerHTML).toEqual(resultsPaneView1.querySelector('.preview-count').innerHTML);
+ });
+
+ it("can be duplicated at the bottom", async () => {
+ atom.config.set('find-and-replace.projectSearchResultsPaneSplitDirection', 'down');
+ openFilesFindView.findEditor.setText('items');
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ const resultsPaneView1 = atom.views.getView(getExistingResultsPane());
+ const pane1 = atom.workspace.getActivePane();
+ const resultsView1 = pane1.getItems()[0].refs.resultsView
+
+ pane1.splitDown({copyActiveItem: true});
+ const pane2 = atom.workspace.getActivePane();
+ const resultsView2 = pane2.getItems()[0].refs.resultsView
+ const resultsPaneView2 = atom.views.getView(pane2.itemForURI(ResultsPaneView.URI));
+ expect(pane1).not.toBe(pane2);
+ expect(resultsPaneView1).not.toBe(resultsPaneView2);
+ expect(resultsPaneView2.querySelector('.preview-count').innerHTML).toEqual(resultsPaneView1.querySelector('.preview-count').innerHTML);
+ });
+ });
+
+ describe("serialization", () => {
+ it("serializes if the case, regex and whole word options", async () => {
+ atom.commands.dispatch(editorElement, 'open-files-find:show');
+ expect(openFilesFindView.refs.caseOptionButton).not.toHaveClass('selected');
+ openFilesFindView.refs.caseOptionButton.click();
+ expect(openFilesFindView.refs.caseOptionButton).toHaveClass('selected');
+
+ expect(openFilesFindView.refs.regexOptionButton).not.toHaveClass('selected');
+ openFilesFindView.refs.regexOptionButton.click();
+ expect(openFilesFindView.refs.regexOptionButton).toHaveClass('selected');
+
+ expect(openFilesFindView.refs.wholeWordOptionButton).not.toHaveClass('selected');
+ openFilesFindView.refs.wholeWordOptionButton.click();
+ expect(openFilesFindView.refs.wholeWordOptionButton).toHaveClass('selected');
+
+ atom.packages.deactivatePackage("find-and-replace");
+
+ activationPromise = atom.packages.activatePackage("find-and-replace").then(function({mainModule}) {
+ mainModule.createViews();
+ return {openFilesFindView} = mainModule;
+ });
+
+ atom.commands.dispatch(editorElement, 'open-files-find:show');
+ await activationPromise;
+
+ expect(openFilesFindView.refs.caseOptionButton).toHaveClass('selected');
+ expect(openFilesFindView.refs.regexOptionButton).toHaveClass('selected');
+ expect(openFilesFindView.refs.wholeWordOptionButton).toHaveClass('selected');
+ })
+ });
+
+ describe("description label", () => {
+ beforeEach(() => {
+ atom.commands.dispatch(editorElement, 'open-files-find:show');
+ });
+
+ it("indicates that it's searching, then shows the results", async () => {
+ openFilesFindView.findEditor.setText('item');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+
+ await openFilesFindView.showResultPane();
+
+ expect(openFilesFindView.refs.descriptionLabel.textContent).toContain('Searching...');
+
+ await searchPromise;
+
+ expect(openFilesFindView.refs.descriptionLabel.textContent).toContain('13 results found in 2 open files');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ expect(openFilesFindView.refs.descriptionLabel.textContent).toContain('13 results found in 2 open files');
+ });
+
+ it("shows an error when the pattern is invalid and clears when no error", async () => {
+ spyOn(pathSearcher, 'searchPaths').andReturn(Promise.resolve()); // TODO: Remove?
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:toggle-regex-option');
+ openFilesFindView.findEditor.setText('[');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+
+ await searchPromise;
+
+ expect(openFilesFindView.refs.descriptionLabel).toHaveClass('text-error');
+ expect(openFilesFindView.refs.descriptionLabel.textContent).toContain('Invalid regular expression');
+
+ openFilesFindView.findEditor.setText('');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+
+ expect(openFilesFindView.refs.descriptionLabel).not.toHaveClass('text-error');
+ expect(openFilesFindView.refs.descriptionLabel.textContent).toContain('Find in Project');
+
+ openFilesFindView.findEditor.setText('items');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+
+ await searchPromise;
+
+ expect(openFilesFindView.refs.descriptionLabel).not.toHaveClass('text-error');
+ expect(openFilesFindView.refs.descriptionLabel.textContent).toContain('items');
+ });
+ });
+
+ describe("regex", () => {
+ beforeEach(() => {
+ atom.commands.dispatch(editorElement, 'open-files-find:show');
+ openFilesFindView.findEditor.setText('i(\\w)ems+');
+ spyOn(pathSearcher, 'searchPaths').andCallFake(async () => {});
+ });
+
+ it("escapes regex patterns by default", async () => {
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ expect(pathSearcher.searchPaths.argsForCall[0][0]).toEqual(/i\(\\w\)ems\+/gi);
+ });
+
+ it("shows an error when the regex pattern is invalid", async () => {
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:toggle-regex-option');
+ openFilesFindView.findEditor.setText('[');
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ expect(openFilesFindView.refs.descriptionLabel).toHaveClass('text-error');
+ });
+
+ describe("when search has not been run yet", () => {
+ it("toggles regex option via an event but does not run the search", () => {
+ expect(openFilesFindView.refs.regexOptionButton).not.toHaveClass('selected');
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:toggle-regex-option');
+ expect(openFilesFindView.refs.regexOptionButton).toHaveClass('selected');
+ expect(pathSearcher.searchPaths).not.toHaveBeenCalled();
+ })
+ });
+
+ describe("when search has been run", () => {
+ beforeEach(async () => {
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+ });
+
+ it("toggles regex option via an event and finds files matching the pattern", async () => {
+ expect(openFilesFindView.refs.regexOptionButton).not.toHaveClass('selected');
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:toggle-regex-option');
+
+ await searchPromise;
+
+ expect(openFilesFindView.refs.regexOptionButton).toHaveClass('selected');
+ expect(pathSearcher.searchPaths.mostRecentCall.args[0]).toEqual(/i(\w)ems+/gi);
+ });
+
+ it("toggles regex option via a button and finds files matching the pattern", async () => {
+ expect(openFilesFindView.refs.regexOptionButton).not.toHaveClass('selected');
+ openFilesFindView.refs.regexOptionButton.click();
+
+ await searchPromise;
+
+ expect(openFilesFindView.refs.regexOptionButton).toHaveClass('selected');
+ expect(pathSearcher.searchPaths.mostRecentCall.args[0]).toEqual(/i(\w)ems+/gi);
+ });
+ });
+ });
+
+ describe("case sensitivity", () => {
+ beforeEach(async () => {
+ atom.commands.dispatch(editorElement, 'open-files-find:show');
+ spyOn(pathSearcher, 'searchPaths').andCallFake(() => Promise.resolve());
+ openFilesFindView.findEditor.setText('ITEMS');
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+ });
+
+ it("runs a case insensitive search by default", () => expect(String(PathSearcher.searchPaths.argsForCall[0][0])).toEqual(String(/ITEMS/gi)));
+
+ it("toggles case sensitive option via an event and finds files matching the pattern", async () => {
+ expect(openFilesFindView.refs.caseOptionButton).not.toHaveClass('selected');
+
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:toggle-case-option');
+ await searchPromise;
+
+ expect(openFilesFindView.refs.caseOptionButton).toHaveClass('selected');
+ expect(pathSearcher.searchPaths.mostRecentCall.args[0]).toEqual(/ITEMS/g);
+ });
+
+ it("toggles case sensitive option via a button and finds files matching the pattern", async () => {
+ expect(openFilesFindView.refs.caseOptionButton).not.toHaveClass('selected');
+
+ openFilesFindView.refs.caseOptionButton.click();
+ await searchPromise;
+
+ expect(openFilesFindView.refs.caseOptionButton).toHaveClass('selected');
+ expect(pathSearcher.searchPaths.mostRecentCall.args[0]).toEqual(/ITEMS/g);
+ });
+ });
+
+ describe("whole word", () => {
+ beforeEach(async () => {
+ atom.commands.dispatch(editorElement, 'open-files-find:show');
+ spyOn(pathSearcher, 'searchPaths').andCallFake(async () => {});
+ openFilesFindView.findEditor.setText('wholeword');
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+ });
+
+ it("does not run whole word search by default", () => {
+ expect(pathSearcher.searchPaths.argsForCall[0][0]).toEqual(/wholeword/gi)
+ });
+
+ it("toggles whole word option via an event and finds files matching the pattern", async () => {
+ expect(openFilesFindView.refs.wholeWordOptionButton).not.toHaveClass('selected');
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:toggle-whole-word-option');
+
+ await searchPromise;
+ expect(openFilesFindView.refs.wholeWordOptionButton).toHaveClass('selected');
+ expect(pathSearcher.searchPaths.mostRecentCall.args[0]).toEqual(/\bwholeword\b/gi);
+ });
+
+ it("toggles whole word option via a button and finds files matching the pattern", async () => {
+ expect(openFilesFindView.refs.wholeWordOptionButton).not.toHaveClass('selected');
+
+ openFilesFindView.refs.wholeWordOptionButton.click();
+ await searchPromise;
+
+ expect(openFilesFindView.refs.wholeWordOptionButton).toHaveClass('selected');
+ expect(pathSearcher.searchPaths.mostRecentCall.args[0]).toEqual(/\bwholeword\b/gi);
+ });
+ });
+
+ describe("when open-files-find:confirm is triggered", () => {
+ it("displays the results and no errors", async () => {
+ openFilesFindView.findEditor.setText('items');
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:confirm');
+
+ await searchPromise;
+
+ const resultsView = getResultsView();
+ await resultsView.heightInvalidationPromise
+ expect(resultsView.element).toBeVisible();
+ expect(resultsView.refs.listView.element.querySelectorAll(".search-result")).toHaveLength(13);
+ })
+ });
+
+ describe("when core:confirm is triggered", () => {
+ beforeEach(() => {
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show')
+ });
+
+ describe("when the there search field is empty", () => {
+ it("does not run the seach but clears the model", () => {
+ spyOn(pathSearcher, 'searchPaths');
+ spyOn(openFilesFindView.model, 'clear');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ expect(pathSearcher.searchPaths).not.toHaveBeenCalled();
+ expect(openFilesFindView.model.clear).toHaveBeenCalled();
+ })
+ });
+
+ it("reruns the search when confirmed again after focusing the window", async () => {
+ openFilesFindView.findEditor.setText('thisdoesnotmatch');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+
+ await searchPromise;
+
+ spyOn(pathSearcher, 'searchPaths');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+
+ await searchPromise;
+
+ expect(pathSearcher.searchPaths).not.toHaveBeenCalled();
+ pathSearcher.searchPaths.reset();
+ window.dispatchEvent(new FocusEvent("focus"));
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+
+ await searchPromise;
+
+ expect(pathSearcher.searchPaths).toHaveBeenCalled();
+ pathSearcher.searchPaths.reset();
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+
+ await searchPromise;
+
+ expect(pathSearcher.searchPaths).not.toHaveBeenCalled();
+ });
+
+ describe("when results exist", () => {
+ beforeEach(() => {
+ openFilesFindView.findEditor.setText('items')
+ });
+
+ it("displays the results and no errors", async () => {
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ const resultsView = getResultsView();
+ const resultsPaneView = getExistingResultsPane();
+
+ await resultsView.heightInvalidationPromise
+ expect(resultsView.element).toBeVisible();
+ expect(resultsView.refs.listView.element.querySelectorAll(".search-result")).toHaveLength(13);
+
+ expect(resultsPaneView.refs.previewCount.textContent).toBe("13 results found in 2 files for items");
+ expect(openFilesFindView.errorMessages).not.toBeVisible();
+ });
+
+ it("updates the results list when a buffer changes", async () => {
+ const buffer = atom.project.bufferForPathSync('sample.js');
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ const resultsView = getResultsView();
+ const resultsPaneView = getExistingResultsPane();
+
+ await resultsView.heightInvalidationPromise
+ expect(resultsView.refs.listView.element.querySelectorAll(".search-result")).toHaveLength(13);
+ expect(resultsPaneView.refs.previewCount.textContent).toBe("13 results found in 2 files for items");
+
+ resultsView.selectFirstResult();
+ for (let i = 0; i < 7; i++) await resultsView.moveDown()
+ expect(resultsView.refs.listView.element.querySelectorAll(".path")[1]).toHaveClass('selected');
+
+ buffer.setText('there is one "items" in this file');
+ advanceClock(buffer.stoppedChangingDelay);
+ await etch.getScheduler().getNextUpdatePromise()
+ expect(resultsPaneView.refs.previewCount.textContent).toBe("8 results found in 2 files for items");
+ expect(resultsView.refs.listView.element.querySelectorAll(".path")[1]).toHaveClass('selected');
+
+ buffer.setText('no matches in this file');
+ advanceClock(buffer.stoppedChangingDelay);
+ await etch.getScheduler().getNextUpdatePromise()
+ expect(resultsPaneView.refs.previewCount.textContent).toBe("7 results found in 1 file for items");
+ });
+ });
+
+ describe("when no results exist", () => {
+ beforeEach(() => {
+ openFilesFindView.findEditor.setText('notintheprojectbro');
+ spyOn(pathSearcher, 'searchPaths').andCallFake(async () => {});
+ });
+
+ it("displays no errors and no results", async () => {
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ const resultsView = getResultsView();
+ expect(openFilesFindView.refs.errorMessages).not.toBeVisible();
+ expect(resultsView.element).toBeVisible();
+ expect(resultsView.refs.listView.element.querySelectorAll(".search-result")).toHaveLength(0);
+ });
+ });
+ });
+
+ describe("history", () => {
+ beforeEach(() => {
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ spyOn(pathSearcher, 'searchPaths').andCallFake(() => {
+ let promise = Promise.resolve();
+ promise.cancel = () => {};
+ return promise;
+ });
+
+ openFilesFindView.findEditor.setText('sort');
+ openFilesFindView.replaceEditor.setText('bort');
+ atom.commands.dispatch(openFilesFindView.findEditor.getElement(), 'core:confirm');
+
+ openFilesFindView.findEditor.setText('items');
+ openFilesFindView.replaceEditor.setText('eyetims');
+ atom.commands.dispatch(openFilesFindView.findEditor.getElement(), 'core:confirm');
+ });
+
+ it("can navigate the entire history stack", () => {
+ expect(openFilesFindView.findEditor.getText()).toEqual('items');
+
+ atom.commands.dispatch(openFilesFindView.findEditor.getElement(), 'core:move-up');
+ expect(openFilesFindView.findEditor.getText()).toEqual('sort');
+
+ atom.commands.dispatch(openFilesFindView.findEditor.getElement(), 'core:move-down');
+ expect(openFilesFindView.findEditor.getText()).toEqual('items');
+
+ atom.commands.dispatch(openFilesFindView.findEditor.getElement(), 'core:move-down');
+ expect(openFilesFindView.findEditor.getText()).toEqual('');
+
+ expect(openFilesFindView.replaceEditor.getText()).toEqual('eyetims');
+
+ atom.commands.dispatch(openFilesFindView.replaceEditor.element, 'core:move-up');
+ expect(openFilesFindView.replaceEditor.getText()).toEqual('bort');
+
+ atom.commands.dispatch(openFilesFindView.replaceEditor.element, 'core:move-down');
+ expect(openFilesFindView.replaceEditor.getText()).toEqual('eyetims');
+
+ atom.commands.dispatch(openFilesFindView.replaceEditor.element, 'core:move-down');
+ expect(openFilesFindView.replaceEditor.getText()).toEqual('');
+ });
+ });
+
+ describe("when find-and-replace:use-selection-as-find-pattern is triggered", () => {
+ it("places the selected text into the find editor", () => {
+ editor.setSelectedBufferRange([[1, 6], [1, 10]]);
+ atom.commands.dispatch(workspaceElement, 'find-and-replace:use-selection-as-find-pattern');
+ expect(openFilesFindView.findEditor.getText()).toBe('sort');
+
+ editor.setSelectedBufferRange([[1, 13], [1, 21]]);
+ atom.commands.dispatch(workspaceElement, 'find-and-replace:use-selection-as-find-pattern');
+ expect(openFilesFindView.findEditor.getText()).toBe('function');
+ });
+
+ it("places the word under the cursor into the find editor", () => {
+ editor.setSelectedBufferRange([[1, 8], [1, 8]]);
+ atom.commands.dispatch(workspaceElement, 'find-and-replace:use-selection-as-find-pattern');
+ expect(openFilesFindView.findEditor.getText()).toBe('sort');
+
+ editor.setSelectedBufferRange([[1, 15], [1, 15]]);
+ atom.commands.dispatch(workspaceElement, 'find-and-replace:use-selection-as-find-pattern');
+ expect(openFilesFindView.findEditor.getText()).toBe('function');
+ });
+
+ it("places the previously selected text into the find editor if no selection and no word under cursor", () => {
+ editor.setSelectedBufferRange([[1, 13], [1, 21]]);
+ atom.commands.dispatch(workspaceElement, 'find-and-replace:use-selection-as-find-pattern');
+ expect(openFilesFindView.findEditor.getText()).toBe('function');
+
+ editor.setSelectedBufferRange([[1, 1], [1, 1]]);
+ atom.commands.dispatch(workspaceElement, 'find-and-replace:use-selection-as-find-pattern');
+ expect(openFilesFindView.findEditor.getText()).toBe('function');
+ });
+
+ it("places selected text into the find editor and escapes it when Regex is enabled", () => {
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:toggle-regex-option');
+ editor.setSelectedBufferRange([[6, 6], [6, 65]]);
+ atom.commands.dispatch(workspaceElement, 'find-and-replace:use-selection-as-find-pattern');
+ expect(openFilesFindView.findEditor.getText()).toBe('current < pivot \\? left\\.push\\(current\\) : right\\.push\\(current\\);');
+ });
+ });
+
+ describe("when there is an error searching", () => {
+ it("displays the errors in the results pane", async () => {
+ openFilesFindView.findEditor.setText('items');
+
+ let errorList;
+ spyOn(pathSearcher, 'searchPaths').andCallFake(async (regex, options, callback) => {
+ const resultsPaneView = getExistingResultsPane();
+ ({errorList} = resultsPaneView.refs);
+ expect(errorList.querySelectorAll("li")).toHaveLength(0);
+
+ callback(null, {path: '/some/path.js', code: 'ENOENT', message: 'Nope'});
+ expect(errorList).toBeVisible();
+ expect(errorList.querySelectorAll("li")).toHaveLength(1);
+
+ callback(null, {path: '/some/path.js', code: 'ENOENT', message: 'Broken'});
+ expect(errorList.querySelectorAll("li")).toHaveLength(2);
+ });
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+
+ await searchPromise;
+
+ expect(errorList).toBeVisible();
+ expect(errorList.querySelectorAll("li")).toHaveLength(2);
+ expect(errorList.querySelectorAll("li")[0].textContent).toBe('Nope');
+ expect(errorList.querySelectorAll("li")[1].textContent).toBe('Broken');
+ })
+ });
+
+ describe("buffer search sharing of the find options", () => {
+ function getResultDecorations(clazz) {
+ const result = [];
+ const decorations = editor.decorationsStateForScreenRowRange(0, editor.getLineCount());
+ for (let id in decorations) {
+ const decoration = decorations[id];
+ if (decoration.properties.class === clazz) {
+ result.push(decoration);
+ }
+ }
+ return result;
+ }
+
+ it("setting the find text does not interfere with the project replace state", async () => {
+ // Not sure why I need to advance the clock before setting the text. If
+ // this advanceClock doesnt happen, the text will be ''. wtf.
+ advanceClock(openFilesFindView.findEditor.getBuffer().stoppedChangingDelay + 1);
+
+ openFilesFindView.findEditor.setText('findme');
+ advanceClock(openFilesFindView.findEditor.getBuffer().stoppedChangingDelay + 1);
+
+ await openFilesFindView.search({onlyRunIfActive: false, onlyRunIfChanged: true});
+ expect(pathSearcher.searchPaths).toHaveBeenCalled();
+ });
+
+ it("shares the buffers and history cyclers between both buffer and open files views", () => {
+ openFilesFindView.findEditor.setText('findme');
+ openFilesFindView.replaceEditor.setText('replaceme');
+
+ atom.commands.dispatch(editorElement, 'find-and-replace:show');
+ expect(findView.findEditor.getText()).toBe('findme');
+ expect(findView.replaceEditor.getText()).toBe('replaceme');
+
+ // add some things to the history
+ atom.commands.dispatch(findView.findEditor.element, 'core:confirm');
+ findView.findEditor.setText('findme1');
+ atom.commands.dispatch(findView.findEditor.element, 'core:confirm');
+ findView.findEditor.setText('');
+
+ atom.commands.dispatch(findView.replaceEditor.element, 'core:confirm');
+ findView.replaceEditor.setText('replaceme1');
+ atom.commands.dispatch(findView.replaceEditor.element, 'core:confirm');
+ findView.replaceEditor.setText('');
+
+ // Back to the open files view to make sure we're using the same cycler
+ atom.commands.dispatch(editorElement, 'open-files-find:show');
+
+ expect(openFilesFindView.findEditor.getText()).toBe('');
+ atom.commands.dispatch(openFilesFindView.findEditor.element, 'core:move-up');
+ expect(openFilesFindView.findEditor.getText()).toBe('findme1');
+ atom.commands.dispatch(openFilesFindView.findEditor.element, 'core:move-up');
+ expect(openFilesFindView.findEditor.getText()).toBe('findme');
+
+ expect(openFilesFindView.replaceEditor.getText()).toBe('');
+ atom.commands.dispatch(openFilesFindView.replaceEditor.element, 'core:move-up');
+ expect(openFilesFindView.replaceEditor.getText()).toBe('replaceme1');
+ atom.commands.dispatch(openFilesFindView.replaceEditor.element, 'core:move-up');
+ expect(openFilesFindView.replaceEditor.getText()).toBe('replaceme');
+ });
+
+ it('highlights the search results in the selected file', async () => {
+ // Process here is to
+ // * open samplejs
+ // * run a search that has sample js results
+ // * that should place the pattern in the buffer find
+ // * focus sample.js by clicking on a sample.js result
+ // * when the file has been activated, it's results for the project search should be highlighted
+
+ editor = await atom.workspace.open('sample.js');
+ expect(getResultDecorations('find-result')).toHaveLength(0);
+
+ openFilesFindView.findEditor.setText('item');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ const resultsView = getResultsView();
+ resultsView.scrollToBottom(); // To load ALL the results
+ expect(resultsView.element).toBeVisible();
+ expect(resultsView.refs.listView.element.querySelectorAll(".search-result")).toHaveLength(13);
+
+ resultsView.selectFirstResult();
+ for (let i = 0; i < 10; i++) await resultsView.moveDown();
+
+ atom.commands.dispatch(resultsView.element, 'core:confirm');
+ await new Promise(resolve => editor.onDidChangeSelectionRange(resolve))
+
+ // sample.js has 6 results
+ expect(getResultDecorations('find-result')).toHaveLength(5);
+ expect(getResultDecorations('current-result')).toHaveLength(1);
+ expect(workspaceElement).toHaveClass('find-visible');
+
+ const initialSelectedRange = editor.getSelectedBufferRange();
+
+ // now we can find next
+ atom.commands.dispatch(atom.views.getView(editor), 'find-and-replace:find-next');
+ expect(editor.getSelectedBufferRange()).not.toEqual(initialSelectedRange);
+
+ // Now we toggle the whole-word option to make sure it is updated in the buffer find
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:toggle-whole-word-option');
+ await searchPromise;
+
+ // sample.js has 0 results for whole word `item`
+ expect(getResultDecorations('find-result')).toHaveLength(0);
+ expect(workspaceElement).toHaveClass('find-visible');
+
+ // Now we toggle the whole-word option to make sure it is updated in the buffer find
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:toggle-whole-word-option');
+ });
+ });
+ });
+
+ describe("replacing", () => {
+ let testDir, sampleJs, sampleCoffee, replacePromise;
+
+ beforeEach(async () => {
+ pathReplacer = new PathReplacer();
+ testDir = path.join(os.tmpdir(), "atom-find-and-replace");
+ sampleJs = path.join(testDir, 'sample.js');
+ sampleCoffee = path.join(testDir, 'sample.coffee');
+
+ fs.makeTreeSync(testDir);
+ fs.writeFileSync(sampleCoffee, fs.readFileSync(require.resolve('./fixtures/sample.coffee')));
+ fs.writeFileSync(sampleJs, fs.readFileSync(require.resolve('./fixtures/sample.js')));
+
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ await activationPromise;
+
+ atom.project.setPaths([testDir]);
+ const spy = spyOn(openFilesFindView, 'replaceAll').andCallFake(() => {
+ replacePromise = spy.originalValue.call(openFilesFindView);
+ });
+ });
+
+ afterEach(async () => {
+ // On Windows, you can not remove a watched directory/file, therefore we
+ // have to close the project before attempting to delete. Unfortunately,
+ // Pathwatcher's close function is also not synchronous. Once
+ // atom/node-pathwatcher#4 is implemented this should be alot cleaner.
+ let activePane = atom.workspace.getActivePane();
+ if (activePane) {
+ for (const item of activePane.getItems()) {
+ if (item.shouldPromptToSave != null) {
+ spyOn(item, 'shouldPromptToSave').andReturn(false);
+ }
+ activePane.destroyItem(item);
+ }
+ }
+
+ for (;;) {
+ try {
+ fs.removeSync(testDir);
+ break
+ } catch (e) {
+ await new Promise(resolve => setTimeout(resolve, 50))
+ }
+ }
+ });
+
+ describe("when the replace string contains an escaped char", () => {
+ let filePath = null;
+
+ beforeEach(() => {
+ let projectPath = temp.mkdirSync("atom");
+ filePath = path.join(projectPath, "tabs.txt");
+ fs.writeFileSync(filePath, "a\nb\na");
+ atom.project.setPaths([projectPath]);
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+
+ spyOn(atom, 'confirm').andReturn(0);
+ });
+
+ describe("when the regex option is chosen", () => {
+ beforeEach(async () => {
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:toggle-regex-option');
+ openFilesFindView.findEditor.setText('a');
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:confirm');
+ await searchPromise;
+ });
+
+ it("finds the escape char", async () => {
+ openFilesFindView.replaceEditor.setText('\\t');
+
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:replace-all');
+ await replacePromise;
+
+ expect(fs.readFileSync(filePath, 'utf8')).toBe("\t\nb\n\t");
+ });
+
+ it("doesn't insert an escaped char if there are multiple backslashs in front of the char", async () => {
+ openFilesFindView.replaceEditor.setText('\\\\t');
+
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:replace-all');
+ await replacePromise;
+
+ expect(fs.readFileSync(filePath, 'utf8')).toBe("\\t\nb\n\\t");
+ });
+ });
+
+ describe("when regex option is not set", () => {
+ beforeEach(async () => {
+ openFilesFindView.findEditor.setText('a');
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:confirm');
+ await searchPromise;
+ });
+
+ it("finds the escape char", async () => {
+ openFilesFindView.replaceEditor.setText('\\t');
+
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:replace-all');
+ await replacePromise;
+
+ expect(fs.readFileSync(filePath, 'utf8')).toBe("\\t\nb\n\\t");
+ });
+ });
+ });
+
+ describe("replace all button enablement", () => {
+ let disposable = null;
+
+ it("is disabled initially", () => {
+ expect(openFilesFindView.refs.replaceAllButton).toHaveClass('disabled')
+ });
+
+ it("is disabled when a search returns no results", async () => {
+ openFilesFindView.findEditor.setText('items');
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:confirm');
+ await searchPromise;
+
+ expect(openFilesFindView.refs.replaceAllButton).not.toHaveClass('disabled');
+
+ openFilesFindView.findEditor.setText('nopenotinthefile');
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:confirm');
+ await searchPromise;
+
+ expect(openFilesFindView.refs.replaceAllButton).toHaveClass('disabled');
+ });
+
+ it("is enabled when a search has results and disabled when there are no results", async () => {
+ openFilesFindView.findEditor.setText('items');
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:confirm');
+
+ await searchPromise;
+
+ disposable = openFilesFindView.replaceTooltipSubscriptions;
+ spyOn(disposable, 'dispose');
+
+ expect(openFilesFindView.refs.replaceAllButton).not.toHaveClass('disabled');
+
+ // The replace all button should still be disabled as the text has been changed and a new search has not been run
+ openFilesFindView.findEditor.setText('itemss');
+ advanceClock(stoppedChangingDelay);
+ expect(openFilesFindView.refs.replaceAllButton).toHaveClass('disabled');
+ expect(disposable.dispose).toHaveBeenCalled();
+
+ // The button should still be disabled because the search and search pattern are out of sync
+ openFilesFindView.replaceEditor.setText('omgomg');
+ advanceClock(stoppedChangingDelay);
+ expect(openFilesFindView.refs.replaceAllButton).toHaveClass('disabled');
+
+ disposable = openFilesFindView.replaceTooltipSubscriptions;
+ spyOn(disposable, 'dispose');
+ openFilesFindView.findEditor.setText('items');
+ advanceClock(stoppedChangingDelay);
+ expect(openFilesFindView.refs.replaceAllButton).not.toHaveClass('disabled');
+
+ openFilesFindView.findEditor.setText('');
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:confirm');
+
+ expect(openFilesFindView.refs.replaceAllButton).toHaveClass('disabled');
+ });
+ });
+
+ describe("when the replace button is pressed", () => {
+ beforeEach(() => {
+ spyOn(atom, 'confirm').andReturn(0);
+ });
+
+ it("runs the search, and replaces all the matches", async () => {
+ openFilesFindView.findEditor.setText('items');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ openFilesFindView.replaceEditor.setText('sunshine');
+ openFilesFindView.refs.replaceAllButton.click();
+ await replacePromise;
+
+ expect(openFilesFindView.errorMessages).not.toBeVisible();
+ expect(openFilesFindView.refs.descriptionLabel.textContent).toContain('Replaced');
+
+ const sampleJsContent = fs.readFileSync(sampleJs, 'utf8');
+ expect(sampleJsContent.match(/items/g)).toBeFalsy();
+ expect(sampleJsContent.match(/sunshine/g)).toHaveLength(6);
+
+ const sampleCoffeeContent = fs.readFileSync(sampleCoffee, 'utf8');
+ expect(sampleCoffeeContent.match(/items/g)).toBeFalsy();
+ expect(sampleCoffeeContent.match(/sunshine/g)).toHaveLength(7);
+ });
+
+ describe("when there are search results after a replace", () => {
+ it("runs the search after the replace", async () => {
+ openFilesFindView.findEditor.setText('items');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ openFilesFindView.replaceEditor.setText('items-123');
+ openFilesFindView.refs.replaceAllButton.click();
+ await replacePromise;
+
+ expect(openFilesFindView.errorMessages).not.toBeVisible();
+ expect(getExistingResultsPane().refs.previewCount.textContent).toContain('13 results found in 2 open files for items');
+ expect(openFilesFindView.refs.descriptionLabel.textContent).toContain('Replaced items with items-123 13 times in 2 open files');
+
+ openFilesFindView.replaceEditor.setText('cats');
+ advanceClock(openFilesFindView.replaceEditor.getBuffer().stoppedChangingDelay);
+ expect(openFilesFindView.refs.descriptionLabel.textContent).not.toContain('Replaced items');
+ expect(openFilesFindView.refs.descriptionLabel.textContent).toContain("13 results found in 2 open files for items");
+ })
+ });
+ });
+
+ describe("when the open-files-find:replace-all is triggered", () => {
+ describe("when no search has been run", () => {
+ beforeEach(() => {
+ spyOn(atom, 'confirm').andReturn(0)
+ });
+
+ it("does nothing", () => {
+ openFilesFindView.findEditor.setText('items');
+ openFilesFindView.replaceEditor.setText('sunshine');
+
+ spyOn(atom, 'beep');
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:replace-all');
+
+ expect(replacePromise).toBeUndefined();
+
+ expect(atom.beep).toHaveBeenCalled();
+ expect(openFilesFindView.refs.descriptionLabel.textContent).toContain("Find in Open Files");
+ });
+ });
+
+ describe("when a search with no results has been run", () => {
+ beforeEach(async () => {
+ spyOn(atom, 'confirm').andReturn(0);
+ openFilesFindView.findEditor.setText('nopenotinthefile');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+
+ await searchPromise;
+ });
+
+ it("doesnt replace anything", () => {
+ openFilesFindView.replaceEditor.setText('sunshine');
+
+ spyOn(pathSearcher, 'searchPaths').andCallThrough();
+ spyOn(atom, 'beep');
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:replace-all');
+
+ // The replacement isnt even run
+ expect(replacePromise).toBeUndefined();
+
+ expect(pathSearcher.searchPaths).not.toHaveBeenCalled();
+ expect(atom.beep).toHaveBeenCalled();
+ expect(openFilesFindView.refs.descriptionLabel.textContent.replace(/( )/g, ' ')).toContain("No results");
+ });
+ });
+
+ describe("when a search with results has been run", () => {
+ beforeEach(async () => {
+ openFilesFindView.findEditor.setText('items');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+
+ await searchPromise;
+ });
+
+ it("messages the user when the search text has changed since that last search", () => {
+ spyOn(atom, 'confirm').andReturn(0);
+ spyOn(pathSearcher, 'searchPaths').andCallThrough();
+
+ openFilesFindView.findEditor.setText('sort');
+ openFilesFindView.replaceEditor.setText('ok');
+
+ advanceClock(stoppedChangingDelay);
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:replace-all');
+
+ expect(replacePromise).toBeUndefined();
+ expect(pathSearcher.searchPaths).not.toHaveBeenCalled();
+ expect(atom.confirm).toHaveBeenCalled();
+ expect(atom.confirm.mostRecentCall.args[0].message).toContain('was changed to');
+ });
+
+ it("replaces all the matches and updates the results view", async () => {
+ spyOn(atom, 'confirm').andReturn(0);
+ openFilesFindView.replaceEditor.setText('sunshine');
+
+ expect(openFilesFindView.errorMessages).not.toBeVisible();
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:replace-all');
+ await replacePromise;
+
+ const resultsView = getResultsView();
+ expect(resultsView.element).toBeVisible();
+ expect(resultsView.refs.listView.element.querySelectorAll(".search-result")).toHaveLength(0);
+
+ expect(openFilesFindView.refs.descriptionLabel.textContent).toContain("Replaced items with sunshine 13 times in 2 open files");
+
+ let sampleJsContent = fs.readFileSync(sampleJs, 'utf8');
+ expect(sampleJsContent.match(/items/g)).toBeFalsy();
+ expect(sampleJsContent.match(/sunshine/g)).toHaveLength(6);
+
+ let sampleCoffeeContent = fs.readFileSync(sampleCoffee, 'utf8');
+ expect(sampleCoffeeContent.match(/items/g)).toBeFalsy();
+ expect(sampleCoffeeContent.match(/sunshine/g)).toHaveLength(7);
+ });
+
+ describe("when the confirm box is cancelled", () => {
+ beforeEach(() => {
+ spyOn(atom, 'confirm').andReturn(1)
+ });
+
+ it("does not replace", async () => {
+ openFilesFindView.replaceEditor.setText('sunshine');
+
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:replace-all');
+ await replacePromise;
+
+ expect(openFilesFindView.refs.descriptionLabel.textContent).toContain("13 results found");
+ });
+ });
+ });
+ });
+
+ describe("when there is an error replacing", () => {
+ beforeEach(async () => {
+ spyOn(atom, 'confirm').andReturn(0);
+ openFilesFindView.findEditor.setText('items');
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:confirm');
+ await searchPromise;
+ });
+
+ it("displays the errors in the results pane", async () => {
+ let errorList
+ spyOn(pathReplacer, 'replacePaths').andCallFake(async (regex, replacement, paths, callback) => {
+ ({ errorList } = getExistingResultsPane().refs);
+ expect(errorList.querySelectorAll("li")).toHaveLength(0);
+
+ callback(null, {path: '/some/path.js', code: 'ENOENT', message: 'Nope'});
+ expect(errorList).toBeVisible();
+ expect(errorList.querySelectorAll("li")).toHaveLength(1);
+
+ callback(null, {path: '/some/path.js', code: 'ENOENT', message: 'Broken'});
+ expect(errorList.querySelectorAll("li")).toHaveLength(2);
+ });
+
+ openFilesFindView.replaceEditor.setText('sunshine');
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:replace-all');
+ await replacePromise;
+
+ expect(errorList).toBeVisible();
+ expect(errorList.querySelectorAll("li")).toHaveLength(2);
+ expect(errorList.querySelectorAll("li")[0].textContent).toBe('Nope');
+ expect(errorList.querySelectorAll("li")[1].textContent).toBe('Broken');
+ });
+ });
+ });
+
+ describe("panel focus", () => {
+ beforeEach(async () => {
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ await activationPromise;
+ });
+
+ it("focuses the find editor when the panel gets focus", () => {
+ openFilesFindView.replaceEditor.element.focus();
+ expect(openFilesFindView.replaceEditor.element).toHaveFocus();
+
+ openFilesFindView.element.focus();
+ expect(openFilesFindView.findEditor.getElement()).toHaveFocus();
+ });
+
+ it("moves focus between editors with find-and-replace:focus-next", () => {
+ openFilesFindView.findEditor.element.focus();
+ expect(openFilesFindView.findEditor.element).toHaveFocus()
+
+ atom.commands.dispatch(openFilesFindView.findEditor.element, 'find-and-replace:focus-next');
+ expect(openFilesFindView.replaceEditor.element).toHaveFocus()
+
+ atom.commands.dispatch(openFilesFindView.replaceEditor.element, 'find-and-replace:focus-next');
+ expect(openFilesFindView.findEditor.element).toHaveFocus()
+
+ atom.commands.dispatch(openFilesFindView.replaceEditor.element, 'find-and-replace:focus-previous');
+ expect(openFilesFindView.replaceEditor.element).toHaveFocus()
+ });
+ });
+
+ describe("panel opening", () => {
+ describe("when a panel is already open on the right", () => {
+ beforeEach(async () => {
+ atom.config.set('find-and-replace.projectSearchResultsPaneSplitDirection', 'right');
+
+ editor = await atom.workspace.open('sample.js');
+ editorElement = atom.views.getView(editor);
+
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ await activationPromise;
+
+ openFilesFindView.findEditor.setText('items');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+ });
+
+ it("doesn't open another panel even if the active pane is vertically split", async () => {
+ atom.commands.dispatch(editorElement, 'pane:split-down');
+ openFilesFindView.findEditor.setText('items');
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ expect(workspaceElement.querySelectorAll('.preview-pane').length).toBe(1);
+ });
+ });
+
+ describe("when a panel is already open at the bottom", () => {
+ beforeEach(async () => {
+ atom.config.set('find-and-replace.projectSearchResultsPaneSplitDirection', 'down');
+
+ editor = await atom.workspace.open('sample.js');
+ editorElement = atom.views.getView(editor);
+
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ await activationPromise;
+
+ openFilesFindView.findEditor.setText('items');
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+ });
+
+ it("doesn't open another panel even if the active pane is horizontally split", async () => {
+ atom.commands.dispatch(editorElement, 'pane:split-right');
+ openFilesFindView.findEditor.setText('items');
+
+ atom.commands.dispatch(openFilesFindView.element, 'core:confirm');
+ await searchPromise;
+
+ expect(workspaceElement.querySelectorAll('.preview-pane').length).toBe(1);
+ });
+ });
+ });
+
+ describe("when language-javascript is active", () => {
+ beforeEach(async () => {
+ await atom.packages.activatePackage("language-javascript");
+ });
+
+ it("uses the regexp grammar when regex-mode is loaded from configuration", async () => {
+ atom.config.set('find-and-replace.useRegex', true);
+
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ await activationPromise;
+
+ expect(openFilesFindView.model.getFindOptions().useRegex).toBe(true);
+ expect(openFilesFindView.findEditor.getGrammar().scopeName).toBe('source.js.regexp');
+ expect(openFilesFindView.replaceEditor.getGrammar().scopeName).toBe('source.js.regexp.replacement');
+ });
+
+ describe("when panel is active", () => {
+ beforeEach(async () => {
+ atom.commands.dispatch(workspaceElement, 'open-files-find:show');
+ await activationPromise;
+ });
+
+ it("does not use regexp grammar when in non-regex mode", () => {
+ expect(openFilesFindView.model.getFindOptions().useRegex).not.toBe(true);
+ expect(openFilesFindView.findEditor.getGrammar().scopeName).toBe('text.plain.null-grammar');
+ expect(openFilesFindView.replaceEditor.getGrammar().scopeName).toBe('text.plain.null-grammar');
+ });
+
+ it("uses regexp grammar when in regex mode and clears the regexp grammar when regex is disabled", () => {
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:toggle-regex-option');
+
+ expect(openFilesFindView.model.getFindOptions().useRegex).toBe(true);
+ expect(openFilesFindView.findEditor.getGrammar().scopeName).toBe('source.js.regexp');
+ expect(openFilesFindView.replaceEditor.getGrammar().scopeName).toBe('source.js.regexp.replacement');
+
+ atom.commands.dispatch(openFilesFindView.element, 'open-files-find:toggle-regex-option');
+
+ expect(openFilesFindView.model.getFindOptions().useRegex).not.toBe(true);
+ expect(openFilesFindView.findEditor.getGrammar().scopeName).toBe('text.plain.null-grammar');
+ expect(openFilesFindView.replaceEditor.getGrammar().scopeName).toBe('text.plain.null-grammar');
+ });
+ });
+ });
+});
+
+function simulateResizeEvent(element) {
+ Array.from(element.children).forEach((child) => {
+ child.dispatchEvent(new AnimationEvent('animationstart'));
+ });
+ advanceClock(1);
+}
diff --git a/styles/find-and-replace.less b/styles/find-and-replace.less
index cef1acbf..d4e3f3e1 100644
--- a/styles/find-and-replace.less
+++ b/styles/find-and-replace.less
@@ -37,7 +37,9 @@ atom-workspace.find-visible {
// Both project and buffer FNR styles
.find-and-replace,
+.open-files-find,
.preview-pane,
+.open-files-find,
.project-find {
@min-width: 200px; // min width before it starts scrolling
@@ -218,6 +220,7 @@ atom-workspace.find-visible {
}
// Project find and replace
+.open-files-find,
.project-find {
@project-input-width: 260px;
@project-block-width: 160px;