diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index fe252fca2d26e..52532c7bb16c1 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -107,7 +107,8 @@ import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/c import { UIKind } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier'; -import { ExcludeSettingOptions, TextSearchCompleteMessageType, TextSearchCompleteMessageTypeNew, TextSearchContextNew, TextSearchMatchNew, oldToNewTextSearchResult } from 'vs/workbench/services/search/common/searchExtTypes'; +import { oldToNewTextSearchResult } from 'vs/workbench/services/search/common/searchExtConversionTypes'; +import { ExcludeSettingOptions, TextSearchCompleteMessageType, TextSearchContextNew, TextSearchMatchNew } from 'vs/workbench/services/search/common/searchExtTypes'; import type * as vscode from 'vscode'; export interface IExtensionRegistries { @@ -1186,17 +1187,17 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, registerFileSearchProvider: (scheme: string, provider: vscode.FileSearchProvider) => { checkProposedApiEnabled(extension, 'fileSearchProvider'); - return extHostSearch.registerFileSearchProvider(scheme, provider); + return extHostSearch.registerFileSearchProviderOld(scheme, provider); }, registerTextSearchProvider: (scheme: string, provider: vscode.TextSearchProvider) => { checkProposedApiEnabled(extension, 'textSearchProvider'); - return extHostSearch.registerTextSearchProvider(scheme, provider); + return extHostSearch.registerTextSearchProviderOld(scheme, provider); }, registerAITextSearchProvider: (scheme: string, provider: vscode.AITextSearchProvider) => { // there are some dependencies on textSearchProvider, so we need to check for both checkProposedApiEnabled(extension, 'aiTextSearchProvider'); checkProposedApiEnabled(extension, 'textSearchProvider'); - return extHostSearch.registerAITextSearchProvider(scheme, provider); + return extHostSearch.registerAITextSearchProviderOld(scheme, provider); }, registerFileSearchProviderNew: (scheme: string, provider: vscode.FileSearchProviderNew) => { checkProposedApiEnabled(extension, 'fileSearchProviderNew'); @@ -1839,7 +1840,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ExcludeSettingOptions: ExcludeSettingOptions, TextSearchContextNew: TextSearchContextNew, TextSearchMatchNew: TextSearchMatchNew, - TextSearchCompleteMessageTypeNew: TextSearchCompleteMessageTypeNew, + TextSearchCompleteMessageTypeNew: TextSearchCompleteMessageType, }; }; } diff --git a/src/vs/workbench/api/common/extHostSearch.ts b/src/vs/workbench/api/common/extHostSearch.ts index 1d4a9f8d747ee..bf7f404973303 100644 --- a/src/vs/workbench/api/common/extHostSearch.ts +++ b/src/vs/workbench/api/common/extHostSearch.ts @@ -16,11 +16,15 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { TextSearchManager } from 'vs/workbench/services/search/common/textSearchManager'; import { CancellationToken } from 'vs/base/common/cancellation'; import { revive } from 'vs/base/common/marshalling'; +import { OldAITextSearchProviderConverter, OldFileSearchProviderConverter, OldTextSearchProviderConverter } from 'vs/workbench/services/search/common/searchExtConversionTypes'; export interface IExtHostSearch extends ExtHostSearchShape { - registerTextSearchProvider(scheme: string, provider: vscode.TextSearchProvider): IDisposable; - registerAITextSearchProvider(scheme: string, provider: vscode.AITextSearchProvider): IDisposable; - registerFileSearchProvider(scheme: string, provider: vscode.FileSearchProvider): IDisposable; + registerTextSearchProviderOld(scheme: string, provider: vscode.TextSearchProvider): IDisposable; + registerAITextSearchProviderOld(scheme: string, provider: vscode.AITextSearchProvider): IDisposable; + registerFileSearchProviderOld(scheme: string, provider: vscode.FileSearchProvider): IDisposable; + registerTextSearchProvider(scheme: string, provider: vscode.TextSearchProviderNew): IDisposable; + registerAITextSearchProvider(scheme: string, provider: vscode.AITextSearchProviderNew): IDisposable; + registerFileSearchProvider(scheme: string, provider: vscode.FileSearchProviderNew): IDisposable; doInternalFileSearchWithCustomCallback(query: IFileQuery, token: CancellationToken, handleFileMatch: (data: URI[]) => void): Promise; } @@ -31,13 +35,13 @@ export class ExtHostSearch implements IExtHostSearch { protected readonly _proxy: MainThreadSearchShape = this.extHostRpc.getProxy(MainContext.MainThreadSearch); protected _handlePool: number = 0; - private readonly _textSearchProvider = new Map(); + private readonly _textSearchProvider = new Map(); private readonly _textSearchUsedSchemes = new Set(); - private readonly _aiTextSearchProvider = new Map(); + private readonly _aiTextSearchProvider = new Map(); private readonly _aiTextSearchUsedSchemes = new Set(); - private readonly _fileSearchProvider = new Map(); + private readonly _fileSearchProvider = new Map(); private readonly _fileSearchUsedSchemes = new Set(); private readonly _fileSearchManager = new FileSearchManager(); @@ -45,14 +49,30 @@ export class ExtHostSearch implements IExtHostSearch { constructor( @IExtHostRpcService private extHostRpc: IExtHostRpcService, @IURITransformerService protected _uriTransformer: IURITransformerService, - @ILogService protected _logService: ILogService + @ILogService protected _logService: ILogService, ) { } protected _transformScheme(scheme: string): string { return this._uriTransformer.transformOutgoingScheme(scheme); } - registerTextSearchProvider(scheme: string, provider: vscode.TextSearchProvider): IDisposable { + registerTextSearchProviderOld(scheme: string, provider: vscode.TextSearchProvider): IDisposable { + if (this._textSearchUsedSchemes.has(scheme)) { + throw new Error(`a text search provider for the scheme '${scheme}' is already registered`); + } + + this._textSearchUsedSchemes.add(scheme); + const handle = this._handlePool++; + this._textSearchProvider.set(handle, new OldTextSearchProviderConverter(provider)); + this._proxy.$registerTextSearchProvider(handle, this._transformScheme(scheme)); + return toDisposable(() => { + this._textSearchUsedSchemes.delete(scheme); + this._textSearchProvider.delete(handle); + this._proxy.$unregisterProvider(handle); + }); + } + + registerTextSearchProvider(scheme: string, provider: vscode.TextSearchProviderNew): IDisposable { if (this._textSearchUsedSchemes.has(scheme)) { throw new Error(`a text search provider for the scheme '${scheme}' is already registered`); } @@ -68,7 +88,23 @@ export class ExtHostSearch implements IExtHostSearch { }); } - registerAITextSearchProvider(scheme: string, provider: vscode.AITextSearchProvider): IDisposable { + registerAITextSearchProviderOld(scheme: string, provider: vscode.AITextSearchProvider): IDisposable { + if (this._aiTextSearchUsedSchemes.has(scheme)) { + throw new Error(`an AI text search provider for the scheme '${scheme}'is already registered`); + } + + this._aiTextSearchUsedSchemes.add(scheme); + const handle = this._handlePool++; + this._aiTextSearchProvider.set(handle, new OldAITextSearchProviderConverter(provider)); + this._proxy.$registerAITextSearchProvider(handle, this._transformScheme(scheme)); + return toDisposable(() => { + this._aiTextSearchUsedSchemes.delete(scheme); + this._aiTextSearchProvider.delete(handle); + this._proxy.$unregisterProvider(handle); + }); + } + + registerAITextSearchProvider(scheme: string, provider: vscode.AITextSearchProviderNew): IDisposable { if (this._aiTextSearchUsedSchemes.has(scheme)) { throw new Error(`an AI text search provider for the scheme '${scheme}'is already registered`); } @@ -84,7 +120,23 @@ export class ExtHostSearch implements IExtHostSearch { }); } - registerFileSearchProvider(scheme: string, provider: vscode.FileSearchProvider): IDisposable { + registerFileSearchProviderOld(scheme: string, provider: vscode.FileSearchProvider): IDisposable { + if (this._fileSearchUsedSchemes.has(scheme)) { + throw new Error(`a file search provider for the scheme '${scheme}' is already registered`); + } + + this._fileSearchUsedSchemes.add(scheme); + const handle = this._handlePool++; + this._fileSearchProvider.set(handle, new OldFileSearchProviderConverter(provider)); + this._proxy.$registerFileSearchProvider(handle, this._transformScheme(scheme)); + return toDisposable(() => { + this._fileSearchUsedSchemes.delete(scheme); + this._fileSearchProvider.delete(handle); + this._proxy.$unregisterProvider(handle); + }); + } + + registerFileSearchProvider(scheme: string, provider: vscode.FileSearchProviderNew): IDisposable { if (this._fileSearchUsedSchemes.has(scheme)) { throw new Error(`a file search provider for the scheme '${scheme}' is already registered`); } @@ -108,7 +160,7 @@ export class ExtHostSearch implements IExtHostSearch { this._proxy.$handleFileMatch(handle, session, batch.map(p => p.resource)); }, token); } else { - throw new Error('3 unknown provider: ' + handle); + throw new Error('unknown provider: ' + handle); } } @@ -146,14 +198,14 @@ export class ExtHostSearch implements IExtHostSearch { $enableExtensionHostSearch(): void { } - protected createTextSearchManager(query: ITextQuery, provider: vscode.TextSearchProvider): TextSearchManager { + protected createTextSearchManager(query: ITextQuery, provider: vscode.TextSearchProviderNew): TextSearchManager { return new TextSearchManager({ query, provider }, { readdir: resource => Promise.resolve([]), toCanonicalName: encoding => encoding }, 'textSearchProvider'); } - protected createAITextSearchManager(query: IAITextQuery, provider: vscode.AITextSearchProvider): TextSearchManager { + protected createAITextSearchManager(query: IAITextQuery, provider: vscode.AITextSearchProviderNew): TextSearchManager { return new TextSearchManager({ query, provider }, { readdir: resource => Promise.resolve([]), toCanonicalName: encoding => encoding diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index 58dc565d6032f..7de20e1b155a6 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -547,7 +547,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac fileEncoding: options.encoding, maxResults: options.maxResults, previewOptions, - surroundingContext: options.afterContext, + surroundingContext: options.afterContext, // TODO: remove ability to have before/after context separately includePattern: includePattern, excludePattern: excludePattern @@ -567,14 +567,14 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac callback({ uri, preview: { - text: result.preview.text, + text: result.previewText, matches: mapArrayOrNot( - result.preview.matches, - m => new Range(m.startLineNumber, m.startColumn, m.endLineNumber, m.endColumn)) + result.rangeLocations, + m => new Range(m.preview.startLineNumber, m.preview.startColumn, m.preview.endLineNumber, m.preview.endColumn)) }, ranges: mapArrayOrNot( - result.ranges, - r => new Range(r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn)) + result.rangeLocations, + r => new Range(r.source.startLineNumber, r.source.startColumn, r.source.endLineNumber, r.source.endColumn)) } satisfies vscode.TextSearchMatch); } else { callback({ diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index d10dbbb7d99d8..573aeb08cdc15 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -161,7 +161,7 @@ export class NativeExtHostSearch extends ExtHostSearch implements IDisposable { return super.$clearCache(cacheKey); } - protected override createTextSearchManager(query: ITextQuery, provider: vscode.TextSearchProvider): TextSearchManager { + protected override createTextSearchManager(query: ITextQuery, provider: vscode.TextSearchProviderNew): TextSearchManager { return new NativeTextSearchManager(query, provider, undefined, 'textSearchProvider'); } } diff --git a/src/vs/workbench/api/test/node/extHostSearch.test.ts b/src/vs/workbench/api/test/node/extHostSearch.test.ts index 9d39289ca197a..6868d44da5eda 100644 --- a/src/vs/workbench/api/test/node/extHostSearch.test.ts +++ b/src/vs/workbench/api/test/node/extHostSearch.test.ts @@ -76,12 +76,12 @@ suite('ExtHostSearch', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); async function registerTestTextSearchProvider(provider: vscode.TextSearchProvider, scheme = 'file'): Promise { - disposables.add(extHostSearch.registerTextSearchProvider(scheme, provider)); + disposables.add(extHostSearch.registerTextSearchProviderOld(scheme, provider)); await rpcProtocol.sync(); } async function registerTestFileSearchProvider(provider: vscode.FileSearchProvider, scheme = 'file'): Promise { - disposables.add(extHostSearch.registerFileSearchProvider(scheme, provider)); + disposables.add(extHostSearch.registerFileSearchProviderOld(scheme, provider)); await rpcProtocol.sync(); } @@ -170,7 +170,7 @@ suite('ExtHostSearch', () => { this._pfs = mockPFS as any; } - protected override createTextSearchManager(query: ITextQuery, provider: vscode.TextSearchProvider): TextSearchManager { + protected override createTextSearchManager(query: ITextQuery, provider: vscode.TextSearchProviderNew): TextSearchManager { return new NativeTextSearchManager(query, provider, this._pfs); } }); @@ -746,13 +746,13 @@ suite('ExtHostSearch', () => { if (resultIsMatch(lineResult)) { actualTextSearchResults.push({ preview: { - text: lineResult.preview.text, + text: lineResult.previewText, matches: mapArrayOrNot( - lineResult.preview.matches, + lineResult.rangeLocations.map(r => r.preview), m => new Range(m.startLineNumber, m.startColumn, m.endLineNumber, m.endColumn)) }, ranges: mapArrayOrNot( - lineResult.ranges, + lineResult.rangeLocations.map(r => r.source), r => new Range(r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn), ), uri: fileMatch.resource diff --git a/src/vs/workbench/contrib/search/browser/notebookSearch/searchNotebookHelpers.ts b/src/vs/workbench/contrib/search/browser/notebookSearch/searchNotebookHelpers.ts index 1211c0c35b3dc..d261e86146a98 100644 --- a/src/vs/workbench/contrib/search/browser/notebookSearch/searchNotebookHelpers.ts +++ b/src/vs/workbench/contrib/search/browser/notebookSearch/searchNotebookHelpers.ts @@ -52,5 +52,5 @@ export function webviewMatchesToTextSearchMatches(webviewMatches: CellWebviewFin new Range(0, rawMatch.searchPreviewInfo.range.start, 0, rawMatch.searchPreviewInfo.range.end), undefined, rawMatch.index) : undefined - ).filter((e): e is ITextSearchMatch => !!e); + ).filter((e): e is TextSearchMatch => !!e); } diff --git a/src/vs/workbench/contrib/search/browser/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchModel.ts index 764f8966664a8..39fa7bb4e45bd 100644 --- a/src/vs/workbench/contrib/search/browser/searchModel.ts +++ b/src/vs/workbench/contrib/search/browser/searchModel.ts @@ -2479,20 +2479,12 @@ export class RangeHighlightDecorations implements IDisposable { }); } - - function textSearchResultToMatches(rawMatch: ITextSearchMatch, fileMatch: FileMatch, isAiContributed: boolean): Match[] { - const previewLines = rawMatch.preview.text.split('\n'); - if (Array.isArray(rawMatch.ranges)) { - return rawMatch.ranges.map((r, i) => { - const previewRange: ISearchRange = (rawMatch.preview.matches)[i]; - return new Match(fileMatch, previewLines, previewRange, r, isAiContributed); - }); - } else { - const previewRange = rawMatch.preview.matches; - const match = new Match(fileMatch, previewLines, previewRange, rawMatch.ranges, isAiContributed); - return [match]; - } + const previewLines = rawMatch.previewText.split('\n'); + return rawMatch.rangeLocations.map((rangeLocation) => { + const previewRange: ISearchRange = rangeLocation.preview; + return new Match(fileMatch, previewLines, previewRange, rangeLocation.source, isAiContributed); + }); } // text search to notebook matches @@ -2500,18 +2492,12 @@ function textSearchResultToMatches(rawMatch: ITextSearchMatch, fileMatch: FileMa export function textSearchMatchesToNotebookMatches(textSearchMatches: ITextSearchMatch[], cell: CellMatch): MatchInNotebook[] { const notebookMatches: MatchInNotebook[] = []; textSearchMatches.forEach((textSearchMatch) => { - const previewLines = textSearchMatch.preview.text.split('\n'); - if (Array.isArray(textSearchMatch.ranges)) { - textSearchMatch.ranges.forEach((r, i) => { - const previewRange: ISearchRange = (textSearchMatch.preview.matches)[i]; - const match = new MatchInNotebook(cell, previewLines, previewRange, r, textSearchMatch.webviewIndex); - notebookMatches.push(match); - }); - } else { - const previewRange = textSearchMatch.preview.matches; - const match = new MatchInNotebook(cell, previewLines, previewRange, textSearchMatch.ranges, textSearchMatch.webviewIndex); + const previewLines = textSearchMatch.previewText.split('\n'); + textSearchMatch.rangeLocations.map((rangeLocation) => { + const previewRange: ISearchRange = rangeLocation.preview; + const match = new MatchInNotebook(cell, previewLines, previewRange, rangeLocation.source, textSearchMatch.webviewIndex); notebookMatches.push(match); - } + }); }); return notebookMatches; } diff --git a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts index f30dcaf0de32d..fbd2327eb1d6b 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts @@ -149,28 +149,28 @@ suite('searchNotebookHelpers', () => { codeWebviewResults = webviewMatchesToTextSearchMatches(codeCellFindMatch.webviewMatches); assert.strictEqual(markdownContentResults.length, 1); - assert.strictEqual(markdownContentResults[0].preview.text, '# Hello World Test\n'); - assertRangesEqual(markdownContentResults[0].preview.matches, [new Range(0, 14, 0, 18)]); - assertRangesEqual(markdownContentResults[0].ranges, [new Range(0, 14, 0, 18)]); + assert.strictEqual(markdownContentResults[0].previewText, '# Hello World Test\n'); + assertRangesEqual(markdownContentResults[0].rangeLocations.map(e => e.preview), [new Range(0, 14, 0, 18)]); + assertRangesEqual(markdownContentResults[0].rangeLocations.map(e => e.source), [new Range(0, 14, 0, 18)]); assert.strictEqual(codeContentResults.length, 2); - assert.strictEqual(codeContentResults[0].preview.text, 'print("test! testing!!")\n'); - assert.strictEqual(codeContentResults[1].preview.text, 'print("this is a Test")\n'); - assertRangesEqual(codeContentResults[0].preview.matches, [new Range(0, 7, 0, 11), new Range(0, 13, 0, 17)]); - assertRangesEqual(codeContentResults[0].ranges, [new Range(0, 7, 0, 11), new Range(0, 13, 0, 17)]); + assert.strictEqual(codeContentResults[0].previewText, 'print("test! testing!!")\n'); + assert.strictEqual(codeContentResults[1].previewText, 'print("this is a Test")\n'); + assertRangesEqual(codeContentResults[0].rangeLocations.map(e => e.preview), [new Range(0, 7, 0, 11), new Range(0, 13, 0, 17)]); + assertRangesEqual(codeContentResults[0].rangeLocations.map(e => e.source), [new Range(0, 7, 0, 11), new Range(0, 13, 0, 17)]); assert.strictEqual(codeWebviewResults.length, 3); - assert.strictEqual(codeWebviewResults[0].preview.text, 'test! testing!!'); - assert.strictEqual(codeWebviewResults[1].preview.text, 'test! testing!!'); - assert.strictEqual(codeWebviewResults[2].preview.text, 'this is a Test'); - - assertRangesEqual(codeWebviewResults[0].preview.matches, [new Range(0, 1, 0, 5)]); - assertRangesEqual(codeWebviewResults[1].preview.matches, [new Range(0, 7, 0, 11)]); - assertRangesEqual(codeWebviewResults[2].preview.matches, [new Range(0, 11, 0, 15)]); - assertRangesEqual(codeWebviewResults[0].ranges, [new Range(0, 1, 0, 5)]); - assertRangesEqual(codeWebviewResults[1].ranges, [new Range(0, 7, 0, 11)]); - assertRangesEqual(codeWebviewResults[2].ranges, [new Range(0, 11, 0, 15)]); + assert.strictEqual(codeWebviewResults[0].previewText, 'test! testing!!'); + assert.strictEqual(codeWebviewResults[1].previewText, 'test! testing!!'); + assert.strictEqual(codeWebviewResults[2].previewText, 'this is a Test'); + + assertRangesEqual(codeWebviewResults[0].rangeLocations.map(e => e.preview), [new Range(0, 1, 0, 5)]); + assertRangesEqual(codeWebviewResults[1].rangeLocations.map(e => e.preview), [new Range(0, 7, 0, 11)]); + assertRangesEqual(codeWebviewResults[2].rangeLocations.map(e => e.preview), [new Range(0, 11, 0, 15)]); + assertRangesEqual(codeWebviewResults[0].rangeLocations.map(e => e.source), [new Range(0, 1, 0, 5)]); + assertRangesEqual(codeWebviewResults[1].rangeLocations.map(e => e.source), [new Range(0, 7, 0, 11)]); + assertRangesEqual(codeWebviewResults[2].rangeLocations.map(e => e.source), [new Range(0, 11, 0, 15)]); }); test('convert ITextSearchMatch to MatchInNotebook', () => { diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index c35d9b87da683..d63cc7c48efc8 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -60,21 +60,24 @@ suite('Search - Viewlet', () => { result.add([{ resource: createFileUriFromPathFromRoot('/foo'), results: [{ - preview: { - text: 'bar', - matches: { - startLineNumber: 0, - startColumn: 0, - endLineNumber: 0, - endColumn: 1 + + previewText: 'bar', + rangeLocations: [ + { + preview: { + startLineNumber: 0, + startColumn: 0, + endLineNumber: 0, + endColumn: 1 + }, + source: { + startLineNumber: 1, + startColumn: 0, + endLineNumber: 1, + endColumn: 1 + } } - }, - ranges: { - startLineNumber: 1, - startColumn: 0, - endLineNumber: 1, - endColumn: 1 - } + ] }] }], '', false); diff --git a/src/vs/workbench/services/search/common/fileSearchManager.ts b/src/vs/workbench/services/search/common/fileSearchManager.ts index 67a4f0bf664a1..a6bf15a5db279 100644 --- a/src/vs/workbench/services/search/common/fileSearchManager.ts +++ b/src/vs/workbench/services/search/common/fileSearchManager.ts @@ -10,8 +10,9 @@ import * as glob from 'vs/base/common/glob'; import * as resources from 'vs/base/common/resources'; import { StopWatch } from 'vs/base/common/stopwatch'; import { URI } from 'vs/base/common/uri'; -import { IFileMatch, IFileSearchProviderStats, IFolderQuery, ISearchCompleteStats, IFileQuery, QueryGlobTester, resolvePatternsForProvider, hasSiblingFn, excludeToGlobPattern } from 'vs/workbench/services/search/common/search'; -import { FileSearchProvider, FileSearchOptions } from 'vs/workbench/services/search/common/searchExtTypes'; +import { IFileMatch, IFileSearchProviderStats, IFolderQuery, ISearchCompleteStats, IFileQuery, QueryGlobTester, resolvePatternsForProvider, hasSiblingFn, excludeToGlobPattern, DEFAULT_MAX_SEARCH_RESULTS } from 'vs/workbench/services/search/common/search'; +import { FileSearchProviderFolderOptions, FileSearchProviderNew, FileSearchProviderOptions } from 'vs/workbench/services/search/common/searchExtTypes'; +import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; interface IInternalFileMatch { base: URI; @@ -27,6 +28,13 @@ interface IDirectoryEntry { basename: string; } +interface FolderQueryInfo { + queryTester: QueryGlobTester; + noSiblingsClauses: boolean; + folder: URI; + tree: IDirectoryTree; +} + interface IDirectoryTree { rootEntries: IDirectoryEntry[]; pathToEntries: { [relativePath: string]: IDirectoryEntry[] }; @@ -45,7 +53,7 @@ class FileSearchEngine { private globalExcludePattern?: glob.ParsedExpression; - constructor(private config: IFileQuery, private provider: FileSearchProvider, private sessionToken?: CancellationToken) { + constructor(private config: IFileQuery, private provider: FileSearchProviderNew, private sessionToken?: CancellationToken) { this.filePattern = config.filePattern; this.includePattern = config.includePattern && glob.parse(config.includePattern); this.maxResults = config.maxResults || undefined; @@ -90,13 +98,13 @@ class FileSearchEngine { }); } - // For each root folder - Promise.all(folderQueries.map(fq => { - return this.searchInFolder(fq, onResult); - })).then(stats => { + // For each root folder' + + // NEW: can just call with an array of folder info + this.doSearch(folderQueries, onResult).then(stats => { resolve({ limitHit: this.isLimitHit, - stats: stats[0] || undefined // Only looking at single-folder workspace stats... + stats: stats || undefined // Only looking at single-folder workspace stats... }); }, (err: Error) => { reject(new Error(toErrorMessage(err))); @@ -104,13 +112,24 @@ class FileSearchEngine { }); } - private async searchInFolder(fq: IFolderQuery, onResult: (match: IInternalFileMatch) => void): Promise { + + private async doSearch(fqs: IFolderQuery[], onResult: (match: IInternalFileMatch) => void): Promise { const cancellation = new CancellationTokenSource(); - const options = this.getSearchOptionsForFolder(fq); - const tree = this.initDirectoryTree(); + const folderOptions = fqs.map(fq => this.getSearchOptionsForFolder(fq)); + const options: FileSearchProviderOptions = { + folderOptions, + maxResults: this.config.maxResults ?? DEFAULT_MAX_SEARCH_RESULTS, + session: this.sessionToken + }; + + + const folderMappings: TernarySearchTree = TernarySearchTree.forUris(); + fqs.forEach(fq => { + const queryTester = new QueryGlobTester(this.config, fq); + const noSiblingsClauses = !queryTester.hasSiblingExcludeClauses(); + folderMappings.set(fq.folder, { queryTester, noSiblingsClauses, folder: fq.folder, tree: this.initDirectoryTree() }); + }); - const queryTester = new QueryGlobTester(this.config, fq); - const noSiblingsClauses = !queryTester.hasSiblingExcludeClauses(); let providerSW: StopWatch; @@ -119,9 +138,7 @@ class FileSearchEngine { providerSW = StopWatch.create(); const results = await this.provider.provideFileSearchResults( - { - pattern: this.config.filePattern || '' - }, + this.config.filePattern || '', options, cancellation.token); const providerTime = providerSW.elapsed(); @@ -131,19 +148,22 @@ class FileSearchEngine { return null; } + if (results) { results.forEach(result => { - const relativePath = path.posix.relative(fq.folder.path, result.path); - if (noSiblingsClauses) { + const fqFolderInfo = folderMappings.findSubstr(result)!; + const relativePath = path.posix.relative(fqFolderInfo.folder.path, result.path); + + if (fqFolderInfo.noSiblingsClauses) { const basename = path.basename(result.path); - this.matchFile(onResult, { base: fq.folder, relativePath, basename }); + this.matchFile(onResult, { base: fqFolderInfo.folder, relativePath, basename }); return; } // TODO: Optimize siblings clauses with ripgrep here. - this.addDirectoryEntries(tree, fq.folder, relativePath, onResult); + this.addDirectoryEntries(fqFolderInfo.tree, fqFolderInfo.folder, relativePath, onResult); }); } @@ -151,7 +171,10 @@ class FileSearchEngine { return null; } - this.matchDirectoryTree(tree, queryTester, onResult); + folderMappings.forEach(e => { + this.matchDirectoryTree(e.tree, e.queryTester, onResult); + }); + return { providerTime, postProcessTime: postProcessSW.elapsed() @@ -162,20 +185,20 @@ class FileSearchEngine { } } - private getSearchOptionsForFolder(fq: IFolderQuery): FileSearchOptions { + private getSearchOptionsForFolder(fq: IFolderQuery): FileSearchProviderFolderOptions { const includes = resolvePatternsForProvider(this.config.includePattern, fq.includePattern); const excludes = excludeToGlobPattern(fq.excludePattern?.folder, resolvePatternsForProvider(this.config.excludePattern, fq.excludePattern?.pattern)); return { folder: fq.folder, - excludes: excludes.map(e => typeof e === 'string' ? e : e.pattern), // TODO- follow baseURI + excludes, includes, - useIgnoreFiles: !fq.disregardIgnoreFiles, - useGlobalIgnoreFiles: !fq.disregardGlobalIgnoreFiles, - useParentIgnoreFiles: !fq.disregardParentIgnoreFiles, + useIgnoreFiles: { + local: !fq.disregardIgnoreFiles, + parent: !fq.disregardParentIgnoreFiles, + global: !fq.disregardGlobalIgnoreFiles + }, followSymlinks: !fq.ignoreSymlinks, - maxResults: this.config.maxResults, - session: this.sessionToken }; } @@ -274,7 +297,7 @@ export class FileSearchManager { private readonly sessions = new Map(); - fileSearch(config: IFileQuery, provider: FileSearchProvider, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): Promise { + fileSearch(config: IFileQuery, provider: FileSearchProviderNew, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): Promise { const sessionTokenSource = this.getSessionTokenSource(config.cacheKey); const engine = new FileSearchEngine(config, provider, sessionTokenSource && sessionTokenSource.token); diff --git a/src/vs/workbench/services/search/common/getFileResults.ts b/src/vs/workbench/services/search/common/getFileResults.ts index f99fc1bc4a06d..2ad8f5db93d4e 100644 --- a/src/vs/workbench/services/search/common/getFileResults.ts +++ b/src/vs/workbench/services/search/common/getFileResults.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITextSearchResult } from 'vs/workbench/services/search/common/search'; -import { TextSearchPreviewOptions } from 'vs/workbench/services/search/common/searchExtTypes'; +import { ITextSearchMatch, ITextSearchPreviewOptions, ITextSearchResult } from 'vs/workbench/services/search/common/search'; import { Range } from 'vs/editor/common/core/range'; export const getFileResults = ( @@ -12,7 +11,7 @@ export const getFileResults = ( pattern: RegExp, options: { surroundingContext: number; - previewOptions: TextSearchPreviewOptions | undefined; + previewOptions: ITextSearchPreviewOptions | undefined; remainingResultQuota: number; } ): ITextSearchResult[] => { @@ -101,10 +100,14 @@ export const getFileResults = ( matchStartIndex + matchedText.length - lineRanges[endLine].start - (endLine === startLine ? offset : 0) ); - const match: ITextSearchResult = { - ranges: fileRange, - preview: { text: previewText, matches: previewRange }, + const match: ITextSearchMatch = { + rangeLocations: [{ + source: fileRange, + preview: previewRange, + }], + previewText: previewText }; + results.push(match); if (options.surroundingContext) { diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index 69c3625811371..dc024775aa793 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -209,17 +209,12 @@ export interface ISearchRange { readonly endColumn: number; } -export interface ITextSearchResultPreview { - text: string; - matches: ISearchRange | ISearchRange[]; - cellFragment?: string; -} - export interface ITextSearchMatch { uri?: U; - ranges: ISearchRange | ISearchRange[]; - preview: ITextSearchResultPreview; + rangeLocations: SearchRangeSetPairing[]; + previewText: string; webviewIndex?: number; + cellFragment?: string; } export interface ITextSearchContext { @@ -231,7 +226,7 @@ export interface ITextSearchContext { export type ITextSearchResult = ITextSearchMatch | ITextSearchContext; export function resultIsMatch(result: ITextSearchResult): result is ITextSearchMatch { - return !!(result).preview; + return !!(result).rangeLocations && !!(result).previewText; } export interface IProgressMessage { @@ -310,20 +305,25 @@ export class FileMatch implements IFileMatch { } } +export interface SearchRangeSetPairing { + source: ISearchRange; + preview: ISearchRange; +} + export class TextSearchMatch implements ITextSearchMatch { - ranges: ISearchRange | ISearchRange[]; - preview: ITextSearchResultPreview; + rangeLocations: SearchRangeSetPairing[] = []; + previewText: string; webviewIndex?: number; - constructor(text: string, range: ISearchRange | ISearchRange[], previewOptions?: ITextSearchPreviewOptions, webviewIndex?: number) { - this.ranges = range; + constructor(text: string, ranges: ISearchRange | ISearchRange[], previewOptions?: ITextSearchPreviewOptions, webviewIndex?: number) { this.webviewIndex = webviewIndex; // Trim preview if this is one match and a single-line match with a preview requested. // Otherwise send the full text, like for replace or for showing multiple previews. // TODO this is fishy. - const ranges = Array.isArray(range) ? range : [range]; - if (previewOptions && previewOptions.matchLines === 1 && isSingleLineRangeList(ranges)) { + const rangesArr = Array.isArray(ranges) ? ranges : [ranges]; + + if (previewOptions && previewOptions.matchLines === 1 && isSingleLineRangeList(rangesArr)) { // 1 line preview requested text = getNLines(text, previewOptions.matchLines); @@ -331,8 +331,7 @@ export class TextSearchMatch implements ITextSearchMatch { let shift = 0; let lastEnd = 0; const leadingChars = Math.floor(previewOptions.charsPerLine / 5); - const matches: ISearchRange[] = []; - for (const range of ranges) { + for (const range of rangesArr) { const previewStart = Math.max(range.startColumn - leadingChars, 0); const previewEnd = range.startColumn + previewOptions.charsPerLine; if (previewStart > lastEnd + leadingChars + SEARCH_ELIDED_MIN_LEN) { @@ -343,18 +342,25 @@ export class TextSearchMatch implements ITextSearchMatch { result += text.slice(lastEnd, previewEnd); } - matches.push(new OneLineRange(0, range.startColumn - shift, range.endColumn - shift)); lastEnd = previewEnd; + this.rangeLocations.push({ + source: range, + preview: new OneLineRange(0, range.startColumn - shift, range.endColumn - shift) + }); + } - this.preview = { text: result, matches: Array.isArray(this.ranges) ? matches : matches[0] }; + this.previewText = result; } else { - const firstMatchLine = Array.isArray(range) ? range[0].startLineNumber : range.startLineNumber; + const firstMatchLine = Array.isArray(ranges) ? ranges[0].startLineNumber : ranges.startLineNumber; + + const rangeLocs = mapArrayOrNot(ranges, r => ({ + preview: new SearchRange(r.startLineNumber - firstMatchLine, r.startColumn, r.endLineNumber - firstMatchLine, r.endColumn), + source: r + })); - this.preview = { - text, - matches: mapArrayOrNot(range, r => new SearchRange(r.startLineNumber - firstMatchLine, r.startColumn, r.endLineNumber - firstMatchLine, r.endColumn)) - }; + this.rangeLocations = Array.isArray(rangeLocs) ? rangeLocs : [rangeLocs]; + this.previewText = text; } } } diff --git a/src/vs/workbench/services/search/common/searchExtConversionTypes.ts b/src/vs/workbench/services/search/common/searchExtConversionTypes.ts new file mode 100644 index 0000000000000..47ae2648adc3b --- /dev/null +++ b/src/vs/workbench/services/search/common/searchExtConversionTypes.ts @@ -0,0 +1,616 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { asArray, coalesce } from 'vs/base/common/arrays'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; +import { IProgress } from 'vs/platform/progress/common/progress'; +import { Range, FileSearchProviderNew, FileSearchProviderOptions, ProviderResult, TextSearchCompleteNew, TextSearchContextNew, TextSearchMatchNew, TextSearchProviderNew, TextSearchProviderOptions, TextSearchQueryNew, TextSearchResultNew, AITextSearchProviderNew, TextSearchCompleteMessage } from 'vs/workbench/services/search/common/searchExtTypes'; + +// old types that are retained for backward compatibility +// TODO: delete this when search apis are adopted by all first-party extensions + +/** + * A relative pattern is a helper to construct glob patterns that are matched + * relatively to a base path. The base path can either be an absolute file path + * or a [workspace folder](#WorkspaceFolder). + */ +export interface RelativePattern { + + /** + * A base file path to which this pattern will be matched against relatively. + */ + base: string; + + /** + * A file glob pattern like `*.{ts,js}` that will be matched on file paths + * relative to the base path. + * + * Example: Given a base of `/home/work/folder` and a file path of `/home/work/folder/index.js`, + * the file glob pattern will match on `index.js`. + */ + pattern: string; +} + +/** + * A file glob pattern to match file paths against. This can either be a glob pattern string + * (like `** /*.{ts,js}` without space before / or `*.{ts,js}`) or a [relative pattern](#RelativePattern). + * + * Glob patterns can have the following syntax: + * * `*` to match zero or more characters in a path segment + * * `?` to match on one character in a path segment + * * `**` to match any number of path segments, including none + * * `{}` to group conditions (e.g. `** /*.{ts,js}` without space before / matches all TypeScript and JavaScript files) + * * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) + * * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) + * + * Note: a backslash (`\`) is not valid within a glob pattern. If you have an existing file + * path to match against, consider to use the [relative pattern](#RelativePattern) support + * that takes care of converting any backslash into slash. Otherwise, make sure to convert + * any backslash to slash when creating the glob pattern. + */ +export type GlobPattern = string | RelativePattern; + +/** + * The parameters of a query for text search. + */ +export interface TextSearchQuery { + /** + * The text pattern to search for. + */ + pattern: string; + + /** + * Whether or not `pattern` should match multiple lines of text. + */ + isMultiline?: boolean; + + /** + * Whether or not `pattern` should be interpreted as a regular expression. + */ + isRegExp?: boolean; + + /** + * Whether or not the search should be case-sensitive. + */ + isCaseSensitive?: boolean; + + /** + * Whether or not to search for whole word matches only. + */ + isWordMatch?: boolean; +} + +/** + * A file glob pattern to match file paths against. + * TODO@roblou - merge this with the GlobPattern docs/definition in vscode.d.ts. + * @see [GlobPattern](#GlobPattern) + */ +export type GlobString = string; + +/** + * Options common to file and text search + */ +export interface SearchOptions { + /** + * The root folder to search within. + */ + folder: URI; + + /** + * Files that match an `includes` glob pattern should be included in the search. + */ + includes: GlobString[]; + + /** + * Files that match an `excludes` glob pattern should be excluded from the search. + */ + excludes: GlobString[]; + + /** + * Whether external files that exclude files, like .gitignore, should be respected. + * See the vscode setting `"search.useIgnoreFiles"`. + */ + useIgnoreFiles: boolean; + + /** + * Whether symlinks should be followed while searching. + * See the vscode setting `"search.followSymlinks"`. + */ + followSymlinks: boolean; + + /** + * Whether global files that exclude files, like .gitignore, should be respected. + * See the vscode setting `"search.useGlobalIgnoreFiles"`. + */ + useGlobalIgnoreFiles: boolean; + + /** + * Whether files in parent directories that exclude files, like .gitignore, should be respected. + * See the vscode setting `"search.useParentIgnoreFiles"`. + */ + useParentIgnoreFiles: boolean; +} + +/** + * Options to specify the size of the result text preview. + * These options don't affect the size of the match itself, just the amount of preview text. + */ +export interface TextSearchPreviewOptions { + /** + * The maximum number of lines in the preview. + * Only search providers that support multiline search will ever return more than one line in the match. + */ + matchLines: number; + + /** + * The maximum number of characters included per line. + */ + charsPerLine: number; +} + +/** + * Options that apply to text search. + */ +export interface TextSearchOptions extends SearchOptions { + /** + * The maximum number of results to be returned. + */ + maxResults: number; + + /** + * Options to specify the size of the result text preview. + */ + previewOptions?: TextSearchPreviewOptions; + + /** + * Exclude files larger than `maxFileSize` in bytes. + */ + maxFileSize?: number; + + /** + * Interpret files using this encoding. + * See the vscode setting `"files.encoding"` + */ + encoding?: string; + + /** + * Number of lines of context to include before each match. + */ + beforeContext?: number; + + /** + * Number of lines of context to include after each match. + */ + afterContext?: number; +} +/** + * Options that apply to AI text search. + */ +export interface AITextSearchOptions extends SearchOptions { + /** + * The maximum number of results to be returned. + */ + maxResults: number; + + /** + * Options to specify the size of the result text preview. + */ + previewOptions?: TextSearchPreviewOptions; + + /** + * Exclude files larger than `maxFileSize` in bytes. + */ + maxFileSize?: number; + + /** + * Number of lines of context to include before each match. + */ + beforeContext?: number; + + /** + * Number of lines of context to include after each match. + */ + afterContext?: number; +} + +/** + * Information collected when text search is complete. + */ +export interface TextSearchComplete { + /** + * Whether the search hit the limit on the maximum number of search results. + * `maxResults` on [`TextSearchOptions`](#TextSearchOptions) specifies the max number of results. + * - If exactly that number of matches exist, this should be false. + * - If `maxResults` matches are returned and more exist, this should be true. + * - If search hits an internal limit which is less than `maxResults`, this should be true. + */ + limitHit?: boolean; + + /** + * Additional information regarding the state of the completed search. + * + * Supports links in markdown syntax: + * - Click to [run a command](command:workbench.action.OpenQuickPick) + * - Click to [open a website](https://aka.ms) + */ + message?: TextSearchCompleteMessage | TextSearchCompleteMessage[]; +} + +/** + * The parameters of a query for file search. + */ +export interface FileSearchQuery { + /** + * The search pattern to match against file paths. + */ + pattern: string; +} + +/** + * Options that apply to file search. + */ +export interface FileSearchOptions extends SearchOptions { + /** + * The maximum number of results to be returned. + */ + maxResults?: number; + + /** + * A CancellationToken that represents the session for this search query. If the provider chooses to, this object can be used as the key for a cache, + * and searches with the same session object can search the same cache. When the token is cancelled, the session is complete and the cache can be cleared. + */ + session?: CancellationToken; +} + +/** + * A preview of the text result. + */ +export interface TextSearchMatchPreview { + /** + * The matching lines of text, or a portion of the matching line that contains the match. + */ + text: string; + + /** + * The Range within `text` corresponding to the text of the match. + * The number of matches must match the TextSearchMatch's range property. + */ + matches: Range | Range[]; +} + +/** + * A match from a text search + */ +export interface TextSearchMatch { + /** + * The uri for the matching document. + */ + uri: URI; + + /** + * The range of the match within the document, or multiple ranges for multiple matches. + */ + ranges: Range | Range[]; + + /** + * A preview of the text match. + */ + preview: TextSearchMatchPreview; +} + +/** + * Checks if the given object is of type TextSearchMatch. + * @param object The object to check. + * @returns True if the object is a TextSearchMatch, false otherwise. + */ +function isTextSearchMatch(object: any): object is TextSearchMatch { + return 'uri' in object && 'ranges' in object && 'preview' in object; +} + +/** + * A line of context surrounding a TextSearchMatch. + */ +export interface TextSearchContext { + /** + * The uri for the matching document. + */ + uri: URI; + + /** + * One line of text. + * previewOptions.charsPerLine applies to this + */ + text: string; + + /** + * The line number of this line of context. + */ + lineNumber: number; +} + +export type TextSearchResult = TextSearchMatch | TextSearchContext; + +/** + * A FileSearchProvider provides search results for files in the given folder that match a query string. It can be invoked by quickaccess or other extensions. + * + * A FileSearchProvider is the more powerful of two ways to implement file search in VS Code. Use a FileSearchProvider if you wish to search within a folder for + * all files that match the user's query. + * + * The FileSearchProvider will be invoked on every keypress in quickaccess. When `workspace.findFiles` is called, it will be invoked with an empty query string, + * and in that case, every file in the folder should be returned. + */ +export interface FileSearchProvider { + /** + * Provide the set of files that match a certain file path pattern. + * @param query The parameters for this query. + * @param options A set of options to consider while searching files. + * @param progress A progress callback that must be invoked for all results. + * @param token A cancellation token. + */ + provideFileSearchResults(query: FileSearchQuery, options: FileSearchOptions, token: CancellationToken): ProviderResult; +} + +/** + * A TextSearchProvider provides search results for text results inside files in the workspace. + */ +export interface TextSearchProvider { + /** + * Provide results that match the given text pattern. + * @param query The parameters for this query. + * @param options A set of options to consider while searching. + * @param progress A progress callback that must be invoked for all results. + * @param token A cancellation token. + */ + provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: IProgress, token: CancellationToken): ProviderResult; +} + +export interface AITextSearchProvider { + /** + * Provide results that match the given text pattern. + * @param query The parameter for this query. + * @param options A set of options to consider while searching. + * @param progress A progress callback that must be invoked for all results. + * @param token A cancellation token. + */ + provideAITextSearchResults(query: string, options: AITextSearchOptions, progress: IProgress, token: CancellationToken): ProviderResult; +} + +/** + * Options that can be set on a findTextInFiles search. + */ +export interface FindTextInFilesOptions { + /** + * A [glob pattern](#GlobPattern) that defines the files to search for. The glob pattern + * will be matched against the file paths of files relative to their workspace. Use a [relative pattern](#RelativePattern) + * to restrict the search results to a [workspace folder](#WorkspaceFolder). + */ + include?: GlobPattern; + + /** + * A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern + * will be matched against the file paths of resulting matches relative to their workspace. When `undefined` only default excludes will + * apply, when `null` no excludes will apply. + */ + exclude?: GlobPattern | null; + + /** + * The maximum number of results to search for + */ + maxResults?: number; + + /** + * Whether external files that exclude files, like .gitignore, should be respected. + * See the vscode setting `"search.useIgnoreFiles"`. + */ + useIgnoreFiles?: boolean; + + /** + * Whether global files that exclude files, like .gitignore, should be respected. + * See the vscode setting `"search.useGlobalIgnoreFiles"`. + */ + useGlobalIgnoreFiles?: boolean; + + /** + * Whether files in parent directories that exclude files, like .gitignore, should be respected. + * See the vscode setting `"search.useParentIgnoreFiles"`. + */ + useParentIgnoreFiles: boolean; + + /** + * Whether symlinks should be followed while searching. + * See the vscode setting `"search.followSymlinks"`. + */ + followSymlinks?: boolean; + + /** + * Interpret files using this encoding. + * See the vscode setting `"files.encoding"` + */ + encoding?: string; + + /** + * Options to specify the size of the result text preview. + */ + previewOptions?: TextSearchPreviewOptions; + + /** + * Number of lines of context to include before each match. + */ + beforeContext?: number; + + /** + * Number of lines of context to include after each match. + */ + afterContext?: number; +} + +function newToOldFileProviderOptions(options: FileSearchProviderOptions): FileSearchOptions[] { + return options.folderOptions.map(folderOption => ({ + folder: folderOption.folder, + excludes: folderOption.excludes.map(e => typeof (e) === 'string' ? e : e.pattern), + includes: folderOption.includes, + useGlobalIgnoreFiles: folderOption.useIgnoreFiles.global, + useIgnoreFiles: folderOption.useIgnoreFiles.local, + useParentIgnoreFiles: folderOption.useIgnoreFiles.parent, + followSymlinks: folderOption.followSymlinks, + maxResults: options.maxResults, + session: options.session // TODO: make sure that we actually use a cancellation token here. + } satisfies FileSearchOptions)); +} + +export class OldFileSearchProviderConverter implements FileSearchProviderNew { + constructor(private provider: FileSearchProvider) { } + + provideFileSearchResults(pattern: string, options: FileSearchProviderOptions, token: CancellationToken): ProviderResult { + const getResult = async () => { + const newOpts = newToOldFileProviderOptions(options); + return Promise.all(newOpts.map( + o => this.provider.provideFileSearchResults({ pattern }, o, token))); + }; + return getResult().then(e => coalesce(e).flat()); + } +} + +function newToOldTextProviderOptions(options: TextSearchProviderOptions): TextSearchOptions[] { + return options.folderOptions.map(folderOption => ({ + folder: folderOption.folder, + excludes: folderOption.excludes.map(e => typeof (e) === 'string' ? e : e.pattern), + includes: folderOption.includes, + useGlobalIgnoreFiles: folderOption.useIgnoreFiles.global, + useIgnoreFiles: folderOption.useIgnoreFiles.local, + useParentIgnoreFiles: folderOption.useIgnoreFiles.parent, + followSymlinks: folderOption.followSymlinks, + maxResults: options.maxResults, + previewOptions: newToOldPreviewOptions(options.previewOptions), + maxFileSize: options.maxFileSize, + encoding: folderOption.encoding, + afterContext: options.surroundingContext, + beforeContext: options.surroundingContext + } satisfies TextSearchOptions)); +} + +export function newToOldPreviewOptions(options: { + matchLines?: number; + charsPerLine?: number; +} | undefined +): { + matchLines: number; + charsPerLine: number; +} | undefined { + if (!options || (options.matchLines === undefined && options.charsPerLine === undefined)) { + return undefined; + } + return { + matchLines: options.matchLines ?? 100, + charsPerLine: options.charsPerLine ?? 10000 + }; +} + +export function oldToNewTextSearchResult(result: TextSearchResult): TextSearchResultNew { + if (isTextSearchMatch(result)) { + const ranges = asArray(result.ranges).map((r, i) => { + const previewArr = asArray(result.preview.matches); + const matchingPreviewRange = previewArr[i]; + return { sourceRange: r, previewRange: matchingPreviewRange }; + }); + return new TextSearchMatchNew(result.uri, ranges, result.preview.text); + } else { + return new TextSearchContextNew(result.uri, result.text, result.lineNumber); + } +} + +export class OldTextSearchProviderConverter implements TextSearchProviderNew { + constructor(private provider: TextSearchProvider) { } + + provideTextSearchResults(query: TextSearchQueryNew, options: TextSearchProviderOptions, progress: IProgress, token: CancellationToken): ProviderResult { + + const progressShim = (oldResult: TextSearchResult) => { + if (!validateProviderResult(oldResult)) { + return; + } + progress.report(oldToNewTextSearchResult(oldResult)); + }; + + const getResult = async () => { + return coalesce(await Promise.all( + newToOldTextProviderOptions(options).map( + o => this.provider.provideTextSearchResults(query, o, { report: (e) => progressShim(e) }, token)))) + .reduce( + (prev, cur) => ({ limitHit: prev.limitHit || cur.limitHit }), + { limitHit: false } + ); + }; + const oldResult = getResult(); + return oldResult.then((e) => { + return { + limitHit: e.limitHit, + message: coalesce(asArray(e.message)) + } satisfies TextSearchCompleteNew; + }); + } +} + +export class OldAITextSearchProviderConverter implements AITextSearchProviderNew { + constructor(private provider: AITextSearchProvider) { } + + provideAITextSearchResults(query: string, options: TextSearchProviderOptions, progress: IProgress, token: CancellationToken): ProviderResult { + const progressShim = (oldResult: TextSearchResult) => { + if (!validateProviderResult(oldResult)) { + return; + } + progress.report(oldToNewTextSearchResult(oldResult)); + }; + + const getResult = async () => { + return coalesce(await Promise.all( + newToOldTextProviderOptions(options).map( + o => this.provider.provideAITextSearchResults(query, o, { report: (e) => progressShim(e) }, token)))) + .reduce( + (prev, cur) => ({ limitHit: prev.limitHit || cur.limitHit }), + { limitHit: false } + ); + }; + const oldResult = getResult(); + return oldResult.then((e) => { + return { + limitHit: e.limitHit, + message: coalesce(asArray(e.message)) + } satisfies TextSearchCompleteNew; + }); + } +} + +function validateProviderResult(result: TextSearchResult): boolean { + if (extensionResultIsMatch(result)) { + if (Array.isArray(result.ranges)) { + if (!Array.isArray(result.preview.matches)) { + console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same type.'); + return false; + } + + if ((result.preview.matches).length !== result.ranges.length) { + console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same length.'); + return false; + } + } else { + if (Array.isArray(result.preview.matches)) { + console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same length.'); + return false; + } + } + } + + return true; +} + +export function extensionResultIsMatch(data: TextSearchResult): data is TextSearchMatch { + return !!(data).preview; +} diff --git a/src/vs/workbench/services/search/common/searchExtTypes.ts b/src/vs/workbench/services/search/common/searchExtTypes.ts index c01c03e80fbcf..7206fa0690c5f 100644 --- a/src/vs/workbench/services/search/common/searchExtTypes.ts +++ b/src/vs/workbench/services/search/common/searchExtTypes.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { asArray } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; import { IProgress } from 'vs/platform/progress/common/progress'; @@ -56,7 +55,9 @@ export type ProviderResult = T | undefined | null | Thenable; } /** - * A match from a text search + * A TextSearchProvider provides search results for text results inside files in the workspace. */ -export interface TextSearchMatch { +export interface TextSearchProviderNew { /** - * The uri for the matching document. + * Provide results that match the given text pattern. + * @param query The parameters for this query. + * @param options A set of options to consider while searching. + * @param progress A progress callback that must be invoked for all results. + * @param token A cancellation token. */ - uri: URI; + provideTextSearchResults(query: TextSearchQueryNew, options: TextSearchProviderOptions, progress: IProgress, token: CancellationToken): ProviderResult; +} +/** + * Information collected when text search is complete. + */ +export interface TextSearchCompleteNew { /** - * The range of the match within the document, or multiple ranges for multiple matches. + * Whether the search hit the limit on the maximum number of search results. + * `maxResults` on {@linkcode TextSearchOptions} specifies the max number of results. + * - If exactly that number of matches exist, this should be false. + * - If `maxResults` matches are returned and more exist, this should be true. + * - If search hits an internal limit which is less than `maxResults`, this should be true. */ - ranges: Range | Range[]; + limitHit?: boolean; /** - * A preview of the text match. + * Additional information regarding the state of the completed search. + * + * Messages with "Information" style support links in markdown syntax: + * - Click to [run a command](command:workbench.action.OpenQuickPick) + * - Click to [open a website](https://aka.ms) + * + * Commands may optionally return { triggerSearch: true } to signal to the editor that the original search should run be again. */ - preview: TextSearchMatchPreview; + message?: TextSearchCompleteMessageNew[]; } /** - * A line of context surrounding a TextSearchMatch. + * A message regarding a completed search. */ -export interface TextSearchContext { +export interface TextSearchCompleteMessageNew { /** - * The uri for the matching document. + * Markdown text of the message. */ - uri: URI; - + text: string; /** - * One line of text. - * previewOptions.charsPerLine applies to this + * Whether the source of the message is trusted, command links are disabled for untrusted message sources. + * Messaged are untrusted by default. */ - text: string; - + trusted?: boolean; /** - * The line number of this line of context. + * The message type, this affects how the message will be rendered. */ - lineNumber: number; + type: TextSearchCompleteMessageType; } -export type TextSearchResult = TextSearchMatch | TextSearchContext; /** * A FileSearchProvider provides search results for files in the given folder that match a query string. It can be invoked by quickaccess or other extensions. @@ -395,7 +409,7 @@ export type TextSearchResult = TextSearchMatch | TextSearchContext; * The FileSearchProvider will be invoked on every keypress in quickaccess. When `workspace.findFiles` is called, it will be invoked with an empty query string, * and in that case, every file in the folder should be returned. */ -export interface FileSearchProvider { +export interface FileSearchProviderNew { /** * Provide the set of files that match a certain file path pattern. * @param query The parameters for this query. @@ -403,13 +417,13 @@ export interface FileSearchProvider { * @param progress A progress callback that must be invoked for all results. * @param token A cancellation token. */ - provideFileSearchResults(query: FileSearchQuery, options: FileSearchOptions, token: CancellationToken): ProviderResult; + provideFileSearchResults(pattern: string, options: FileSearchProviderOptions, token: CancellationToken): ProviderResult; } /** * A TextSearchProvider provides search results for text results inside files in the workspace. */ -export interface TextSearchProvider { +export interface TextSearchProviderNew { /** * Provide results that match the given text pattern. * @param query The parameters for this query. @@ -417,125 +431,51 @@ export interface TextSearchProvider { * @param progress A progress callback that must be invoked for all results. * @param token A cancellation token. */ - provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: IProgress, token: CancellationToken): ProviderResult; -} - -export interface AITextSearchProvider { - /** - * Provide results that match the given text pattern. - * @param query The parameter for this query. - * @param options A set of options to consider while searching. - * @param progress A progress callback that must be invoked for all results. - * @param token A cancellation token. - */ - provideAITextSearchResults(query: string, options: AITextSearchOptions, progress: IProgress, token: CancellationToken): ProviderResult; + provideTextSearchResults(query: TextSearchQueryNew, options: TextSearchProviderOptions, progress: IProgress, token: CancellationToken): ProviderResult; } /** - * Options that can be set on a findTextInFiles search. + * Information collected when text search is complete. */ -export interface FindTextInFilesOptions { - /** - * A [glob pattern](#GlobPattern) that defines the files to search for. The glob pattern - * will be matched against the file paths of files relative to their workspace. Use a [relative pattern](#RelativePattern) - * to restrict the search results to a [workspace folder](#WorkspaceFolder). - */ - include?: GlobPattern; - +export interface TextSearchCompleteNew { /** - * A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern - * will be matched against the file paths of resulting matches relative to their workspace. When `undefined` only default excludes will - * apply, when `null` no excludes will apply. - */ - exclude?: GlobPattern | null; - - /** - * The maximum number of results to search for - */ - maxResults?: number; - - /** - * Whether external files that exclude files, like .gitignore, should be respected. - * See the vscode setting `"search.useIgnoreFiles"`. - */ - useIgnoreFiles?: boolean; - - /** - * Whether global files that exclude files, like .gitignore, should be respected. - * See the vscode setting `"search.useGlobalIgnoreFiles"`. - */ - useGlobalIgnoreFiles?: boolean; - - /** - * Whether files in parent directories that exclude files, like .gitignore, should be respected. - * See the vscode setting `"search.useParentIgnoreFiles"`. - */ - useParentIgnoreFiles: boolean; - - /** - * Whether symlinks should be followed while searching. - * See the vscode setting `"search.followSymlinks"`. - */ - followSymlinks?: boolean; - - /** - * Interpret files using this encoding. - * See the vscode setting `"files.encoding"` - */ - encoding?: string; - - /** - * Options to specify the size of the result text preview. - */ - previewOptions?: TextSearchPreviewOptions; - - /** - * Number of lines of context to include before each match. + * Whether the search hit the limit on the maximum number of search results. + * `maxResults` on {@linkcode TextSearchOptions} specifies the max number of results. + * - If exactly that number of matches exist, this should be false. + * - If `maxResults` matches are returned and more exist, this should be true. + * - If search hits an internal limit which is less than `maxResults`, this should be true. */ - beforeContext?: number; + limitHit?: boolean; /** - * Number of lines of context to include after each match. + * Additional information regarding the state of the completed search. + * + * Messages with "Information" style support links in markdown syntax: + * - Click to [run a command](command:workbench.action.OpenQuickPick) + * - Click to [open a website](https://aka.ms) + * + * Commands may optionally return { triggerSearch: true } to signal to the editor that the original search should run be again. */ - afterContext?: number; + message?: TextSearchCompleteMessageNew[]; } -// NEW TYPES -// added temporarily for testing new API shape /** - * A result payload for a text search, pertaining to matches within a single file. - */ -export type TextSearchResultNew = TextSearchMatchNew | TextSearchContextNew; - -/** - * The main match information for a {@link TextSearchResultNew}. + * A message regarding a completed search. */ -export class TextSearchMatchNew { +export interface TextSearchCompleteMessageNew { /** - * @param uri The uri for the matching document. - * @param ranges The ranges associated with this match. - * @param previewText The text that is used to preview the match. The highlighted range in `previewText` is specified in `ranges`. + * Markdown text of the message. */ - constructor( - public uri: URI, - public ranges: { sourceRange: Range; previewRange: Range }[], - public previewText: string) { } - -} - -/** - * The potential context information for a {@link TextSearchResultNew}. - */ -export class TextSearchContextNew { + text: string; /** - * @param uri The uri for the matching document. - * @param text The line of context text. - * @param lineNumber The line number of this line of context. + * Whether the source of the message is trusted, command links are disabled for untrusted message sources. + * Messaged are untrusted by default. */ - constructor( - public uri: URI, - public text: string, - public lineNumber: number) { } + trusted?: boolean; + /** + * The message type, this affects how the message will be rendered. + */ + type: TextSearchCompleteMessageType; } /** @@ -559,20 +499,43 @@ export enum ExcludeSettingOptions { SearchAndFilesExclude = 3 } -export enum TextSearchCompleteMessageTypeNew { +export enum TextSearchCompleteMessageType { Information = 1, Warning = 2, } -function isTextSearchMatch(object: any): object is TextSearchMatch { - return 'uri' in object && 'ranges' in object && 'preview' in object; + +/** + * A message regarding a completed search. + */ +export interface TextSearchCompleteMessage { + /** + * Markdown text of the message. + */ + text: string; + /** + * Whether the source of the message is trusted, command links are disabled for untrusted message sources. + */ + trusted?: boolean; + /** + * The message type, this affects how the message will be rendered. + */ + type: TextSearchCompleteMessageType; } -export function oldToNewTextSearchResult(result: TextSearchResult): TextSearchResultNew { - if (isTextSearchMatch(result)) { - const ranges = asArray(result.ranges).map(r => ({ sourceRange: r, previewRange: r })); - return new TextSearchMatchNew(result.uri, ranges, result.preview.text); - } else { - return new TextSearchContextNew(result.uri, result.text, result.lineNumber); - } + +/** + * An AITextSearchProvider provides additional AI text search results in the workspace. + */ +export interface AITextSearchProviderNew { + /** + * WARNING: VERY EXPERIMENTAL. + * + * Provide results that match the given text pattern. + * @param query The parameter for this query. + * @param options A set of options to consider while searching. + * @param progress A progress callback that must be invoked for all results. + * @param token A cancellation token. + */ + provideAITextSearchResults(query: string, options: TextSearchProviderOptions, progress: IProgress, token: CancellationToken): ProviderResult; } diff --git a/src/vs/workbench/services/search/common/searchExtTypesInternal.ts b/src/vs/workbench/services/search/common/searchExtTypesInternal.ts index 37ec6163bf5c1..22a5b808d3b94 100644 --- a/src/vs/workbench/services/search/common/searchExtTypesInternal.ts +++ b/src/vs/workbench/services/search/common/searchExtTypesInternal.ts @@ -2,12 +2,20 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { FileSearchOptions, TextSearchOptions } from './searchExtTypes'; +import { FileSearchProviderFolderOptions, FileSearchProviderOptions, TextSearchProviderFolderOptions, TextSearchProviderOptions } from 'vs/workbench/services/search/common/searchExtTypes'; interface RipgrepSearchOptionsCommon { numThreads?: number; } -export interface RipgrepTextSearchOptions extends TextSearchOptions, RipgrepSearchOptionsCommon { } +export type TextSearchProviderOptionsRipgrep = Omit, 'folderOptions'> & { + folderOptions: TextSearchProviderFolderOptions; +}; -export interface RipgrepFileSearchOptions extends FileSearchOptions, RipgrepSearchOptionsCommon { } +export type FileSearchProviderOptionsRipgrep = & { + folderOptions: FileSearchProviderFolderOptions; +} & FileSearchProviderOptions; + +export interface RipgrepTextSearchOptions extends TextSearchProviderOptionsRipgrep, RipgrepSearchOptionsCommon { } + +export interface RipgrepFileSearchOptions extends FileSearchProviderOptionsRipgrep, RipgrepSearchOptionsCommon { } diff --git a/src/vs/workbench/services/search/common/searchHelpers.ts b/src/vs/workbench/services/search/common/searchHelpers.ts index 8831622c25e59..4f01ae27bf9bf 100644 --- a/src/vs/workbench/services/search/common/searchHelpers.ts +++ b/src/vs/workbench/services/search/common/searchHelpers.ts @@ -81,9 +81,9 @@ export function getTextSearchMatchWithModelContext(matches: ITextSearchMatch[], } function getMatchStartEnd(match: ITextSearchMatch): { start: number; end: number } { - const matchRanges = match.ranges; - const matchStartLine = Array.isArray(matchRanges) ? matchRanges[0].startLineNumber : matchRanges.startLineNumber; - const matchEndLine = Array.isArray(matchRanges) ? matchRanges[matchRanges.length - 1].endLineNumber : matchRanges.endLineNumber; + const matchRanges = match.rangeLocations.map(e => e.source); + const matchStartLine = matchRanges[0].startLineNumber; + const matchEndLine = matchRanges[matchRanges.length - 1].endLineNumber; return { start: matchStartLine, diff --git a/src/vs/workbench/services/search/common/textSearchManager.ts b/src/vs/workbench/services/search/common/textSearchManager.ts index 0ce5df2fc2cbd..cd0549c57d082 100644 --- a/src/vs/workbench/services/search/common/textSearchManager.ts +++ b/src/vs/workbench/services/search/common/textSearchManager.ts @@ -3,28 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { mapArrayOrNot } from 'vs/base/common/arrays'; import { isThenable } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Schemas } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import * as resources from 'vs/base/common/resources'; +import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; import { URI } from 'vs/base/common/uri'; -import { DEFAULT_MAX_SEARCH_RESULTS, excludeToGlobPattern, hasSiblingPromiseFn, IAITextQuery, IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, ITextSearchStats, QueryGlobTester, QueryType, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search'; -import { AITextSearchProvider, Range, TextSearchComplete, TextSearchMatch, TextSearchOptions, TextSearchProvider, TextSearchQuery, TextSearchResult } from 'vs/workbench/services/search/common/searchExtTypes'; +import { DEFAULT_MAX_SEARCH_RESULTS, hasSiblingPromiseFn, IAITextQuery, IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, excludeToGlobPattern, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, ITextSearchStats, QueryGlobTester, QueryType, resolvePatternsForProvider, ISearchRange } from 'vs/workbench/services/search/common/search'; +import { AITextSearchProviderNew, TextSearchCompleteNew, TextSearchMatchNew, TextSearchProviderFolderOptions, TextSearchProviderNew, TextSearchProviderOptions, TextSearchQueryNew, TextSearchResultNew } from 'vs/workbench/services/search/common/searchExtTypes'; export interface IFileUtils { readdir: (resource: URI) => Promise; toCanonicalName: (encoding: string) => string; } interface IAITextQueryProviderPair { - query: IAITextQuery; provider: AITextSearchProvider; + query: IAITextQuery; provider: AITextSearchProviderNew; } interface ITextQueryProviderPair { - query: ITextQuery; provider: TextSearchProvider; + query: ITextQuery; provider: TextSearchProviderNew; } +interface FolderQueryInfo { + queryTester: QueryGlobTester; + folder: URI; + folderIdx: number; +} + export class TextSearchManager { private collector: TextSearchResultsCollector | null = null; @@ -48,14 +54,14 @@ export class TextSearchManager { this.collector = new TextSearchResultsCollector(onProgress); let isCanceled = false; - const onResult = (result: TextSearchResult, folderIdx: number) => { + const onResult = (result: TextSearchResultNew, folderIdx: number) => { if (isCanceled) { return; } if (!this.isLimitHit) { const resultSize = this.resultSize(result); - if (extensionResultIsMatch(result) && typeof this.query.maxResults === 'number' && this.resultCount + resultSize > this.query.maxResults) { + if (result instanceof TextSearchMatchNew && typeof this.query.maxResults === 'number' && this.resultCount + resultSize > this.query.maxResults) { this.isLimitHit = true; isCanceled = true; tokenSource.cancel(); @@ -65,27 +71,22 @@ export class TextSearchManager { const newResultSize = this.resultSize(result); this.resultCount += newResultSize; - if (newResultSize > 0 || !extensionResultIsMatch(result)) { + const a = result instanceof TextSearchMatchNew; + + if (newResultSize > 0 || !a) { this.collector!.add(result, folderIdx); } } }; // For each root folder - Promise.all(folderQueries.map((fq, i) => { - return this.searchInFolder(fq, r => onResult(r, i), tokenSource.token); - })).then(results => { + this.doSearch(folderQueries, onResult, tokenSource.token).then(result => { tokenSource.dispose(); this.collector!.flush(); - const someFolderHitLImit = results.some(result => !!result && !!result.limitHit); resolve({ - limitHit: this.isLimitHit || someFolderHitLImit, - messages: results.flatMap(result => { - if (!result?.message) { return []; } - if (Array.isArray(result.message)) { return result.message; } - else { return [result.message]; } - }), + limitHit: this.isLimitHit || result?.limitHit, + messages: this.getMessagesFromResults(result), stats: { type: this.processType } @@ -98,8 +99,14 @@ export class TextSearchManager { }); } - private resultSize(result: TextSearchResult): number { - if (extensionResultIsMatch(result)) { + private getMessagesFromResults(result: TextSearchCompleteNew | null | undefined) { + if (!result?.message) { return []; } + if (Array.isArray(result.message)) { return result.message; } + return [result.message]; + } + + private resultSize(result: TextSearchResultNew): number { + if (result instanceof TextSearchMatchNew) { return Array.isArray(result.ranges) ? result.ranges.length : 1; @@ -110,29 +117,22 @@ export class TextSearchManager { } } - private trimResultToSize(result: TextSearchMatch, size: number): TextSearchMatch { - const rangesArr = Array.isArray(result.ranges) ? result.ranges : [result.ranges]; - const matchesArr = Array.isArray(result.preview.matches) ? result.preview.matches : [result.preview.matches]; - - return { - ranges: rangesArr.slice(0, size), - preview: { - matches: matchesArr.slice(0, size), - text: result.preview.text - }, - uri: result.uri - }; + private trimResultToSize(result: TextSearchMatchNew, size: number): TextSearchMatchNew { + return new TextSearchMatchNew(result.uri, result.ranges.slice(0, size), result.previewText); } - private async searchInFolder(folderQuery: IFolderQuery, onResult: (result: TextSearchResult) => void, token: CancellationToken): Promise { - const queryTester = new QueryGlobTester(this.query, folderQuery); + private async doSearch(folderQueries: IFolderQuery[], onResult: (result: TextSearchResultNew, folderIdx: number) => void, token: CancellationToken): Promise { + const folderMappings: TernarySearchTree = TernarySearchTree.forUris(); + folderQueries.forEach((fq, i) => { + const queryTester = new QueryGlobTester(this.query, fq); + folderMappings.set(fq.folder, { queryTester, folder: fq.folder, folderIdx: i }); + }); + const testingPs: Promise[] = []; const progress = { - report: (result: TextSearchResult) => { - if (!this.validateProviderResult(result)) { - return; - } + report: (result: TextSearchResultNew) => { + const folderQuery = folderMappings.findSubstr(result.uri)!; const hasSibling = folderQuery.folder.scheme === Schemas.file ? hasSiblingPromiseFn(() => { return this.fileUtils.readdir(resources.dirname(result.uri)); @@ -142,23 +142,32 @@ export class TextSearchManager { const relativePath = resources.relativePath(folderQuery.folder, result.uri); if (relativePath) { // This method is only async when the exclude contains sibling clauses - const included = queryTester.includedInQuery(relativePath, path.basename(relativePath), hasSibling); + const included = folderQuery.queryTester.includedInQuery(relativePath, path.basename(relativePath), hasSibling); if (isThenable(included)) { testingPs.push( included.then(isIncluded => { if (isIncluded) { - onResult(result); + onResult(result, folderQuery.folderIdx); } })); } else if (included) { - onResult(result); + onResult(result, folderQuery.folderIdx); } } } }; - const searchOptions = this.getSearchOptionsForFolder(folderQuery); - + const folderOptions = folderQueries.map(fq => this.getSearchOptionsForFolder(fq)); + const searchOptions: TextSearchProviderOptions = { + folderOptions, + maxFileSize: this.query.maxFileSize, + maxResults: this.query.maxResults ?? DEFAULT_MAX_SEARCH_RESULTS, + previewOptions: this.query.previewOptions, + surroundingContext: this.query.surroundingContext ?? 0, + }; + if ('usePCRE2' in this.query) { + (searchOptions).usePCRE2 = this.query.usePCRE2; + } let result; if (this.queryProviderPair.query.type === QueryType.aiText) { @@ -173,56 +182,27 @@ export class TextSearchManager { return result; } - private validateProviderResult(result: TextSearchResult): boolean { - if (extensionResultIsMatch(result)) { - if (Array.isArray(result.ranges)) { - if (!Array.isArray(result.preview.matches)) { - console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same type.'); - return false; - } - - if ((result.preview.matches).length !== result.ranges.length) { - console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same length.'); - return false; - } - } else { - if (Array.isArray(result.preview.matches)) { - console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same length.'); - return false; - } - } - } - - return true; - } - - private getSearchOptionsForFolder(fq: IFolderQuery): TextSearchOptions { + private getSearchOptionsForFolder(fq: IFolderQuery): TextSearchProviderFolderOptions { const includes = resolvePatternsForProvider(this.query.includePattern, fq.includePattern); const excludes = excludeToGlobPattern(fq.excludePattern?.folder, resolvePatternsForProvider(this.query.excludePattern, fq.excludePattern?.pattern)); const options = { folder: URI.from(fq.folder), - excludes: excludes.map(e => typeof (e) === 'string' ? e : e.pattern), + excludes, includes, - useIgnoreFiles: !fq.disregardIgnoreFiles, - useGlobalIgnoreFiles: !fq.disregardGlobalIgnoreFiles, - useParentIgnoreFiles: !fq.disregardParentIgnoreFiles, + useIgnoreFiles: { + local: !fq.disregardIgnoreFiles, + parent: !fq.disregardParentIgnoreFiles, + global: !fq.disregardGlobalIgnoreFiles + }, followSymlinks: !fq.ignoreSymlinks, - encoding: fq.fileEncoding && this.fileUtils.toCanonicalName(fq.fileEncoding), - maxFileSize: this.query.maxFileSize, - maxResults: this.query.maxResults ?? DEFAULT_MAX_SEARCH_RESULTS, - previewOptions: this.query.previewOptions, - afterContext: this.query.surroundingContext, - beforeContext: this.query.surroundingContext, + encoding: (fq.fileEncoding && this.fileUtils.toCanonicalName(fq.fileEncoding)) ?? '', }; - if ('usePCRE2' in this.query) { - (options).usePCRE2 = this.query.usePCRE2; - } return options; } } -function patternInfoToQuery(patternInfo: IPatternInfo): TextSearchQuery { +function patternInfoToQuery(patternInfo: IPatternInfo): TextSearchQueryNew { return { isCaseSensitive: patternInfo.isCaseSensitive || false, isRegExp: patternInfo.isRegExp || false, @@ -243,7 +223,7 @@ export class TextSearchResultsCollector { this._batchedCollector = new BatchedCollector(512, items => this.sendItems(items)); } - add(data: TextSearchResult, folderIdx: number): void { + add(data: TextSearchResultNew, folderIdx: number): void { // Collects TextSearchResults into IInternalFileMatches and collates using BatchedCollector. // This is efficient for ripgrep which sends results back one file at a time. It wouldn't be efficient for other search // providers that send results in random order. We could do this step afterwards instead. @@ -280,25 +260,25 @@ export class TextSearchResultsCollector { } } -function extensionResultToFrontendResult(data: TextSearchResult): ITextSearchResult { +function extensionResultToFrontendResult(data: TextSearchResultNew): ITextSearchResult { // Warning: result from RipgrepTextSearchEH has fake Range. Don't depend on any other props beyond these... - if (extensionResultIsMatch(data)) { + if (data instanceof TextSearchMatchNew) { return { - preview: { - matches: mapArrayOrNot(data.preview.matches, m => ({ - startLineNumber: m.start.line, - startColumn: m.start.character, - endLineNumber: m.end.line, - endColumn: m.end.character - })), - text: data.preview.text - }, - ranges: mapArrayOrNot(data.ranges, r => ({ - startLineNumber: r.start.line, - startColumn: r.start.character, - endLineNumber: r.end.line, - endColumn: r.end.character - })) + previewText: data.previewText, + rangeLocations: data.ranges.map(r => ({ + preview: { + startLineNumber: r.previewRange.start.line, + startColumn: r.previewRange.start.character, + endLineNumber: r.previewRange.end.line, + endColumn: r.previewRange.end.character + } satisfies ISearchRange, + source: { + startLineNumber: r.sourceRange.start.line, + startColumn: r.sourceRange.start.character, + endLineNumber: r.sourceRange.end.line, + endColumn: r.sourceRange.end.character + } satisfies ISearchRange, + })), } satisfies ITextSearchMatch; } else { return { @@ -308,9 +288,6 @@ function extensionResultToFrontendResult(data: TextSearchResult): ITextSearchRes } } -export function extensionResultIsMatch(data: TextSearchResult): data is TextSearchMatch { - return !!(data).preview; -} /** * Collects items that have a size - before the cumulative size of collected items reaches START_BATCH_AFTER_COUNT, the callback is called for every diff --git a/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts b/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts index bff10b050d6cf..3325258033ee5 100644 --- a/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts +++ b/src/vs/workbench/services/search/node/ripgrepSearchProvider.ts @@ -6,34 +6,49 @@ import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { OutputChannel } from 'vs/workbench/services/search/node/ripgrepSearchUtils'; import { RipgrepTextSearchEngine } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine'; -import { TextSearchProvider, TextSearchComplete, TextSearchResult, TextSearchQuery, TextSearchOptions, } from 'vs/workbench/services/search/common/searchExtTypes'; +import { TextSearchProviderNew, TextSearchCompleteNew, TextSearchResultNew, TextSearchQueryNew, TextSearchProviderOptions, } from 'vs/workbench/services/search/common/searchExtTypes'; import { Progress } from 'vs/platform/progress/common/progress'; import { Schemas } from 'vs/base/common/network'; import type { RipgrepTextSearchOptions } from 'vs/workbench/services/search/common/searchExtTypesInternal'; -export class RipgrepSearchProvider implements TextSearchProvider { +export class RipgrepSearchProvider implements TextSearchProviderNew { private inProgress: Set = new Set(); constructor(private outputChannel: OutputChannel, private getNumThreads: () => Promise) { process.once('exit', () => this.dispose()); } - async provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Promise { + async provideTextSearchResults(query: TextSearchQueryNew, options: TextSearchProviderOptions, progress: Progress, token: CancellationToken): Promise { const numThreads = await this.getNumThreads(); const engine = new RipgrepTextSearchEngine(this.outputChannel, numThreads); - const extendedOptions: RipgrepTextSearchOptions = { - ...options, - numThreads, - }; - if (options.folder.scheme === Schemas.vscodeUserData) { - // Ripgrep search engine can only provide file-scheme results, but we want to use it to search some schemes that are backed by the filesystem, but with some other provider as the frontend, - // case in point vscode-userdata. In these cases we translate the query to a file, and translate the results back to the frontend scheme. - const translatedOptions = { ...extendedOptions, folder: options.folder.with({ scheme: Schemas.file }) }; - const progressTranslator = new Progress(data => progress.report({ ...data, uri: data.uri.with({ scheme: options.folder.scheme }) })); - return this.withToken(token, token => engine.provideTextSearchResults(query, translatedOptions, progressTranslator, token)); - } else { - return this.withToken(token, token => engine.provideTextSearchResults(query, extendedOptions, progress, token)); - } + + return Promise.all(options.folderOptions.map(folderOption => { + + const extendedOptions: RipgrepTextSearchOptions = { + folderOptions: folderOption, + numThreads, + maxResults: options.maxResults, + previewOptions: options.previewOptions, + maxFileSize: options.maxFileSize, + surroundingContext: options.surroundingContext + }; + if (folderOption.folder.scheme === Schemas.vscodeUserData) { + // Ripgrep search engine can only provide file-scheme results, but we want to use it to search some schemes that are backed by the filesystem, but with some other provider as the frontend, + // case in point vscode-userdata. In these cases we translate the query to a file, and translate the results back to the frontend scheme. + const translatedOptions = { ...extendedOptions, folder: folderOption.folder.with({ scheme: Schemas.file }) }; + const progressTranslator = new Progress(data => progress.report({ ...data, uri: data.uri.with({ scheme: folderOption.folder.scheme }) })); + return this.withToken(token, token => engine.provideTextSearchResultsWithRgOptions(query, translatedOptions, progressTranslator, token)); + } else { + return this.withToken(token, token => engine.provideTextSearchResultsWithRgOptions(query, extendedOptions, progress, token)); + } + })).then((e => { + const complete: TextSearchCompleteNew = { + // todo: get this to actually check + limitHit: e.some(complete => !!complete && complete.limitHit) + }; + return complete; + })); + } private async withToken(token: CancellationToken, fn: (token: CancellationToken) => Promise): Promise { diff --git a/src/vs/workbench/services/search/node/ripgrepSearchUtils.ts b/src/vs/workbench/services/search/node/ripgrepSearchUtils.ts index f9901923001f2..1a7b467f49331 100644 --- a/src/vs/workbench/services/search/node/ripgrepSearchUtils.ts +++ b/src/vs/workbench/services/search/node/ripgrepSearchUtils.ts @@ -3,10 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { mapArrayOrNot } from 'vs/base/common/arrays'; -import { URI } from 'vs/base/common/uri'; import { ILogService } from 'vs/platform/log/common/log'; -import { SearchRange, TextSearchMatch } from 'vs/workbench/services/search/common/search'; +import { SearchRange } from 'vs/workbench/services/search/common/search'; import * as searchExtTypes from 'vs/workbench/services/search/common/searchExtTypes'; export type Maybe = T | null | undefined; @@ -15,29 +13,11 @@ export function anchorGlob(glob: string): string { return glob.startsWith('**') || glob.startsWith('/') ? glob : `/${glob}`; } -/** - * Create a vscode.TextSearchMatch by using our internal TextSearchMatch type for its previewOptions logic. - */ -export function createTextSearchResult(uri: URI, text: string, range: searchExtTypes.Range | searchExtTypes.Range[], previewOptions?: searchExtTypes.TextSearchPreviewOptions): searchExtTypes.TextSearchMatch { - const searchRange = mapArrayOrNot(range, rangeToSearchRange); - - const internalResult = new TextSearchMatch(text, searchRange, previewOptions); - const internalPreviewRange = internalResult.preview.matches; - return { - ranges: mapArrayOrNot(searchRange, searchRangeToRange), - uri, - preview: { - text: internalResult.preview.text, - matches: mapArrayOrNot(internalPreviewRange, searchRangeToRange) - } - }; -} - -function rangeToSearchRange(range: searchExtTypes.Range): SearchRange { +export function rangeToSearchRange(range: searchExtTypes.Range): SearchRange { return new SearchRange(range.start.line, range.start.character, range.end.line, range.end.character); } -function searchRangeToRange(range: SearchRange): searchExtTypes.Range { +export function searchRangeToRange(range: SearchRange): searchExtTypes.Range { return new searchExtTypes.Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); } diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts index 4740a66111967..838781af9d3ee 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts @@ -6,19 +6,20 @@ import * as cp from 'child_process'; import { EventEmitter } from 'events'; import { StringDecoder } from 'string_decoder'; -import { coalesce } from 'vs/base/common/arrays'; +import { coalesce, mapArrayOrNot } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { groupBy } from 'vs/base/common/collections'; import { splitGlobAware } from 'vs/base/common/glob'; import { createRegExp, escapeRegExpCharacters } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { Progress } from 'vs/platform/progress/common/progress'; -import { IExtendedExtensionSearchOptions, SearchError, SearchErrorCode, serializeSearchError } from 'vs/workbench/services/search/common/search'; -import { Range, TextSearchComplete, TextSearchContext, TextSearchMatch, TextSearchOptions, TextSearchPreviewOptions, TextSearchQuery, TextSearchResult } from 'vs/workbench/services/search/common/searchExtTypes'; +import { DEFAULT_MAX_SEARCH_RESULTS, IExtendedExtensionSearchOptions, ITextSearchPreviewOptions, SearchError, SearchErrorCode, serializeSearchError, TextSearchMatch } from 'vs/workbench/services/search/common/search'; +import { Range, TextSearchCompleteNew, TextSearchContextNew, TextSearchMatchNew, TextSearchProviderOptions, TextSearchQueryNew, TextSearchResultNew } from 'vs/workbench/services/search/common/searchExtTypes'; import { AST as ReAST, RegExpParser, RegExpVisitor } from 'vscode-regexpp'; import { rgPath } from '@vscode/ripgrep'; -import { anchorGlob, createTextSearchResult, IOutputChannel, Maybe } from './ripgrepSearchUtils'; +import { anchorGlob, IOutputChannel, Maybe, rangeToSearchRange, searchRangeToRange } from './ripgrepSearchUtils'; import type { RipgrepTextSearchOptions } from 'vs/workbench/services/search/common/searchExtTypesInternal'; +import { newToOldPreviewOptions } from 'vs/workbench/services/search/common/searchExtConversionTypes'; // 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'); @@ -27,11 +28,31 @@ export class RipgrepTextSearchEngine { constructor(private outputChannel: IOutputChannel, private readonly _numThreads?: number | undefined) { } - provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): Promise { + provideTextSearchResults(query: TextSearchQueryNew, options: TextSearchProviderOptions, progress: Progress, token: CancellationToken): Promise { + return Promise.all(options.folderOptions.map(folderOption => { + const extendedOptions: RipgrepTextSearchOptions = { + folderOptions: folderOption, + numThreads: this._numThreads, + maxResults: options.maxResults, + previewOptions: options.previewOptions, + maxFileSize: options.maxFileSize, + surroundingContext: options.surroundingContext + }; + return this.provideTextSearchResultsWithRgOptions(query, extendedOptions, progress, token); + })).then((e => { + const complete: TextSearchCompleteNew = { + // todo: get this to actually check + limitHit: e.some(complete => !!complete && complete.limitHit) + }; + return complete; + })); + } + + provideTextSearchResultsWithRgOptions(query: TextSearchQueryNew, options: RipgrepTextSearchOptions, progress: Progress, token: CancellationToken): Promise { this.outputChannel.appendLine(`provideTextSearchResults ${query.pattern}, ${JSON.stringify({ ...options, ...{ - folder: options.folder.toString() + folder: options.folderOptions.folder.toString() } })}`); @@ -44,7 +65,7 @@ export class RipgrepTextSearchEngine { }; const rgArgs = getRgArgs(query, extendedOptions); - const cwd = options.folder.fsPath; + const cwd = options.folderOptions.folder.fsPath; const escapedArgs = rgArgs .map(arg => arg.match(/^-/) ? arg : `'${arg}'`) @@ -59,8 +80,8 @@ export class RipgrepTextSearchEngine { }); let gotResult = false; - const ripgrepParser = new RipgrepParser(options.maxResults, options.folder, options.previewOptions); - ripgrepParser.on('result', (match: TextSearchResult) => { + const ripgrepParser = new RipgrepParser(options.maxResults ?? DEFAULT_MAX_SEARCH_RESULTS, options.folderOptions.folder, newToOldPreviewOptions(options.previewOptions)); + ripgrepParser.on('result', (match: TextSearchResultNew) => { gotResult = true; dataWithoutResult = ''; progress.report(match); @@ -188,7 +209,7 @@ export class RipgrepParser extends EventEmitter { private numResults = 0; - constructor(private maxResults: number, private root: URI, private previewOptions?: TextSearchPreviewOptions) { + constructor(private maxResults: number, private root: URI, private previewOptions?: ITextSearchPreviewOptions) { super(); this.stringDecoder = new StringDecoder(); } @@ -202,7 +223,7 @@ export class RipgrepParser extends EventEmitter { } - override on(event: 'result', listener: (result: TextSearchResult) => void): this; + override on(event: 'result', listener: (result: TextSearchResultNew) => void): this; override on(event: 'hitLimit', listener: () => void): this; override on(event: string, listener: (...args: any[]) => void): this { super.on(event, listener); @@ -243,6 +264,7 @@ export class RipgrepParser extends EventEmitter { this.remainder = dataStr.substring(prevIdx); } + private handleLine(outputLine: string): void { if (this.isDone || !outputLine) { return; @@ -268,12 +290,12 @@ export class RipgrepParser extends EventEmitter { } else if (parsedLine.type === 'context') { const contextPath = bytesOrTextToString(parsedLine.data.path); const uri = URI.joinPath(this.root, contextPath); - const result = this.createTextSearchContext(parsedLine.data, uri); + const result = this.createTextSearchContexts(parsedLine.data, uri); result.forEach(r => this.onResult(r)); } } - private createTextSearchMatch(data: IRgMatch, uri: URI): TextSearchMatch { + private createTextSearchMatch(data: IRgMatch, uri: URI): TextSearchMatchNew { const lineNumber = data.line_number - 1; const fullText = bytesOrTextToString(data.lines); const fullTextBytes = Buffer.from(fullText); @@ -326,25 +348,30 @@ export class RipgrepParser extends EventEmitter { return new Range(startLineNumber, startCol, endLineNumber, endCol); })); - return createTextSearchResult(uri, fullText, ranges, this.previewOptions); + const searchRange = mapArrayOrNot(ranges, rangeToSearchRange); + + const internalResult = new TextSearchMatch(fullText, searchRange, this.previewOptions); + return new TextSearchMatchNew( + uri, + internalResult.rangeLocations.map(e => ( + { + sourceRange: searchRangeToRange(e.source), + previewRange: searchRangeToRange(e.preview), + } + )), + internalResult.previewText); } - private createTextSearchContext(data: IRgMatch, uri: URI): TextSearchContext[] { + private createTextSearchContexts(data: IRgMatch, uri: URI): TextSearchContextNew[] { const text = bytesOrTextToString(data.lines); const startLine = data.line_number; return text .replace(/\r?\n$/, '') .split('\n') - .map((line, i) => { - return { - text: line, - uri, - lineNumber: startLine + i - }; - }); + .map((line, i) => new TextSearchContextNew(uri, line, startLine + i)); } - private onResult(match: TextSearchResult): void { + private onResult(match: TextSearchResultNew): void { this.emit('result', match); } } @@ -373,12 +400,12 @@ function getNumLinesAndLastNewlineLength(text: string): { numLines: number; last } // exported for testing -export function getRgArgs(query: TextSearchQuery, options: RipgrepTextSearchOptions): string[] { +export function getRgArgs(query: TextSearchQueryNew, options: RipgrepTextSearchOptions): string[] { const args = ['--hidden', '--no-require-git']; args.push(query.isCaseSensitive ? '--case-sensitive' : '--ignore-case'); const { doubleStarIncludes, otherIncludes } = groupBy( - options.includes, + options.folderOptions.includes, (include: string) => include.startsWith('**') ? 'doubleStarIncludes' : 'otherIncludes'); if (otherIncludes && otherIncludes.length) { @@ -402,7 +429,7 @@ export function getRgArgs(query: TextSearchQuery, options: RipgrepTextSearchOpti }); } - options.excludes + options.folderOptions.excludes.map(e => typeof (e) === 'string' ? e : e.pattern) .map(anchorGlob) .forEach(rgGlob => args.push('-g', `!${rgGlob}`)); @@ -410,8 +437,8 @@ export function getRgArgs(query: TextSearchQuery, options: RipgrepTextSearchOpti args.push('--max-filesize', options.maxFileSize + ''); } - if (options.useIgnoreFiles) { - if (!options.useParentIgnoreFiles) { + if (options.folderOptions.useIgnoreFiles.local) { + if (!options.folderOptions.useIgnoreFiles.parent) { args.push('--no-ignore-parent'); } } else { @@ -419,12 +446,12 @@ export function getRgArgs(query: TextSearchQuery, options: RipgrepTextSearchOpti args.push('--no-ignore'); } - if (options.followSymlinks) { + if (options.folderOptions.followSymlinks) { args.push('--follow'); } - if (options.encoding && options.encoding !== 'utf8') { - args.push('--encoding', options.encoding); + if (options.folderOptions.encoding && options.folderOptions.encoding !== 'utf8') { + args.push('--encoding', options.folderOptions.encoding); } if (options.numThreads) { @@ -470,7 +497,7 @@ export function getRgArgs(query: TextSearchQuery, options: RipgrepTextSearchOpti } args.push('--no-config'); - if (!options.useGlobalIgnoreFiles) { + if (!options.folderOptions.useIgnoreFiles.global) { args.push('--no-ignore-global'); } @@ -480,12 +507,9 @@ export function getRgArgs(query: TextSearchQuery, options: RipgrepTextSearchOpti args.push('--multiline'); } - if (options.beforeContext) { - args.push('--before-context', options.beforeContext + ''); - } - - if (options.afterContext) { - args.push('--after-context', options.afterContext + ''); + if (options.surroundingContext) { + args.push('--before-context', options.surroundingContext + ''); + args.push('--after-context', options.surroundingContext + ''); } // Folder to search diff --git a/src/vs/workbench/services/search/node/textSearchAdapter.ts b/src/vs/workbench/services/search/node/textSearchAdapter.ts index 58145f0af5639..dd42e98acf403 100644 --- a/src/vs/workbench/services/search/node/textSearchAdapter.ts +++ b/src/vs/workbench/services/search/node/textSearchAdapter.ts @@ -5,7 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import * as pfs from 'vs/base/node/pfs'; -import { IFileMatch, IProgressMessage, ITextQuery, ITextSearchMatch, ISerializedFileMatch, ISerializedSearchSuccess } from 'vs/workbench/services/search/common/search'; +import { IFileMatch, IProgressMessage, ITextQuery, ITextSearchMatch, ISerializedFileMatch, ISerializedSearchSuccess, resultIsMatch } from 'vs/workbench/services/search/common/search'; import { RipgrepTextSearchEngine } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine'; import { NativeTextSearchManager } from 'vs/workbench/services/search/node/textSearchManager'; @@ -50,9 +50,9 @@ function fileMatchToSerialized(match: IFileMatch): ISerializedFileMatch { path: match.resource && match.resource.fsPath, results: match.results, numMatches: (match.results || []).reduce((sum, r) => { - if (!!(r).ranges) { + if (resultIsMatch(r)) { const m = r; - return sum + (Array.isArray(m.ranges) ? m.ranges.length : 1); + return sum + m.rangeLocations.length; } else { return sum + 1; } diff --git a/src/vs/workbench/services/search/node/textSearchManager.ts b/src/vs/workbench/services/search/node/textSearchManager.ts index 34cf4cce31174..0da430976f261 100644 --- a/src/vs/workbench/services/search/node/textSearchManager.ts +++ b/src/vs/workbench/services/search/node/textSearchManager.ts @@ -6,12 +6,12 @@ import { toCanonicalName } from 'vs/workbench/services/textfile/common/encoding'; import * as pfs from 'vs/base/node/pfs'; import { ITextQuery, ITextSearchStats } from 'vs/workbench/services/search/common/search'; -import { TextSearchProvider } from 'vs/workbench/services/search/common/searchExtTypes'; +import { TextSearchProviderNew } from 'vs/workbench/services/search/common/searchExtTypes'; import { TextSearchManager } from 'vs/workbench/services/search/common/textSearchManager'; export class NativeTextSearchManager extends TextSearchManager { - constructor(query: ITextQuery, provider: TextSearchProvider, _pfs: typeof pfs = pfs, processType: ITextSearchStats['type'] = 'searchProcess') { + constructor(query: ITextQuery, provider: TextSearchProviderNew, _pfs: typeof pfs = pfs, processType: ITextSearchStats['type'] = 'searchProcess') { super({ query, provider }, { readdir: resource => _pfs.Promises.readdir(resource.fsPath), toCanonicalName: name => toCanonicalName(name) diff --git a/src/vs/workbench/services/search/test/common/search.test.ts b/src/vs/workbench/services/search/test/common/search.test.ts index 5f0ec330f8ab4..81ace1f9e722e 100644 --- a/src/vs/workbench/services/search/test/common/search.test.ts +++ b/src/vs/workbench/services/search/test/common/search.test.ts @@ -14,59 +14,64 @@ suite('TextSearchResult', () => { }; function assertOneLinePreviewRangeText(text: string, result: TextSearchMatch): void { + assert.strictEqual(result.rangeLocations.length, 1); assert.strictEqual( - result.preview.text.substring((result.preview.matches).startColumn, (result.preview.matches).endColumn), + result.previewText.substring((result.rangeLocations[0].preview).startColumn, (result.rangeLocations[0].preview).endColumn), text); } + function getFirstSourceFromResult(result: TextSearchMatch): OneLineRange { + return result.rangeLocations.map(e => e.source)[0]; + } + ensureNoDisposablesAreLeakedInTestSuite(); test('empty without preview options', () => { const range = new OneLineRange(5, 0, 0); const result = new TextSearchMatch('', range); - assert.deepStrictEqual(result.ranges, range); + assert.deepStrictEqual(getFirstSourceFromResult(result), range); assertOneLinePreviewRangeText('', result); }); test('empty with preview options', () => { const range = new OneLineRange(5, 0, 0); const result = new TextSearchMatch('', range, previewOptions1); - assert.deepStrictEqual(result.ranges, range); + assert.deepStrictEqual(getFirstSourceFromResult(result), range); assertOneLinePreviewRangeText('', result); }); test('short without preview options', () => { const range = new OneLineRange(5, 4, 7); const result = new TextSearchMatch('foo bar', range); - assert.deepStrictEqual(result.ranges, range); + assert.deepStrictEqual(getFirstSourceFromResult(result), range); assertOneLinePreviewRangeText('bar', result); }); test('short with preview options', () => { const range = new OneLineRange(5, 4, 7); const result = new TextSearchMatch('foo bar', range, previewOptions1); - assert.deepStrictEqual(result.ranges, range); + assert.deepStrictEqual(getFirstSourceFromResult(result), range); assertOneLinePreviewRangeText('bar', result); }); test('leading', () => { const range = new OneLineRange(5, 25, 28); const result = new TextSearchMatch('long text very long text foo', range, previewOptions1); - assert.deepStrictEqual(result.ranges, range); + assert.deepStrictEqual(getFirstSourceFromResult(result), range); assertOneLinePreviewRangeText('foo', result); }); test('trailing', () => { const range = new OneLineRange(5, 0, 3); const result = new TextSearchMatch('foo long text very long text long text very long text long text very long text long text very long text long text very long text', range, previewOptions1); - assert.deepStrictEqual(result.ranges, range); + assert.deepStrictEqual(getFirstSourceFromResult(result), range); assertOneLinePreviewRangeText('foo', result); }); test('middle', () => { const range = new OneLineRange(5, 30, 33); const result = new TextSearchMatch('long text very long text long foo text very long text long text very long text long text very long text long text very long text', range, previewOptions1); - assert.deepStrictEqual(result.ranges, range); + assert.deepStrictEqual(getFirstSourceFromResult(result), range); assertOneLinePreviewRangeText('foo', result); }); @@ -78,7 +83,7 @@ suite('TextSearchResult', () => { const range = new OneLineRange(0, 4, 7); const result = new TextSearchMatch('foo bar', range, previewOptions); - assert.deepStrictEqual(result.ranges, range); + assert.deepStrictEqual(getFirstSourceFromResult(result), range); assertOneLinePreviewRangeText('b', result); }); @@ -90,12 +95,13 @@ suite('TextSearchResult', () => { const range = new SearchRange(5, 4, 6, 3); const result = new TextSearchMatch('foo bar\nfoo bar', range, previewOptions); - assert.deepStrictEqual(result.ranges, range); - assert.strictEqual(result.preview.text, 'foo bar\nfoo bar'); - assert.strictEqual((result.preview.matches).startLineNumber, 0); - assert.strictEqual((result.preview.matches).startColumn, 4); - assert.strictEqual((result.preview.matches).endLineNumber, 1); - assert.strictEqual((result.preview.matches).endColumn, 3); + assert.deepStrictEqual(getFirstSourceFromResult(result), range); + assert.strictEqual(result.previewText, 'foo bar\nfoo bar'); + assert.strictEqual(result.rangeLocations.length, 1); + assert.strictEqual(result.rangeLocations[0].preview.startLineNumber, 0); + assert.strictEqual(result.rangeLocations[0].preview.startColumn, 4); + assert.strictEqual(result.rangeLocations[0].preview.endLineNumber, 1); + assert.strictEqual(result.rangeLocations[0].preview.endColumn, 3); }); test('compacts multiple ranges on long lines', () => { @@ -108,8 +114,8 @@ suite('TextSearchResult', () => { const range2 = new SearchRange(5, 133, 5, 136); const range3 = new SearchRange(5, 141, 5, 144); const result = new TextSearchMatch('foo bar 123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 foo bar baz bar', [range1, range2, range3], previewOptions); - assert.deepStrictEqual(result.preview.matches, [new OneLineRange(0, 4, 7), new OneLineRange(0, 42, 45), new OneLineRange(0, 50, 53)]); - assert.strictEqual(result.preview.text, 'foo bar 123456⟪ 117 characters skipped ⟫o bar baz bar'); + assert.deepStrictEqual(result.rangeLocations.map(e => e.preview), [new OneLineRange(0, 4, 7), new OneLineRange(0, 42, 45), new OneLineRange(0, 50, 53)]); + assert.strictEqual(result.previewText, 'foo bar 123456⟪ 117 characters skipped ⟫o bar baz bar'); }); test('trims lines endings', () => { @@ -119,8 +125,8 @@ suite('TextSearchResult', () => { charsPerLine: 10000 }; - assert.strictEqual(new TextSearchMatch('foo bar\n', range, previewOptions).preview.text, 'foo bar'); - assert.strictEqual(new TextSearchMatch('foo bar\r\n', range, previewOptions).preview.text, 'foo bar'); + assert.strictEqual(new TextSearchMatch('foo bar\n', range, previewOptions).previewText, 'foo bar'); + assert.strictEqual(new TextSearchMatch('foo bar\r\n', range, previewOptions).previewText, 'foo bar'); }); // test('all lines of multiline match', () => { diff --git a/src/vs/workbench/services/search/test/common/searchHelpers.test.ts b/src/vs/workbench/services/search/test/common/searchHelpers.test.ts index 54c86c854024d..bf9cabe8706d3 100644 --- a/src/vs/workbench/services/search/test/common/searchHelpers.test.ts +++ b/src/vs/workbench/services/search/test/common/searchHelpers.test.ts @@ -39,9 +39,9 @@ suite('SearchHelpers', () => { test('simple', () => { const results = editorMatchesToTextSearchResults([new FindMatch(new Range(6, 1, 6, 2), null)], mockTextModel); assert.strictEqual(results.length, 1); - assert.strictEqual(results[0].preview.text, '6\n'); - assertRangesEqual(results[0].preview.matches, [new Range(0, 0, 0, 1)]); - assertRangesEqual(results[0].ranges, [new Range(5, 0, 5, 1)]); + assert.strictEqual(results[0].previewText, '6\n'); + assertRangesEqual(results[0].rangeLocations.map(e => e.preview), [new Range(0, 0, 0, 1)]); + assertRangesEqual(results[0].rangeLocations.map(e => e.source), [new Range(5, 0, 5, 1)]); }); test('multiple', () => { @@ -53,23 +53,23 @@ suite('SearchHelpers', () => { ], mockTextModel); assert.strictEqual(results.length, 2); - assertRangesEqual(results[0].preview.matches, [ + assertRangesEqual(results[0].rangeLocations.map(e => e.preview), [ new Range(0, 0, 0, 1), new Range(0, 3, 2, 1), ]); - assertRangesEqual(results[0].ranges, [ + assertRangesEqual(results[0].rangeLocations.map(e => e.source), [ new Range(5, 0, 5, 1), new Range(5, 3, 7, 1), ]); - assert.strictEqual(results[0].preview.text, '6\n7\n8\n'); + assert.strictEqual(results[0].previewText, '6\n7\n8\n'); - assertRangesEqual(results[1].preview.matches, [ + assertRangesEqual(results[1].rangeLocations.map(e => e.preview), [ new Range(0, 0, 1, 2), ]); - assertRangesEqual(results[1].ranges, [ + assertRangesEqual(results[1].rangeLocations.map(e => e.source), [ new Range(8, 0, 9, 2), ]); - assert.strictEqual(results[1].preview.text, '9\n10\n'); + assert.strictEqual(results[1].previewText, '9\n10\n'); }); }); @@ -102,11 +102,13 @@ suite('SearchHelpers', () => { test('no context', () => { const matches = [{ - preview: { - text: 'foo', - matches: new Range(0, 0, 0, 10) - }, - ranges: new Range(0, 0, 0, 10) + previewText: 'foo', + rangeLocations: [ + { + preview: new Range(0, 0, 0, 10), + source: new Range(0, 0, 0, 10) + } + ] }]; assert.deepStrictEqual(getTextSearchMatchWithModelContext(matches, mockTextModel, getQuery()), matches); @@ -114,12 +116,15 @@ suite('SearchHelpers', () => { test('simple', () => { const matches = [{ - preview: { - text: 'foo', - matches: new Range(0, 0, 0, 10) - }, - ranges: new Range(1, 0, 1, 10) - }]; + previewText: 'foo', + rangeLocations: [ + { + preview: new Range(0, 0, 0, 10), + source: new Range(1, 0, 1, 10) + } + ] + } + ]; assert.deepStrictEqual(getTextSearchMatchWithModelContext(matches, mockTextModel, getQuery(1)), [ { @@ -137,18 +142,22 @@ suite('SearchHelpers', () => { test('multiple matches next to each other', () => { const matches = [ { - preview: { - text: 'foo', - matches: new Range(0, 0, 0, 10) - }, - ranges: new Range(1, 0, 1, 10) + previewText: 'foo', + rangeLocations: [ + { + preview: new Range(0, 0, 0, 10), + source: new Range(1, 0, 1, 10) + } + ] }, { - preview: { - text: 'bar', - matches: new Range(0, 0, 0, 10) - }, - ranges: new Range(2, 0, 2, 10) + previewText: 'bar', + rangeLocations: [ + { + preview: new Range(0, 0, 0, 10), + source: new Range(2, 0, 2, 10) + } + ] }]; assert.deepStrictEqual(getTextSearchMatchWithModelContext(matches, mockTextModel, getQuery(1)), [ @@ -167,18 +176,22 @@ suite('SearchHelpers', () => { test('boundaries', () => { const matches = [ { - preview: { - text: 'foo', - matches: new Range(0, 0, 0, 10) - }, - ranges: new Range(0, 0, 0, 10) + previewText: 'foo', + rangeLocations: [ + { + preview: new Range(0, 0, 0, 10), + source: new Range(0, 0, 0, 10) + } + ] }, { - preview: { - text: 'bar', - matches: new Range(0, 0, 0, 10) - }, - ranges: new Range(MOCK_LINE_COUNT - 1, 0, MOCK_LINE_COUNT - 1, 10) + previewText: 'bar', + rangeLocations: [ + { + preview: new Range(0, 0, 0, 10), + source: new Range(MOCK_LINE_COUNT - 1, 0, MOCK_LINE_COUNT - 1, 10) + } + ] }]; assert.deepStrictEqual(getTextSearchMatchWithModelContext(matches, mockTextModel, getQuery(1)), [ diff --git a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngineUtils.test.ts b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngineUtils.test.ts index e93c4efb2691e..9a1c18534734e 100644 --- a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngineUtils.test.ts +++ b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngineUtils.test.ts @@ -7,8 +7,9 @@ import assert from 'assert'; import { joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { fixRegexNewline, IRgMatch, IRgMessage, RipgrepParser, unicodeEscapesToPCRE2, fixNewline, getRgArgs, performBraceExpansionForRipgrep } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine'; -import { Range, TextSearchOptions, TextSearchQuery, TextSearchResult } from 'vs/workbench/services/search/common/searchExtTypes'; +import { Range, TextSearchMatchNew, TextSearchQueryNew, TextSearchResultNew } from 'vs/workbench/services/search/common/searchExtTypes'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { RipgrepTextSearchOptions } from 'vs/workbench/services/search/common/searchExtTypesInternal'; suite('RipgrepTextSearchEngine', () => { ensureNoDisposablesAreLeakedInTestSuite(); @@ -104,10 +105,10 @@ suite('RipgrepTextSearchEngine', () => { suite('RipgrepParser', () => { const TEST_FOLDER = URI.file('/foo/bar'); - function testParser(inputData: string[], expectedResults: TextSearchResult[]): void { + function testParser(inputData: string[], expectedResults: TextSearchResultNew[]): void { const testParser = new RipgrepParser(1000, TEST_FOLDER); - const actualResults: TextSearchResult[] = []; + const actualResults: TextSearchResultNew[] = []; testParser.on('result', r => { actualResults.push(r); }); @@ -146,14 +147,14 @@ suite('RipgrepTextSearchEngine', () => { makeRgMatch('file1.js', 'foobar', 4, [{ start: 3, end: 6 }]) ], [ - { - preview: { - text: 'foobar', - matches: [new Range(0, 3, 0, 6)] - }, - uri: joinPath(TEST_FOLDER, 'file1.js'), - ranges: [new Range(3, 3, 3, 6)] - } + new TextSearchMatchNew( + joinPath(TEST_FOLDER, 'file1.js'), + [{ + previewRange: new Range(0, 3, 0, 6), + sourceRange: new Range(3, 3, 3, 6), + }], + 'foobar' + ) ]); }); @@ -165,30 +166,30 @@ suite('RipgrepTextSearchEngine', () => { makeRgMatch('app2/file3.js', 'foobar', 4, [{ start: 3, end: 6 }]), ], [ - { - preview: { - text: 'foobar', - matches: [new Range(0, 3, 0, 6)] - }, - uri: joinPath(TEST_FOLDER, 'file1.js'), - ranges: [new Range(3, 3, 3, 6)] - }, - { - preview: { - text: 'foobar', - matches: [new Range(0, 3, 0, 6)] - }, - uri: joinPath(TEST_FOLDER, 'app/file2.js'), - ranges: [new Range(3, 3, 3, 6)] - }, - { - preview: { - text: 'foobar', - matches: [new Range(0, 3, 0, 6)] - }, - uri: joinPath(TEST_FOLDER, 'app2/file3.js'), - ranges: [new Range(3, 3, 3, 6)] - } + new TextSearchMatchNew( + joinPath(TEST_FOLDER, 'file1.js'), + [{ + previewRange: new Range(0, 3, 0, 6), + sourceRange: new Range(3, 3, 3, 6), + }], + 'foobar' + ), + new TextSearchMatchNew( + joinPath(TEST_FOLDER, 'app/file2.js'), + [{ + previewRange: new Range(0, 3, 0, 6), + sourceRange: new Range(3, 3, 3, 6), + }], + 'foobar' + ), + new TextSearchMatchNew( + joinPath(TEST_FOLDER, 'app2/file3.js'), + [{ + previewRange: new Range(0, 3, 0, 6), + sourceRange: new Range(3, 3, 3, 6), + }], + 'foobar' + ) ]); }); @@ -210,30 +211,30 @@ suite('RipgrepTextSearchEngine', () => { dataStrs[2].substring(25) ], [ - { - preview: { - text: 'foo bar', - matches: [new Range(0, 3, 0, 7)] - }, - uri: joinPath(TEST_FOLDER, 'file1.js'), - ranges: [new Range(3, 3, 3, 7)] - }, - { - preview: { - text: 'foobar', - matches: [new Range(0, 3, 0, 6)] - }, - uri: joinPath(TEST_FOLDER, 'app/file2.js'), - ranges: [new Range(3, 3, 3, 6)] - }, - { - preview: { - text: 'foobar', - matches: [new Range(0, 3, 0, 6)] - }, - uri: joinPath(TEST_FOLDER, 'app2/file3.js'), - ranges: [new Range(3, 3, 3, 6)] - } + new TextSearchMatchNew( + joinPath(TEST_FOLDER, 'file1.js'), + [{ + previewRange: new Range(0, 3, 0, 7), + sourceRange: new Range(3, 3, 3, 7), + }], + 'foo bar' + ), + new TextSearchMatchNew( + joinPath(TEST_FOLDER, 'app/file2.js'), + [{ + previewRange: new Range(0, 3, 0, 6), + sourceRange: new Range(3, 3, 3, 6), + }], + 'foobar' + ), + new TextSearchMatchNew( + joinPath(TEST_FOLDER, 'app2/file3.js'), + [{ + previewRange: new Range(0, 3, 0, 6), + sourceRange: new Range(3, 3, 3, 6), + }], + 'foobar' + ) ]); }); @@ -245,22 +246,26 @@ suite('RipgrepTextSearchEngine', () => { makeRgMatch('file1.js', '', 5, []), ], [ - { - preview: { - text: 'foobar', - matches: [new Range(0, 0, 0, 1)] - }, - uri: joinPath(TEST_FOLDER, 'file1.js'), - ranges: [new Range(3, 0, 3, 1)] - }, - { - preview: { - text: '', - matches: [new Range(0, 0, 0, 0)] - }, - uri: joinPath(TEST_FOLDER, 'file1.js'), - ranges: [new Range(4, 0, 4, 0)] - } + new TextSearchMatchNew( + joinPath(TEST_FOLDER, 'file1.js'), + [ + { + previewRange: new Range(0, 0, 0, 1), + sourceRange: new Range(3, 0, 3, 1), + } + ], + 'foobar' + ), + new TextSearchMatchNew( + joinPath(TEST_FOLDER, 'file1.js'), + [ + { + previewRange: new Range(0, 0, 0, 0), + sourceRange: new Range(4, 0, 4, 0), + } + ], + '' + ) ]); }); @@ -270,14 +275,20 @@ suite('RipgrepTextSearchEngine', () => { makeRgMatch('file1.js', 'foobarbazquux', 4, [{ start: 0, end: 4 }, { start: 6, end: 10 }]), ], [ - { - preview: { - text: 'foobarbazquux', - matches: [new Range(0, 0, 0, 4), new Range(0, 6, 0, 10)] - }, - uri: joinPath(TEST_FOLDER, 'file1.js'), - ranges: [new Range(3, 0, 3, 4), new Range(3, 6, 3, 10)] - } + new TextSearchMatchNew( + joinPath(TEST_FOLDER, 'file1.js'), + [ + { + previewRange: new Range(0, 0, 0, 4), + sourceRange: new Range(3, 0, 3, 4), + }, + { + previewRange: new Range(0, 6, 0, 10), + sourceRange: new Range(3, 6, 3, 10), + } + ], + 'foobarbazquux' + ) ]); }); @@ -287,14 +298,20 @@ suite('RipgrepTextSearchEngine', () => { makeRgMatch('file1.js', 'foo\nbar\nbaz\nquux', 4, [{ start: 0, end: 5 }, { start: 8, end: 13 }]), ], [ - { - preview: { - text: 'foo\nbar\nbaz\nquux', - matches: [new Range(0, 0, 1, 1), new Range(2, 0, 3, 1)] - }, - uri: joinPath(TEST_FOLDER, 'file1.js'), - ranges: [new Range(3, 0, 4, 1), new Range(5, 0, 6, 1)] - } + new TextSearchMatchNew( + joinPath(TEST_FOLDER, 'file1.js'), + [ + { + previewRange: new Range(0, 0, 1, 1), + sourceRange: new Range(3, 0, 4, 1), + }, + { + previewRange: new Range(2, 0, 3, 1), + sourceRange: new Range(5, 0, 6, 1), + } + ], + 'foo\nbar\nbaz\nquux' + ) ]); }); }); @@ -303,19 +320,24 @@ suite('RipgrepTextSearchEngine', () => { test('simple includes', () => { // Only testing the args that come from includes. function testGetRgArgs(includes: string[], expectedFromIncludes: string[]): void { - const query: TextSearchQuery = { + const query: TextSearchQueryNew = { pattern: 'test' }; - const options: TextSearchOptions = { - includes: includes, - excludes: [], + const options: RipgrepTextSearchOptions = { + folderOptions: { + includes: includes, + excludes: [], + useIgnoreFiles: { + local: false, + global: false, + parent: false + }, + followSymlinks: false, + folder: URI.file('/some/folder'), + encoding: 'utf8', + }, maxResults: 1000, - useIgnoreFiles: false, - followSymlinks: false, - useGlobalIgnoreFiles: false, - useParentIgnoreFiles: false, - folder: URI.file('/some/folder') }; const expected = [ '--hidden', diff --git a/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts b/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts index bdd1d36661812..97cafd0ec901d 100644 --- a/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts +++ b/src/vs/workbench/services/search/test/node/textSearch.integrationTest.ts @@ -312,7 +312,7 @@ flakySuite('TextSearch-integration', function () { }; return doSearchTest(config, 1).then(results => { - const matchRange = (results[0].results![0]).ranges; + const matchRange = (results[0].results![0]).rangeLocations.map(e => e.source); assert.deepStrictEqual(matchRange, [{ startLineNumber: 0, startColumn: 1, @@ -333,7 +333,7 @@ flakySuite('TextSearch-integration', function () { assert.strictEqual(results.length, 3); assert.strictEqual(results[0].results!.length, 1); const match = results[0].results![0]; - assert.strictEqual((match.ranges).length, 5); + assert.strictEqual((match.rangeLocations.map(e => e.source)).length, 5); }); }); diff --git a/src/vs/workbench/services/search/test/node/textSearchManager.test.ts b/src/vs/workbench/services/search/test/node/textSearchManager.test.ts index 1eb64535a8d47..59f3750bf86a3 100644 --- a/src/vs/workbench/services/search/test/node/textSearchManager.test.ts +++ b/src/vs/workbench/services/search/test/node/textSearchManager.test.ts @@ -9,15 +9,15 @@ import { URI } from 'vs/base/common/uri'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; import { Progress } from 'vs/platform/progress/common/progress'; import { ITextQuery, QueryType } from 'vs/workbench/services/search/common/search'; -import { ProviderResult, TextSearchComplete, TextSearchOptions, TextSearchProvider, TextSearchQuery, TextSearchResult } from 'vs/workbench/services/search/common/searchExtTypes'; +import { ProviderResult, TextSearchCompleteNew, TextSearchProviderOptions, TextSearchProviderNew, TextSearchQueryNew, TextSearchResultNew } from 'vs/workbench/services/search/common/searchExtTypes'; import { NativeTextSearchManager } from 'vs/workbench/services/search/node/textSearchManager'; suite('NativeTextSearchManager', () => { test('fixes encoding', async () => { let correctEncoding = false; - const provider: TextSearchProvider = { - provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): ProviderResult { - correctEncoding = options.encoding === 'windows-1252'; + const provider: TextSearchProviderNew = { + provideTextSearchResults(query: TextSearchQueryNew, options: TextSearchProviderOptions, progress: Progress, token: CancellationToken): ProviderResult { + correctEncoding = options.folderOptions[0].encoding === 'windows-1252'; return null; } diff --git a/src/vscode-dts/vscode.proposed.textSearchProviderNew.d.ts b/src/vscode-dts/vscode.proposed.textSearchProviderNew.d.ts index 1c9a66d83765c..146f070819889 100644 --- a/src/vscode-dts/vscode.proposed.textSearchProviderNew.d.ts +++ b/src/vscode-dts/vscode.proposed.textSearchProviderNew.d.ts @@ -113,25 +113,25 @@ declare module 'vscode' { /** * Options to specify the size of the result text preview. */ - previewOptions: { + previewOptions?: { /** * The maximum number of lines in the preview. * Only search providers that support multiline search will ever return more than one line in the match. - * Defaults to 100. + * Defaults to 100 in the default ripgrep search provider, but extension-contributed providers can enforce their own default. */ - matchLines: number; + matchLines?: number; /** * The maximum number of characters included per line. - * Defaults to 10000. + * Defaults to 10000 in the default ripgrep search provider, but extension-contributed providers can enforce their own default. */ - charsPerLine: number; + charsPerLine?: number; }; /** * Exclude files larger than `maxFileSize` in bytes. */ - maxFileSize: number; + maxFileSize?: number; /** * Number of lines of context to include before and after each match.