implement closed-notebook search (#174287)

This commit is contained in:
Andrea Mah 2023-07-11 12:11:35 -07:00 committed by GitHub
parent b008409c62
commit 60578ed419
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 532 additions and 129 deletions

View 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 }>;
}

View file

@ -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);
}

View 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));
}
}

View file

@ -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
}
}
});

View file

@ -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());
}

View file

@ -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);
}
}

View file

@ -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();

View file

@ -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);
}
});

View file

@ -410,6 +410,9 @@ export interface ISearchConfigurationProperties {
badges: boolean;
};
defaultViewMode: ViewMode;
experimental: {
closedNotebookRichContentResults: boolean;
};
}
export interface ISearchConfiguration extends IFilesConfiguration {