diff --git a/extensions/search-rg/src/ripgrepTextSearch.ts b/extensions/search-rg/src/ripgrepTextSearch.ts index 7ba477c724c..07d6541dd73 100644 --- a/extensions/search-rg/src/ripgrepTextSearch.ts +++ b/extensions/search-rg/src/ripgrepTextSearch.ts @@ -67,7 +67,7 @@ export class RipgrepTextSearchEngine { }); let gotResult = false; - this.ripgrepParser = new RipgrepParser(MAX_TEXT_RESULTS, cwd); + this.ripgrepParser = new RipgrepParser(MAX_TEXT_RESULTS, cwd, options.previewOptions); this.ripgrepParser.on('result', (match: vscode.TextSearchResult) => { gotResult = true; progress.report(match); @@ -160,7 +160,7 @@ export class RipgrepParser extends EventEmitter { private numResults = 0; - constructor(private maxResults: number, private rootFolder: string) { + constructor(private maxResults: number, private rootFolder: string, private previewOptions?: vscode.TextSearchPreviewOptions) { super(); this.stringDecoder = new StringDecoder(); } @@ -293,12 +293,22 @@ export class RipgrepParser extends EventEmitter { lineMatches .map(range => { + let trimmedPreview = preview; + let trimmedPreviewRange = range; + if (this.previewOptions) { + const previewStart = Math.max(range.start.character - this.previewOptions.leadingChars, 0); + trimmedPreview = preview.substr(previewStart, this.previewOptions.totalChars - previewStart); + if (previewStart > 0) { + trimmedPreviewRange = new vscode.Range(0, range.start.character - previewStart, 0, range.end.character - previewStart); + } + } + return { uri: vscode.Uri.file(path.join(this.rootFolder, this.currentFile)), range, preview: { - text: preview, - match: new vscode.Range(0, range.start.character, 0, range.end.character) + text: trimmedPreview, + match: trimmedPreviewRange || new vscode.Range(0, range.start.character, 0, range.end.character) } }; }) diff --git a/src/vs/platform/search/common/search.ts b/src/vs/platform/search/common/search.ts index 5129ea788b9..b27650c5057 100644 --- a/src/vs/platform/search/common/search.ts +++ b/src/vs/platform/search/common/search.ts @@ -85,6 +85,7 @@ export interface ICommonQueryOptions { disregardExcludeSettings?: boolean; ignoreSymlinks?: boolean; maxFileSize?: number; + previewOptions?: ITextSearchPreviewOptions; } export interface IQueryOptions extends ICommonQueryOptions { @@ -132,15 +133,33 @@ export interface IPatternInfo { export interface IFileMatch { resource?: U; - lineMatches?: ILineMatch[]; + matches?: ITextSearchResult[]; } export type IRawFileMatch2 = IFileMatch; -export interface ILineMatch { - preview: string; - lineNumber: number; - offsetAndLengths: number[][]; +export interface ITextSearchPreviewOptions { + maxLines: number; + leadingChars: number; + totalChars: number; +} + +export interface ISearchRange { + readonly startLineNumber: number; + readonly startColumn: number; + readonly endLineNumber: number; + readonly endColumn: number; +} + +export interface ITextSearchResultPreview { + text: string; + match: ISearchRange; +} + +export interface ITextSearchResult { + uri?: uri; + range: ISearchRange; + preview: ITextSearchResultPreview; } export interface IProgress { @@ -204,18 +223,47 @@ export interface IFileIndexProviderStats { filesWalked: number; } -// ---- very simple implementation of the search model -------------------- - export class FileMatch implements IFileMatch { - public lineMatches: LineMatch[] = []; + public matches: ITextSearchResult[] = []; constructor(public resource: uri) { // empty } } -export class LineMatch implements ILineMatch { - constructor(public preview: string, public lineNumber: number, public offsetAndLengths: number[][]) { - // empty +export class TextSearchResult implements ITextSearchResult { + range: ISearchRange; + preview: ITextSearchResultPreview; + + constructor(fullLine: string, range: ISearchRange, previewOptions?: ITextSearchPreviewOptions) { + this.range = range; + if (previewOptions) { + const previewStart = Math.max(range.startColumn - previewOptions.leadingChars, 0); + const previewEnd = Math.max(previewOptions.totalChars + previewStart, range.endColumn); + + this.preview = { + text: fullLine.substring(previewStart, previewEnd), + match: new OneLineRange(0, range.startColumn - previewStart, range.endColumn - previewStart) + }; + } else { + this.preview = { + text: fullLine, + match: new OneLineRange(0, range.startColumn, range.endColumn) + }; + } + } +} + +export class OneLineRange implements ISearchRange { + startLineNumber: number; + startColumn: number; + endLineNumber: number; + endColumn: number; + + constructor(lineNumber: number, startColumn: number, endColumn: number) { + this.startLineNumber = lineNumber; + this.startColumn = startColumn; + this.endLineNumber = lineNumber; + this.endColumn = endColumn; } } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 48b1f768187..30b4867e085 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -77,6 +77,12 @@ declare module 'vscode' { followSymlinks: boolean; } + export interface TextSearchPreviewOptions { + maxLines: number; + leadingChars: number; + totalChars: number; + } + /** * Options that apply to text search. */ @@ -86,10 +92,7 @@ declare module 'vscode' { */ maxResults: number; - /** - * TODO@roblou - total length? # of context lines? leading and trailing # of chars? - */ - previewOptions?: any; + previewOptions?: TextSearchPreviewOptions; /** * Exclude files larger than `maxFileSize` in bytes. diff --git a/src/vs/workbench/api/electron-browser/mainThreadSearch.ts b/src/vs/workbench/api/electron-browser/mainThreadSearch.ts index 9028249c12b..1021d2d8c71 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSearch.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSearch.ts @@ -78,7 +78,7 @@ class SearchOperation { addMatch(match: IFileMatch): void { if (this.matches.has(match.resource.toString())) { // Merge with previous IFileMatches - this.matches.get(match.resource.toString()).lineMatches.push(...match.lineMatches); + this.matches.get(match.resource.toString()).matches.push(...match.matches); } else { this.matches.set(match.resource.toString(), match); } @@ -149,10 +149,10 @@ class RemoteSearchProvider implements ISearchResultProvider, IDisposable { const searchOp = this._searches.get(session); dataOrUri.forEach(result => { - if ((result).lineMatches) { + if ((result).matches) { searchOp.addMatch({ resource: URI.revive((result).resource), - lineMatches: (result).lineMatches + matches: (result).matches }); } else { searchOp.addMatch({ diff --git a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts index b5eb1639ae0..e2148ffffc2 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts @@ -181,7 +181,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { const query = queryBuilder.text(pattern, folders, options); const onProgress = (p: ISearchProgressItem) => { - if (p.lineMatches) { + if (p.matches) { this._proxy.$handleTextSearchResult(p, requestId); } }; diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index afdaaa29ef2..58b755902c6 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -5,20 +5,20 @@ 'use strict'; import * as path from 'path'; -import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { isPromiseCanceledError } from 'vs/base/common/errors'; import * as glob from 'vs/base/common/glob'; +import { toDisposable } from 'vs/base/common/lifecycle'; import * as resources from 'vs/base/common/resources'; +import { StopWatch } from 'vs/base/common/stopwatch'; import URI, { UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import * as extfs from 'vs/base/node/extfs'; -import { IFileMatch, IFolderQuery, IPatternInfo, IRawSearchQuery, ISearchCompleteStats, ISearchQuery, IFileSearchProviderStats } from 'vs/platform/search/common/search'; +import { IFileMatch, IFileSearchProviderStats, IFolderQuery, IPatternInfo, IRawSearchQuery, ISearchCompleteStats, ISearchQuery, ITextSearchResult } from 'vs/platform/search/common/search'; +import { FileIndexSearchManager, IDirectoryEntry, IDirectoryTree, IInternalFileMatch, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/api/node/extHostSearch.fileIndex'; import * as vscode from 'vscode'; import { ExtHostSearchShape, IMainContext, MainContext, MainThreadSearchShape } from './extHost.protocol'; -import { toDisposable } from 'vs/base/common/lifecycle'; -import { IInternalFileMatch, QueryGlobTester, resolvePatternsForProvider, IDirectoryTree, IDirectoryEntry, FileIndexSearchManager } from 'vs/workbench/api/node/extHostSearch.fileIndex'; -import { StopWatch } from 'vs/base/common/stopwatch'; -import { isPromiseCanceledError } from 'vs/base/common/errors'; export interface ISchemeTransformer { transformOutgoing(scheme: string): string; @@ -172,22 +172,16 @@ class TextSearchResultsCollector { if (!this._currentFileMatch) { this._currentFileMatch = { resource: data.uri, - lineMatches: [] + matches: [] }; } - // TODO@roblou - line text is sent for every match - const matchRange = data.preview.match; - this._currentFileMatch.lineMatches.push({ - lineNumber: data.range.start.line, - preview: data.preview.text, - offsetAndLengths: [[matchRange.start.character, matchRange.end.character - matchRange.start.character]] - }); + this._currentFileMatch.matches.push(extensionResultToFrontendResult(data)); } private pushToCollector(): void { const size = this._currentFileMatch ? - this._currentFileMatch.lineMatches.reduce((acc, match) => acc + match.offsetAndLengths.length, 0) : + this._currentFileMatch.matches.length : 0; this._batchedCollector.addItem(this._currentFileMatch, size); } @@ -202,6 +196,26 @@ class TextSearchResultsCollector { } } +function extensionResultToFrontendResult(data: vscode.TextSearchResult): ITextSearchResult { + return { + preview: { + match: { + startLineNumber: data.preview.match.start.line, + startColumn: data.preview.match.start.character, + endLineNumber: data.preview.match.end.line, + endColumn: data.preview.match.end.character + }, + text: data.preview.text + }, + range: { + startLineNumber: data.range.start.line, + startColumn: data.range.start.character, + endLineNumber: data.range.end.line, + endColumn: data.range.end.character + } + }; +} + /** * Collects items that have a size - before the cumulative size of collected items reaches START_BATCH_AFTER_COUNT, the callback is called for every * set of items collected. @@ -414,7 +428,8 @@ class TextSearchEngine { followSymlinks: !this.config.ignoreSymlinks, encoding: this.config.fileEncoding, maxFileSize: this.config.maxFileSize, - maxResults: this.config.maxResults + maxResults: this.config.maxResults, + previewOptions: this.config.previewOptions }; } } diff --git a/src/vs/workbench/parts/search/browser/searchView.ts b/src/vs/workbench/parts/search/browser/searchView.ts index 301c4fc6f08..b589ee18ac2 100644 --- a/src/vs/workbench/parts/search/browser/searchView.ts +++ b/src/vs/workbench/parts/search/browser/searchView.ts @@ -108,6 +108,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { private readonly selectCurrentMatchEmitter: Emitter; private delayedRefresh: Delayer; private changedWhileHidden: boolean; + private isWide: boolean; private searchWithoutFolderMessageBuilder: Builder; @@ -826,8 +827,10 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { } if (this.size.width >= SearchView.WIDE_VIEW_SIZE) { + this.isWide = true; dom.addClass(this.getContainer(), SearchView.WIDE_CLASS_NAME); } else { + this.isWide = false; dom.removeClass(this.getContainer(), SearchView.WIDE_CLASS_NAME); } @@ -1081,7 +1084,12 @@ export class SearchView extends Viewlet implements IViewlet, IPanel { disregardIgnoreFiles: !useExcludesAndIgnoreFiles, disregardExcludeSettings: !useExcludesAndIgnoreFiles, excludePattern, - includePattern + includePattern, + previewOptions: { + leadingChars: 5, + maxLines: 1, + totalChars: this.isWide ? 1000 : 100 + } }; const folderResources = this.contextService.getWorkspace().folders; diff --git a/src/vs/workbench/parts/search/common/queryBuilder.ts b/src/vs/workbench/parts/search/common/queryBuilder.ts index 08bea8ac1cd..1ee9968ff2e 100644 --- a/src/vs/workbench/parts/search/common/queryBuilder.ts +++ b/src/vs/workbench/parts/search/common/queryBuilder.ts @@ -95,7 +95,8 @@ export class QueryBuilder { useRipgrep, disregardIgnoreFiles: options.disregardIgnoreFiles || !useIgnoreFiles, disregardExcludeSettings: options.disregardExcludeSettings, - ignoreSymlinks + ignoreSymlinks, + previewOptions: options.previewOptions }; // Filter extraFileResources against global include/exclude patterns - they are already expected to not belong to a workspace diff --git a/src/vs/workbench/parts/search/common/searchModel.ts b/src/vs/workbench/parts/search/common/searchModel.ts index a4597dd28b6..376fe0f666a 100644 --- a/src/vs/workbench/parts/search/common/searchModel.ts +++ b/src/vs/workbench/parts/search/common/searchModel.ts @@ -3,39 +3,50 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as objects from 'vs/base/common/objects'; -import * as strings from 'vs/base/common/strings'; -import * as errors from 'vs/base/common/errors'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { TPromise } from 'vs/base/common/winjs.base'; +import * as errors from 'vs/base/common/errors'; +import { anyEvent, Emitter, Event, fromPromise, stopwatch } from 'vs/base/common/event'; +import { getBaseLabel } from 'vs/base/common/labels'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ResourceMap, TernarySearchTree, values } from 'vs/base/common/map'; +import * as objects from 'vs/base/common/objects'; import URI from 'vs/base/common/uri'; -import { values, ResourceMap, TernarySearchTree } from 'vs/base/common/map'; -import { Event, Emitter, fromPromise, stopwatch, anyEvent } from 'vs/base/common/event'; -import { ISearchService, ISearchProgressItem, ISearchComplete, ISearchQuery, IPatternInfo, IFileMatch, ITextSearchStats } from 'vs/platform/search/common/search'; -import { ReplacePattern } from 'vs/platform/search/common/replace'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { TPromise } from 'vs/base/common/winjs.base'; import { Range } from 'vs/editor/common/core/range'; -import { ITextModel, IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness, FindMatch } from 'vs/editor/common/model'; -import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { IReplaceService } from 'vs/workbench/parts/search/common/replace'; -import { IProgressRunner } from 'vs/platform/progress/common/progress'; +import { FindMatch, IModelDeltaDecoration, ITextModel, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IProgressRunner } from 'vs/platform/progress/common/progress'; +import { ReplacePattern } from 'vs/platform/search/common/replace'; +import { IFileMatch, IPatternInfo, ISearchComplete, ISearchProgressItem, ISearchQuery, ISearchService, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, TextSearchResult } from 'vs/platform/search/common/search'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { overviewRulerFindMatchForeground } from 'vs/platform/theme/common/colorRegistry'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; -import { getBaseLabel } from 'vs/base/common/labels'; +import { IReplaceService } from 'vs/workbench/parts/search/common/replace'; export class Match { - private _lineText: string; private _id: string; private _range: Range; + private _previewText: string; + private _rangeInPreviewText: Range; - constructor(private _parent: FileMatch, text: string, lineNumber: number, offset: number, length: number) { - this._lineText = text; - this._range = new Range(1 + lineNumber, 1 + offset, 1 + lineNumber, 1 + offset + length); - this._id = this._parent.id() + '>' + lineNumber + '>' + offset + this.getMatchString(); + constructor(private _parent: FileMatch, _result: ITextSearchResult) { + this._range = new Range( + _result.range.startLineNumber + 1, + _result.range.startColumn + 1, + _result.range.endLineNumber + 1, + _result.range.endColumn + 1); + + this._rangeInPreviewText = new Range( + _result.preview.match.startLineNumber + 1, + _result.preview.match.startColumn + 1, + _result.preview.match.endLineNumber + 1, + _result.preview.match.endColumn + 1); + this._previewText = _result.preview.text; + + this._id = this._parent.id() + '>' + this._range + this.getMatchString(); } public id(): string { @@ -47,7 +58,7 @@ export class Match { } public text(): string { - return this._lineText; + return this._previewText; } public range(): Range { @@ -55,11 +66,9 @@ export class Match { } public preview(): { before: string; inside: string; after: string; } { - let before = this._lineText.substring(0, this._range.startColumn - 1), + const before = this._previewText.substring(0, this._rangeInPreviewText.startColumn - 1), inside = this.getMatchString(), - after = this._lineText.substring(this._range.endColumn - 1, Math.min(this._range.endColumn + 150, this._lineText.length)); - - before = strings.lcut(before, 26); + after = this._previewText.substring(this._rangeInPreviewText.endColumn - 1); return { before, @@ -75,7 +84,7 @@ export class Match { // If match string is not matching then regex pattern has a lookahead expression if (replaceString === null) { - replaceString = searchModel.replacePattern.getReplaceString(matchString + this._lineText.substring(this._range.endColumn - 1)); + replaceString = searchModel.replacePattern.getReplaceString(matchString + this._previewText.substring(this._rangeInPreviewText.endColumn - 1)); } // Match string is still not matching. Could be unsupported matches (multi-line). @@ -87,7 +96,7 @@ export class Match { } public getMatchString(): string { - return this._lineText.substring(this._range.startColumn - 1, this._range.endColumn - 1); + return this._previewText.substring(this._rangeInPreviewText.startColumn - 1, this._rangeInPreviewText.endColumn - 1); } } @@ -134,7 +143,7 @@ export class FileMatch extends Disposable { private _updateScheduler: RunOnceScheduler; private _modelDecorations: string[] = []; - constructor(private _query: IPatternInfo, private _maxResults: number, private _parent: FolderMatch, private rawMatch: IFileMatch, + constructor(private _query: IPatternInfo, private _previewOptions: ITextSearchPreviewOptions, private _maxResults: number, private _parent: FolderMatch, private rawMatch: IFileMatch, @IModelService private modelService: IModelService, @IReplaceService private replaceService: IReplaceService) { super(); this._resource = this.rawMatch.resource; @@ -152,11 +161,9 @@ export class FileMatch extends Disposable { this.bindModel(model); this.updateMatchesForModel(); } else { - this.rawMatch.lineMatches.forEach((rawLineMatch) => { - rawLineMatch.offsetAndLengths.forEach(offsetAndLength => { - let match = new Match(this, rawLineMatch.preview, rawLineMatch.lineNumber, offsetAndLength[0], offsetAndLength[1]); - this.add(match); - }); + this.rawMatch.matches.forEach((rawLineMatch) => { + let match = new Match(this, rawLineMatch); + this.add(match); }); } } @@ -222,7 +229,12 @@ export class FileMatch extends Disposable { private updateMatches(matches: FindMatch[], modelChange: boolean) { matches.forEach(m => { - let match = new Match(this, this._model.getLineContent(m.range.startLineNumber), m.range.startLineNumber - 1, m.range.startColumn - 1, m.range.endColumn - m.range.startColumn); + const textSearchResult = new TextSearchResult( + this._model.getLineContent(m.range.startLineNumber), + new Range(m.range.startLineNumber - 1, m.range.startColumn - 1, m.range.startLineNumber - 1, m.range.endColumn), + this._previewOptions); + const match = new Match(this, textSearchResult); + if (!this._removedMatches.has(match.id())) { this.add(match); if (this.isMatchSelected(match)) { @@ -392,16 +404,16 @@ export class FolderMatch extends Disposable { } public add(raw: IFileMatch[], silent: boolean): void { - let changed: FileMatch[] = []; + const changed: FileMatch[] = []; raw.forEach((rawFileMatch) => { if (this._fileMatches.has(rawFileMatch.resource)) { this._fileMatches.get(rawFileMatch.resource).dispose(); } - let fileMatch = this.instantiationService.createInstance(FileMatch, this._query.contentPattern, this._query.maxResults, this, rawFileMatch); + const fileMatch = this.instantiationService.createInstance(FileMatch, this._query.contentPattern, this._query.previewOptions, this._query.maxResults, this, rawFileMatch); this.doAdd(fileMatch); changed.push(fileMatch); - let disposable = fileMatch.onChange(() => this.onFileChange(fileMatch)); + const disposable = fileMatch.onChange(() => this.onFileChange(fileMatch)); fileMatch.onDispose(() => disposable.dispose()); }); if (!silent && changed.length) { diff --git a/src/vs/workbench/parts/search/test/browser/searchActions.test.ts b/src/vs/workbench/parts/search/test/browser/searchActions.test.ts index 47f5db6be49..8a46a0b3801 100644 --- a/src/vs/workbench/parts/search/test/browser/searchActions.test.ts +++ b/src/vs/workbench/parts/search/test/browser/searchActions.test.ts @@ -129,13 +129,26 @@ suite('Search Actions', () => { function aFileMatch(): FileMatch { let rawMatch: IFileMatch = { resource: URI.file('somepath' + ++counter), - lineMatches: [] + matches: [] }; - return instantiationService.createInstance(FileMatch, null, null, null, rawMatch); + return instantiationService.createInstance(FileMatch, null, null, null, null, rawMatch); } function aMatch(fileMatch: FileMatch): Match { - let match = new Match(fileMatch, 'some match', ++counter, 0, 2); + const line = ++counter; + const range = { + startLineNumber: line, + startColumn: 0, + endLineNumber: line, + endColumn: 2 + }; + let match = new Match(fileMatch, { + preview: { + text: 'some match', + match: range + }, + range + }); fileMatch.add(match); return match; } diff --git a/src/vs/workbench/parts/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/parts/search/test/browser/searchViewlet.test.ts index 2c273d2744a..45eb45be630 100644 --- a/src/vs/workbench/parts/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/parts/search/test/browser/searchViewlet.test.ts @@ -9,7 +9,7 @@ import uri from 'vs/base/common/uri'; import { Match, FileMatch, SearchResult } from 'vs/workbench/parts/search/common/searchModel'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { SearchDataSource, SearchSorter } from 'vs/workbench/parts/search/browser/searchResultsView'; -import { IFileMatch, ILineMatch } from 'vs/platform/search/common/search'; +import { IFileMatch, TextSearchResult, OneLineRange, ITextSearchResult } from 'vs/platform/search/common/search'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; @@ -31,9 +31,22 @@ suite('Search - Viewlet', () => { let ds = instantiation.createInstance(SearchDataSource); let result: SearchResult = instantiation.createInstance(SearchResult, null); result.query = { type: 1, folderQueries: [{ folder: uri.parse('file://c:/') }] }; + + const range = { + startLineNumber: 1, + startColumn: 0, + endLineNumber: 1, + endColumn: 1 + }; result.add([{ resource: uri.parse('file:///c:/foo'), - lineMatches: [{ lineNumber: 1, preview: 'bar', offsetAndLengths: [[0, 1]] }] + matches: [{ + preview: { + text: 'bar', + match: range + }, + range + }] }]); let fileMatch = result.matches()[0]; @@ -41,7 +54,7 @@ suite('Search - Viewlet', () => { assert.equal(ds.getId(null, result), 'root'); assert.equal(ds.getId(null, fileMatch), 'file:///c%3A/foo'); - assert.equal(ds.getId(null, lineMatch), 'file:///c%3A/foo>1>0b'); + assert.equal(ds.getId(null, lineMatch), 'file:///c%3A/foo>[2,1 -> 2,2]b'); assert(!ds.hasChildren(null, 'foo')); assert(ds.hasChildren(null, result)); @@ -53,9 +66,9 @@ suite('Search - Viewlet', () => { let fileMatch1 = aFileMatch('C:\\foo'); let fileMatch2 = aFileMatch('C:\\with\\path'); let fileMatch3 = aFileMatch('C:\\with\\path\\foo'); - let lineMatch1 = new Match(fileMatch1, 'bar', 1, 1, 1); - let lineMatch2 = new Match(fileMatch1, 'bar', 2, 1, 1); - let lineMatch3 = new Match(fileMatch1, 'bar', 2, 1, 1); + let lineMatch1 = new Match(fileMatch1, new TextSearchResult('bar', new OneLineRange(0, 1, 1))); + let lineMatch2 = new Match(fileMatch1, new TextSearchResult('bar', new OneLineRange(2, 1, 1))); + let lineMatch3 = new Match(fileMatch1, new TextSearchResult('bar', new OneLineRange(2, 1, 1))); let s = new SearchSorter(); @@ -69,12 +82,12 @@ suite('Search - Viewlet', () => { assert(s.compare(null, lineMatch2, lineMatch3) === 0); }); - function aFileMatch(path: string, searchResult?: SearchResult, ...lineMatches: ILineMatch[]): FileMatch { + function aFileMatch(path: string, searchResult?: SearchResult, ...lineMatches: ITextSearchResult[]): FileMatch { let rawMatch: IFileMatch = { resource: uri.file('C:\\' + path), - lineMatches: lineMatches + matches: lineMatches }; - return instantiation.createInstance(FileMatch, null, null, searchResult, rawMatch); + return instantiation.createInstance(FileMatch, null, null, null, searchResult, rawMatch); } function stubModelService(instantiationService: TestInstantiationService): IModelService { diff --git a/src/vs/workbench/parts/search/test/common/searchModel.test.ts b/src/vs/workbench/parts/search/test/common/searchModel.test.ts index d505add8c44..56125398146 100644 --- a/src/vs/workbench/parts/search/test/common/searchModel.test.ts +++ b/src/vs/workbench/parts/search/test/common/searchModel.test.ts @@ -16,7 +16,7 @@ import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { IFileMatch, IFileSearchStats, IFolderQuery, ILineMatch, ISearchComplete, ISearchProgressItem, ISearchQuery, ISearchService } from 'vs/platform/search/common/search'; +import { IFileMatch, IFileSearchStats, IFolderQuery, ISearchComplete, ISearchProgressItem, ISearchQuery, ISearchService, ITextSearchResult, TextSearchResult, OneLineRange } from 'vs/platform/search/common/search'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { SearchModel } from 'vs/workbench/parts/search/common/searchModel'; @@ -41,6 +41,7 @@ const nullEvent = new class { } }; +const lineOneRange = new OneLineRange(1, 0, 1); suite('SearchModel', () => { @@ -104,7 +105,11 @@ suite('SearchModel', () => { } test('Search Model: Search adds to results', async () => { - let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))]; + let results = [ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', new OneLineRange(1, 1, 4)), + new TextSearchResult('preview 1', new OneLineRange(1, 4, 11))), + aRawMatch('file://c:/2', new TextSearchResult('preview 2', lineOneRange))]; instantiationService.stub(ISearchService, searchServiceWithResults(results)); let testObject: SearchModel = instantiationService.createInstance(SearchModel); @@ -130,7 +135,12 @@ suite('SearchModel', () => { test('Search Model: Search reports telemetry on search completed', async () => { let target = instantiationService.spy(ITelemetryService, 'publicLog'); - let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))]; + let results = [ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', new OneLineRange(1, 1, 4)), + new TextSearchResult('preview 1', new OneLineRange(1, 4, 11))), + aRawMatch('file://c:/2', + new TextSearchResult('preview 2', lineOneRange))]; instantiationService.stub(ISearchService, searchServiceWithResults(results)); let testObject: SearchModel = instantiationService.createInstance(SearchModel); @@ -168,7 +178,7 @@ suite('SearchModel', () => { instantiationService.stub(ITelemetryService, 'publicLog', target1); instantiationService.stub(ISearchService, searchServiceWithResults( - [aRawMatch('file://c:/1', aLineMatch('some preview'))], + [aRawMatch('file://c:/1', new TextSearchResult('some preview', lineOneRange))], { results: [], stats: testSearchStats })); let testObject = instantiationService.createInstance(SearchModel); @@ -229,7 +239,12 @@ suite('SearchModel', () => { }); test('Search Model: Search results are cleared during search', async () => { - let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))]; + let results = [ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', new OneLineRange(1, 1, 4)), + new TextSearchResult('preview 1', new OneLineRange(1, 4, 11))), + aRawMatch('file://c:/2', + new TextSearchResult('preview 2', lineOneRange))]; instantiationService.stub(ISearchService, searchServiceWithResults(results)); let testObject: SearchModel = instantiationService.createInstance(SearchModel); await testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); @@ -254,7 +269,10 @@ suite('SearchModel', () => { }); test('getReplaceString returns proper replace string for regExpressions', async () => { - let results = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]]))]; + let results = [ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', new OneLineRange(1, 1, 4)), + new TextSearchResult('preview 1', new OneLineRange(1, 4, 11)))]; instantiationService.stub(ISearchService, searchServiceWithResults(results)); let testObject: SearchModel = instantiationService.createInstance(SearchModel); @@ -281,12 +299,8 @@ suite('SearchModel', () => { assert.equal('helloe', match.replaceString); }); - function aRawMatch(resource: string, ...lineMatches: ILineMatch[]): IFileMatch { - return { resource: URI.parse(resource), lineMatches }; - } - - function aLineMatch(preview: string, lineNumber: number = 1, offsetAndLengths: number[][] = [[0, 1]]): ILineMatch { - return { preview, lineNumber, offsetAndLengths }; + function aRawMatch(resource: string, ...matches: ITextSearchResult[]): IFileMatch { + return { resource: URI.parse(resource), matches }; } function stub(arg1: any, arg2: any, arg3: any): sinon.SinonStub { diff --git a/src/vs/workbench/parts/search/test/common/searchResult.test.ts b/src/vs/workbench/parts/search/test/common/searchResult.test.ts index e9f486bc193..2d324073590 100644 --- a/src/vs/workbench/parts/search/test/common/searchResult.test.ts +++ b/src/vs/workbench/parts/search/test/common/searchResult.test.ts @@ -9,7 +9,7 @@ import * as sinon from 'sinon'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { Match, FileMatch, SearchResult, SearchModel } from 'vs/workbench/parts/search/common/searchModel'; import URI from 'vs/base/common/uri'; -import { IFileMatch, ILineMatch } from 'vs/platform/search/common/search'; +import { IFileMatch, TextSearchResult, OneLineRange, ITextSearchResult } from 'vs/platform/search/common/search'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { Range } from 'vs/editor/common/core/range'; @@ -19,6 +19,8 @@ import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IReplaceService } from 'vs/workbench/parts/search/common/replace'; +const lineOneRange = new OneLineRange(1, 0, 1); + suite('SearchResult', () => { let instantiationService: TestInstantiationService; @@ -33,21 +35,17 @@ suite('SearchResult', () => { test('Line Match', function () { let fileMatch = aFileMatch('folder/file.txt', null); - let lineMatch = new Match(fileMatch, 'foo bar', 1, 0, 3); + let lineMatch = new Match(fileMatch, new TextSearchResult('foo bar', new OneLineRange(1, 0, 3))); assert.equal(lineMatch.text(), 'foo bar'); assert.equal(lineMatch.range().startLineNumber, 2); assert.equal(lineMatch.range().endLineNumber, 2); assert.equal(lineMatch.range().startColumn, 1); assert.equal(lineMatch.range().endColumn, 4); - assert.equal('file:///folder/file.txt>1>0foo', lineMatch.id()); + assert.equal('file:///folder/file.txt>[2,1 -> 2,4]foo', lineMatch.id()); }); test('Line Match - Remove', function () { - let fileMatch = aFileMatch('folder/file.txt', aSearchResult(), ...[{ - preview: 'foo bar', - lineNumber: 1, - offsetAndLengths: [[0, 3]] - }]); + let fileMatch = aFileMatch('folder/file.txt', aSearchResult(), new TextSearchResult('foo bar', new OneLineRange(1, 0, 3))); let lineMatch = fileMatch.matches()[0]; fileMatch.remove(lineMatch); assert.equal(fileMatch.matches().length, 0); @@ -66,15 +64,11 @@ suite('SearchResult', () => { }); test('File Match: Select an existing match', function () { - let testObject = aFileMatch('folder/file.txt', aSearchResult(), ...[{ - preview: 'foo', - lineNumber: 1, - offsetAndLengths: [[0, 3]] - }, { - preview: 'bar', - lineNumber: 1, - offsetAndLengths: [[5, 3]] - }]); + let testObject = aFileMatch( + 'folder/file.txt', + aSearchResult(), + new TextSearchResult('foo', new OneLineRange(1, 0, 3)), + new TextSearchResult('bar', new OneLineRange(1, 5, 3))); testObject.setSelectedMatch(testObject.matches()[0]); @@ -82,15 +76,11 @@ suite('SearchResult', () => { }); test('File Match: Select non existing match', function () { - let testObject = aFileMatch('folder/file.txt', aSearchResult(), ...[{ - preview: 'foo', - lineNumber: 1, - offsetAndLengths: [[0, 3]] - }, { - preview: 'bar', - lineNumber: 1, - offsetAndLengths: [[5, 3]] - }]); + let testObject = aFileMatch( + 'folder/file.txt', + aSearchResult(), + new TextSearchResult('foo', new OneLineRange(1, 0, 3)), + new TextSearchResult('bar', new OneLineRange(1, 5, 3))); let target = testObject.matches()[0]; testObject.remove(target); @@ -100,15 +90,11 @@ suite('SearchResult', () => { }); test('File Match: isSelected return true for selected match', function () { - let testObject = aFileMatch('folder/file.txt', aSearchResult(), ...[{ - preview: 'foo', - lineNumber: 1, - offsetAndLengths: [[0, 3]] - }, { - preview: 'bar', - lineNumber: 1, - offsetAndLengths: [[5, 3]] - }]); + let testObject = aFileMatch( + 'folder/file.txt', + aSearchResult(), + new TextSearchResult('foo', new OneLineRange(1, 0, 3)), + new TextSearchResult('bar', new OneLineRange(1, 5, 3))); let target = testObject.matches()[0]; testObject.setSelectedMatch(target); @@ -116,32 +102,20 @@ suite('SearchResult', () => { }); test('File Match: isSelected return false for un-selected match', function () { - let testObject = aFileMatch('folder/file.txt', aSearchResult(), ...[{ - preview: 'foo', - lineNumber: 1, - offsetAndLengths: [[0, 3]] - }, { - preview: 'bar', - lineNumber: 1, - offsetAndLengths: [[5, 3]] - }]); - + let testObject = aFileMatch('folder/file.txt', + aSearchResult(), + new TextSearchResult('foo', new OneLineRange(1, 0, 3)), + new TextSearchResult('bar', new OneLineRange(1, 5, 3))); testObject.setSelectedMatch(testObject.matches()[0]); - assert.ok(!testObject.isMatchSelected(testObject.matches()[1])); }); test('File Match: unselect', function () { - let testObject = aFileMatch('folder/file.txt', aSearchResult(), ...[{ - preview: 'foo', - lineNumber: 1, - offsetAndLengths: [[0, 3]] - }, { - preview: 'bar', - lineNumber: 1, - offsetAndLengths: [[5, 3]] - }]); - + let testObject = aFileMatch( + 'folder/file.txt', + aSearchResult(), + new TextSearchResult('foo', new OneLineRange(1, 0, 3)), + new TextSearchResult('bar', new OneLineRange(1, 5, 3))); testObject.setSelectedMatch(testObject.matches()[0]); testObject.setSelectedMatch(null); @@ -149,16 +123,11 @@ suite('SearchResult', () => { }); test('File Match: unselect when not selected', function () { - let testObject = aFileMatch('folder/file.txt', aSearchResult(), ...[{ - preview: 'foo', - lineNumber: 1, - offsetAndLengths: [[0, 3]] - }, { - preview: 'bar', - lineNumber: 1, - offsetAndLengths: [[5, 3]] - }]); - + let testObject = aFileMatch( + 'folder/file.txt', + aSearchResult(), + new TextSearchResult('foo', new OneLineRange(1, 0, 3)), + new TextSearchResult('bar', new OneLineRange(1, 5, 3))); testObject.setSelectedMatch(null); assert.equal(null, testObject.getSelectedMatch()); @@ -167,7 +136,7 @@ suite('SearchResult', () => { test('Alle Drei Zusammen', function () { let searchResult = instantiationService.createInstance(SearchResult, null); let fileMatch = aFileMatch('far/boo', searchResult); - let lineMatch = new Match(fileMatch, 'foo bar', 1, 0, 3); + let lineMatch = new Match(fileMatch, new TextSearchResult('foo bar', new OneLineRange(1, 0, 3))); assert(lineMatch.parent() === fileMatch); assert(fileMatch.parent() === searchResult); @@ -175,7 +144,10 @@ suite('SearchResult', () => { test('Adding a raw match will add a file match with line matches', function () { let testObject = aSearchResult(); - let target = [aRawMatch('file://c:/', aLineMatch('preview 1', 1, [[1, 3], [4, 7]]), aLineMatch('preview 2'))]; + let target = [aRawMatch('file://c:/', + new TextSearchResult('preview 1', new OneLineRange(1, 1, 4)), + new TextSearchResult('preview 1', new OneLineRange(1, 4, 11)), + new TextSearchResult('preview 2', lineOneRange))]; testObject.add(target); @@ -200,7 +172,12 @@ suite('SearchResult', () => { test('Adding multiple raw matches', function () { let testObject = aSearchResult(); - let target = [aRawMatch('file://c:/1', aLineMatch('preview 1', 1, [[1, 3], [4, 7]])), aRawMatch('file://c:/2', aLineMatch('preview 2'))]; + let target = [ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', new OneLineRange(1, 1, 4)), + new TextSearchResult('preview 1', new OneLineRange(1, 4, 11))), + aRawMatch('file://c:/2', + new TextSearchResult('preview 2', lineOneRange))]; testObject.add(target); @@ -228,7 +205,11 @@ suite('SearchResult', () => { let target2 = sinon.spy(); let testObject = aSearchResult(); - testObject.add([aRawMatch('file://c:/1', aLineMatch('preview 1')), aRawMatch('file://c:/2', aLineMatch('preview 2'))]); + testObject.add([ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', lineOneRange)), + aRawMatch('file://c:/2', + new TextSearchResult('preview 2', lineOneRange))]); testObject.matches()[0].onDispose(target1); testObject.matches()[1].onDispose(target2); @@ -243,7 +224,9 @@ suite('SearchResult', () => { test('remove triggers change event', function () { let target = sinon.spy(); let testObject = aSearchResult(); - testObject.add([aRawMatch('file://c:/1', aLineMatch('preview 1'))]); + testObject.add([ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', lineOneRange))]); let objectRoRemove = testObject.matches()[0]; testObject.onChange(target); @@ -256,7 +239,9 @@ suite('SearchResult', () => { test('remove triggers change event', function () { let target = sinon.spy(); let testObject = aSearchResult(); - testObject.add([aRawMatch('file://c:/1', aLineMatch('preview 1'))]); + testObject.add([ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', lineOneRange))]); let objectRoRemove = testObject.matches()[0]; testObject.onChange(target); @@ -268,7 +253,9 @@ suite('SearchResult', () => { test('Removing all line matches and adding back will add file back to result', function () { let testObject = aSearchResult(); - testObject.add([aRawMatch('file://c:/1', aLineMatch('preview 1'))]); + testObject.add([ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', lineOneRange))]); let target = testObject.matches()[0]; let matchToRemove = target.matches()[0]; target.remove(matchToRemove); @@ -283,7 +270,9 @@ suite('SearchResult', () => { test('replace should remove the file match', function () { instantiationService.stubPromise(IReplaceService, 'replace', null); let testObject = aSearchResult(); - testObject.add([aRawMatch('file://c:/1', aLineMatch('preview 1'))]); + testObject.add([ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', lineOneRange))]); testObject.replace(testObject.matches()[0]); @@ -294,7 +283,9 @@ suite('SearchResult', () => { let target = sinon.spy(); instantiationService.stubPromise(IReplaceService, 'replace', null); let testObject = aSearchResult(); - testObject.add([aRawMatch('file://c:/1', aLineMatch('preview 1'))]); + testObject.add([ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', lineOneRange))]); testObject.onChange(target); let objectRoRemove = testObject.matches()[0]; @@ -307,7 +298,11 @@ suite('SearchResult', () => { test('replaceAll should remove all file matches', function () { instantiationService.stubPromise(IReplaceService, 'replace', null); let testObject = aSearchResult(); - testObject.add([aRawMatch('file://c:/1', aLineMatch('preview 1')), aRawMatch('file://c:/2', aLineMatch('preview 2'))]); + testObject.add([ + aRawMatch('file://c:/1', + new TextSearchResult('preview 1', lineOneRange)), + aRawMatch('file://c:/2', + new TextSearchResult('preview 2', lineOneRange))]); testObject.replaceAll(null); @@ -358,12 +353,12 @@ suite('SearchResult', () => { // lineHasNoDecoration(oneModel, 2); //}); - function aFileMatch(path: string, searchResult?: SearchResult, ...lineMatches: ILineMatch[]): FileMatch { + function aFileMatch(path: string, searchResult?: SearchResult, ...lineMatches: ITextSearchResult[]): FileMatch { let rawMatch: IFileMatch = { resource: URI.file('/' + path), - lineMatches: lineMatches + matches: lineMatches }; - return instantiationService.createInstance(FileMatch, null, null, searchResult, rawMatch); + return instantiationService.createInstance(FileMatch, null, null, null, searchResult, rawMatch); } function aSearchResult(): SearchResult { @@ -372,12 +367,8 @@ suite('SearchResult', () => { return searchModel.searchResult; } - function aRawMatch(resource: string, ...lineMatches: ILineMatch[]): IFileMatch { - return { resource: URI.parse(resource), lineMatches }; - } - - function aLineMatch(preview: string, lineNumber: number = 1, offsetAndLengths: number[][] = [[0, 1]]): ILineMatch { - return { preview, lineNumber, offsetAndLengths }; + function aRawMatch(resource: string, ...matches: ITextSearchResult[]): IFileMatch { + return { resource: URI.parse(resource), matches }; } function stubModelService(instantiationService: TestInstantiationService): IModelService { diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearch.ts b/src/vs/workbench/services/search/node/ripgrepTextSearch.ts index 5acd1f01b5c..94751dc40b8 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearch.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearch.ts @@ -16,9 +16,10 @@ import * as strings from 'vs/base/common/strings'; import { TPromise } from 'vs/base/common/winjs.base'; import * as encoding from 'vs/base/node/encoding'; import * as extfs from 'vs/base/node/extfs'; -import { IProgress, ITextSearchStats } from 'vs/platform/search/common/search'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { IProgress, ITextSearchPreviewOptions, ITextSearchStats, TextSearchResult } from 'vs/platform/search/common/search'; import { rgPath } from 'vscode-ripgrep'; -import { FileMatch, IFolderSearch, IRawSearch, ISerializedFileMatch, LineMatch, ISerializedSearchSuccess } from './search'; +import { FileMatch, IFolderSearch, IRawSearch, ISerializedFileMatch, ISerializedSearchSuccess } from './search'; // If vscode-ripgrep is in an .asar file, then the binary is unpacked. const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked'); @@ -77,7 +78,7 @@ export class RipgrepEngine { this.rgProc = cp.spawn(rgDiskPath, rgArgs.args, { cwd }); process.once('exit', this.killRgProcFn); - this.ripgrepParser = new RipgrepParser(this.config.maxResults, cwd, this.config.extraFiles); + this.ripgrepParser = new RipgrepParser(this.config.maxResults, cwd, this.config.extraFiles, this.config.previewOptions); this.ripgrepParser.on('result', (match: ISerializedFileMatch) => { if (this.postProcessExclusions) { const handleResultP = (>this.postProcessExclusions(match.path, undefined, glob.hasSiblingPromiseFn(() => getSiblings(match.path)))) @@ -197,7 +198,7 @@ export class RipgrepParser extends EventEmitter { private numResults = 0; - constructor(private maxResults: number, private rootFolder: string, extraFiles?: string[]) { + constructor(private maxResults: number, private rootFolder: string, extraFiles?: string[], private previewOptions?: ITextSearchPreviewOptions) { super(); this.stringDecoder = new StringDecoder(); @@ -275,7 +276,6 @@ export class RipgrepParser extends EventEmitter { text = strings.stripUTF8BOM(text); } - const lineMatch = new LineMatch(text, lineNum); if (!this.fileMatch) { // When searching a single file and no folderQueries, rg does not print the file line, so create it here const singleFile = this.extraSearchFiles[0]; @@ -286,8 +286,6 @@ export class RipgrepParser extends EventEmitter { this.fileMatch = this.getFileMatch(singleFile); } - this.fileMatch.addMatch(lineMatch); - let lastMatchEndPos = 0; let matchTextStartPos = -1; @@ -296,6 +294,7 @@ export class RipgrepParser extends EventEmitter { let textRealIdx = 0; let hitLimit = false; + const matchRanges: IRange[] = []; const realTextParts: string[] = []; for (let i = 0; i < text.length - (RipgrepParser.MATCH_END_MARKER.length - 1);) { @@ -311,7 +310,7 @@ export class RipgrepParser extends EventEmitter { const chunk = text.slice(matchTextStartPos, i); realTextParts.push(chunk); if (!hitLimit) { - lineMatch.addMatch(matchTextStartRealIdx, textRealIdx - matchTextStartRealIdx); + matchRanges.push(new Range(lineNum, matchTextStartRealIdx, lineNum, textRealIdx)); } matchTextStartPos = -1; @@ -336,7 +335,9 @@ export class RipgrepParser extends EventEmitter { // Replace preview with version without color codes const preview = realTextParts.join(''); - lineMatch.preview = preview; + matchRanges + .map(r => new TextSearchResult(preview, r, this.previewOptions)) + .forEach(m => this.fileMatch.addMatch(m)); if (hitLimit) { this.cancel(); diff --git a/src/vs/workbench/services/search/node/search.ts b/src/vs/workbench/services/search/node/search.ts index 5bf3d2059e2..e3f65aa8fbe 100644 --- a/src/vs/workbench/services/search/node/search.ts +++ b/src/vs/workbench/services/search/node/search.ts @@ -5,11 +5,11 @@ 'use strict'; -import { TPromise } from 'vs/base/common/winjs.base'; -import { IExpression } from 'vs/base/common/glob'; -import { IProgress, ILineMatch, IPatternInfo, IFileSearchStats, ISearchEngineStats, ITextSearchStats } from 'vs/platform/search/common/search'; -import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; import { Event } from 'vs/base/common/event'; +import { IExpression } from 'vs/base/common/glob'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IFileSearchStats, IPatternInfo, IProgress, ISearchEngineStats, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats } from 'vs/platform/search/common/search'; +import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; export interface IFolderSearch { folder: string; @@ -34,6 +34,7 @@ export interface IRawSearch { maxFilesize?: number; useRipgrep?: boolean; disregardIgnoreFiles?: boolean; + previewOptions?: ITextSearchPreviewOptions; } export interface ITelemetryEvent { @@ -96,7 +97,7 @@ export function isSerializedSearchSuccess(arg: ISerializedSearchComplete): arg i export interface ISerializedFileMatch { path: string; - lineMatches?: ILineMatch[]; + matches?: ITextSearchResult[]; numMatches?: number; } @@ -107,56 +108,22 @@ export type IFileSearchProgressItem = IRawFileMatch | IRawFileMatch[] | IProgres export class FileMatch implements ISerializedFileMatch { path: string; - lineMatches: LineMatch[]; + matches: ITextSearchResult[]; constructor(path: string) { this.path = path; - this.lineMatches = []; + this.matches = []; } - addMatch(lineMatch: LineMatch): void { - this.lineMatches.push(lineMatch); + addMatch(match: ITextSearchResult): void { + this.matches.push(match); } serialize(): ISerializedFileMatch { - let lineMatches: ILineMatch[] = []; - let numMatches = 0; - - for (let i = 0; i < this.lineMatches.length; i++) { - numMatches += this.lineMatches[i].offsetAndLengths.length; - lineMatches.push(this.lineMatches[i].serialize()); - } - return { path: this.path, - lineMatches, - numMatches + matches: this.matches, + numMatches: this.matches.length }; } } - -export class LineMatch implements ILineMatch { - preview: string; - lineNumber: number; - offsetAndLengths: number[][]; - - constructor(preview: string, lineNumber: number) { - this.preview = preview.replace(/(\r|\n)*$/, ''); - this.lineNumber = lineNumber; - this.offsetAndLengths = []; - } - - addMatch(offset: number, length: number): void { - this.offsetAndLengths.push([offset, length]); - } - - serialize(): ILineMatch { - const result = { - preview: this.preview, - lineNumber: this.lineNumber, - offsetAndLengths: this.offsetAndLengths - }; - - return result; - } -} \ No newline at end of file diff --git a/src/vs/workbench/services/search/node/searchService.ts b/src/vs/workbench/services/search/node/searchService.ts index 1d2b87147d9..b27c2476c58 100644 --- a/src/vs/workbench/services/search/node/searchService.ts +++ b/src/vs/workbench/services/search/node/searchService.ts @@ -22,13 +22,14 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IDebugParams, IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; -import { FileMatch, ICachedSearchStats, IFileMatch, IFolderQuery, IProgress, ISearchComplete, ISearchConfiguration, ISearchEngineStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, LineMatch, pathIncludedInQuery, QueryType, SearchProviderType, IFileSearchStats } from 'vs/platform/search/common/search'; +import { FileMatch, ICachedSearchStats, IFileMatch, IFolderQuery, IProgress, ISearchComplete, ISearchConfiguration, ISearchEngineStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, pathIncludedInQuery, QueryType, SearchProviderType, IFileSearchStats, TextSearchResult } from 'vs/platform/search/common/search'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IRawSearch, IRawSearchService, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, isSerializedSearchComplete, isSerializedSearchSuccess } from './search'; import { ISearchChannel, SearchChannelClient } from './searchIpc'; +import { Range } from 'vs/editor/common/core/range'; export class SearchService extends Disposable implements ISearchService { public _serviceBrand: any; @@ -346,7 +347,10 @@ export class SearchService extends Disposable implements ISearchService { localResults.set(resource, fileMatch); matches.forEach((match) => { - fileMatch.lineMatches.push(new LineMatch(model.getLineContent(match.range.startLineNumber), match.range.startLineNumber - 1, [[match.range.startColumn - 1, match.range.endColumn - match.range.startColumn]])); + fileMatch.matches.push(new TextSearchResult( + model.getLineContent(match.range.startLineNumber), + new Range(match.range.startLineNumber - 1, match.range.startColumn - 1, match.range.startLineNumber - 1, match.range.endColumn), + query.previewOptions)); }); } else { localResults.set(resource, null); @@ -458,7 +462,8 @@ export class DiskSearch implements ISearchResultProvider { cacheKey: query.cacheKey, useRipgrep: query.useRipgrep, disregardIgnoreFiles: query.disregardIgnoreFiles, - ignoreSymlinks: query.ignoreSymlinks + ignoreSymlinks: query.ignoreSymlinks, + previewOptions: query.previewOptions }; for (const q of existingFolders) { @@ -536,10 +541,8 @@ export class DiskSearch implements ISearchResultProvider { private static createFileMatch(data: ISerializedFileMatch): FileMatch { let fileMatch = new FileMatch(uri.file(data.path)); - if (data.lineMatches) { - for (let j = 0; j < data.lineMatches.length; j++) { - fileMatch.lineMatches.push(new LineMatch(data.lineMatches[j].preview, data.lineMatches[j].lineNumber, data.lineMatches[j].offsetAndLengths)); - } + if (data.matches) { + fileMatch.matches.push(...data.matches); // TODO why } return fileMatch; } diff --git a/src/vs/workbench/services/search/node/textSearch.ts b/src/vs/workbench/services/search/node/textSearch.ts index 5c7cb143ea9..02b15dafa4a 100644 --- a/src/vs/workbench/services/search/node/textSearch.ts +++ b/src/vs/workbench/services/search/node/textSearch.ts @@ -11,7 +11,7 @@ import { IProgress } from 'vs/platform/search/common/search'; import { FileWalker } from 'vs/workbench/services/search/node/fileSearch'; import { IRawSearch, ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch } from './search'; import { ITextSearchWorkerProvider } from './textSearchWorkerProvider'; -import { ISearchWorker } from './worker/searchWorkerIpc'; +import { ISearchWorker, ISearchWorkerSearchArgs } from './worker/searchWorkerIpc'; export class Engine implements ISearchEngine { @@ -95,7 +95,7 @@ export class Engine implements ISearchEngine { this.nextWorker = (this.nextWorker + 1) % this.workers.length; const maxResults = this.config.maxResults && (this.config.maxResults - this.numResults); - const searchArgs = { absolutePaths: batch, maxResults, pattern: this.config.contentPattern, fileEncoding }; + const searchArgs: ISearchWorkerSearchArgs = { absolutePaths: batch, maxResults, pattern: this.config.contentPattern, fileEncoding, previewOptions: this.config.previewOptions }; worker.search(searchArgs).then(result => { if (!result || this.limitReached || this.isCanceled) { return unwind(batchBytes); diff --git a/src/vs/workbench/services/search/node/worker/searchWorker.ts b/src/vs/workbench/services/search/node/worker/searchWorker.ts index d35999d4eda..b1e340ac07b 100644 --- a/src/vs/workbench/services/search/node/worker/searchWorker.ts +++ b/src/vs/workbench/services/search/node/worker/searchWorker.ts @@ -7,16 +7,17 @@ import * as fs from 'fs'; import * as gracefulFs from 'graceful-fs'; -gracefulFs.gracefulify(fs); - import { onUnexpectedError } from 'vs/base/common/errors'; import * as strings from 'vs/base/common/strings'; import { TPromise } from 'vs/base/common/winjs.base'; -import { LineMatch, FileMatch } from '../search'; -import { UTF16le, UTF16be, UTF8, UTF8_with_bom, encodingExists, decode, bomLength, detectEncodingFromBuffer } from 'vs/base/node/encoding'; - +import { bomLength, decode, detectEncodingFromBuffer, encodingExists, UTF16be, UTF16le, UTF8, UTF8_with_bom } from 'vs/base/node/encoding'; +import { Range } from 'vs/editor/common/core/range'; +import { ITextSearchPreviewOptions, TextSearchResult } from 'vs/platform/search/common/search'; +import { FileMatch } from '../search'; import { ISearchWorker, ISearchWorkerSearchArgs, ISearchWorkerSearchResult } from './searchWorkerIpc'; +gracefulFs.gracefulify(fs); + interface ReadLinesOptions { bufferLength: number; encoding: string; @@ -95,7 +96,7 @@ export class SearchWorkerEngine { // Search in the given path, and when it's finished, search in the next path in absolutePaths const startSearchInFile = (absolutePath: string): TPromise => { - return this.searchInFile(absolutePath, contentPattern, fileEncoding, args.maxResults && (args.maxResults - result.numMatches)).then(fileResult => { + return this.searchInFile(absolutePath, contentPattern, fileEncoding, args.maxResults && (args.maxResults - result.numMatches), args.previewOptions).then(fileResult => { // Finish early if search is canceled if (this.isCanceled) { return; @@ -124,13 +125,12 @@ export class SearchWorkerEngine { this.isCanceled = true; } - private searchInFile(absolutePath: string, contentPattern: RegExp, fileEncoding: string, maxResults?: number): TPromise { + private searchInFile(absolutePath: string, contentPattern: RegExp, fileEncoding: string, maxResults?: number, previewOptions?: ITextSearchPreviewOptions): TPromise { let fileMatch: FileMatch = null; let limitReached = false; let numMatches = 0; const perLineCallback = (line: string, lineNumber: number) => { - let lineMatch: LineMatch = null; let match = contentPattern.exec(line); // Record all matches into file result @@ -139,12 +139,8 @@ export class SearchWorkerEngine { fileMatch = new FileMatch(absolutePath); } - if (lineMatch === null) { - lineMatch = new LineMatch(line, lineNumber); - fileMatch.addMatch(lineMatch); - } - - lineMatch.addMatch(match.index, match[0].length); + const lineMatch = new TextSearchResult(line, new Range(lineNumber, match.index, lineNumber, match.index + match[0].length), previewOptions); + fileMatch.addMatch(lineMatch); numMatches++; if (maxResults && numMatches >= maxResults) { diff --git a/src/vs/workbench/services/search/node/worker/searchWorkerIpc.ts b/src/vs/workbench/services/search/node/worker/searchWorkerIpc.ts index be137d2fc97..5fed1210eec 100644 --- a/src/vs/workbench/services/search/node/worker/searchWorkerIpc.ts +++ b/src/vs/workbench/services/search/node/worker/searchWorkerIpc.ts @@ -8,7 +8,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { IChannel } from 'vs/base/parts/ipc/node/ipc'; import { ISerializedFileMatch } from '../search'; -import { IPatternInfo } from 'vs/platform/search/common/search'; +import { IPatternInfo, ITextSearchPreviewOptions } from 'vs/platform/search/common/search'; import { SearchWorker } from './searchWorker'; import { Event } from 'vs/base/common/event'; @@ -17,6 +17,7 @@ export interface ISearchWorkerSearchArgs { fileEncoding: string; absolutePaths: string[]; maxResults?: number; + previewOptions?: ITextSearchPreviewOptions; } export interface ISearchWorkerSearchResult { diff --git a/src/vs/workbench/services/search/test/node/ripgrepTextSearch.test.ts b/src/vs/workbench/services/search/test/node/ripgrepTextSearch.test.ts index f55b1f172c7..8e2f789924c 100644 --- a/src/vs/workbench/services/search/test/node/ripgrepTextSearch.test.ts +++ b/src/vs/workbench/services/search/test/node/ripgrepTextSearch.test.ts @@ -5,16 +5,13 @@ 'use strict'; -import * as path from 'path'; import * as assert from 'assert'; - +import * as path from 'path'; import * as arrays from 'vs/base/common/arrays'; import * as platform from 'vs/base/common/platform'; - -import { RipgrepParser, getAbsoluteGlob, fixDriveC, fixRegexEndingPattern } from 'vs/workbench/services/search/node/ripgrepTextSearch'; +import { fixDriveC, fixRegexEndingPattern, getAbsoluteGlob, RipgrepParser } from 'vs/workbench/services/search/node/ripgrepTextSearch'; import { ISerializedFileMatch } from 'vs/workbench/services/search/node/search'; - suite('RipgrepParser', () => { const rootFolder = '/workspace'; const fileSectionEnd = '\n'; @@ -76,16 +73,40 @@ suite('RipgrepParser', () => { { numMatches: 2, path: path.join(rootFolder, 'a.txt'), - lineMatches: [ + matches: [ { - lineNumber: 0, - preview: 'beforematchafter', - offsetAndLengths: [[6, 5]] + preview: { + match: { + endColumn: 11, + endLineNumber: 0, + startColumn: 6, + startLineNumber: 0, + }, + text: 'beforematchafter' + }, + range: { + endColumn: 11, + endLineNumber: 0, + startColumn: 6, + startLineNumber: 0, + } }, { - lineNumber: 1, - preview: 'beforematchafter', - offsetAndLengths: [[6, 5]] + preview: { + match: { + endColumn: 11, + endLineNumber: 0, + startColumn: 6, + startLineNumber: 0, + }, + text: 'beforematchafter' + }, + range: { + endColumn: 11, + endLineNumber: 1, + startColumn: 6, + startLineNumber: 1, + } } ] }); @@ -168,8 +189,9 @@ suite('RipgrepParser', () => { const results = parseInput(inputBufs); assert.equal(results.length, 1); - assert.equal(results[0].lineMatches.length, 1); - assert.deepEqual(results[0].lineMatches[0].offsetAndLengths, [[7, 5]]); + assert.equal(results[0].matches.length, 1); + assert.equal(results[0].matches[0].range.startColumn, 7); + assert.equal(results[0].matches[0].range.endColumn, 12); } }); }); diff --git a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts index d1a70c00f48..67628e45629 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostSearch.test.ts @@ -650,14 +650,15 @@ suite('ExtHostSearch', () => { const actualTextSearchResults: vscode.TextSearchResult[] = []; for (let fileMatch of actual) { // Make relative - for (let lineMatch of fileMatch.lineMatches) { - for (let [offset, length] of lineMatch.offsetAndLengths) { - actualTextSearchResults.push({ - preview: { text: lineMatch.preview, match: null }, - range: new Range(lineMatch.lineNumber, offset, lineMatch.lineNumber, length + offset), - uri: fileMatch.resource - }); - } + for (let lineMatch of fileMatch.matches) { + actualTextSearchResults.push({ + preview: { + text: lineMatch.preview.text, + match: new Range(lineMatch.preview.match.startLineNumber, lineMatch.preview.match.startColumn, lineMatch.preview.match.endLineNumber, lineMatch.preview.match.endColumn) + }, + range: new Range(lineMatch.range.startLineNumber, lineMatch.range.startColumn, lineMatch.range.endLineNumber, lineMatch.range.endColumn), + uri: fileMatch.resource + }); } }