diff --git a/lib/project-find-view.js b/lib/project-find-view.js index bac9dec8..897db490 100644 --- a/lib/project-find-view.js +++ b/lib/project-find-view.js @@ -318,7 +318,7 @@ class ProjectFindView { return searchPromise; } - search(options) { + async 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); @@ -330,13 +330,13 @@ class ProjectFindView { let {onlyRunIfActive, onlyRunIfChanged} = options; if ((onlyRunIfActive && !this.model.active) || !findPattern) return Promise.resolve(); - return this.showResultPane().then(() => { - try { - return this.model.search(findPattern, pathsPattern, replacePattern, options); - } catch (e) { - this.setErrorMessage(e.message); - } - }); + await this.showResultPane() + + try { + return await this.model.search(findPattern, pathsPattern, replacePattern, options); + } catch (e) { + this.setErrorMessage(e.message); + } } replaceAll() { diff --git a/lib/project/results-model.js b/lib/project/results-model.js index 171393fe..3fa39d2f 100644 --- a/lib/project/results-model.js +++ b/lib/project/results-model.js @@ -5,16 +5,57 @@ 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: Range.fromObject(m.range), - leadingContextLines: m.leadingContextLines, - trailingContextLines: m.trailingContextLines - }) - ) + const matches = [] + + for (const m of result.matches) { + const range = Range.fromObject(m.range) + const matchSplit = m.matchText.split('\n') + const linesSplit = m.lineText.split('\n') + + // If the result spans across multiple lines, process each of + // them separately by creating separate `matches` objects for + // each line on the match. + for (let row = range.start.row; row <= range.end.row; row++) { + const lineText = linesSplit[row - range.start.row] + const matchText = matchSplit[row - range.start.row] + + // When receiving multiline results from opened buffers, only + // the first result line is provided on the `lineText` property. + // This makes it impossible to properly render the part of the result + // that's part of other lines. + // In order to prevent an error we just need to ignore these parts. + if (lineText === undefined || matchText === undefined) { + continue + } + + // Adapt the range column number based on which line we're at: + // - the first line of a multiline result will always start at the range start + // and will end at the end of the line. + // - middle lines will start at 0 and end at the end of the line + // - last line will start at 0 and end at the range end. + const startColumn = row === range.start.row ? range.start.column : 0 + const endColumn = row === range.end.row ? range.end.column : lineText.length + + matches.push({ + matchText, + lineText, + lineTextOffset: m.lineTextOffset, + range: { + start: { + row, + column: startColumn + }, + end: { + row, + column: endColumn + } + }, + leadingContextLines: m.leadingContextLines, + trailingContextLines: m.trailingContextLines + }) + } + } + return new Result({filePath: result.filePath, matches}) } else { return null @@ -146,7 +187,7 @@ module.exports = class ResultsModel { ) } - search (findPattern, pathsPattern, replacePattern, options = {}) { + async search (findPattern, pathsPattern, replacePattern, options = {}) { if (!this.shouldRerunSearch(findPattern, pathsPattern, options)) { this.emitter.emit('did-noop-search') return Promise.resolve() @@ -195,17 +236,18 @@ module.exports = class ResultsModel { }) this.emitter.emit('did-start-searching', this.inProgressSearchPromise) - return this.inProgressSearchPromise.then(message => { - if (message === 'cancelled') { - this.emitter.emit('did-cancel-searching') - } else { - const resultsSummary = this.getResultsSummary() - this.metricsReporter.sendSearchEvent(Date.now() - startTime, resultsSummary.matchCount) - this.inProgressSearchPromise = null - this.emitter.emit('did-finish-searching', resultsSummary) - } - }) + const message = await this.inProgressSearchPromise + + if (message === 'cancelled') { + this.emitter.emit('did-cancel-searching') + } else { + const resultsSummary = this.getResultsSummary() + + this.metricsReporter.sendSearchEvent(Date.now() - startTime, resultsSummary.matchCount) + this.inProgressSearchPromise = null + this.emitter.emit('did-finish-searching', resultsSummary) + } } replace (pathsPattern, replacePattern, replacementPaths) {