mirror of
https://github.com/Microsoft/vscode
synced 2024-10-02 17:32:41 +00:00
implement closed-notebook search (#174287)
This commit is contained in:
parent
b008409c62
commit
60578ed419
18
src/vs/workbench/contrib/search/browser/notebookSearch.ts
Normal file
18
src/vs/workbench/contrib/search/browser/notebookSearch.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { ResourceSet } from 'vs/base/common/map';
|
||||
import { ITextQuery, ISearchProgressItem, ISearchComplete } from 'vs/workbench/services/search/common/search';
|
||||
|
||||
export const INotebookSearchService = createDecorator<INotebookSearchService>('notebookSearchService');
|
||||
|
||||
export interface INotebookSearchService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
notebookSearch(query: ITextQuery, token: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise<{ completeData: ISearchComplete; scannedFiles: ResourceSet }>;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { ReplacePreviewContentProvider } from 'vs/workbench/contrib/search/browser/replaceService';
|
||||
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/browser/notebookSearch';
|
||||
import { NotebookSearchService } from 'vs/workbench/contrib/search/browser/notebookSearchService';
|
||||
|
||||
export function registerContributions(): void {
|
||||
registerSingleton(INotebookSearchService, NotebookSearchService, InstantiationType.Delayed);
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ReplacePreviewContentProvider, LifecyclePhase.Starting);
|
||||
}
|
310
src/vs/workbench/contrib/search/browser/notebookSearchService.ts
Normal file
310
src/vs/workbench/contrib/search/browser/notebookSearchService.ts
Normal file
|
@ -0,0 +1,310 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* 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/browser/notebookSearch';
|
||||
import { IFileMatchWithCells, ICellMatch, CellSearchModel, contentMatchesToTextSearchMatches, webviewMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/browser/searchNotebookHelpers';
|
||||
import { IEditorResolverService, priorityToRank } from 'vs/workbench/services/editor/common/editorResolverService';
|
||||
import { ITextQuery, IFileQuery, QueryType, ISearchProgressItem, ISearchComplete, ISearchConfigurationProperties, 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<IFileMatchWithCells | null>;
|
||||
limitHit: boolean;
|
||||
}
|
||||
|
||||
class NotebookDataCache {
|
||||
private _entries: ResourceMap<INotebookDataEditInfo>;
|
||||
// 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<INotebookDataEditInfo>(uri => this.uriIdentityService.extUri.getComparisonKey(uri));
|
||||
}
|
||||
|
||||
private async getSerializer(notebookUri: URI): Promise<INotebookSerializer | undefined> {
|
||||
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<NotebookData> {
|
||||
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<URI[]> {
|
||||
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());
|
||||
}
|
||||
|
||||
async notebookSearch(query: ITextQuery, token: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise<{ completeData: ISearchComplete; scannedFiles: ResourceSet }> {
|
||||
|
||||
if (query.type !== QueryType.Text) {
|
||||
return {
|
||||
completeData: {
|
||||
messages: [],
|
||||
limitHit: false,
|
||||
results: [],
|
||||
},
|
||||
scannedFiles: new ResourceSet()
|
||||
};
|
||||
}
|
||||
const searchStart = Date.now();
|
||||
|
||||
const localNotebookWidgets = this.getLocalNotebookWidgets();
|
||||
const localNotebookFiles = localNotebookWidgets.map(widget => widget.viewModel!.uri);
|
||||
const localResultPromise = this.getLocalNotebookResults(query, token, localNotebookWidgets);
|
||||
const searchLocalEnd = Date.now();
|
||||
|
||||
const experimentalNotebooksEnabled = this.configurationService.getValue<ISearchConfigurationProperties>('search').experimental?.closedNotebookRichContentResults ?? false;
|
||||
|
||||
let closedResultsPromise: Promise<INotebookSearchMatchResults | undefined> = Promise.resolve(undefined);
|
||||
if (experimentalNotebooksEnabled) {
|
||||
closedResultsPromise = this.getClosedNotebookResults(query, new ResourceSet(localNotebookFiles, uri => this.uriIdentityService.extUri.getComparisonKey(uri)), token);
|
||||
}
|
||||
|
||||
const resolved = (await Promise.all([localResultPromise, closedResultsPromise])).filter((result): result is INotebookSearchMatchResults => !!result);
|
||||
const resultArray = resolved.map(elem => elem.results);
|
||||
|
||||
const results = arrays.coalesce(resultArray.flatMap(map => Array.from(map.values())));
|
||||
const scannedFiles = new ResourceSet(resultArray.flatMap(map => Array.from(map.keys())), uri => this.uriIdentityService.extUri.getComparisonKey(uri));
|
||||
if (onProgress) {
|
||||
results.forEach(onProgress);
|
||||
}
|
||||
this.logService.trace(`local notebook search time | ${searchLocalEnd - searchStart}ms`);
|
||||
return {
|
||||
completeData: {
|
||||
messages: [],
|
||||
limitHit: resolved.reduce((prev, cur) => prev || cur.limitHit, false),
|
||||
results,
|
||||
},
|
||||
scannedFiles
|
||||
};
|
||||
}
|
||||
|
||||
private async getClosedNotebookResults(textQuery: ITextQuery, scannedFiles: ResourceSet, token: CancellationToken): Promise<INotebookSearchMatchResults> {
|
||||
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<IFileMatchWithCells | null>(uri => this.uriIdentityService.extUri.getComparisonKey(uri));
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
const filesToScan = await this.runFileQueries(includes, token, textQuery);
|
||||
const deserializedNotebooks = new ResourceMap<NotebookTextModel>();
|
||||
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 cells = deserializedNotebooks.get(uri)?.cells ?? (await this._notebookDataCache.getNotebookData(uri)).cells;
|
||||
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
cells.forEach((cell, index) => {
|
||||
const target = textQuery.contentPattern.pattern;
|
||||
const cellModel = cell instanceof NotebookCellTextModel ? new CellSearchModel('', cell.textBuffer, uri, index) : new CellSearchModel(cell.source, undefined, uri, index);
|
||||
|
||||
const matches = cellModel.find(target);
|
||||
if (matches.length > 0) {
|
||||
const cellMatch: ICellMatch = {
|
||||
cell: cellModel,
|
||||
index: index,
|
||||
contentResults: contentMatchesToTextSearchMatches(matches, 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<NotebookEditorWidget>): Promise<INotebookSearchMatchResults> {
|
||||
const localResults = new ResourceMap<IFileMatchWithCells | null>(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,
|
||||
includeMarkupPreview: query.contentPattern.notebookInfo?.isInNotebookMarkdownPreview,
|
||||
includeCodeInput: query.contentPattern.notebookInfo?.isInNotebookCellInput,
|
||||
includeOutput: query.contentPattern.notebookInfo?.isInNotebookCellOutput,
|
||||
}, token, false, true);
|
||||
|
||||
|
||||
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<NotebookEditorWidget> {
|
||||
const notebookWidgets = this.notebookEditorService.retrieveAllExistingWidgets();
|
||||
return notebookWidgets
|
||||
.map(widget => widget.value)
|
||||
.filter((val): val is NotebookEditorWidget => !!val && !!(val.viewModel));
|
||||
}
|
||||
}
|
|
@ -21,6 +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 { 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';
|
||||
|
@ -46,6 +47,7 @@ registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, Instantiation
|
|||
registerSingleton(ISearchHistoryService, SearchHistoryService, InstantiationType.Delayed);
|
||||
|
||||
replaceContributions();
|
||||
notebookSearchContributions();
|
||||
searchWidgetContributions();
|
||||
|
||||
const SEARCH_MODE_CONFIG = 'search.mode';
|
||||
|
@ -349,6 +351,11 @@ configurationRegistry.registerConfiguration({
|
|||
nls.localize('scm.defaultViewMode.list', "Shows search results as a list.")
|
||||
],
|
||||
'description': nls.localize('search.defaultViewMode', "Controls the default search result view mode.")
|
||||
},
|
||||
'search.experimental.closedNotebookRichContentResults': {
|
||||
type: 'boolean',
|
||||
description: nls.localize('search.experimental.closedNotebookResults', "Show notebook editor rich content results for closed notebooks. Please refresh your search results after changing this setting."),
|
||||
default: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { compareFileExtensions, compareFileNames, comparePaths } from 'vs/base/common/comparers';
|
||||
|
@ -12,11 +12,10 @@ import * as errors from 'vs/base/common/errors';
|
|||
import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event';
|
||||
import { Lazy } from 'vs/base/common/lazy';
|
||||
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ResourceMap, ResourceSet } from 'vs/base/common/map';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { lcut } from 'vs/base/common/strings';
|
||||
import { TernarySearchTree } from 'vs/base/common/ternarySearchTree';
|
||||
import { isNumber } from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { FindMatch, IModelDeltaDecoration, ITextModel, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model';
|
||||
|
@ -26,6 +25,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
|||
import { IFileService, IFileStatWithPartialMetadata } from 'vs/platform/files/common/files';
|
||||
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { minimapFindMatch, overviewRulerFindMatchForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
|
@ -36,10 +36,11 @@ import { CellEditState, CellFindMatchWithIndex, CellWebviewFindMatch, ICellViewM
|
|||
import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget';
|
||||
import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService';
|
||||
import { NotebookCellsChangeType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { INotebookSearchService } from 'vs/workbench/contrib/search/browser/notebookSearch';
|
||||
import { IReplaceService } from 'vs/workbench/contrib/search/browser/replace';
|
||||
import { ICellMatch, IFileMatchWithCells, contentMatchesToTextSearchMatches, isIFileMatchWithCells, webviewMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/browser/searchNotebookHelpers';
|
||||
import { CellSearchModel, ICellMatch, contentMatchesToTextSearchMatches, isIFileMatchWithCells, rawCellPrefix, webviewMatchesToTextSearchMatches } from 'vs/workbench/contrib/search/browser/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, QueryType, resultIsMatch, SearchCompletionExitCode, SearchSortOrder } from 'vs/workbench/services/search/common/search';
|
||||
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 { addContextToEditorMatches, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers';
|
||||
|
||||
export class Match {
|
||||
|
@ -186,7 +187,7 @@ export class CellMatch {
|
|||
|
||||
constructor(
|
||||
private readonly _parent: FileMatch,
|
||||
private readonly _cell: ICellViewModel,
|
||||
private _cell: ICellViewModel | CellSearchModel,
|
||||
private readonly _cellIndex: number,
|
||||
) {
|
||||
|
||||
|
@ -230,6 +231,10 @@ export class CellMatch {
|
|||
}
|
||||
|
||||
public addContext(textSearchMatches: ITextSearchMatch[]) {
|
||||
if (this.cell instanceof CellSearchModel) {
|
||||
// todo: get closed notebook results in search editor
|
||||
return;
|
||||
}
|
||||
this.cell.resolveTextModel().then((textModel) => {
|
||||
const textResultsWithContext = addContextToEditorMatches(textSearchMatches, textModel, this.parent.parent().query!);
|
||||
const contexts = textResultsWithContext.filter((result => !resultIsMatch(result)) as ((a: any) => a is ITextSearchContext));
|
||||
|
@ -247,6 +252,10 @@ export class CellMatch {
|
|||
}
|
||||
|
||||
|
||||
setCellModel(cell: ICellViewModel) {
|
||||
this._cell = cell;
|
||||
}
|
||||
|
||||
get parent(): FileMatch {
|
||||
return this._parent;
|
||||
}
|
||||
|
@ -259,7 +268,7 @@ export class CellMatch {
|
|||
return this._cellIndex;
|
||||
}
|
||||
|
||||
get cell(): ICellViewModel {
|
||||
get cell(): ICellViewModel | CellSearchModel {
|
||||
return this._cell;
|
||||
}
|
||||
|
||||
|
@ -764,8 +773,8 @@ export class FileMatch extends Disposable implements IFileMatch {
|
|||
this._findMatchDecorationModel = undefined;
|
||||
}
|
||||
}
|
||||
private updateNotebookMatches(matches: CellFindMatchWithIndex[], modelChange: boolean): void {
|
||||
|
||||
private updateNotebookMatches(matches: CellFindMatchWithIndex[], modelChange: boolean): void {
|
||||
if (!this._notebookEditorWidget) {
|
||||
return;
|
||||
}
|
||||
|
@ -774,12 +783,20 @@ export class FileMatch extends Disposable implements IFileMatch {
|
|||
this._cellMatches.clear();
|
||||
this._lastEditorWidgetIdForUpdate = this._notebookEditorWidget.getId();
|
||||
}
|
||||
|
||||
matches.forEach(match => {
|
||||
let cell = this._cellMatches.get(match.cell.id);
|
||||
if (!cell) {
|
||||
cell = new CellMatch(this, match.cell, match.index);
|
||||
|
||||
|
||||
let existingCell = this._cellMatches.get(match.cell.id);
|
||||
if (this._notebookEditorWidget && !existingCell) {
|
||||
const index = this._notebookEditorWidget.getCellIndex(match.cell);
|
||||
const existingRawCell = this._cellMatches.get(`${rawCellPrefix}${index}`);
|
||||
if (existingRawCell) {
|
||||
existingRawCell.setCellModel(match.cell);
|
||||
this._cellMatches.delete(`${rawCellPrefix}${index}`);
|
||||
existingCell = existingRawCell;
|
||||
}
|
||||
}
|
||||
const cell = existingCell ?? new CellMatch(this, match.cell, match.index);
|
||||
cell.addContentMatches(contentMatchesToTextSearchMatches(match.contentMatches, match.cell));
|
||||
cell.addWebviewMatches(webviewMatchesToTextSearchMatches(match.webviewMatches));
|
||||
this._cellMatches.set(cell.id, cell);
|
||||
|
@ -846,7 +863,8 @@ export class FileMatch extends Disposable implements IFileMatch {
|
|||
}
|
||||
|
||||
private async highlightCurrentFindMatchDecoration(match: MatchInNotebook): Promise<number | null> {
|
||||
if (!this._findMatchDecorationModel) {
|
||||
if (!this._findMatchDecorationModel || match.cell instanceof CellSearchModel) {
|
||||
// match cell should never be a CellSearchModel if the notebook is open
|
||||
return null;
|
||||
}
|
||||
if (match.webviewIndex === undefined) {
|
||||
|
@ -857,7 +875,8 @@ export class FileMatch extends Disposable implements IFileMatch {
|
|||
}
|
||||
|
||||
private revealCellRange(match: MatchInNotebook, outputOffset: number | null) {
|
||||
if (!this._notebookEditorWidget) {
|
||||
if (!this._notebookEditorWidget || match.cell instanceof CellSearchModel) {
|
||||
// match cell should never be a CellSearchModel if the notebook is open
|
||||
return;
|
||||
}
|
||||
if (match.webviewIndex !== undefined) {
|
||||
|
@ -1929,8 +1948,8 @@ export class SearchModel extends Disposable {
|
|||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
|
||||
@INotebookEditorService private readonly notebookEditorService: INotebookEditorService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@INotebookSearchService private readonly notebookSearchService: INotebookSearchService,
|
||||
) {
|
||||
super();
|
||||
this._searchResult = this.instantiationService.createInstance(SearchResult, this);
|
||||
|
@ -1972,79 +1991,8 @@ export class SearchModel extends Disposable {
|
|||
return this._searchResult;
|
||||
}
|
||||
|
||||
private async getLocalNotebookResults(query: ITextQuery, token: CancellationToken): Promise<{ results: ResourceMap<IFileMatchWithCells | null>; limitHit: boolean }> {
|
||||
const localResults = new ResourceMap<IFileMatchWithCells | null>(uri => this.uriIdentityService.extUri.getComparisonKey(uri));
|
||||
let limitHit = false;
|
||||
|
||||
if (query.type === QueryType.Text) {
|
||||
const notebookWidgets = this.notebookEditorService.retrieveAllExistingWidgets();
|
||||
for (const borrowWidget of notebookWidgets) {
|
||||
const widget = borrowWidget.value;
|
||||
if (!widget || !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,
|
||||
includeMarkupPreview: query.contentPattern.notebookInfo?.isInNotebookMarkdownPreview,
|
||||
includeCodeInput: query.contentPattern.notebookInfo?.isInNotebookCellInput,
|
||||
includeOutput: query.contentPattern.notebookInfo?.isInNotebookCellOutput,
|
||||
}, token, false, true);
|
||||
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
async notebookSearch(query: ITextQuery, token: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise<{ completeData: ISearchComplete; scannedFiles: ResourceSet }> {
|
||||
const localResults = await this.getLocalNotebookResults(query, token);
|
||||
if (onProgress) {
|
||||
arrays.coalesce([...localResults.results.values()]).forEach(onProgress);
|
||||
}
|
||||
return {
|
||||
completeData: {
|
||||
messages: [],
|
||||
limitHit: localResults.limitHit,
|
||||
results: arrays.coalesce([...localResults.results.values()]),
|
||||
},
|
||||
scannedFiles: new ResourceSet([...localResults.results.keys()], uri => this.uriIdentityService.extUri.getComparisonKey(uri))
|
||||
};
|
||||
}
|
||||
|
||||
private async doSearch(query: ITextQuery, progressEmitter: Emitter<void>, searchQuery: ITextQuery, onProgress?: (result: ISearchProgressItem) => void): Promise<ISearchComplete> {
|
||||
const searchStart = Date.now();
|
||||
const tokenSource = this.currentCancelTokenSource = new CancellationTokenSource();
|
||||
const onProgressCall = (p: ISearchProgressItem) => {
|
||||
progressEmitter.fire();
|
||||
|
@ -2052,13 +2000,15 @@ export class SearchModel extends Disposable {
|
|||
|
||||
onProgress?.(p);
|
||||
};
|
||||
const notebookResult = await this.notebookSearch(query, this.currentCancelTokenSource.token, onProgressCall);
|
||||
const notebookResult = await this.notebookSearchService.notebookSearch(query, this.currentCancelTokenSource.token, onProgressCall);
|
||||
const currentResult = await this.searchService.textSearch(
|
||||
searchQuery,
|
||||
this.currentCancelTokenSource.token, onProgressCall,
|
||||
notebookResult?.scannedFiles
|
||||
);
|
||||
tokenSource.dispose();
|
||||
const searchLength = Date.now() - searchStart;
|
||||
this.logService.trace(`whole search time | ${searchLength}ms`);
|
||||
return notebookResult ? { ...currentResult, ...notebookResult.completeData } : currentResult;
|
||||
}
|
||||
|
||||
|
@ -2305,6 +2255,8 @@ export class RangeHighlightDecorations implements IDisposable {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function textSearchResultToMatches(rawMatch: ITextSearchMatch, fileMatch: FileMatch): Match[] {
|
||||
const previewLines = rawMatch.preview.text.split('\n');
|
||||
if (Array.isArray(rawMatch.ranges)) {
|
||||
|
@ -2364,3 +2316,4 @@ function getFileMatches(matches: (FileMatch | FolderMatchWithResource)[]): FileM
|
|||
|
||||
return fileMatches.concat(folderMatches.map(e => e.allDownstreamFileMatches()).flat());
|
||||
}
|
||||
|
||||
|
|
|
@ -3,17 +3,21 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { FindMatch } from 'vs/editor/common/model';
|
||||
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 { Range } from 'vs/editor/common/core/range';
|
||||
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';
|
||||
|
||||
export interface IFileMatchWithCells extends IFileMatch {
|
||||
cellResults: ICellMatch[];
|
||||
}
|
||||
|
||||
export interface ICellMatch {
|
||||
cell: ICellViewModel;
|
||||
cell: ICellViewModel | CellSearchModel;
|
||||
index: number;
|
||||
contentResults: ITextSearchMatch[];
|
||||
webviewResults: ITextSearchMatch[];
|
||||
|
@ -24,7 +28,7 @@ export function isIFileMatchWithCells(object: IFileMatch): object is IFileMatchW
|
|||
|
||||
// to text search results
|
||||
|
||||
export function contentMatchesToTextSearchMatches(contentMatches: FindMatch[], cell: ICellViewModel): ITextSearchMatch[] {
|
||||
export function contentMatchesToTextSearchMatches(contentMatches: FindMatch[], cell: ICellViewModel | CellSearchModel): ITextSearchMatch[] {
|
||||
let previousEndLine = -1;
|
||||
const contextGroupings: FindMatch[][] = [];
|
||||
let currentContextGrouping: FindMatch[] = [];
|
||||
|
@ -73,4 +77,56 @@ export function webviewMatchesToTextSearchMatches(webviewMatches: CellWebviewFin
|
|||
).filter((e): e is ITextSearchMatch => !!e);
|
||||
}
|
||||
|
||||
// experimental
|
||||
|
||||
export const rawCellPrefix = 'rawCell#';
|
||||
export class CellSearchModel extends Disposable {
|
||||
constructor(readonly _source: string, private _textBuffer: IReadonlyTextBuffer | undefined, private _uri: URI, private _cellIndex: number) {
|
||||
super();
|
||||
}
|
||||
|
||||
get id() {
|
||||
return `${rawCellPrefix}${this._cellIndex}`;
|
||||
}
|
||||
|
||||
get uri() {
|
||||
return this._uri;
|
||||
}
|
||||
|
||||
public getFullModelRange(): Range {
|
||||
const lineCount = this.textBuffer.getLineCount();
|
||||
return new Range(1, 1, lineCount, this.getLineMaxColumn(lineCount));
|
||||
}
|
||||
|
||||
public getLineMaxColumn(lineNumber: number): number {
|
||||
if (lineNumber < 1 || lineNumber > this.textBuffer.getLineCount()) {
|
||||
throw new Error('Illegal value for lineNumber');
|
||||
}
|
||||
return this.textBuffer.getLineLength(lineNumber) + 1;
|
||||
}
|
||||
|
||||
get textBuffer() {
|
||||
if (this._textBuffer) {
|
||||
return this._textBuffer;
|
||||
}
|
||||
|
||||
const builder = new PieceTreeTextBufferBuilder();
|
||||
builder.acceptChunk(this._source);
|
||||
const bufferFactory = builder.finish(true);
|
||||
const { textBuffer, disposable } = bufferFactory.create(DefaultEndOfLine.LF);
|
||||
this._textBuffer = textBuffer;
|
||||
this._register(disposable);
|
||||
|
||||
return this._textBuffer;
|
||||
}
|
||||
|
||||
find(target: string): FindMatch[] {
|
||||
const searchParams = new SearchParams(target, false, false, null);
|
||||
const searchData = searchParams.parseSearchRequest();
|
||||
if (!searchData) {
|
||||
return [];
|
||||
}
|
||||
const fullRange = this.getFullModelRange();
|
||||
return this.textBuffer.findMatchesLineByLine(fullRange, searchData, true, 5000);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1861,15 +1861,19 @@ export class SearchView extends ViewPane {
|
|||
const oldParentMatches = element instanceof Match ? element.parent().matches() : [];
|
||||
const resource = resourceInput ?? (element instanceof Match ? element.parent().resource : (<FileMatch>element).resource);
|
||||
let editor: IEditorPane | undefined;
|
||||
|
||||
const options = {
|
||||
preserveFocus,
|
||||
pinned,
|
||||
selection,
|
||||
revealIfVisible: true,
|
||||
indexedCellOptions: element instanceof MatchInNotebook ? { cellIndex: element.cellIndex, selection: element.range } : undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
editor = await this.editorService.openEditor({
|
||||
resource: resource,
|
||||
options: {
|
||||
preserveFocus,
|
||||
pinned,
|
||||
selection,
|
||||
revealIfVisible: true
|
||||
}
|
||||
options,
|
||||
}, sideBySide ? SIDE_GROUP : ACTIVE_GROUP);
|
||||
|
||||
const editorControl = editor?.getControl();
|
||||
|
|
|
@ -14,7 +14,7 @@ import { ModelService } from 'vs/editor/common/services/modelService';
|
|||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { IFileMatch, IFileSearchStats, IFolderQuery, ISearchComplete, ISearchProgressItem, ISearchQuery, ISearchService, ITextQuery, ITextSearchMatch, OneLineRange, QueryType, TextSearchMatch } from 'vs/workbench/services/search/common/search';
|
||||
import { IFileMatch, IFileQuery, IFileSearchStats, IFolderQuery, ISearchComplete, ISearchProgressItem, ISearchQuery, ISearchService, ITextSearchMatch, OneLineRange, QueryType, TextSearchMatch } from 'vs/workbench/services/search/common/search';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { CellMatch, MatchInNotebook, SearchModel } from 'vs/workbench/contrib/search/browser/searchModel';
|
||||
|
@ -35,6 +35,8 @@ 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';
|
||||
import { ResourceMap, ResourceSet } from 'vs/base/common/map';
|
||||
import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
|
||||
import { INotebookSearchService } from 'vs/workbench/contrib/search/browser/notebookSearch';
|
||||
|
||||
const nullEvent = new class {
|
||||
id: number = -1;
|
||||
|
@ -82,6 +84,7 @@ suite('SearchModel', () => {
|
|||
instantiationService = new TestInstantiationService();
|
||||
instantiationService.stub(ITelemetryService, NullTelemetryService);
|
||||
instantiationService.stub(ILabelService, { getUriBasenameLabel: (uri: URI) => '' });
|
||||
instantiationService.stub(INotebookService, { getNotebookTextModels: () => [] });
|
||||
instantiationService.stub(IModelService, stubModelService(instantiationService));
|
||||
instantiationService.stub(INotebookEditorService, stubNotebookEditorService(instantiationService));
|
||||
instantiationService.stub(ISearchService, {});
|
||||
|
@ -101,6 +104,14 @@ suite('SearchModel', () => {
|
|||
resolve(complete!);
|
||||
});
|
||||
});
|
||||
},
|
||||
fileSearch(query: IFileQuery, token?: CancellationToken): Promise<ISearchComplete> {
|
||||
return new Promise(resolve => {
|
||||
queueMicrotask(() => {
|
||||
resolve({ results: results, messages: [] });
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -111,6 +122,13 @@ suite('SearchModel', () => {
|
|||
return new Promise((resolve, reject) => {
|
||||
reject(error);
|
||||
});
|
||||
},
|
||||
fileSearch(query: IFileQuery, token?: CancellationToken): Promise<ISearchComplete> {
|
||||
return new Promise((resolve, reject) => {
|
||||
queueMicrotask(() => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -125,6 +143,43 @@ suite('SearchModel', () => {
|
|||
resolve(<any>{});
|
||||
});
|
||||
});
|
||||
},
|
||||
fileSearch(query: IFileQuery, token?: CancellationToken): Promise<ISearchComplete> {
|
||||
token?.onCancellationRequested(() => tokenSource.cancel());
|
||||
return new Promise(resolve => {
|
||||
queueMicrotask(() => {
|
||||
resolve(<any>{});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
function notebookSearchServiceWithInfo(results: IFileMatchWithCells[], tokenSource: CancellationTokenSource | undefined): INotebookSearchService {
|
||||
return <INotebookSearchService>{
|
||||
_serviceBrand: undefined,
|
||||
notebookSearch(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void, notebookURIs?: ResourceSet): Promise<{ completeData: ISearchComplete; scannedFiles: ResourceSet }> {
|
||||
token?.onCancellationRequested(() => tokenSource?.cancel());
|
||||
const localResults = new ResourceMap<IFileMatchWithCells | null>(uri => uri.path);
|
||||
|
||||
results.forEach(r => {
|
||||
localResults.set(r.resource, r);
|
||||
});
|
||||
|
||||
if (onProgress) {
|
||||
arrays.coalesce([...localResults.values()]).forEach(onProgress);
|
||||
}
|
||||
return Promise.resolve(
|
||||
{
|
||||
completeData: {
|
||||
messages: [],
|
||||
results: arrays.coalesce([...localResults.values()]),
|
||||
limitHit: false
|
||||
},
|
||||
scannedFiles: new ResourceSet([...localResults.keys()]),
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -136,6 +191,7 @@ suite('SearchModel', () => {
|
|||
new TextSearchMatch('preview 1', new OneLineRange(1, 4, 11))),
|
||||
aRawMatch('/2', new TextSearchMatch('preview 2', lineOneRange))];
|
||||
instantiationService.stub(ISearchService, searchServiceWithResults(results));
|
||||
instantiationService.stub(INotebookSearchService, notebookSearchServiceWithInfo([], undefined));
|
||||
|
||||
const testObject: SearchModel = instantiationService.createInstance(SearchModel);
|
||||
await testObject.search({ contentPattern: { pattern: 'somestring' }, type: QueryType.Text, folderQueries });
|
||||
|
@ -238,26 +294,9 @@ suite('SearchModel', () => {
|
|||
webviewResults: webviewMatchesToTextSearchMatches(webviewMatches),
|
||||
};
|
||||
|
||||
const notebookSearchService = instantiationService.stub(INotebookSearchService, notebookSearchServiceWithInfo([aRawMatchWithCells('/1', cellMatchMd, cellMatchCode)], undefined));
|
||||
const notebookSearch = sinon.spy(notebookSearchService, "notebookSearch");
|
||||
const model: SearchModel = instantiationService.createInstance(SearchModel);
|
||||
const notebookSearch = sinon.stub(model, "notebookSearch").callsFake((query: ITextQuery, token: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise<{ completeData: ISearchComplete; scannedFiles: ResourceSet }> => {
|
||||
const localResults = new ResourceMap<IFileMatchWithCells | null>(uri => uri.path);
|
||||
const fileMatch = aRawMatchWithCells('/1', cellMatchMd, cellMatchCode);
|
||||
localResults.set(notebookUri, fileMatch);
|
||||
|
||||
if (onProgress) {
|
||||
arrays.coalesce([...localResults.values()]).forEach(onProgress);
|
||||
}
|
||||
return Promise.resolve(
|
||||
{
|
||||
completeData: {
|
||||
messages: [],
|
||||
results: arrays.coalesce([...localResults.values()]),
|
||||
limitHit: false
|
||||
},
|
||||
scannedFiles: new ResourceSet([...localResults.keys()]),
|
||||
});
|
||||
});
|
||||
|
||||
await model.search({ contentPattern: { pattern: 'test' }, type: QueryType.Text, folderQueries });
|
||||
const actual = model.searchResult.matches();
|
||||
|
||||
|
@ -309,6 +348,7 @@ suite('SearchModel', () => {
|
|||
aRawMatch('/2',
|
||||
new TextSearchMatch('preview 2', lineOneRange))];
|
||||
instantiationService.stub(ISearchService, searchServiceWithResults(results));
|
||||
instantiationService.stub(INotebookSearchService, notebookSearchServiceWithInfo([], undefined));
|
||||
|
||||
const testObject: SearchModel = instantiationService.createInstance(SearchModel);
|
||||
await testObject.search({ contentPattern: { pattern: 'somestring' }, type: QueryType.Text, folderQueries });
|
||||
|
@ -325,6 +365,7 @@ suite('SearchModel', () => {
|
|||
instantiationService.stub(ITelemetryService, 'publicLog', target1);
|
||||
|
||||
instantiationService.stub(ISearchService, searchServiceWithResults([]));
|
||||
instantiationService.stub(INotebookSearchService, notebookSearchServiceWithInfo([], undefined));
|
||||
|
||||
const testObject = instantiationService.createInstance(SearchModel);
|
||||
const result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: QueryType.Text, folderQueries });
|
||||
|
@ -346,6 +387,7 @@ suite('SearchModel', () => {
|
|||
instantiationService.stub(ISearchService, searchServiceWithResults(
|
||||
[aRawMatch('/1', new TextSearchMatch('some preview', lineOneRange))],
|
||||
{ results: [], stats: testSearchStats, messages: [] }));
|
||||
instantiationService.stub(INotebookSearchService, notebookSearchServiceWithInfo([], undefined));
|
||||
|
||||
const testObject = instantiationService.createInstance(SearchModel);
|
||||
const result = testObject.search({ contentPattern: { pattern: 'somestring' }, type: QueryType.Text, folderQueries });
|
||||
|
@ -412,6 +454,7 @@ suite('SearchModel', () => {
|
|||
aRawMatch('/2',
|
||||
new TextSearchMatch('preview 2', lineOneRange))];
|
||||
instantiationService.stub(ISearchService, searchServiceWithResults(results));
|
||||
instantiationService.stub(INotebookSearchService, notebookSearchServiceWithInfo([], undefined));
|
||||
const testObject: SearchModel = instantiationService.createInstance(SearchModel);
|
||||
await testObject.search({ contentPattern: { pattern: 'somestring' }, type: QueryType.Text, folderQueries });
|
||||
assert.ok(!testObject.searchResult.isEmpty());
|
||||
|
@ -425,19 +468,11 @@ suite('SearchModel', () => {
|
|||
test('Search Model: Previous search is cancelled when new search is called', async () => {
|
||||
const tokenSource = new CancellationTokenSource();
|
||||
instantiationService.stub(ISearchService, canceleableSearchService(tokenSource));
|
||||
instantiationService.stub(INotebookSearchService, notebookSearchServiceWithInfo([], tokenSource));
|
||||
const testObject: SearchModel = instantiationService.createInstance(SearchModel);
|
||||
sinon.stub(testObject, "notebookSearch").callsFake((_, token) => {
|
||||
token?.onCancellationRequested(() => tokenSource.cancel());
|
||||
|
||||
return new Promise(resolve => {
|
||||
queueMicrotask(() => {
|
||||
resolve(<any>{});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
testObject.search({ contentPattern: { pattern: 'somestring' }, type: QueryType.Text, folderQueries });
|
||||
instantiationService.stub(ISearchService, searchServiceWithResults([]));
|
||||
instantiationService.stub(INotebookSearchService, notebookSearchServiceWithInfo([], undefined));
|
||||
testObject.search({ contentPattern: { pattern: 'somestring' }, type: QueryType.Text, folderQueries });
|
||||
|
||||
assert.ok(tokenSource.token.isCancellationRequested);
|
||||
|
@ -449,6 +484,7 @@ suite('SearchModel', () => {
|
|||
new TextSearchMatch('preview 1', new OneLineRange(1, 1, 4)),
|
||||
new TextSearchMatch('preview 1', new OneLineRange(1, 4, 11)))];
|
||||
instantiationService.stub(ISearchService, searchServiceWithResults(results));
|
||||
instantiationService.stub(INotebookSearchService, notebookSearchServiceWithInfo([], undefined));
|
||||
|
||||
const testObject: SearchModel = instantiationService.createInstance(SearchModel);
|
||||
await testObject.search({ contentPattern: { pattern: 're' }, type: QueryType.Text, folderQueries });
|
||||
|
@ -494,5 +530,5 @@ suite('SearchModel', () => {
|
|||
instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService());
|
||||
return instantiationService.createInstance(NotebookEditorWidgetService);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -410,6 +410,9 @@ export interface ISearchConfigurationProperties {
|
|||
badges: boolean;
|
||||
};
|
||||
defaultViewMode: ViewMode;
|
||||
experimental: {
|
||||
closedNotebookRichContentResults: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ISearchConfiguration extends IFilesConfiguration {
|
||||
|
|
Loading…
Reference in a new issue