diff --git a/lib/project/results-model.coffee b/lib/project/results-model.coffee deleted file mode 100644 index 3169fa69..00000000 --- a/lib/project/results-model.coffee +++ /dev/null @@ -1,283 +0,0 @@ -_ = require 'underscore-plus' -{Emitter, TextEditor} = require 'atom' -escapeHelper = require '../escape-helper' - -class Result - @create: (result) -> - if result?.matches?.length - matches = result.matches.map((m) -> - return { - matchText: m.matchText, - lineText: m.lineText, - lineTextOffset: m.lineTextOffset, - range: m.range, - leadingContextLines: m.leadingContextLines, - trailingContextLines: m.trailingContextLines - } - ) - new Result({filePath: result.filePath, matches}) - else - null - - constructor: (result) -> - _.extend(this, result) - -module.exports = -class ResultsModel - constructor: (@findOptions) -> - @emitter = new Emitter - - atom.workspace.getCenter().observeActivePaneItem (item) => - if item instanceof TextEditor - item.onDidStopChanging => @onContentsModified(item) - - @clear() - - onDidClear: (callback) -> - @emitter.on 'did-clear', callback - - onDidClearSearchState: (callback) -> - @emitter.on 'did-clear-search-state', callback - - onDidClearReplacementState: (callback) -> - @emitter.on 'did-clear-replacement-state', callback - - onDidSearchPaths: (callback) -> - @emitter.on 'did-search-paths', callback - - onDidErrorForPath: (callback) -> - @emitter.on 'did-error-for-path', callback - - onDidNoopSearch: (callback) -> - @emitter.on 'did-noop-search', callback - - onDidStartSearching: (callback) -> - @emitter.on 'did-start-searching', callback - - onDidCancelSearching: (callback) -> - @emitter.on 'did-cancel-searching', callback - - onDidFinishSearching: (callback) -> - @emitter.on 'did-finish-searching', callback - - onDidStartReplacing: (callback) -> - @emitter.on 'did-start-replacing', callback - - onDidFinishReplacing: (callback) -> - @emitter.on 'did-finish-replacing', callback - - onDidSearchPath: (callback) -> - @emitter.on 'did-search-path', callback - - onDidReplacePath: (callback) -> - @emitter.on 'did-replace-path', callback - - onDidAddResult: (callback) -> - @emitter.on 'did-add-result', callback - - onDidRemoveResult: (callback) -> - @emitter.on 'did-remove-result', callback - - clear: -> - @clearSearchState() - @clearReplacementState() - @emitter.emit 'did-clear', @getResultsSummary() - - clearSearchState: -> - @pathCount = 0 - @matchCount = 0 - @regex = null - @results = {} - @paths = [] - @active = false - @searchErrors = null - - if @inProgressSearchPromise? - @inProgressSearchPromise.cancel() - @inProgressSearchPromise = null - - @emitter.emit 'did-clear-search-state', @getResultsSummary() - - clearReplacementState: -> - @replacePattern = null - @replacedPathCount = null - @replacementCount = null - @replacementErrors = null - @emitter.emit 'did-clear-replacement-state', @getResultsSummary() - - shouldRerunSearch: (findPattern, pathsPattern, replacePattern, options={}) -> - {onlyRunIfChanged} = options - if onlyRunIfChanged and findPattern? and pathsPattern? and findPattern is @lastFindPattern and pathsPattern is @lastPathsPattern - false - else - true - - search: (findPattern, pathsPattern, replacePattern, options={}) -> - unless @shouldRerunSearch(findPattern, pathsPattern, replacePattern, options) - @emitter.emit 'did-noop-search' - return Promise.resolve() - - {keepReplacementState} = options - if keepReplacementState - @clearSearchState() - else - @clear() - - @lastFindPattern = findPattern - @lastPathsPattern = pathsPattern - @findOptions.set(_.extend({findPattern, replacePattern, pathsPattern}, options)) - @regex = @findOptions.getFindPatternRegex() - - @active = true - searchPaths = @pathsArrayFromPathsPattern(pathsPattern) - - onPathsSearched = (numberOfPathsSearched) => - @emitter.emit 'did-search-paths', numberOfPathsSearched - - leadingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountBefore') - trailingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountAfter') - @inProgressSearchPromise = atom.workspace.scan @regex, {paths: searchPaths, onPathsSearched, leadingContextLineCount, trailingContextLineCount}, (result, error) => - if result - @setResult(result.filePath, Result.create(result)) - else - @searchErrors ?= [] - @searchErrors.push(error) - @emitter.emit 'did-error-for-path', error - - @emitter.emit 'did-start-searching', @inProgressSearchPromise - @inProgressSearchPromise.then (message) => - if message is 'cancelled' - @emitter.emit 'did-cancel-searching' - else - @inProgressSearchPromise = null - @emitter.emit 'did-finish-searching', @getResultsSummary() - - replace: (pathsPattern, replacePattern, replacementPaths) -> - return unless @findOptions.findPattern and @regex? - - @findOptions.set({replacePattern, pathsPattern}) - - replacePattern = escapeHelper.unescapeEscapeSequence(replacePattern) if @findOptions.useRegex - - @active = false # not active until the search is finished - @replacedPathCount = 0 - @replacementCount = 0 - - promise = atom.workspace.replace @regex, replacePattern, replacementPaths, (result, error) => - if result - if result.replacements - @replacedPathCount++ - @replacementCount += result.replacements - @emitter.emit 'did-replace-path', result - else - @replacementErrors ?= [] - @replacementErrors.push(error) - @emitter.emit 'did-error-for-path', error - - @emitter.emit 'did-start-replacing', promise - promise.then => - @emitter.emit 'did-finish-replacing', @getResultsSummary() - @search(@findOptions.findPattern, @findOptions.pathsPattern, @findOptions.replacePattern, {keepReplacementState: true}) - .catch (e) -> - console.error e.stack - - setActive: (isActive) -> - @active = isActive if (isActive and @findOptions.findPattern) or not isActive - - getActive: -> @active - - getFindOptions: -> @findOptions - - getLastFindPattern: -> @lastFindPattern - - getResultsSummary: -> - findPattern = @lastFindPattern ? @findOptions.findPattern - replacePattern = @findOptions.replacePattern - { - findPattern - replacePattern - @pathCount - @matchCount - @searchErrors - @replacedPathCount - @replacementCount - @replacementErrors - } - - getPathCount: -> - @pathCount - - getMatchCount: -> - @matchCount - - getPaths: -> - @paths - - getResult: (filePath) -> - @results[filePath] - - getResultAt: (index) -> - @results[@paths[index]] - - setResult: (filePath, result) -> - if result - @addResult(filePath, result) - else - @removeResult(filePath) - - addResult: (filePath, result) -> - filePathInsertedIndex = null - filePathUpdatedIndex = null - if @results[filePath] - @matchCount -= @results[filePath].matches.length - filePathUpdatedIndex = @paths.indexOf(filePath) - else - @pathCount++ - filePathInsertedIndex = binaryIndex(@paths, filePath, stringCompare) - @paths.splice(filePathInsertedIndex, 0, filePath) - - @matchCount += result.matches.length - - @results[filePath] = result - @emitter.emit 'did-add-result', {filePath, result, filePathInsertedIndex, filePathUpdatedIndex} - - removeResult: (filePath) -> - if @results[filePath] - @pathCount-- - @matchCount -= @results[filePath].matches.length - - filePathRemovedIndex = @paths.indexOf(filePath) - @paths = _.without(@paths, filePath) - delete @results[filePath] - @emitter.emit 'did-remove-result', {filePath, filePathRemovedIndex} - - onContentsModified: (editor) => - return unless @active and @regex - return unless editor.getPath() - - matches = [] - leadingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountBefore') - trailingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountAfter') - editor.scan @regex, {leadingContextLineCount, trailingContextLineCount}, (match) -> - matches.push(match) - - result = Result.create({filePath: editor.getPath(), matches}) - @setResult(editor.getPath(), result) - @emitter.emit 'did-finish-searching', @getResultsSummary() - - pathsArrayFromPathsPattern: (pathsPattern) -> - (inputPath.trim() for inputPath in pathsPattern.trim().split(',') when inputPath) - -stringCompare = (a, b) -> a.localeCompare(b) - -binaryIndex = (array, value, comparator) -> - # Lifted from underscore's _.sortedIndex ; adds a flexible comparator - low = 0 - high = array.length - while low < high - mid = Math.floor((low + high) / 2) - if comparator(array[mid], value) < 0 - low = mid + 1 - else - high = mid - low diff --git a/lib/project/results-model.js b/lib/project/results-model.js new file mode 100644 index 00000000..72cd63aa --- /dev/null +++ b/lib/project/results-model.js @@ -0,0 +1,353 @@ +const _ = require('underscore-plus') +const {Emitter, TextEditor} = require('atom') +const escapeHelper = require('../escape-helper') + +class Result { + static create (result) { + if (result && result.matches && result.matches.length) { + const matches = result.matches.map(m => + ({ + matchText: m.matchText, + lineText: m.lineText, + lineTextOffset: m.lineTextOffset, + range: m.range, + leadingContextLines: m.leadingContextLines, + trailingContextLines: m.trailingContextLines + }) + ) + return new Result({filePath: result.filePath, matches}) + } else { + return null + } + } + + constructor (result) { + _.extend(this, result) + } +} + +module.exports = class ResultsModel { + constructor (findOptions) { + this.onContentsModified = this.onContentsModified.bind(this) + this.findOptions = findOptions + this.emitter = new Emitter() + + atom.workspace.getCenter().observeActivePaneItem(item => { + if (item instanceof TextEditor) { + item.onDidStopChanging(() => this.onContentsModified(item)) + } + }) + + this.clear() + } + + onDidClear (callback) { + return this.emitter.on('did-clear', callback) + } + + onDidClearSearchState (callback) { + return this.emitter.on('did-clear-search-state', callback) + } + + onDidClearReplacementState (callback) { + return this.emitter.on('did-clear-replacement-state', callback) + } + + onDidSearchPaths (callback) { + return this.emitter.on('did-search-paths', callback) + } + + onDidErrorForPath (callback) { + return this.emitter.on('did-error-for-path', callback) + } + + onDidNoopSearch (callback) { + return this.emitter.on('did-noop-search', callback) + } + + onDidStartSearching (callback) { + return this.emitter.on('did-start-searching', callback) + } + + onDidCancelSearching (callback) { + return this.emitter.on('did-cancel-searching', callback) + } + + onDidFinishSearching (callback) { + return this.emitter.on('did-finish-searching', callback) + } + + onDidStartReplacing (callback) { + return this.emitter.on('did-start-replacing', callback) + } + + onDidFinishReplacing (callback) { + return this.emitter.on('did-finish-replacing', callback) + } + + onDidSearchPath (callback) { + return this.emitter.on('did-search-path', callback) + } + + onDidReplacePath (callback) { + return this.emitter.on('did-replace-path', callback) + } + + onDidAddResult (callback) { + return this.emitter.on('did-add-result', callback) + } + + onDidRemoveResult (callback) { + return this.emitter.on('did-remove-result', callback) + } + + clear () { + this.clearSearchState() + this.clearReplacementState() + this.emitter.emit('did-clear', this.getResultsSummary()) + } + + clearSearchState () { + this.pathCount = 0 + this.matchCount = 0 + this.regex = null + this.results = {} + this.paths = [] + this.active = false + this.searchErrors = null + + if (this.inProgressSearchPromise != null) { + this.inProgressSearchPromise.cancel() + this.inProgressSearchPromise = null + } + + this.emitter.emit('did-clear-search-state', this.getResultsSummary()) + } + + clearReplacementState () { + this.replacePattern = null + this.replacedPathCount = null + this.replacementCount = null + this.replacementErrors = null + this.emitter.emit('did-clear-replacement-state', this.getResultsSummary()) + } + + shouldRerunSearch (findPattern, pathsPattern, replacePattern, options) { + if (options == null) { options = {} } + const {onlyRunIfChanged} = options + return !(onlyRunIfChanged && (findPattern != null) && (pathsPattern != null) && + (findPattern === this.lastFindPattern) && (pathsPattern === this.lastPathsPattern)) + } + + search (findPattern, pathsPattern, replacePattern, options) { + if (options == null) { options = {} } + if (!this.shouldRerunSearch(findPattern, pathsPattern, 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.lastPathsPattern = pathsPattern + this.findOptions.set(_.extend({findPattern, replacePattern, pathsPattern}, options)) + this.regex = this.findOptions.getFindPatternRegex() + + this.active = true + const searchPaths = this.pathsArrayFromPathsPattern(pathsPattern) + + const onPathsSearched = numberOfPathsSearched => { + this.emitter.emit('did-search-paths', numberOfPathsSearched) + } + + const leadingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountBefore') + const trailingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountAfter') + this.inProgressSearchPromise = atom.workspace.scan( + this.regex, + { + paths: searchPaths, + onPathsSearched, + leadingContextLineCount, + trailingContextLineCount + }, + (result, error) => { + if (result) { + this.setResult(result.filePath, Result.create(result)) + } else { + if (this.searchErrors == null) { this.searchErrors = [] } + this.searchErrors.push(error) + this.emitter.emit('did-error-for-path', error) + } + }) + + 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 } + + this.findOptions.set({replacePattern, pathsPattern}) + + 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 = atom.workspace.replace(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.search(this.findOptions.findPattern, this.findOptions.pathsPattern, + this.findOptions.replacePattern, {keepReplacementState: true}) + }).catch(e => console.error(e.stack)) + } + + setActive (isActive) { + if ((isActive && this.findOptions.findPattern) || !isActive) { + this.active = isActive + } + } + + getActive () { return this.active } + + getFindOptions () { return this.findOptions } + + getLastFindPattern () { return this.lastFindPattern } + + getResultsSummary () { + const findPattern = this.lastFindPattern != null ? this.lastFindPattern : this.findOptions.findPattern + const { replacePattern } = this.findOptions + return { + findPattern, + replacePattern, + pathCount: this.pathCount, + matchCount: this.matchCount, + searchErrors: this.searchErrors, + replacedPathCount: this.replacedPathCount, + replacementCount: this.replacementCount, + replacementErrors: this.replacementErrors + } + } + + getPathCount () { + return this.pathCount + } + + getMatchCount () { + return this.matchCount + } + + getPaths () { + return this.paths + } + + getResult (filePath) { + return this.results[filePath] + } + + getResultAt (index) { + return this.results[this.paths[index]] + } + + setResult (filePath, result) { + if (result) { + this.addResult(filePath, result) + } else { + this.removeResult(filePath) + } + } + + addResult (filePath, result) { + let filePathInsertedIndex = null + let filePathUpdatedIndex = null + if (this.results[filePath]) { + this.matchCount -= this.results[filePath].matches.length + filePathUpdatedIndex = this.paths.indexOf(filePath) + } else { + this.pathCount++ + filePathInsertedIndex = binaryIndex(this.paths, filePath, stringCompare) + this.paths.splice(filePathInsertedIndex, 0, filePath) + } + + this.matchCount += result.matches.length + + this.results[filePath] = result + this.emitter.emit('did-add-result', {filePath, result, filePathInsertedIndex, filePathUpdatedIndex}) + } + + removeResult (filePath) { + if (this.results[filePath]) { + this.pathCount-- + this.matchCount -= this.results[filePath].matches.length + + const filePathRemovedIndex = this.paths.indexOf(filePath) + this.paths = _.without(this.paths, filePath) + delete this.results[filePath] + this.emitter.emit('did-remove-result', {filePath, filePathRemovedIndex}) + } + } + + onContentsModified (editor) { + if (!this.active || !this.regex || !editor.getPath()) { return } + + const matches = [] + const leadingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountBefore') + const trailingContextLineCount = atom.config.get('find-and-replace.searchContextLineCountAfter') + editor.scan(this.regex, + {leadingContextLineCount, trailingContextLineCount}, + (match) => matches.push(match) + ) + + const result = Result.create({filePath: editor.getPath(), matches}) + this.setResult(editor.getPath(), result) + this.emitter.emit('did-finish-searching', this.getResultsSummary()) + } + + pathsArrayFromPathsPattern (pathsPattern) { + return pathsPattern.trim().split(',').map((inputPath) => inputPath.trim()) + } +} + +var stringCompare = (a, b) => a.localeCompare(b) + +var binaryIndex = function (array, value, comparator) { + // Lifted from underscore's _.sortedIndex ; adds a flexible comparator + let low = 0 + let high = array.length + while (low < high) { + const mid = Math.floor((low + high) / 2) + if (comparator(array[mid], value) < 0) { + low = mid + 1 + } else { + high = mid + } + } + return low +}