diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index d39bf7de5e0..4e69ba9023a 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -14,11 +14,14 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ILogService } from 'vs/platform/log/common/log'; import { NotebookDto } from 'vs/workbench/api/browser/mainThreadNotebookDto'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; -import { INotebookCellStatusBarItemProvider, INotebookContributionData, NotebookData, NotebookExtensionDescription, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookCellStatusBarItemProvider, INotebookContributionData, INotebookExclusiveDocumentFilter, NotebookData, NotebookExtensionDescription, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { ExtHostContext, ExtHostNotebookShape, MainContext, MainThreadNotebookShape } from '../common/extHost.protocol'; +import { IRelativePattern } from 'vs/base/common/glob'; +import { INotebookFileMatchNoModel } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; +import { revive } from 'vs/base/common/marshalling'; @extHostNamedCustomer(MainContext.MainThreadNotebook) export class MainThreadNotebooks implements MainThreadNotebookShape { @@ -81,6 +84,28 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { resource: uri }; }, + searchInNotebooks: async (textQuery, token): Promise<{ results: INotebookFileMatchNoModel[]; limitHit: boolean }> => { + const contributedType = this._notebookService.getContributedNotebookType(viewType); + if (!contributedType) { + return { results: [], limitHit: false }; + } + const fileNames = contributedType.selectors; + + const includes = fileNames.map((selector) => { + const globPattern = (selector as INotebookExclusiveDocumentFilter).include || selector as IRelativePattern | string; + return globPattern.toString(); + }); + + const searchComplete = await this._proxy.$searchInNotebooks(handle, includes, textQuery, token); + const revivedResults: INotebookFileMatchNoModel[] = searchComplete.results.map(result => { + const resource = URI.revive(result.resource); + return { + resource, + cellResults: result.cellResults.map(e => revive(e)) + }; + }); + return { results: revivedResults, limitHit: searchComplete.limitHit }; + } })); if (data) { diff --git a/src/vs/workbench/api/browser/mainThreadSearch.ts b/src/vs/workbench/api/browser/mainThreadSearch.ts index 1ff5cdfb75c..797a4ee92a4 100644 --- a/src/vs/workbench/api/browser/mainThreadSearch.ts +++ b/src/vs/workbench/api/browser/mainThreadSearch.ts @@ -11,6 +11,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { IFileMatch, IFileQuery, IRawFileMatch2, ISearchComplete, ISearchCompleteStats, ISearchProgressItem, ISearchResultProvider, ISearchService, ITextQuery, QueryType, SearchProviderType } from 'vs/workbench/services/search/common/search'; import { ExtHostContext, ExtHostSearchShape, MainContext, MainThreadSearchShape } from '../common/extHost.protocol'; +import { revive } from 'vs/base/common/marshalling'; @extHostNamedCustomer(MainContext.MainThreadSearch) export class MainThreadSearch implements MainThreadSearchShape { @@ -160,10 +161,7 @@ class RemoteSearchProvider implements ISearchResultProvider, IDisposable { dataOrUri.forEach(result => { if ((result).results) { - searchOp.addMatch({ - resource: URI.revive((result).resource), - results: (result).results - }); + searchOp.addMatch(revive((result))); } else { searchOp.addMatch({ resource: URI.revive(result) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 9d6fa52947d..55d4b89597e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -176,7 +176,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService)); const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadBulkEdits))); - const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem)); + const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch)); const extHostNotebookDocuments = rpcProtocol.set(ExtHostContext.ExtHostNotebookDocuments, new ExtHostNotebookDocuments(extHostNotebook)); const extHostNotebookEditors = rpcProtocol.set(ExtHostContext.ExtHostNotebookEditors, new ExtHostNotebookEditors(extHostLogService, extHostNotebook)); const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook, extHostCommands, extHostLogService)); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index ba58ea650d2..94d1595db80 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -80,6 +80,7 @@ import { CandidatePort } from 'vs/workbench/services/remote/common/tunnelModel'; import { ITextQueryBuilderOptions } from 'vs/workbench/services/search/common/queryBuilder'; import * as search from 'vs/workbench/services/search/common/search'; import { ISaveProfileResult } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { IRawClosedNotebookFileMatch } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; export interface IWorkspaceData extends IStaticWorkspaceData { folders: { uri: UriComponents; name: string; index: number }[]; @@ -2470,6 +2471,8 @@ export interface ExtHostNotebookShape extends ExtHostNotebookDocumentsAndEditors $dataToNotebook(handle: number, data: VSBuffer, token: CancellationToken): Promise>; $notebookToData(handle: number, data: SerializableObjectWithBuffers, token: CancellationToken): Promise; $saveNotebook(handle: number, uri: UriComponents, versionId: number, options: files.IWriteFileOptions, token: CancellationToken): Promise; + + $searchInNotebooks(handle: number, filenamePattern: string[], textQuery: search.ITextQuery, token: CancellationToken): Promise<{ results: IRawClosedNotebookFileMatch[]; limitHit: boolean }>; } export interface ExtHostNotebookDocumentSaveParticipantShape { diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index e61788f49e1..2107cfaa21f 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -9,7 +9,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; import { IRelativePattern } from 'vs/base/common/glob'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ResourceMap } from 'vs/base/common/map'; +import { ResourceMap, ResourceSet } from 'vs/base/common/map'; import { MarshalledId } from 'vs/base/common/marshallingIds'; import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { assertIsDefined } from 'vs/base/common/types'; @@ -32,8 +32,10 @@ import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; import { filter } from 'vs/base/common/objects'; import { Schemas } from 'vs/base/common/network'; - - +import { IFileQuery, ITextQuery, QueryType } from 'vs/workbench/services/search/common/search'; +import { INotebookCellMatchNoModel, INotebookFileMatchNoModel, IRawClosedNotebookFileMatch, genericCellMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; +import { IExtHostSearch } from 'vs/workbench/api/common/extHostSearch'; +import { CellSearchModel } from 'vs/workbench/contrib/search/common/cellSearchModel'; export class ExtHostNotebookController implements ExtHostNotebookShape { private static _notebookStatusBarItemProviderHandlePool: number = 0; @@ -74,7 +76,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { commands: ExtHostCommands, private _textDocumentsAndEditors: ExtHostDocumentsAndEditors, private _textDocuments: ExtHostDocuments, - private _extHostFileSystem: IExtHostConsumerFileSystem + private _extHostFileSystem: IExtHostConsumerFileSystem, + private _extHostSearch: IExtHostSearch ) { this._notebookProxy = mainContext.getProxy(MainContext.MainThreadNotebook); this._notebookDocumentsProxy = mainContext.getProxy(MainContext.MainThreadNotebookDocuments); @@ -372,6 +375,135 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { return fileStats; } + /** + * Search for query in all notebooks that can be deserialized by the serializer fetched by `handle`. + * + * @param handle used to get notebook serializer + * @param filenamePattern the filename pattern to match files that can be deserialized + * @param textQuery the text query to search using + * @param token cancellation token + * @returns `IRawClosedNotebookFileMatch` for every file. Files without matches will just have a `IRawClosedNotebookFileMatch` + * with no `cellResults`. This allows the caller to know what was searched in already, even if it did not yield results. + */ + async $searchInNotebooks(handle: number, filenamePattern: string[], textQuery: ITextQuery, token: CancellationToken): Promise<{ results: IRawClosedNotebookFileMatch[]; limitHit: boolean }> { + const serializer = this._notebookSerializer.get(handle)?.serializer; + if (!serializer) { + return { + limitHit: false, + results: [] + }; + } + + const runFileQueries = async (includes: (string)[], token: CancellationToken, textQuery: ITextQuery): Promise => { + const uris = new ResourceSet(); + await Promise.all(includes.map(include => { + const query: IFileQuery = { + type: QueryType.File, + filePattern: include, + folderQueries: textQuery.folderQueries, + maxResults: textQuery.maxResults, + }; + return this._extHostSearch.doInternalFileSearchWithCustomCallback(query, token, (data) => { + data.forEach(e => uris.add(e)); + }); + })); + + return Array.from(uris.keys()); + }; + + const filesToScan = await runFileQueries(filenamePattern, token, textQuery); + + const results = new ResourceMap(); + let limitHit = false; + const promises = filesToScan.map(async (uri) => { + const cellMatches: INotebookCellMatchNoModel[] = []; + + try { + if (token.isCancellationRequested) { + return; + } + if (textQuery.maxResults && [...results.values()].reduce((acc, value) => acc + value.cellResults.length, 0) > textQuery.maxResults) { + limitHit = true; + return; + } + + const simpleCells: Array<{ input: string; outputs: string[] }> = []; + const notebook = this._documents.get(uri); + if (notebook) { + const cells = notebook.apiNotebook.getCells(); + cells.forEach(e => simpleCells.push( + { + input: e.document.getText(), + outputs: e.outputs.flatMap(value => value.items.map(output => output.data.toString())) + } + )); + } else { + const fileContent = await this._extHostFileSystem.value.readFile(uri); + const bytes = VSBuffer.fromString(fileContent.toString()); + const notebook = await serializer.deserializeNotebook(bytes.buffer, token); + if (token.isCancellationRequested) { + return; + } + const data = typeConverters.NotebookData.from(notebook); + + data.cells.forEach(cell => simpleCells.push( + { + input: cell.source, + outputs: cell.outputs.flatMap(value => value.items.map(output => output.valueBytes.toString())) + } + )); + } + + + if (token.isCancellationRequested) { + return; + } + + simpleCells.forEach((cell, index) => { + const target = textQuery.contentPattern.pattern; + const cellModel = new CellSearchModel(cell.input, undefined, cell.outputs); + + const inputMatches = cellModel.findInInputs(target); + const outputMatches = cellModel.findInOutputs(target); + const webviewResults = outputMatches + .flatMap(outputMatch => + genericCellMatchesToTextSearchMatches(outputMatch.matches, outputMatch.textBuffer)) + .map((textMatch, index) => { + textMatch.webviewIndex = index; + return textMatch; + }); + + if (inputMatches.length > 0 || outputMatches.length > 0) { + const cellMatch: INotebookCellMatchNoModel = { + index: index, + contentResults: genericCellMatchesToTextSearchMatches(inputMatches, cellModel.inputTextBuffer), + webviewResults + }; + cellMatches.push(cellMatch); + } + }); + + const fileMatch = { + resource: uri, cellResults: cellMatches + }; + results.set(uri, fileMatch); + return; + + } catch (e) { + return; + } + + }); + + await Promise.all(promises); + return { + limitHit, + results: [...results.values()] + }; + } + + + private async _validateWriteFile(uri: URI, options: files.IWriteFileOptions) { const stat = await this._extHostFileSystem.value.stat(uri); // Dirty write prevention diff --git a/src/vs/workbench/api/common/extHostSearch.ts b/src/vs/workbench/api/common/extHostSearch.ts index ff6e93020d1..d0289669abf 100644 --- a/src/vs/workbench/api/common/extHostSearch.ts +++ b/src/vs/workbench/api/common/extHostSearch.ts @@ -14,10 +14,12 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IRawFileQuery, ISearchCompleteStats, IFileQuery, IRawTextQuery, IRawQuery, ITextQuery, IFolderQuery } from 'vs/workbench/services/search/common/search'; import { URI, UriComponents } from 'vs/base/common/uri'; import { TextSearchManager } from 'vs/workbench/services/search/common/textSearchManager'; +import { CancellationToken } from 'vs/base/common/cancellation'; export interface IExtHostSearch extends ExtHostSearchShape { registerTextSearchProvider(scheme: string, provider: vscode.TextSearchProvider): IDisposable; registerFileSearchProvider(scheme: string, provider: vscode.FileSearchProvider): IDisposable; + doInternalFileSearchWithCustomCallback(query: IFileQuery, token: CancellationToken, handleFileMatch: (data: URI[]) => void): Promise; } export const IExtHostSearch = createDecorator('IExtHostSearch'); @@ -88,6 +90,10 @@ export class ExtHostSearch implements ExtHostSearchShape { } } + async doInternalFileSearchWithCustomCallback(query: IFileQuery, token: CancellationToken, handleFileMatch: (data: URI[]) => void): Promise { + return { messages: [] }; + } + $clearCache(cacheKey: string): Promise { this._fileSearchManager.clearCache(cacheKey); diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index cd93d1743be..19ba0baec9f 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -29,9 +29,10 @@ import { GlobPattern } from 'vs/workbench/api/common/extHostTypeConverters'; import { Range } from 'vs/workbench/api/common/extHostTypes'; import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; import { ITextQueryBuilderOptions } from 'vs/workbench/services/search/common/queryBuilder'; -import { IRawFileMatch2, resultIsMatch } from 'vs/workbench/services/search/common/search'; +import { IRawFileMatch2, ITextSearchResult, resultIsMatch } from 'vs/workbench/services/search/common/search'; import * as vscode from 'vscode'; import { ExtHostWorkspaceShape, IRelativePatternDto, IWorkspaceData, MainContext, MainThreadMessageOptions, MainThreadMessageServiceShape, MainThreadWorkspaceShape } from './extHost.protocol'; +import { revive } from 'vs/base/common/marshalling'; export interface IExtHostWorkspaceProvider { getWorkspaceFolder2(uri: vscode.Uri, resolveParent?: boolean): Promise; @@ -509,7 +510,8 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac } const uri = URI.revive(p.resource); - p.results!.forEach(result => { + p.results!.forEach(rawResult => { + const result: ITextSearchResult = revive(rawResult); if (resultIsMatch(result)) { callback({ uri, diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index 45cab9925c5..8b1f4bebe15 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -85,14 +85,14 @@ export class NativeExtHostSearch extends ExtHostSearch implements IDisposable { return super.$provideFileSearchResults(handle, session, rawQuery, token); } - private doInternalFileSearch(handle: number, session: number, rawQuery: IFileQuery, token: vscode.CancellationToken): Promise { + override doInternalFileSearchWithCustomCallback(rawQuery: IFileQuery, token: vscode.CancellationToken, handleFileMatch: (data: URI[]) => void): Promise { const onResult = (ev: ISerializedSearchProgressItem) => { if (isSerializedFileMatch(ev)) { ev = [ev]; } if (Array.isArray(ev)) { - this._proxy.$handleFileMatch(handle, session, ev.map(m => URI.file(m.path))); + handleFileMatch(ev.map(m => URI.file(m.path))); return; } @@ -108,6 +108,12 @@ export class NativeExtHostSearch extends ExtHostSearch implements IDisposable { return >this._internalFileSearchProvider.doFileSearch(rawQuery, onResult, token); } + private async doInternalFileSearch(handle: number, session: number, rawQuery: IFileQuery, token: vscode.CancellationToken): Promise { + return this.doInternalFileSearchWithCustomCallback(rawQuery, token, (data) => { + this._proxy.$handleFileMatch(handle, session, data); + }); + } + override $clearCache(cacheKey: string): Promise { this._internalFileSearchProvider?.clearCache(cacheKey); diff --git a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts index 7de8e1f9e75..806a6b685dd 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebook.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebook.test.ts @@ -27,6 +27,8 @@ import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import { ExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; import { ExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ExtHostSearch } from 'vs/workbench/api/common/extHostSearch'; +import { URITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; suite('NotebookCell#Document', function () { let rpcProtocol: TestRPCProtocol; @@ -36,6 +38,7 @@ suite('NotebookCell#Document', function () { let extHostNotebooks: ExtHostNotebookController; let extHostNotebookDocuments: ExtHostNotebookDocuments; let extHostConsumerFileSystem: ExtHostConsumerFileSystem; + let extHostSearch: ExtHostSearch; const notebookUri = URI.parse('test:///notebook.file'); const disposables = new DisposableStore(); @@ -58,11 +61,12 @@ suite('NotebookCell#Document', function () { extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); extHostConsumerFileSystem = new ExtHostConsumerFileSystem(rpcProtocol, new ExtHostFileSystemInfo()); + extHostSearch = new ExtHostSearch(rpcProtocol, new URITransformerService(null), new NullLogService()); extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService(), new class extends mock() { override onExtensionError(): boolean { return true; } - }), extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem); + }), extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch); extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks); const reg = extHostNotebooks.registerNotebookSerializer(nullExtensionDescription, 'test', new class extends mock() { }); diff --git a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts index 0fabe6e6ac6..8af3ece68d9 100644 --- a/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts +++ b/src/vs/workbench/api/test/browser/extHostNotebookKernel.test.ts @@ -29,6 +29,8 @@ import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import { ExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; import { ExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo'; import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; +import { ExtHostSearch } from 'vs/workbench/api/common/extHostSearch'; +import { URITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; suite('NotebookKernel', function () { let rpcProtocol: TestRPCProtocol; @@ -40,6 +42,7 @@ suite('NotebookKernel', function () { let extHostNotebookDocuments: ExtHostNotebookDocuments; let extHostCommands: ExtHostCommands; let extHostConsumerFileSystem: ExtHostConsumerFileSystem; + let extHostSearch: ExtHostSearch; const notebookUri = URI.parse('test:///notebook.file'); const kernelData = new Map(); @@ -101,7 +104,8 @@ suite('NotebookKernel', function () { } }); extHostConsumerFileSystem = new ExtHostConsumerFileSystem(rpcProtocol, new ExtHostFileSystemInfo()); - extHostNotebooks = new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem); + extHostSearch = new ExtHostSearch(rpcProtocol, new URITransformerService(null), new NullLogService()); + extHostNotebooks = new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extHostConsumerFileSystem, extHostSearch); extHostNotebookDocuments = new ExtHostNotebookDocuments(extHostNotebooks); diff --git a/src/vs/workbench/api/test/node/extHostSearch.test.ts b/src/vs/workbench/api/test/node/extHostSearch.test.ts index 7942b81654e..1d903c40815 100644 --- a/src/vs/workbench/api/test/node/extHostSearch.test.ts +++ b/src/vs/workbench/api/test/node/extHostSearch.test.ts @@ -8,6 +8,7 @@ import { mapArrayOrNot } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { isCancellationError } from 'vs/base/common/errors'; +import { revive } from 'vs/base/common/marshalling'; import { joinPath } from 'vs/base/common/resources'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as pfs from 'vs/base/node/pfs'; @@ -119,12 +120,7 @@ suite('ExtHostSearch', () => { } await rpcProtocol.sync(); - const results = (mockMainThreadSearch.results).map(r => ({ - ...r, - ...{ - resource: URI.revive(r.resource) - } - })); + const results: IFileMatch[] = revive(mockMainThreadSearch.results); return { results, stats: stats! }; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 6614adc8fbf..792c21f2a18 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -15,6 +15,8 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IFileStatWithMetadata, IWriteFileOptions } from 'vs/platform/files/common/files'; +import { ITextQuery } from 'vs/workbench/services/search/common/search'; +import { INotebookFileMatchNoModel } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; export const INotebookService = createDecorator('notebookService'); @@ -31,6 +33,7 @@ export interface INotebookSerializer { dataToNotebook(data: VSBuffer): Promise; notebookToData(data: NotebookData): Promise; save(uri: URI, versionId: number, options: IWriteFileOptions, token: CancellationToken): Promise; + searchInNotebooks(textQuery: ITextQuery, token: CancellationToken): Promise<{ results: INotebookFileMatchNoModel[]; limitHit: boolean }>; } export interface INotebookRawData { diff --git a/src/vs/workbench/contrib/search/browser/notebookSearchContributions.ts b/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchContributions.ts similarity index 96% rename from src/vs/workbench/contrib/search/browser/notebookSearchContributions.ts rename to src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchContributions.ts index 57d73a21c3f..a1ac98e26b2 100644 --- a/src/vs/workbench/contrib/search/browser/notebookSearchContributions.ts +++ b/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchContributions.ts @@ -8,7 +8,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INotebookSearchService } from 'vs/workbench/contrib/search/common/notebookSearch'; -import { NotebookSearchService } from 'vs/workbench/contrib/search/browser/notebookSearchService'; +import { NotebookSearchService } from 'vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService'; export function registerContributions(): void { registerSingleton(INotebookSearchService, NotebookSearchService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts b/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts new file mode 100644 index 00000000000..c4526c8129c --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/notebookSearch/notebookSearchService.ts @@ -0,0 +1,213 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; +import { ResourceSet, ResourceMap } from 'vs/base/common/map'; +import { URI } from 'vs/base/common/uri'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { INotebookSearchService } from 'vs/workbench/contrib/search/common/notebookSearch'; +import { INotebookCellMatchWithModel, INotebookFileMatchWithModel, contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/browser/notebookSearch/searchNotebookHelpers'; +import { ITextQuery, QueryType, ISearchProgressItem, ISearchComplete, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; +import * as arrays from 'vs/base/common/arrays'; +import { isNumber } from 'vs/base/common/types'; +import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; +import { priorityToRank } from 'vs/workbench/services/editor/common/editorResolverService'; +import { INotebookFileMatchNoModel } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; +import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; + +interface IOpenNotebookSearchResults { + results: ResourceMap; + limitHit: boolean; +} +interface IClosedNotebookSearchResults { + results: ResourceMap | null>; + limitHit: boolean; +} +export class NotebookSearchService implements INotebookSearchService { + declare readonly _serviceBrand: undefined; + constructor( + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @INotebookEditorService private readonly notebookEditorService: INotebookEditorService, + @ILogService private readonly logService: ILogService, + @INotebookService private readonly notebookService: INotebookService, + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + } + + + notebookSearch(query: ITextQuery, token: CancellationToken | undefined, searchInstanceID: string, onProgress?: (result: ISearchProgressItem) => void): { + openFilesToScan: ResourceSet; + completeData: Promise; + allScannedFiles: Promise; + } { + + if (query.type !== QueryType.Text) { + return { + openFilesToScan: new ResourceSet(), + completeData: Promise.resolve({ + messages: [], + limitHit: false, + results: [], + }), + allScannedFiles: Promise.resolve(new ResourceSet()), + }; + } + + const localNotebookWidgets = this.getLocalNotebookWidgets(); + const localNotebookFiles = localNotebookWidgets.map(widget => widget.viewModel!.uri); + const getAllResults = (): { completeData: Promise; allScannedFiles: Promise } => { + const searchStart = Date.now(); + + const localResultPromise = this.getLocalNotebookResults(query, token ?? CancellationToken.None, localNotebookWidgets, searchInstanceID); + const searchLocalEnd = Date.now(); + + const experimentalNotebooksEnabled = this.configurationService.getValue('search').experimental?.closedNotebookRichContentResults ?? false; + + let closedResultsPromise: Promise = Promise.resolve(undefined); + if (experimentalNotebooksEnabled) { + closedResultsPromise = this.getClosedNotebookResults(query, new ResourceSet(localNotebookFiles, uri => this.uriIdentityService.extUri.getComparisonKey(uri)), token ?? CancellationToken.None); + } + + const promise = Promise.all([localResultPromise, closedResultsPromise]); + return { + completeData: promise.then((resolvedPromise) => { + const openNotebookResult = resolvedPromise[0]; + const closedNotebookResult = resolvedPromise[1]; + + const resolved = resolvedPromise.filter((e): e is IOpenNotebookSearchResults | IClosedNotebookSearchResults => !!e); + const resultArray = [...openNotebookResult.results.values(), ...closedNotebookResult?.results.values() ?? []]; + const results = arrays.coalesce(resultArray); + if (onProgress) { + results.forEach(onProgress); + } + this.logService.trace(`local notebook search time | ${searchLocalEnd - searchStart}ms`); + return { + messages: [], + limitHit: resolved.reduce((prev, cur) => prev || cur.limitHit, false), + results, + }; + }), + allScannedFiles: promise.then(resolvedPromise => { + const openNotebookResults = resolvedPromise[0]; + const closedNotebookResults = resolvedPromise[1]; + const results = arrays.coalesce([...openNotebookResults.results.keys(), ...closedNotebookResults?.results.keys() ?? []]); + return new ResourceSet(results, uri => this.uriIdentityService.extUri.getComparisonKey(uri)); + }) + }; + }; + const promiseResults = getAllResults(); + return { + openFilesToScan: new ResourceSet(localNotebookFiles), + completeData: promiseResults.completeData, + allScannedFiles: promiseResults.allScannedFiles + }; + } + + private async getClosedNotebookResults(textQuery: ITextQuery, scannedFiles: ResourceSet, token: CancellationToken): Promise { + + const sortedContributedTypes = [...this.notebookService.getContributedNotebookTypes()].sort((a, b) => priorityToRank(b.priority) - priorityToRank(a.priority)); + + const getResultsFromProviderInfo = async (providerInfo: NotebookProviderInfo) => { + const serializer = (await this.notebookService.withNotebookDataProvider(providerInfo.id)).serializer; + return await serializer.searchInNotebooks(textQuery, token); + }; + + const start = Date.now(); + const searchComplete = (await Promise.all(sortedContributedTypes.map(async e => await getResultsFromProviderInfo(e)))); + const results = searchComplete.flatMap(e => e.results); + let limitHit = searchComplete.some(e => e.limitHit); + + // results are already sorted with high priority first, filter out duplicates. + const uniqueResults = new ResourceMap(uri => this.uriIdentityService.extUri.getComparisonKey(uri)); + + let numResults = 0; + for (const result of results) { + if (textQuery.maxResults && numResults >= textQuery.maxResults) { + limitHit = true; + break; + } + + if (!scannedFiles.has(result.resource) && !uniqueResults.has(result.resource)) { + uniqueResults.set(result.resource, result.cellResults.length > 0 ? result : null); + numResults++; + } + } + + const end = Date.now(); + this.logService.trace(`query: ${textQuery.contentPattern.pattern}`); + this.logService.trace(`closed notebook search time | ${end - start}ms`); + + return { + results: uniqueResults, + limitHit + }; + } + + private async getLocalNotebookResults(query: ITextQuery, token: CancellationToken, widgets: Array, searchID: string): Promise { + const localResults = new ResourceMap(uri => this.uriIdentityService.extUri.getComparisonKey(uri)); + let limitHit = false; + + for (const widget of widgets) { + if (!widget.hasModel()) { + continue; + } + const askMax = isNumber(query.maxResults) ? query.maxResults + 1 : Number.MAX_SAFE_INTEGER; + let matches = await widget + .find(query.contentPattern.pattern, { + regex: query.contentPattern.isRegExp, + wholeWord: query.contentPattern.isWordMatch, + caseSensitive: query.contentPattern.isCaseSensitive, + includeMarkupInput: query.contentPattern.notebookInfo?.isInNotebookMarkdownInput ?? true, + includeMarkupPreview: query.contentPattern.notebookInfo?.isInNotebookMarkdownPreview ?? true, + includeCodeInput: query.contentPattern.notebookInfo?.isInNotebookCellInput ?? true, + includeOutput: query.contentPattern.notebookInfo?.isInNotebookCellOutput ?? true, + }, token, false, true, searchID); + + const uri = widget.viewModel!.uri; + + if (matches.length) { + if (askMax && matches.length >= askMax) { + limitHit = true; + matches = matches.slice(0, askMax - 1); + } + const cellResults: INotebookCellMatchWithModel[] = matches.map(match => { + const contentResults = contentMatchesToTextSearchMatches(match.contentMatches, match.cell); + const webviewResults = webviewMatchesToTextSearchMatches(match.webviewMatches); + return { + cell: match.cell, + index: match.index, + contentResults: contentResults, + webviewResults: webviewResults, + }; + }); + + const fileMatch: INotebookFileMatchWithModel = { + resource: uri, cellResults: cellResults + }; + localResults.set(uri, fileMatch); + } else { + localResults.set(uri, null); + } + } + + return { + results: localResults, + limitHit + }; + } + + + private getLocalNotebookWidgets(): Array { + const notebookWidgets = this.notebookEditorService.retrieveAllExistingWidgets(); + return notebookWidgets + .map(widget => widget.value) + .filter((val): val is NotebookEditorWidget => !!val && val.hasModel()); + } +} + + diff --git a/src/vs/workbench/contrib/search/browser/notebookSearch/searchNotebookHelpers.ts b/src/vs/workbench/contrib/search/browser/notebookSearch/searchNotebookHelpers.ts new file mode 100644 index 00000000000..1211c0c35b3 --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/notebookSearch/searchNotebookHelpers.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { FindMatch } from 'vs/editor/common/model'; +import { IFileMatch, ITextSearchMatch, TextSearchMatch } from 'vs/workbench/services/search/common/search'; +import { Range } from 'vs/editor/common/core/range'; +import { INotebookCellMatchNoModel, INotebookFileMatchNoModel, genericCellMatchesToTextSearchMatches, rawCellPrefix } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; +import { CellWebviewFindMatch, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { URI } from 'vs/base/common/uri'; + +export type INotebookCellMatch = INotebookCellMatchWithModel | INotebookCellMatchNoModel; +export type INotebookFileMatch = INotebookFileMatchWithModel | INotebookFileMatchNoModel; + +export function getIDFromINotebookCellMatch(match: INotebookCellMatch): string { + if (isINotebookCellMatchWithModel(match)) { + return match.cell.id; + } else { + return `${rawCellPrefix}${match.index}`; + } +} +export interface INotebookFileMatchWithModel extends IFileMatch { + cellResults: INotebookCellMatchWithModel[]; +} + +export interface INotebookCellMatchWithModel extends INotebookCellMatchNoModel { + cell: ICellViewModel; +} + +export function isINotebookFileMatchWithModel(object: any): object is INotebookFileMatchWithModel { + return 'cellResults' in object && object.cellResults instanceof Array && object.cellResults.every(isINotebookCellMatchWithModel); +} + +export function isINotebookCellMatchWithModel(object: any): object is INotebookCellMatchWithModel { + return 'cell' in object; +} + +export function contentMatchesToTextSearchMatches(contentMatches: FindMatch[], cell: ICellViewModel): ITextSearchMatch[] { + return genericCellMatchesToTextSearchMatches( + contentMatches, + cell.textBuffer + ); +} + +export function webviewMatchesToTextSearchMatches(webviewMatches: CellWebviewFindMatch[]): ITextSearchMatch[] { + return webviewMatches + .map(rawMatch => + (rawMatch.searchPreviewInfo) ? + new TextSearchMatch( + rawMatch.searchPreviewInfo.line, + new Range(0, rawMatch.searchPreviewInfo.range.start, 0, rawMatch.searchPreviewInfo.range.end), + undefined, + rawMatch.index) : undefined + ).filter((e): e is ITextSearchMatch => !!e); +} diff --git a/src/vs/workbench/contrib/search/browser/notebookSearchService.ts b/src/vs/workbench/contrib/search/browser/notebookSearchService.ts deleted file mode 100644 index 19b45f30903..00000000000 --- a/src/vs/workbench/contrib/search/browser/notebookSearchService.ts +++ /dev/null @@ -1,339 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { streamToBuffer } from 'vs/base/common/buffer'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IRelativePattern } from 'vs/base/common/glob'; -import { ResourceSet, ResourceMap } from 'vs/base/common/map'; -import { URI } from 'vs/base/common/uri'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IFileService } from 'vs/platform/files/common/files'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; -import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { INotebookExclusiveDocumentFilter, NotebookData } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { INotebookSearchService } from 'vs/workbench/contrib/search/common/notebookSearch'; -import { IFileMatchWithCells, ICellMatch, CellSearchModel, contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches, genericCellMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/browser/searchNotebookHelpers'; -import { IEditorResolverService, priorityToRank } from 'vs/workbench/services/editor/common/editorResolverService'; -import { ITextQuery, QueryType, ISearchProgressItem, ISearchComplete, ISearchConfigurationProperties, IFileQuery, ISearchService } from 'vs/workbench/services/search/common/search'; -import * as arrays from 'vs/base/common/arrays'; -import { isNumber } from 'vs/base/common/types'; - -interface INotebookDataEditInfo { - notebookData: NotebookData; - mTime: number; -} - -interface INotebookSearchMatchResults { - results: ResourceMap; - limitHit: boolean; -} - -class NotebookDataCache { - private _entries: ResourceMap; - // private _serializer: INotebookSerializer | undefined; - - constructor( - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IFileService private readonly fileService: IFileService, - @INotebookService private readonly notebookService: INotebookService, - @IEditorResolverService private readonly editorResolverService: IEditorResolverService, - ) { - this._entries = new ResourceMap(uri => this.uriIdentityService.extUri.getComparisonKey(uri)); - } - - private async getSerializer(notebookUri: URI): Promise { - const registeredEditorInfo = this.editorResolverService.getEditors(notebookUri); - const priorityEditorInfo = registeredEditorInfo.reduce((acc, val) => - priorityToRank(acc.priority) > priorityToRank(val.priority) ? acc : val - ); - const info = await this.notebookService.withNotebookDataProvider(priorityEditorInfo.id); - if (!(info instanceof SimpleNotebookProviderInfo)) { - return undefined; - } - return info.serializer; - } - - async getNotebookData(notebookUri: URI): Promise { - const mTime = (await this.fileService.stat(notebookUri)).mtime; - - const entry = this._entries.get(notebookUri); - - if (entry && entry.mTime === mTime) { - return entry.notebookData; - } else { - - let _data: NotebookData = { - metadata: {}, - cells: [] - }; - - const content = await this.fileService.readFileStream(notebookUri); - const bytes = await streamToBuffer(content.value); - const serializer = await this.getSerializer(notebookUri); - if (!serializer) { - //unsupported - throw new Error(`serializer not initialized`); - } - _data = await serializer.dataToNotebook(bytes); - this._entries.set(notebookUri, { notebookData: _data, mTime }); - return _data; - } - } - -} - -export class NotebookSearchService implements INotebookSearchService { - declare readonly _serviceBrand: undefined; - private _notebookDataCache: NotebookDataCache; - constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @INotebookEditorService private readonly notebookEditorService: INotebookEditorService, - @ILogService private readonly logService: ILogService, - @INotebookService private readonly notebookService: INotebookService, - @ISearchService private readonly searchService: ISearchService, - @IConfigurationService private readonly configurationService: IConfigurationService, - - ) { - this._notebookDataCache = this.instantiationService.createInstance(NotebookDataCache); - } - - private async runFileQueries(includes: (string)[], token: CancellationToken, textQuery: ITextQuery): Promise { - const promises = includes.map(include => { - const query: IFileQuery = { - type: QueryType.File, - filePattern: include, - folderQueries: textQuery.folderQueries, - maxResults: textQuery.maxResults, - }; - return this.searchService.fileSearch( - query, - token - ); - }); - const result = (await Promise.all(promises)).map(sc => sc.results.map(fm => fm.resource)).flat(); - const uris = new ResourceSet(result, uri => this.uriIdentityService.extUri.getComparisonKey(uri)); - return Array.from(uris.keys()); - } - - notebookSearch(query: ITextQuery, token: CancellationToken | undefined, searchInstanceID: string, onProgress?: (result: ISearchProgressItem) => void): { - openFilesToScan: ResourceSet; - completeData: Promise; - allScannedFiles: Promise; - } { - - if (query.type !== QueryType.Text) { - return { - openFilesToScan: new ResourceSet(), - completeData: Promise.resolve({ - messages: [], - limitHit: false, - results: [], - }), - allScannedFiles: Promise.resolve(new ResourceSet()), - }; - } - - const localNotebookWidgets = this.getLocalNotebookWidgets(); - const localNotebookFiles = localNotebookWidgets.map(widget => widget.viewModel!.uri); - const getAllResults = (): { completeData: Promise; allScannedFiles: Promise } => { - const searchStart = Date.now(); - - const localResultPromise = this.getLocalNotebookResults(query, token ?? CancellationToken.None, localNotebookWidgets, searchInstanceID); - const searchLocalEnd = Date.now(); - - const experimentalNotebooksEnabled = this.configurationService.getValue('search').experimental?.closedNotebookRichContentResults ?? false; - - let closedResultsPromise: Promise = Promise.resolve(undefined); - if (experimentalNotebooksEnabled) { - closedResultsPromise = this.getClosedNotebookResults(query, new ResourceSet(localNotebookFiles, uri => this.uriIdentityService.extUri.getComparisonKey(uri)), token ?? CancellationToken.None); - } - - const promise = Promise.all([localResultPromise, closedResultsPromise]); - return { - completeData: promise.then(resolvedPromise => { - const resolved = resolvedPromise.filter((e): e is INotebookSearchMatchResults => !!e); - const resultArray = resolved.map(elem => elem.results); - const results = arrays.coalesce(resultArray.flatMap(map => Array.from(map.values()))); - if (onProgress) { - results.forEach(onProgress); - } - this.logService.trace(`local notebook search time | ${searchLocalEnd - searchStart}ms`); - return { - messages: [], - limitHit: resolved.reduce((prev, cur) => prev || cur.limitHit, false), - results, - }; - }), - allScannedFiles: promise.then(resolvedPromise => { - const resolved = resolvedPromise.filter((e): e is INotebookSearchMatchResults => !!e); - const resultArray = resolved.map(elem => elem.results); - return new ResourceSet(resultArray.flatMap(map => Array.from(map.keys())), uri => this.uriIdentityService.extUri.getComparisonKey(uri)); - }) - }; - }; - const promiseResults = getAllResults(); - return { - openFilesToScan: new ResourceSet(localNotebookFiles), - completeData: promiseResults.completeData, - allScannedFiles: promiseResults.allScannedFiles - }; - } - - private async getClosedNotebookResults(textQuery: ITextQuery, scannedFiles: ResourceSet, token: CancellationToken): Promise { - const infoProviders = this.notebookService.getContributedNotebookTypes(); - const includes = infoProviders.flatMap( - (provider) => { - return provider.selectors.map((selector) => { - const globPattern = (selector as INotebookExclusiveDocumentFilter).include || selector as IRelativePattern | string; - return globPattern.toString(); - } - ); - } - ); - - const results = new ResourceMap(uri => this.uriIdentityService.extUri.getComparisonKey(uri)); - - const start = Date.now(); - - const filesToScan = await this.runFileQueries(includes, token, textQuery); - const deserializedNotebooks = new ResourceMap(); - const textModels = this.notebookService.getNotebookTextModels(); - for (const notebook of textModels) { - deserializedNotebooks.set(notebook.uri, notebook); - } - - const promises = filesToScan.map(async (uri) => { - const cellMatches: ICellMatch[] = []; - if (scannedFiles.has(uri)) { - return; - } - - try { - if (token.isCancellationRequested) { - return; - } - - const notebook = deserializedNotebooks.get(uri) ?? (await this._notebookDataCache.getNotebookData(uri)); - const cells = notebook.cells; - - if (token.isCancellationRequested) { - return; - } - - cells.forEach((cell, index) => { - const target = textQuery.contentPattern.pattern; - const cellModel = cell instanceof NotebookCellTextModel ? new CellSearchModel('', cell.textBuffer, cell.outputs.flatMap(value => value.outputs), uri, index) : new CellSearchModel(cell.source, undefined, cell.outputs.flatMap(value => value.outputs), uri, index); - - const inputMatches = cellModel.findInInputs(target); - const outputMatches = cellModel.findInOutputs(target); - const webviewResults = outputMatches - .flatMap(outputMatch => - genericCellMatchesToTextSearchMatches(outputMatch.matches, outputMatch.textBuffer, cellModel)) - .map((textMatch, index) => { - textMatch.webviewIndex = index; - return textMatch; - }); - - if (inputMatches.length > 0 || outputMatches.length > 0) { - const cellMatch: ICellMatch = { - cell: cellModel, - index: index, - contentResults: contentMatchesToTextSearchMatches(inputMatches, cellModel), - webviewResults - }; - cellMatches.push(cellMatch); - } - }); - - const fileMatch = cellMatches.length > 0 ? { - resource: uri, cellResults: cellMatches - } : null; - results.set(uri, fileMatch); - return; - - } catch (e) { - this.logService.info('error: ' + e); - return; - } - - }); - - await Promise.all(promises); - const end = Date.now(); - - this.logService.trace(`query: ${textQuery.contentPattern.pattern}`); - this.logService.trace(`closed notebook search time | ${end - start}ms`); - return { - results: results, - limitHit: false - }; - } - - private async getLocalNotebookResults(query: ITextQuery, token: CancellationToken, widgets: Array, searchID: string): Promise { - const localResults = new ResourceMap(uri => this.uriIdentityService.extUri.getComparisonKey(uri)); - let limitHit = false; - - for (const widget of widgets) { - if (!widget.viewModel) { - continue; - } - const askMax = isNumber(query.maxResults) ? query.maxResults + 1 : Number.MAX_SAFE_INTEGER; - let matches = await widget - .find(query.contentPattern.pattern, { - regex: query.contentPattern.isRegExp, - wholeWord: query.contentPattern.isWordMatch, - caseSensitive: query.contentPattern.isCaseSensitive, - includeMarkupInput: query.contentPattern.notebookInfo?.isInNotebookMarkdownInput ?? true, - includeMarkupPreview: query.contentPattern.notebookInfo?.isInNotebookMarkdownPreview ?? true, - includeCodeInput: query.contentPattern.notebookInfo?.isInNotebookCellInput ?? true, - includeOutput: query.contentPattern.notebookInfo?.isInNotebookCellOutput ?? true, - }, token, false, true, searchID); - - - if (matches.length) { - if (askMax && matches.length >= askMax) { - limitHit = true; - matches = matches.slice(0, askMax - 1); - } - const cellResults: ICellMatch[] = matches.map(match => { - const contentResults = contentMatchesToTextSearchMatches(match.contentMatches, match.cell); - const webviewResults = webviewMatchesToTextSearchMatches(match.webviewMatches); - return { - cell: match.cell, - index: match.index, - contentResults: contentResults, - webviewResults: webviewResults, - }; - }); - - const fileMatch: IFileMatchWithCells = { - resource: widget.viewModel.uri, cellResults: cellResults - }; - localResults.set(widget.viewModel.uri, fileMatch); - } else { - localResults.set(widget.viewModel.uri, null); - } - } - - return { - results: localResults, - limitHit - }; - } - - - private getLocalNotebookWidgets(): Array { - const notebookWidgets = this.notebookEditorService.retrieveAllExistingWidgets(); - return notebookWidgets - .map(widget => widget.value) - .filter((val): val is NotebookEditorWidget => !!val && !!(val.viewModel)); - } -} diff --git a/src/vs/workbench/contrib/search/browser/replaceService.ts b/src/vs/workbench/contrib/search/browser/replaceService.ts index 8463663fb9f..3f7fe26fa79 100644 --- a/src/vs/workbench/contrib/search/browser/replaceService.ts +++ b/src/vs/workbench/contrib/search/browser/replaceService.ts @@ -202,7 +202,7 @@ export class ReplaceService implements IReplaceService { if (!arg.isReadonly()) { // only apply edits if it's not a webview match, since webview matches are read-only const match = arg; - edits.push(this.createEdit(match, match.replaceString, match.cell.uri)); + edits.push(this.createEdit(match, match.replaceString, match.cell?.uri)); } } else { const match = arg; diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index a341863546a..f0db4c34f6a 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -21,7 +21,7 @@ import { Extensions as ViewExtensions, IViewContainersRegistry, IViewDescriptor, import { GotoSymbolQuickAccessProvider } from 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess'; import { AnythingQuickAccessProvider } from 'vs/workbench/contrib/search/browser/anythingQuickAccess'; import { registerContributions as replaceContributions } from 'vs/workbench/contrib/search/browser/replaceContributions'; -import { registerContributions as notebookSearchContributions } from 'vs/workbench/contrib/search/browser/notebookSearchContributions'; +import { registerContributions as notebookSearchContributions } from 'vs/workbench/contrib/search/browser/notebookSearch/notebookSearchContributions'; import { searchViewIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; import { registerContributions as searchWidgetContributions } from 'vs/workbench/contrib/search/browser/searchWidget'; diff --git a/src/vs/workbench/contrib/search/browser/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchModel.ts index ec6a10068f7..0a7a6b2b3bf 100644 --- a/src/vs/workbench/contrib/search/browser/searchModel.ts +++ b/src/vs/workbench/contrib/search/browser/searchModel.ts @@ -37,11 +37,13 @@ import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/note import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; import { NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IReplaceService } from 'vs/workbench/contrib/search/browser/replace'; -import { CellSearchModel, ICellMatch, contentMatchesToTextSearchMatches, isIFileMatchWithCells, rawCellPrefix, webviewMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/browser/searchNotebookHelpers'; +import { contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches, INotebookCellMatchWithModel, isINotebookFileMatchWithModel, isINotebookCellMatchWithModel, getIDFromINotebookCellMatch } from 'vs/workbench/contrib/search/browser/notebookSearch/searchNotebookHelpers'; import { INotebookSearchService } from 'vs/workbench/contrib/search/common/notebookSearch'; +import { rawCellPrefix, INotebookCellMatchNoModel, isINotebookFileMatchNoModel } from 'vs/workbench/contrib/search/common/searchNotebookHelpers'; import { ReplacePattern } from 'vs/workbench/services/search/common/replace'; import { IFileMatch, IPatternInfo, ISearchComplete, ISearchConfigurationProperties, ISearchProgressItem, ISearchRange, ISearchService, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchPreviewOptions, ITextSearchResult, ITextSearchStats, OneLineRange, resultIsMatch, SearchCompletionExitCode, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { getTextSearchMatchWithModelContext, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers'; +import { CellSearchModel } from 'vs/workbench/contrib/search/common/cellSearchModel'; export class Match { @@ -187,7 +189,7 @@ export class CellMatch { constructor( private readonly _parent: FileMatch, - private _cell: ICellViewModel | CellSearchModel, + private _cell: ICellViewModel | undefined, private readonly _cellIndex: number, ) { @@ -240,7 +242,7 @@ export class CellMatch { } public addContext(textSearchMatches: ITextSearchMatch[]) { - if (this.cell instanceof CellSearchModel) { + if (!this.cell) { // todo: get closed notebook results in search editor return; } @@ -270,14 +272,14 @@ export class CellMatch { } get id(): string { - return this._cell.id; + return this._cell?.id ?? `${rawCellPrefix}${this.cellIndex}`; } get cellIndex(): number { return this._cellIndex; } - get cell(): ICellViewModel | CellSearchModel { + get cell(): ICellViewModel | undefined { return this._cell; } @@ -445,11 +447,11 @@ export class FileMatch extends Disposable implements IFileMatch { return this._cellMatches.get(cellID); } - addCellMatch(rawCell: ICellMatch) { - const cellMatch = new CellMatch(this, rawCell.cell, rawCell.index); + addCellMatch(rawCell: INotebookCellMatchNoModel | INotebookCellMatchWithModel) { + const cellMatch = new CellMatch(this, isINotebookCellMatchWithModel(rawCell) ? rawCell.cell : undefined, rawCell.index); this._cellMatches.set(cellMatch.id, cellMatch); - this.addWebviewMatchesToCell(rawCell.cell.id, rawCell.webviewResults); - this.addContentMatchesToCell(rawCell.cell.id, rawCell.contentResults); + this.addWebviewMatchesToCell(cellMatch.id, rawCell.webviewResults); + this.addContentMatchesToCell(cellMatch.id, rawCell.contentResults); } get closestRoot(): FolderMatchWorkspaceRoot | null { @@ -480,7 +482,7 @@ export class FileMatch extends Disposable implements IFileMatch { }); } - if (isIFileMatchWithCells(this.rawMatch)) { + if (isINotebookFileMatchWithModel(this.rawMatch) || isINotebookFileMatchNoModel(this.rawMatch)) { this.rawMatch.cellResults?.forEach(cell => this.addCellMatch(cell)); this.setNotebookFindMatchDecorationsUsingCellMatches(this.cellMatches()); this._onChange.fire({ forceUpdateModel: true }); @@ -885,7 +887,7 @@ export class FileMatch extends Disposable implements IFileMatch { } private async highlightCurrentFindMatchDecoration(match: MatchInNotebook): Promise { - if (!this._findMatchDecorationModel || match.cell instanceof CellSearchModel) { + if (!this._findMatchDecorationModel || !match.cell) { // match cell should never be a CellSearchModel if the notebook is open return null; } @@ -897,7 +899,7 @@ export class FileMatch extends Disposable implements IFileMatch { } private revealCellRange(match: MatchInNotebook, outputOffset: number | null) { - if (!this._notebookEditorWidget || match.cell instanceof CellSearchModel) { + if (!this._notebookEditorWidget || !match.cell) { // match cell should never be a CellSearchModel if the notebook is open return; } @@ -1159,9 +1161,9 @@ export class FolderMatch extends Disposable { } // add cell matches - if (isIFileMatchWithCells(rawFileMatch)) { + if (isINotebookFileMatchWithModel(rawFileMatch) || isINotebookFileMatchNoModel(rawFileMatch)) { rawFileMatch.cellResults?.forEach(rawCellMatch => { - const existingCellMatch = existingFileMatch.getCellMatch(rawCellMatch.cell.id); + const existingCellMatch = existingFileMatch.getCellMatch(getIDFromINotebookCellMatch(rawCellMatch)); if (existingCellMatch) { existingCellMatch.addContentMatches(rawCellMatch.contentResults); existingCellMatch.addWebviewMatches(rawCellMatch.webviewResults); diff --git a/src/vs/workbench/contrib/search/browser/searchNotebookHelpers.ts b/src/vs/workbench/contrib/search/common/cellSearchModel.ts similarity index 51% rename from src/vs/workbench/contrib/search/browser/searchNotebookHelpers.ts rename to src/vs/workbench/contrib/search/common/cellSearchModel.ts index 92e2526770f..d95e923c456 100644 --- a/src/vs/workbench/contrib/search/browser/searchNotebookHelpers.ts +++ b/src/vs/workbench/contrib/search/common/cellSearchModel.ts @@ -3,111 +3,23 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DefaultEndOfLine, FindMatch, IReadonlyTextBuffer } from 'vs/editor/common/model'; -import { CellWebviewFindMatch, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { IFileMatch, ITextSearchMatch, TextSearchMatch } from 'vs/workbench/services/search/common/search'; +import { Disposable } from 'vs/base/common/lifecycle'; import { Range } from 'vs/editor/common/core/range'; +import { DefaultEndOfLine, FindMatch, IReadonlyTextBuffer } from 'vs/editor/common/model'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { SearchParams } from 'vs/editor/common/model/textModelSearch'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { URI } from 'vs/base/common/uri'; -import { IOutputItemDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; - -export interface IFileMatchWithCells extends IFileMatch { - cellResults: ICellMatch[]; -} - -export interface ICellMatch { - cell: ICellViewModel | CellSearchModel; - index: number; - contentResults: ITextSearchMatch[]; - webviewResults: ITextSearchMatch[]; -} -export function isIFileMatchWithCells(object: IFileMatch): object is IFileMatchWithCells { - return 'cellResults' in object; -} - -// to text search results - -export function contentMatchesToTextSearchMatches(contentMatches: FindMatch[], cell: ICellViewModel | CellSearchModel): ITextSearchMatch[] { - return genericCellMatchesToTextSearchMatches( - contentMatches, - cell instanceof CellSearchModel ? cell.inputTextBuffer : cell.textBuffer, - cell - ); -} - -export function genericCellMatchesToTextSearchMatches(contentMatches: FindMatch[], buffer: IReadonlyTextBuffer, cell: ICellViewModel | CellSearchModel) { - let previousEndLine = -1; - const contextGroupings: FindMatch[][] = []; - let currentContextGrouping: FindMatch[] = []; - - contentMatches.forEach((match) => { - if (match.range.startLineNumber !== previousEndLine) { - if (currentContextGrouping.length > 0) { - contextGroupings.push([...currentContextGrouping]); - currentContextGrouping = []; - } - } - - currentContextGrouping.push(match); - previousEndLine = match.range.endLineNumber; - }); - - if (currentContextGrouping.length > 0) { - contextGroupings.push([...currentContextGrouping]); - } - - const textSearchResults = contextGroupings.map((grouping) => { - const lineTexts: string[] = []; - const firstLine = grouping[0].range.startLineNumber; - const lastLine = grouping[grouping.length - 1].range.endLineNumber; - for (let i = firstLine; i <= lastLine; i++) { - lineTexts.push(buffer.getLineContent(i)); - } - return new TextSearchMatch( - lineTexts.join('\n') + '\n', - grouping.map(m => new Range(m.range.startLineNumber - 1, m.range.startColumn - 1, m.range.endLineNumber - 1, m.range.endColumn - 1)), - ); - }); - - return textSearchResults; -} - -export function webviewMatchesToTextSearchMatches(webviewMatches: CellWebviewFindMatch[]): ITextSearchMatch[] { - return webviewMatches - .map(rawMatch => - (rawMatch.searchPreviewInfo) ? - new TextSearchMatch( - rawMatch.searchPreviewInfo.line, - new Range(0, rawMatch.searchPreviewInfo.range.start, 0, rawMatch.searchPreviewInfo.range.end), - undefined, - rawMatch.index) : undefined - ).filter((e): e is ITextSearchMatch => !!e); -} - -// experimental interface RawOutputFindMatch { textBuffer: IReadonlyTextBuffer; matches: FindMatch[]; } -export const rawCellPrefix = 'rawCell#'; export class CellSearchModel extends Disposable { private _outputTextBuffers: IReadonlyTextBuffer[] | undefined = undefined; - constructor(readonly _source: string, private _inputTextBuffer: IReadonlyTextBuffer | undefined, private _outputs: IOutputItemDto[], private _uri: URI, private _cellIndex: number) { + constructor(readonly _source: string, private _inputTextBuffer: IReadonlyTextBuffer | undefined, private _outputs: string[]) { super(); } - get id() { - return `${rawCellPrefix}${this._cellIndex}`; - } - - get uri() { - return this._uri; - } - private _getFullModelRange(buffer: IReadonlyTextBuffer): Range { const lineCount = buffer.getLineCount(); return new Range(1, 1, lineCount, this._getLineMaxColumn(buffer, lineCount)); @@ -137,7 +49,7 @@ export class CellSearchModel extends Disposable { if (!this._outputTextBuffers) { this._outputTextBuffers = this._outputs.map((output) => { const builder = new PieceTreeTextBufferBuilder(); - builder.acceptChunk(output.data.toString()); + builder.acceptChunk(output); const bufferFactory = builder.finish(true); const { textBuffer, disposable } = bufferFactory.create(DefaultEndOfLine.LF); this._register(disposable); diff --git a/src/vs/workbench/contrib/search/common/searchNotebookHelpers.ts b/src/vs/workbench/contrib/search/common/searchNotebookHelpers.ts new file mode 100644 index 00000000000..d991f46a1ff --- /dev/null +++ b/src/vs/workbench/contrib/search/common/searchNotebookHelpers.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { FindMatch, IReadonlyTextBuffer } from 'vs/editor/common/model'; +import { TextSearchMatch, IFileMatch, ITextSearchMatch } from 'vs/workbench/services/search/common/search'; +import { Range } from 'vs/editor/common/core/range'; +import { URI, UriComponents } from 'vs/base/common/uri'; + +export type IRawClosedNotebookFileMatch = INotebookFileMatchNoModel; + +export interface INotebookFileMatchNoModel extends IFileMatch { + cellResults: INotebookCellMatchNoModel[]; +} + +export interface INotebookCellMatchNoModel { + index: number; + contentResults: ITextSearchMatch[]; + webviewResults: ITextSearchMatch[]; +} + +export function isINotebookFileMatchNoModel(object: IFileMatch): object is INotebookFileMatchNoModel { + return 'cellResults' in object; +} + +export const rawCellPrefix = 'rawCell#'; + +export function genericCellMatchesToTextSearchMatches(contentMatches: FindMatch[], buffer: IReadonlyTextBuffer) { + let previousEndLine = -1; + const contextGroupings: FindMatch[][] = []; + let currentContextGrouping: FindMatch[] = []; + + contentMatches.forEach((match) => { + if (match.range.startLineNumber !== previousEndLine) { + if (currentContextGrouping.length > 0) { + contextGroupings.push([...currentContextGrouping]); + currentContextGrouping = []; + } + } + + currentContextGrouping.push(match); + previousEndLine = match.range.endLineNumber; + }); + + if (currentContextGrouping.length > 0) { + contextGroupings.push([...currentContextGrouping]); + } + + const textSearchResults = contextGroupings.map((grouping) => { + const lineTexts: string[] = []; + const firstLine = grouping[0].range.startLineNumber; + const lastLine = grouping[grouping.length - 1].range.endLineNumber; + for (let i = firstLine; i <= lastLine; i++) { + lineTexts.push(buffer.getLineContent(i)); + } + return new TextSearchMatch( + lineTexts.join('\n') + '\n', + grouping.map(m => new Range(m.range.startLineNumber - 1, m.range.startColumn - 1, m.range.endLineNumber - 1, m.range.endColumn - 1)), + ); + }); + + return textSearchResults; +} + diff --git a/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts b/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts index 140f3ec64d1..11f2670f776 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts @@ -30,7 +30,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { TestEditorGroupsService, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl'; import { createFileUriFromPathFromRoot, getRootName } from 'vs/workbench/contrib/search/test/browser/searchTestCommon'; -import { ICellMatch, IFileMatchWithCells, contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/browser/searchNotebookHelpers'; +import { INotebookCellMatchWithModel, INotebookFileMatchWithModel, contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/browser/notebookSearch/searchNotebookHelpers'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { FindMatch, IReadonlyTextBuffer } from 'vs/editor/common/model'; @@ -226,7 +226,7 @@ suite('SearchModel', () => { } - function notebookSearchServiceWithInfo(results: IFileMatchWithCells[], tokenSource: CancellationTokenSource | undefined): INotebookSearchService { + function notebookSearchServiceWithInfo(results: INotebookFileMatchWithModel[], tokenSource: CancellationTokenSource | undefined): INotebookSearchService { return { _serviceBrand: undefined, notebookSearch(query: ITextQuery, token: CancellationToken | undefined, searchInstanceID: string, onProgress?: (result: ISearchProgressItem) => void): { @@ -238,7 +238,7 @@ suite('SearchModel', () => { if (disposable) { store.add(disposable); } - const localResults = new ResourceMap(uri => uri.path); + const localResults = new ResourceMap(uri => uri.path); results.forEach(r => { localResults.set(r.resource, r); @@ -354,14 +354,14 @@ suite('SearchModel', () => { } } ]; - const cellMatchMd: ICellMatch = { + const cellMatchMd: INotebookCellMatchWithModel = { cell: mdInputCell, index: 0, contentResults: contentMatchesToTextSearchMatches(findMatchMds, mdInputCell), webviewResults: [] }; - const cellMatchCode: ICellMatch = { + const cellMatchCode: INotebookCellMatchWithModel = { cell: codeCell, index: 1, contentResults: contentMatchesToTextSearchMatches(findMatchCodeCells, codeCell), @@ -388,12 +388,11 @@ suite('SearchModel', () => { assert.ok(notebookFileMatches[4].range().equalsRange(new Range(1, 8, 1, 12))); notebookFileMatches.forEach(match => match instanceof MatchInNotebook); - // assert(notebookFileMatches[0] instanceof MatchInNotebook); - assert((notebookFileMatches[0] as MatchInNotebook).cell.id === 'mdInputCell'); - assert((notebookFileMatches[1] as MatchInNotebook).cell.id === 'codeCell'); - assert((notebookFileMatches[2] as MatchInNotebook).cell.id === 'codeCell'); - assert((notebookFileMatches[3] as MatchInNotebook).cell.id === 'codeCell'); - assert((notebookFileMatches[4] as MatchInNotebook).cell.id === 'codeCell'); + assert((notebookFileMatches[0] as MatchInNotebook).cell?.id === 'mdInputCell'); + assert((notebookFileMatches[1] as MatchInNotebook).cell?.id === 'codeCell'); + assert((notebookFileMatches[2] as MatchInNotebook).cell?.id === 'codeCell'); + assert((notebookFileMatches[3] as MatchInNotebook).cell?.id === 'codeCell'); + assert((notebookFileMatches[4] as MatchInNotebook).cell?.id === 'codeCell'); const mdCellMatchProcessed = (notebookFileMatches[0] as MatchInNotebook).cellParent; const codeCellMatchProcessed = (notebookFileMatches[1] as MatchInNotebook).cellParent; @@ -598,7 +597,7 @@ suite('SearchModel', () => { return { resource: createFileUriFromPathFromRoot(resource), results }; } - function aRawMatchWithCells(resource: string, ...cells: ICellMatch[]) { + function aRawMatchWithCells(resource: string, ...cells: INotebookCellMatchWithModel[]) { return { resource: createFileUriFromPathFromRoot(resource), cellResults: cells }; } 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 ff2c6a9039e..0a591e21457 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts @@ -9,7 +9,7 @@ import { FindMatch, IReadonlyTextBuffer } from 'vs/editor/common/model'; import { IFileMatch, ISearchRange, ITextSearchMatch, QueryType } from 'vs/workbench/services/search/common/search'; import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/browser/searchNotebookHelpers'; +import { contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/browser/notebookSearch/searchNotebookHelpers'; import { CellFindMatchModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; import { CellMatch, FileMatch, FolderMatch, SearchModel, textSearchMatchesToNotebookMatches } from 'vs/workbench/contrib/search/browser/searchModel'; import { URI } from 'vs/base/common/uri'; @@ -40,6 +40,7 @@ suite('searchNotebookHelpers', () => { instantiationService.stub(IModelService, modelService); instantiationService.stub(INotebookEditorService, notebookEditorService); mdInputCell = { + id: 'mdCell', cellKind: CellKind.Markup, textBuffer: { getLineContent(lineNumber: number): string { if (lineNumber === 1) { @@ -53,6 +54,7 @@ suite('searchNotebookHelpers', () => { const findMatchMds = [new FindMatch(new Range(1, 15, 1, 19), ['Test'])]; codeCell = { + id: 'codeCell', cellKind: CellKind.Code, textBuffer: { getLineContent(lineNumber: number): string { if (lineNumber === 1) { @@ -180,18 +182,18 @@ suite('searchNotebookHelpers', () => { const codeWebviewContentMatchObjs = textSearchMatchesToNotebookMatches(codeWebviewResults, codeCellMatch); - assert.strictEqual(markdownCellContentMatchObjs[0].cell.id, mdCellMatch.id); + assert.strictEqual(markdownCellContentMatchObjs[0].cell?.id, mdCellMatch.id); assertRangesEqual(markdownCellContentMatchObjs[0].range(), [new Range(1, 15, 1, 19)]); - assert.strictEqual(codeCellContentMatchObjs[0].cell.id, codeCellMatch.id); - assert.strictEqual(codeCellContentMatchObjs[1].cell.id, codeCellMatch.id); + assert.strictEqual(codeCellContentMatchObjs[0].cell?.id, codeCellMatch.id); + assert.strictEqual(codeCellContentMatchObjs[1].cell?.id, codeCellMatch.id); assertRangesEqual(codeCellContentMatchObjs[0].range(), [new Range(1, 8, 1, 12)]); assertRangesEqual(codeCellContentMatchObjs[1].range(), [new Range(1, 14, 1, 18)]); assertRangesEqual(codeCellContentMatchObjs[2].range(), [new Range(2, 18, 2, 22)]); - assert.strictEqual(codeWebviewContentMatchObjs[0].cell.id, codeCellMatch.id); - assert.strictEqual(codeWebviewContentMatchObjs[1].cell.id, codeCellMatch.id); - assert.strictEqual(codeWebviewContentMatchObjs[2].cell.id, codeCellMatch.id); + assert.strictEqual(codeWebviewContentMatchObjs[0].cell?.id, codeCellMatch.id); + assert.strictEqual(codeWebviewContentMatchObjs[1].cell?.id, codeCellMatch.id); + assert.strictEqual(codeWebviewContentMatchObjs[2].cell?.id, codeCellMatch.id); assertRangesEqual(codeWebviewContentMatchObjs[0].range(), [new Range(1, 2, 1, 6)]); assertRangesEqual(codeWebviewContentMatchObjs[1].range(), [new Range(1, 8, 1, 12)]); assertRangesEqual(codeWebviewContentMatchObjs[2].range(), [new Range(1, 12, 1, 16)]); diff --git a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts index ad53f73d11c..32053f58f96 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts @@ -28,10 +28,10 @@ import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/se import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { TestEditorGroupsService, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl'; -import { ICellMatch, IFileMatchWithCells } from 'vs/workbench/contrib/search/browser/searchNotebookHelpers'; import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { addToSearchResult, createFileUriFromPathFromRoot, getRootName } from 'vs/workbench/contrib/search/test/browser/searchTestCommon'; +import { INotebookCellMatchWithModel, INotebookFileMatchWithModel } from 'vs/workbench/contrib/search/browser/notebookSearch/searchNotebookHelpers'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -274,10 +274,10 @@ suite('SearchResult', () => { addToSearchResult(testObject, target); assert.strictEqual(6, testObject.count()); - assert.deepStrictEqual(fileMatch1.cellResults[0].contentResults, (addFileMatch.getCall(0).args[0][0] as IFileMatchWithCells).cellResults[0].contentResults); - assert.deepStrictEqual(fileMatch1.cellResults[0].webviewResults, (addFileMatch.getCall(0).args[0][0] as IFileMatchWithCells).cellResults[0].webviewResults); - assert.deepStrictEqual(fileMatch2.cellResults[0].contentResults, (addFileMatch.getCall(0).args[0][1] as IFileMatchWithCells).cellResults[0].contentResults); - assert.deepStrictEqual(fileMatch2.cellResults[0].webviewResults, (addFileMatch.getCall(0).args[0][1] as IFileMatchWithCells).cellResults[0].webviewResults); + assert.deepStrictEqual(fileMatch1.cellResults[0].contentResults, (addFileMatch.getCall(0).args[0][0] as INotebookFileMatchWithModel).cellResults[0].contentResults); + assert.deepStrictEqual(fileMatch1.cellResults[0].webviewResults, (addFileMatch.getCall(0).args[0][0] as INotebookFileMatchWithModel).cellResults[0].webviewResults); + assert.deepStrictEqual(fileMatch2.cellResults[0].contentResults, (addFileMatch.getCall(0).args[0][1] as INotebookFileMatchWithModel).cellResults[0].contentResults); + assert.deepStrictEqual(fileMatch2.cellResults[0].webviewResults, (addFileMatch.getCall(0).args[0][1] as INotebookFileMatchWithModel).cellResults[0].webviewResults); }); test('Dispose disposes matches', function () { @@ -552,7 +552,7 @@ suite('SearchResult', () => { return { resource: createFileUriFromPathFromRoot(resource), results }; } - function aRawFileMatchWithCells(resource: string, ...cellMatches: ICellMatch[]): IFileMatchWithCells { + function aRawFileMatchWithCells(resource: string, ...cellMatches: INotebookCellMatchWithModel[]): INotebookFileMatchWithModel { return { resource: createFileUriFromPathFromRoot(resource), cellResults: cellMatches diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts index e685bbdc511..c017b2db0ed 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts @@ -113,7 +113,7 @@ function matchesToSearchResultFormat(resource: URI, sortedMatches: Match[], matc } function cellMatchToSearchResultFormat(cellMatch: CellMatch, labelFormatter: (x: URI) => string, shouldUseHeader: boolean): SearchResultSerialization { - return matchesToSearchResultFormat(cellMatch.cell.uri, cellMatch.contentMatches.sort(searchMatchComparer), cellMatch.context, labelFormatter, shouldUseHeader); + return matchesToSearchResultFormat(cellMatch.cell?.uri ?? cellMatch.parent.resource, cellMatch.contentMatches.sort(searchMatchComparer), cellMatch.context, labelFormatter, shouldUseHeader); } const contentPatternToSearchConfiguration = (pattern: ITextQuery, includes: string, excludes: string, contextLines: number): SearchConfiguration => { diff --git a/src/vs/workbench/services/search/browser/searchService.ts b/src/vs/workbench/services/search/browser/searchService.ts index f1fc4a45c5b..9a8bd127476 100644 --- a/src/vs/workbench/services/search/browser/searchService.ts +++ b/src/vs/workbench/services/search/browser/searchService.ts @@ -26,6 +26,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { Emitter, Event } from 'vs/base/common/event'; import { localize } from 'vs/nls'; import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAccess'; +import { revive } from 'vs/base/common/marshalling'; export class RemoteSearchService extends SearchService { constructor( @@ -99,21 +100,16 @@ export class LocalFileSearchWorkerClient extends Disposable implements ISearchRe return; } - const reviveMatch = (result: IFileMatch): IFileMatch => ({ - resource: URI.revive(result.resource), - results: result.results - }); - queryDisposables.add(this.onDidReceiveTextSearchMatch(e => { if (e.queryId === queryId) { - onProgress?.(reviveMatch(e.match)); + onProgress?.(revive(e.match)); } })); const ignorePathCasing = this.uriIdentityService.extUri.ignorePathCasing(fq.folder); const folderResults = await proxy.searchDirectory(handle, query, fq, ignorePathCasing, queryId); for (const folderResult of folderResults.results) { - results.push(reviveMatch(folderResult)); + results.push(revive(folderResult)); } if (folderResults.limitHit) { diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index 46189842db4..7803e26cb47 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -164,7 +164,7 @@ export interface IExtendedExtensionSearchOptions { export interface IFileMatch { resource: U; - results?: ITextSearchResult[]; + results?: ITextSearchResult[]; } export type IRawFileMatch2 = IFileMatch; @@ -187,20 +187,20 @@ export interface ITextSearchResultPreview { cellFragment?: string; } -export interface ITextSearchMatch { - uri?: URI; +export interface ITextSearchMatch { + uri?: U; ranges: ISearchRange | ISearchRange[]; preview: ITextSearchResultPreview; webviewIndex?: number; } -export interface ITextSearchContext { - uri?: URI; +export interface ITextSearchContext { + uri?: U; text: string; lineNumber: number; } -export type ITextSearchResult = ITextSearchMatch | ITextSearchContext; +export type ITextSearchResult = ITextSearchMatch | ITextSearchContext; export function resultIsMatch(result: ITextSearchResult): result is ITextSearchMatch { return !!(result).preview;