From 1b8da5037efedefae9b012897fe77929b7db955a Mon Sep 17 00:00:00 2001 From: Andrea Mah <31675041+andreamah@users.noreply.github.com> Date: Mon, 13 Feb 2023 11:46:06 -0800 Subject: [PATCH] Add ability to search in notebook inputs (under experimental flag) (#167952) --- .../contrib/find/findMatchDecorationModel.ts | 110 ++--- .../browser/contrib/find/findModel.ts | 11 +- .../notebook/browser/notebookBrowser.ts | 11 +- .../notebook/browser/notebookEditorWidget.ts | 6 +- .../browser/services/notebookEditorService.ts | 4 + .../services/notebookEditorServiceImpl.ts | 21 + .../browser/view/renderers/webviewMessages.ts | 9 + .../test/browser/contrib/find.test.ts | 18 +- .../search/{common => browser}/replace.ts | 2 +- .../search/browser/replaceContributions.ts | 2 +- .../contrib/search/browser/replaceService.ts | 39 +- .../search/browser/search.contribution.ts | 7 +- .../search/browser/searchActionsBase.ts | 2 +- .../search/browser/searchActionsCopy.ts | 2 +- .../search/browser/searchActionsFind.ts | 2 +- .../search/browser/searchActionsNav.ts | 2 +- .../browser/searchActionsRemoveReplace.ts | 6 +- .../search/browser/searchActionsTopBar.ts | 2 +- .../search/{common => browser}/searchModel.ts | 407 ++++++++++++++++-- .../search/browser/searchNotebookHelpers.ts | 101 +++++ .../search/browser/searchResultsView.ts | 2 +- .../contrib/search/browser/searchView.ts | 57 ++- .../search/test/browser/mockSearchTree.ts | 2 +- .../search/test/browser/searchActions.test.ts | 16 +- .../{common => browser}/searchModel.test.ts | 25 +- .../browser/searchNotebookHelpers.test.ts | 65 +++ .../{common => browser}/searchResult.test.ts | 72 +++- .../search/test/browser/searchViewlet.test.ts | 19 +- .../textsearch.perf.integrationTest.ts | 2 +- .../searchEditor/browser/searchEditor.ts | 2 +- .../browser/searchEditorActions.ts | 2 +- .../browser/searchEditorSerialization.ts | 2 +- .../services/search/common/search.ts | 6 +- .../services/search/common/searchService.ts | 6 +- 34 files changed, 877 insertions(+), 165 deletions(-) rename src/vs/workbench/contrib/search/{common => browser}/replace.ts (97%) rename src/vs/workbench/contrib/search/{common => browser}/searchModel.ts (79%) create mode 100644 src/vs/workbench/contrib/search/browser/searchNotebookHelpers.ts rename src/vs/workbench/contrib/search/test/{common => browser}/searchModel.test.ts (93%) create mode 100644 src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts rename src/vs/workbench/contrib/search/test/{common => browser}/searchResult.test.ts (90%) diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findMatchDecorationModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findMatchDecorationModel.ts index 2f407b50de5d3..e5ac8c7adcb5c 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findMatchDecorationModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findMatchDecorationModel.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; -import { FindMatch, IModelDeltaDecoration } from 'vs/editor/common/model'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { FindDecorations } from 'vs/editor/contrib/find/browser/findDecorations'; +import { Range } from 'vs/editor/common/core/range'; import { overviewRulerSelectionHighlightForeground, overviewRulerFindMatchForeground } from 'vs/platform/theme/common/colorRegistry'; -import { CellFindMatchWithIndex, CellWebviewFindMatch, ICellModelDecorations, ICellModelDeltaDecorations, ICellViewModel, INotebookDeltaDecoration, INotebookEditor, NotebookOverviewRulerLane, } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellFindMatchWithIndex, ICellModelDecorations, ICellModelDeltaDecorations, ICellViewModel, INotebookDeltaDecoration, INotebookEditor, NotebookOverviewRulerLane, } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; export class FindMatchDecorationModel extends Disposable { private _allMatchesDecorations: ICellModelDecorations[] = []; @@ -25,65 +26,72 @@ export class FindMatchDecorationModel extends Disposable { return this._currentMatchDecorations; } - public async highlightCurrentFindMatchDecoration(cell: ICellViewModel, match: FindMatch | CellWebviewFindMatch): Promise { + public clearDecorations() { + this.clearCurrentFindMatchDecoration(); + this.setAllFindMatchesDecorations([]); + } - if (match instanceof FindMatch) { - this.clearCurrentFindMatchDecoration(); - // match is an editor FindMatch, we update find match decoration in the editor - // we will highlight the match in the webview - this._notebookEditor.changeModelDecorations(accessor => { - const findMatchesOptions: ModelDecorationOptions = FindDecorations._CURRENT_FIND_MATCH_DECORATION; - - const decorations: IModelDeltaDecoration[] = [ - { range: match.range, options: findMatchesOptions } - ]; - const deltaDecoration: ICellModelDeltaDecorations = { - ownerId: cell.handle, - decorations: decorations - }; - - this._currentMatchDecorations = { - kind: 'input', - decorations: accessor.deltaDecorations(this._currentMatchDecorations?.kind === 'input' ? this._currentMatchDecorations.decorations : [], [deltaDecoration]) - }; - }); + public async highlightCurrentFindMatchDecorationInCell(cell: ICellViewModel, cellRange: Range): Promise { - this._currentMatchCellDecorations = this._notebookEditor.deltaCellDecorations(this._currentMatchCellDecorations, [{ + this.clearCurrentFindMatchDecoration(); + + // match is an editor FindMatch, we update find match decoration in the editor + // we will highlight the match in the webview + this._notebookEditor.changeModelDecorations(accessor => { + const findMatchesOptions: ModelDecorationOptions = FindDecorations._CURRENT_FIND_MATCH_DECORATION; + + const decorations: IModelDeltaDecoration[] = [ + { range: cellRange, options: findMatchesOptions } + ]; + const deltaDecoration: ICellModelDeltaDecorations = { ownerId: cell.handle, - handle: cell.handle, - options: { - overviewRuler: { - color: overviewRulerSelectionHighlightForeground, - modelRanges: [match.range], - includeOutput: false, - position: NotebookOverviewRulerLane.Center - } + decorations: decorations + }; + + this._currentMatchDecorations = { + kind: 'input', + decorations: accessor.deltaDecorations(this._currentMatchDecorations?.kind === 'input' ? this._currentMatchDecorations.decorations : [], [deltaDecoration]) + }; + }); + + this._currentMatchCellDecorations = this._notebookEditor.deltaCellDecorations(this._currentMatchCellDecorations, [{ + ownerId: cell.handle, + handle: cell.handle, + options: { + overviewRuler: { + color: overviewRulerSelectionHighlightForeground, + modelRanges: [cellRange], + includeOutput: false, + position: NotebookOverviewRulerLane.Center } - } as INotebookDeltaDecoration]); + } + } as INotebookDeltaDecoration]); - return null; - } else { - this.clearCurrentFindMatchDecoration(); + return null; + } - const offset = await this._notebookEditor.highlightFind(cell, match.index); - this._currentMatchDecorations = { kind: 'output', index: match.index }; + public async highlightCurrentFindMatchDecorationInWebview(cell: ICellViewModel, index: number): Promise { - this._currentMatchCellDecorations = this._notebookEditor.deltaCellDecorations(this._currentMatchCellDecorations, [{ - ownerId: cell.handle, - handle: cell.handle, - options: { - overviewRuler: { - color: overviewRulerSelectionHighlightForeground, - modelRanges: [], - includeOutput: true, - position: NotebookOverviewRulerLane.Center - } + this.clearCurrentFindMatchDecoration(); + + const offset = await this._notebookEditor.highlightFind(index); + this._currentMatchDecorations = { kind: 'output', index: index }; + + this._currentMatchCellDecorations = this._notebookEditor.deltaCellDecorations(this._currentMatchCellDecorations, [{ + ownerId: cell.handle, + handle: cell.handle, + options: { + overviewRuler: { + color: overviewRulerSelectionHighlightForeground, + modelRanges: [], + includeOutput: true, + position: NotebookOverviewRulerLane.Center } - } as INotebookDeltaDecoration]); + } + } as INotebookDeltaDecoration]); - return offset; - } + return offset; } public clearCurrentFindMatchDecoration() { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts index 429ab5ae9c47d..98968e618046a 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts @@ -457,11 +457,12 @@ export class FindModel extends Disposable { private async highlightCurrentFindMatchDecoration(cellIndex: number, matchIndex: number): Promise { const cell = this._findMatches[cellIndex].cell; const match = this._findMatches[cellIndex].getMatch(matchIndex); - return this._findMatchDecorationModel.highlightCurrentFindMatchDecoration(cell, - (matchIndex < this._findMatches[cellIndex].contentMatches.length) ? - (match as FindMatch) : - (match as CellWebviewFindMatch) - ); + + if (matchIndex < this._findMatches[cellIndex].contentMatches.length) { + return this._findMatchDecorationModel.highlightCurrentFindMatchDecorationInCell(cell, (match as FindMatch).range); + } else { + return this._findMatchDecorationModel.highlightCurrentFindMatchDecorationInWebview(cell, (match as CellWebviewFindMatch).index); + } } clear() { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 3548488429082..369cd5c09a5cf 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -666,7 +666,7 @@ export interface INotebookEditor { getNextVisibleCellIndex(index: number): number | undefined; getPreviousVisibleCellIndex(index: number): number | undefined; find(query: string, options: INotebookSearchOptions, token: CancellationToken): Promise; - highlightFind(cell: ICellViewModel, matchIndex: number): Promise; + highlightFind(matchIndex: number): Promise; unHighlightFind(matchIndex: number): Promise; findStop(): void; showProgress(): void; @@ -730,8 +730,17 @@ export interface IActiveNotebookEditorDelegate extends INotebookEditorDelegate { getNextVisibleCellIndex(index: number): number; } +export interface ISearchPreviewInfo { + line: string; + range: { + start: number; + end: number; + }; +} + export interface CellWebviewFindMatch { readonly index: number; + readonly searchPreviewInfo?: ISearchPreviewInfo; } export type CellContentFindMatch = FindMatch; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 690091e518494..64685b8ff14e4 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -125,6 +125,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD readonly onDidChangeCellState = this._onDidChangeCellState.event; private readonly _onDidChangeViewCells = this._register(new Emitter()); readonly onDidChangeViewCells: Event = this._onDidChangeViewCells.event; + private readonly _onWillChangeModel = this._register(new Emitter()); + readonly onWillChangeModel: Event = this._onWillChangeModel.event; private readonly _onDidChangeModel = this._register(new Emitter()); readonly onDidChangeModel: Event = this._onDidChangeModel.event; private readonly _onDidChangeOptions = this._register(new Emitter()); @@ -158,7 +160,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD private readonly _onDidResizeOutputEmitter = this._register(new Emitter()); readonly onDidResizeOutput = this._onDidResizeOutputEmitter.event; - //#endregion private _overlayContainer!: HTMLElement; private _notebookTopToolbarContainer!: HTMLElement; @@ -209,6 +210,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } set viewModel(newModel: NotebookViewModel | undefined) { + this._onWillChangeModel.fire(this._notebookViewModel?.notebookDocument); this._notebookViewModel = newModel; this._onDidChangeModel.fire(newModel?.notebookDocument); } @@ -2462,7 +2464,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return ret; } - async highlightFind(cell: CodeCellViewModel, matchIndex: number): Promise { + async highlightFind(matchIndex: number): Promise { if (!this._webview) { return 0; } diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookEditorService.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookEditorService.ts index ac80184de3e8b..c9f22f1f832de 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookEditorService.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookEditorService.ts @@ -9,6 +9,8 @@ import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebo import { INotebookEditor, INotebookEditorCreationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { Event } from 'vs/base/common/event'; import { Dimension } from 'vs/base/browser/dom'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { URI } from 'vs/base/common/uri'; export const INotebookEditorService = createDecorator('INotebookEditorWidgetService'); @@ -21,6 +23,8 @@ export interface INotebookEditorService { retrieveWidget(accessor: ServicesAccessor, group: IEditorGroup, input: NotebookEditorInput, creationOptions?: INotebookEditorCreationOptions, dimension?: Dimension): IBorrowValue; + retrieveExistingWidgetFromURI(resource: URI): IBorrowValue | undefined; + retrieveAllExistingWidgets(): IBorrowValue[]; onDidAddNotebookEditor: Event; onDidRemoveNotebookEditor: Event; addNotebookEditor(editor: INotebookEditor): void; diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts index d83e19338cb01..e61cfbe818125 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl.ts @@ -14,6 +14,7 @@ import { INotebookEditor, INotebookEditorCreationOptions } from 'vs/workbench/co import { Emitter } from 'vs/base/common/event'; import { GroupIdentifier } from 'vs/workbench/common/editor'; import { Dimension } from 'vs/base/browser/dom'; +import { URI } from 'vs/base/common/uri'; export class NotebookEditorWidgetService implements INotebookEditorService { @@ -129,6 +130,26 @@ export class NotebookEditorWidgetService implements INotebookEditorService { targetMap.set(input.resource, widget); } + retrieveExistingWidgetFromURI(resource: URI): IBorrowValue | undefined { + for (const widgetInfo of this._borrowableEditors.values()) { + const widget = widgetInfo.get(resource); + if (widget) { + return this._createBorrowValue(widget.token!, widget); + } + } + return undefined; + } + + retrieveAllExistingWidgets(): IBorrowValue[] { + const ret: IBorrowValue[] = []; + for (const widgetInfo of this._borrowableEditors.values()) { + for (const widget of widgetInfo.values()) { + ret.push(this._createBorrowValue(widget.token!, widget)); + } + } + return ret; + } + retrieveWidget(accessor: ServicesAccessor, group: IEditorGroup, input: NotebookEditorInput, creationOptions?: INotebookEditorCreationOptions, initialDimension?: Dimension): IBorrowValue { let value = this._borrowableEditors.get(group.id)?.get(input.resource); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index 8bbfadd91543e..3e46646d23e05 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -411,11 +411,20 @@ export interface IFindStopMessage { readonly type: 'findStop'; } +export interface ISearchPreviewInfo { + line: string; + range: { + start: number; + end: number; + }; +} + export interface IFindMatch { readonly type: 'preview' | 'output'; readonly cellId: string; readonly id: string; readonly index: number; + readonly searchPreviewInfo?: ISearchPreviewInfo; } export interface IDidFindMessage extends BaseToWebviewMessage { diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts index 60d80fcd37d36..b941e70675e6f 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/find.test.ts @@ -277,9 +277,23 @@ suite('Notebook Find', () => { mdModel.contentMatches.push(new FindMatch(new Range(1, 1, 1, 2), [])); assert.strictEqual(mdModel.length, 1); mdModel.webviewMatches.push({ - index: 0 + index: 0, + searchPreviewInfo: { + line: '', + range: { + start: 0, + end: 0, + } + } }, { - index: 1 + index: 1, + searchPreviewInfo: { + line: '', + range: { + start: 0, + end: 0, + } + } }); assert.strictEqual(mdModel.length, 3); diff --git a/src/vs/workbench/contrib/search/common/replace.ts b/src/vs/workbench/contrib/search/browser/replace.ts similarity index 97% rename from src/vs/workbench/contrib/search/common/replace.ts rename to src/vs/workbench/contrib/search/browser/replace.ts index 8c1320c1f9cc3..4c9949259ec85 100644 --- a/src/vs/workbench/contrib/search/common/replace.ts +++ b/src/vs/workbench/contrib/search/browser/replace.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Match, FileMatch, FileMatchOrMatch } from 'vs/workbench/contrib/search/common/searchModel'; +import { Match, FileMatch, FileMatchOrMatch } from 'vs/workbench/contrib/search/browser/searchModel'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; diff --git a/src/vs/workbench/contrib/search/browser/replaceContributions.ts b/src/vs/workbench/contrib/search/browser/replaceContributions.ts index 42176f2aeecb5..1fbf571ff2bbc 100644 --- a/src/vs/workbench/contrib/search/browser/replaceContributions.ts +++ b/src/vs/workbench/contrib/search/browser/replaceContributions.ts @@ -3,7 +3,7 @@ * 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 { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; +import { IReplaceService } from 'vs/workbench/contrib/search/browser/replace'; import { ReplaceService, 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'; diff --git a/src/vs/workbench/contrib/search/browser/replaceService.ts b/src/vs/workbench/contrib/search/browser/replaceService.ts index d653f37720370..c991021c2b9bc 100644 --- a/src/vs/workbench/contrib/search/browser/replaceService.ts +++ b/src/vs/workbench/contrib/search/browser/replaceService.ts @@ -7,11 +7,11 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import * as network from 'vs/base/common/network'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; +import { IReplaceService } from 'vs/workbench/contrib/search/browser/replace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; -import { Match, FileMatch, FileMatchOrMatch, ISearchWorkbenchService } from 'vs/workbench/contrib/search/common/searchModel'; +import { Match, FileMatch, FileMatchOrMatch, ISearchWorkbenchService, NotebookMatch } from 'vs/workbench/contrib/search/browser/searchModel'; import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -27,6 +27,8 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { dirname } from 'vs/base/common/resources'; import { Promises } from 'vs/base/common/async'; import { SaveSourceRegistry } from 'vs/workbench/common/editor'; +import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; const REPLACE_PREVIEW = 'replacePreview'; @@ -99,7 +101,8 @@ export class ReplaceService implements IReplaceService { @IEditorService private readonly editorService: IEditorService, @ITextModelService private readonly textModelResolverService: ITextModelService, @IBulkEditService private readonly bulkEditorService: IBulkEditService, - @ILabelService private readonly labelService: ILabelService + @ILabelService private readonly labelService: ILabelService, + @INotebookEditorModelResolverService private readonly notebookEditorModelResolverService: INotebookEditorModelResolverService ) { } replace(match: Match): Promise; @@ -109,7 +112,22 @@ export class ReplaceService implements IReplaceService { const edits = this.createEdits(arg, resource); await this.bulkEditorService.apply(edits, { progress }); - return Promises.settled(edits.map(async e => this.textFileService.files.get(e.resource)?.save({ source: ReplaceService.REPLACE_SAVE_SOURCE }))); + const rawTextPromises = edits.map(async e => { + if (e.resource.scheme === network.Schemas.vscodeNotebookCell) { + const notebookResource = CellUri.parse(e.resource)?.notebook; + if (notebookResource) { + // todo: find whether there is a common API for saving notebooks and text files + const ref = await this.notebookEditorModelResolverService.resolve(notebookResource); + await ref.object.save({ source: ReplaceService.REPLACE_SAVE_SOURCE }); + ref.dispose(); + } + return; + } else { + return this.textFileService.files.get(e.resource)?.save({ source: ReplaceService.REPLACE_SAVE_SOURCE }); + } + }); + + return Promises.settled(rawTextPromises); } async openReplacePreview(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise { @@ -177,8 +195,13 @@ export class ReplaceService implements IReplaceService { const edits: ResourceTextEdit[] = []; if (arg instanceof Match) { - const match = arg; - edits.push(this.createEdit(match, match.replaceString, resource)); + if (arg instanceof NotebookMatch) { + const match = arg; + edits.push(this.createEdit(match, match.replaceString, match.cell.uri)); + } else { + const match = arg; + edits.push(this.createEdit(match, match.replaceString, resource)); + } } if (arg instanceof FileMatch) { @@ -189,7 +212,9 @@ export class ReplaceService implements IReplaceService { arg.forEach(element => { const fileMatch = element; if (fileMatch.count() > 0) { - edits.push(...fileMatch.matches().map(match => this.createEdit(match, match.replaceString, resource))); + edits.push(...fileMatch.matches().map( + match => this.createEdit(match, match.replaceString, (match instanceof NotebookMatch) ? match.cell.uri : resource) + )); } }); } diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 2940c53bd64d5..652eb8009207c 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -26,7 +26,7 @@ import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; import { registerContributions as searchWidgetContributions } from 'vs/workbench/contrib/search/browser/searchWidget'; import { SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess'; import { ISearchHistoryService, SearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService'; -import { ISearchWorkbenchService, SearchWorkbenchService } from 'vs/workbench/contrib/search/common/searchModel'; +import { ISearchWorkbenchService, SearchWorkbenchService } from 'vs/workbench/contrib/search/browser/searchModel'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { SearchSortOrder, SEARCH_EXCLUDE_CONFIG, VIEWLET_ID, ViewMode, VIEW_ID } from 'vs/workbench/services/search/common/search'; import { Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; @@ -350,6 +350,11 @@ configurationRegistry.registerConfiguration({ ], 'description': nls.localize('search.defaultViewMode', "Controls the default search result view mode.") }, + 'search.experimental.notebookSearch': { + type: 'boolean', + description: nls.localize('search.experimental.notebookSearch', "Controls whether to use the experimental notebook search in the global search."), + default: false + }, } }); diff --git a/src/vs/workbench/contrib/search/browser/searchActionsBase.ts b/src/vs/workbench/contrib/search/browser/searchActionsBase.ts index 678e459720d6c..92cbc8b0e91f0 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsBase.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsBase.ts @@ -9,7 +9,7 @@ import * as nls from 'vs/nls'; import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; import { IViewsService } from 'vs/workbench/common/views'; import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; -import { FileMatch, FolderMatch, Match, RenderableMatch, searchComparer } from 'vs/workbench/contrib/search/common/searchModel'; +import { FileMatch, FolderMatch, Match, RenderableMatch, searchComparer } from 'vs/workbench/contrib/search/browser/searchModel'; import { ISearchConfigurationProperties, VIEW_ID } from 'vs/workbench/services/search/common/search'; export const category = { value: nls.localize('search', "Search"), original: 'Search' }; diff --git a/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts b/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts index 0e1acbdf9d78b..28d437a82f84f 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts @@ -8,7 +8,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { ILabelService } from 'vs/platform/label/common/label'; import { IViewsService } from 'vs/workbench/common/views'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; -import { FileMatch, FolderMatch, FolderMatchWithResource, Match, RenderableMatch, searchMatchComparer } from 'vs/workbench/contrib/search/common/searchModel'; +import { FileMatch, FolderMatch, FolderMatchWithResource, Match, RenderableMatch, searchMatchComparer } from 'vs/workbench/contrib/search/browser/searchModel'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; diff --git a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts index 17fc4b72cdcee..7dc6d889188d6 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts @@ -11,7 +11,7 @@ import { IListService, WorkbenchCompressibleObjectTree } from 'vs/platform/list/ import { IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; -import { FileMatch, FolderMatchWithResource, Match, RenderableMatch } from 'vs/workbench/contrib/search/common/searchModel'; +import { FileMatch, FolderMatchWithResource, Match, RenderableMatch } from 'vs/workbench/contrib/search/browser/searchModel'; import { OpenSearchEditorArgs } from 'vs/workbench/contrib/searchEditor/browser/searchEditor.contribution'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ISearchConfiguration, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; diff --git a/src/vs/workbench/contrib/search/browser/searchActionsNav.ts b/src/vs/workbench/contrib/search/browser/searchActionsNav.ts index 4d54a81a28ac5..aa95a92160a7d 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsNav.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsNav.ts @@ -12,7 +12,7 @@ import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listSe import { IViewsService } from 'vs/workbench/common/views'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; -import { FileMatchOrMatch, FolderMatch, RenderableMatch } from 'vs/workbench/contrib/search/common/searchModel'; +import { FileMatchOrMatch, FolderMatch, RenderableMatch } from 'vs/workbench/contrib/search/browser/searchModel'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; import { SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; diff --git a/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts b/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts index cd55030865f89..f3b966b22c2fe 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts @@ -12,8 +12,8 @@ import { IViewsService } from 'vs/workbench/common/views'; import { searchRemoveIcon, searchReplaceIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; -import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; -import { arrayContainsElementOrParent, FileMatch, FolderMatch, Match, RenderableMatch, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; +import { IReplaceService } from 'vs/workbench/contrib/search/browser/replace'; +import { arrayContainsElementOrParent, FileMatch, FolderMatch, Match, NotebookMatch, RenderableMatch, SearchResult } from 'vs/workbench/contrib/search/browser/searchModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ISearchConfiguration, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -309,7 +309,7 @@ function performReplace(accessor: ServicesAccessor, if (nextFocusElement instanceof Match) { const useReplacePreview = configurationService.getValue().search.useReplacePreview; - if (!useReplacePreview || hasToOpenFile(accessor, nextFocusElement)) { + if (!useReplacePreview || hasToOpenFile(accessor, nextFocusElement) || nextFocusElement instanceof NotebookMatch) { viewlet?.open(nextFocusElement, true); } else { accessor.get(IReplaceService).openReplacePreview(nextFocusElement, true); diff --git a/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts index 793d8558f55e3..5db628b417498 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts @@ -11,7 +11,7 @@ import { IViewsService } from 'vs/workbench/common/views'; import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchRefreshIcon, searchShowAsList, searchShowAsTree, searchStopIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; import { ISearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService'; -import { FileMatch, FolderMatch, FolderMatchNoRoot, FolderMatchWorkspaceRoot, Match, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; +import { FileMatch, FolderMatch, FolderMatchNoRoot, FolderMatchWorkspaceRoot, Match, SearchResult } from 'vs/workbench/contrib/search/browser/searchModel'; import { VIEW_ID } from 'vs/workbench/services/search/common/search'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; diff --git a/src/vs/workbench/contrib/search/common/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchModel.ts similarity index 79% rename from src/vs/workbench/contrib/search/common/searchModel.ts rename to src/vs/workbench/contrib/search/browser/searchModel.ts index 650cd706d5dc4..06e7a0e3d17a7 100644 --- a/src/vs/workbench/contrib/search/common/searchModel.ts +++ b/src/vs/workbench/contrib/search/browser/searchModel.ts @@ -3,18 +3,20 @@ * 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 { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { compareFileExtensions, compareFileNames, comparePaths } from 'vs/base/common/comparers'; import { memoize } from 'vs/base/common/decorators'; 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 } from 'vs/base/common/map'; +import { ResourceMap, ResourceSet } 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'; @@ -30,24 +32,28 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { minimapFindMatch, overviewRulerFindMatchForeground } from 'vs/platform/theme/common/colorRegistry'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; +import { FindMatchDecorationModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findMatchDecorationModel'; +import { CellEditState, CellFindMatchWithIndex } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +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 { IReplaceService } from 'vs/workbench/contrib/search/browser/replace'; +import { notebookEditorMatchesToTextSearchResults, NotebookMatchInfo, NotebookTextSearchMatch } 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, 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, QueryType, resultIsMatch, SearchCompletionExitCode, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { addContextToEditorMatches, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers'; export class Match { private static readonly MAX_PREVIEW_CHARS = 250; - - private _id: string; - private _range: Range; + protected _id: string; + protected _range: Range; private _oneLinePreviewText: string; private _rangeInPreviewText: ISearchRange; - // For replace private _fullPreviewRange: ISearchRange; - constructor(private _parent: FileMatch, private _fullPreviewLines: string[], _fullPreviewRange: ISearchRange, _documentRange: ISearchRange) { + constructor(protected _parent: FileMatch, private _fullPreviewLines: string[], _fullPreviewRange: ISearchRange, _documentRange: ISearchRange) { this._oneLinePreviewText = _fullPreviewLines[_fullPreviewRange.startLineNumber]; const adjustedEndCol = _fullPreviewRange.startLineNumber === _fullPreviewRange.endLineNumber ? _fullPreviewRange.endColumn : @@ -174,6 +180,47 @@ export class Match { } } +export class NotebookMatch extends Match { + constructor(_parent: FileMatch, _fullPreviewLines: string[], _fullPreviewRange: ISearchRange, _documentRange: ISearchRange, private _notebookMatchInfo: NotebookMatchInfo) { + super(_parent, _fullPreviewLines, _fullPreviewRange, _documentRange); + + this._id = this._parent.id() + '>' + this._notebookMatchInfo.cellIndex + '_' + this.notebookMatchTypeString() + this.getRangeString() + this._range + this.getMatchString(); + } + + private notebookMatchTypeString(): string { + return this.isWebviewMatch() ? 'webview' : 'content'; + } + + private getRangeString(): string { + return `[${this._notebookMatchInfo.matchStartIndex},${this._notebookMatchInfo.matchStartIndex}]`; + } + + public isWebviewMatch() { + return this._notebookMatchInfo.webviewMatchInfo !== undefined; + } + + get cellIndex() { + return this._notebookMatchInfo.cellIndex; + } + + get matchStartIndex() { + return this._notebookMatchInfo.matchStartIndex; + } + + get matchEndIndex() { + return this._notebookMatchInfo.matchEndIndex; + } + + get webviewIndex() { + return this._notebookMatchInfo.webviewMatchInfo?.index; + } + + get cell() { + return this._notebookMatchInfo.cell; + } +} + + export class FileMatch extends Disposable implements IFileMatch { private static readonly _CURRENT_FIND_MATCH = ModelDecorationOptions.register({ @@ -218,14 +265,18 @@ export class FileMatch extends Disposable implements IFileMatch { private _resource: URI; private _fileStat?: IFileStatWithPartialMetadata; private _model: ITextModel | null = null; + private _notebookEditorWidget: NotebookEditorWidget | null = null; private _modelListener: IDisposable | null = null; + private _editorWidgetListener: IDisposable | null = null; private _matches: Map; private _removedMatches: Set; private _selectedMatch: Match | null = null; private _name: Lazy; private _updateScheduler: RunOnceScheduler; + private _notebookUpdateScheduler: RunOnceScheduler; private _modelDecorations: string[] = []; + private _findMatchDecorationModel: FindMatchDecorationModel | undefined; private _context: Map = new Map(); public get context(): Map { @@ -241,13 +292,16 @@ export class FileMatch extends Disposable implements IFileMatch { private _closestRoot: FolderMatchWorkspaceRoot | null, @IModelService private readonly modelService: IModelService, @IReplaceService private readonly replaceService: IReplaceService, - @ILabelService labelService: ILabelService, + @ILabelService readonly labelService: ILabelService, + @INotebookEditorService private readonly notebookEditorService: INotebookEditorService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); this._resource = this.rawMatch.resource; this._matches = new Map(); this._removedMatches = new Set(); this._updateScheduler = new RunOnceScheduler(this.updateMatchesForModel.bind(this), 250); + this._notebookUpdateScheduler = new RunOnceScheduler(this.updateMatchesForEditorWidget.bind(this), 250); this._name = new Lazy(() => labelService.getUriBasenameLabel(this.resource)); this.createMatches(); } @@ -256,9 +310,13 @@ export class FileMatch extends Disposable implements IFileMatch { return this._closestRoot; } - private createMatches(): void { + private async createMatches(): Promise { const model = this.modelService.getModel(this._resource); - if (model) { + const experimentalNotebooksEnabled = this.configurationService.getValue('search').experimental.notebookSearch; + const notebookEditorWidgetBorrow = experimentalNotebooksEnabled ? this.notebookEditorService.retrieveExistingWidgetFromURI(this._resource) : undefined; + if (notebookEditorWidgetBorrow?.value) { + await this.bindNotebookEditorWidget(notebookEditorWidgetBorrow.value); + } else if (model) { this.bindModel(model); this.updateMatchesForModel(); } else { @@ -299,6 +357,39 @@ export class FileMatch extends Disposable implements IFileMatch { } } + async bindNotebookEditorWidget(widget: NotebookEditorWidget) { + + if (this._notebookEditorWidget === widget) { + return; + } + + this._notebookEditorWidget = widget; + + this._findMatchDecorationModel?.dispose(); + this._findMatchDecorationModel = new FindMatchDecorationModel(widget); + this._editorWidgetListener = this._notebookEditorWidget.textModel?.onDidChangeContent((e) => { + if (!e.rawEvents.some(event => event.kind === NotebookCellsChangeType.ChangeCellContent || event.kind === NotebookCellsChangeType.ModelChange)) { + return; + } + this._notebookUpdateScheduler.schedule(); + }) ?? null; + await this.updateMatchesForEditorWidget(); + } + + unbindNotebookEditorWidget(widget?: NotebookEditorWidget) { + if (widget && this._notebookEditorWidget !== widget) { + return; + } + + this.updateMatchesForEditorWidget(); + if (this._notebookEditorWidget) { + this._notebookUpdateScheduler.cancel(); + this._findMatchDecorationModel?.dispose(); + this._editorWidgetListener?.dispose(); + } + this._notebookEditorWidget = null; + } + private updateMatchesForModel(): void { // this is called from a timeout and might fire // after the model has been disposed @@ -311,14 +402,35 @@ export class FileMatch extends Disposable implements IFileMatch { const matches = this._model .findMatches(this._query.pattern, this._model.getFullModelRange(), !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? Number.MAX_SAFE_INTEGER); - this.updateMatches(matches, true); + this.updateMatches(matches, true, this._model); + } + + private async updateMatchesForEditorWidget(): Promise { + if (!this._notebookEditorWidget) { + return; + } + this._matches = new Map(); + + const wordSeparators = this._query.isWordMatch && this._query.wordSeparators ? this._query.wordSeparators : null; + const allMatches = await this._notebookEditorWidget + .find(this._query.pattern, { + regex: this._query.isRegExp, + wholeWord: this._query.isWordMatch, + caseSensitive: this._query.isCaseSensitive, + wordSeparators: wordSeparators ?? undefined, + includeMarkupInput: true, + includeMarkupPreview: false, + includeCodeInput: true, + includeOutput: false, + }, CancellationToken.None, true); + + this.updateNotebookMatches(allMatches, true); } private updatesMatchesForLineAfterReplace(lineNumber: number, modelChange: boolean): void { if (!this._model) { return; } - const range = { startLineNumber: lineNumber, startColumn: this._model.getLineMinColumn(lineNumber), @@ -330,15 +442,28 @@ export class FileMatch extends Disposable implements IFileMatch { const wordSeparators = this._query.isWordMatch && this._query.wordSeparators ? this._query.wordSeparators : null; const matches = this._model.findMatches(this._query.pattern, range, !!this._query.isRegExp, !!this._query.isCaseSensitive, wordSeparators, false, this._maxResults ?? Number.MAX_SAFE_INTEGER); - this.updateMatches(matches, modelChange); + this.updateMatches(matches, modelChange, this._model); + this.updateMatchesForEditorWidget(); } - private updateMatches(matches: FindMatch[], modelChange: boolean): void { - if (!this._model) { - return; - } + private updateNotebookMatches(matches: CellFindMatchWithIndex[], modelChange: boolean): void { + const textSearchResults = notebookEditorMatchesToTextSearchResults(matches, this._previewOptions); + textSearchResults.forEach(textSearchResult => { + textSearchResultToNotebookMatches(textSearchResult, this).forEach(match => { + if (!this._removedMatches.has(match.id())) { + this.add(match); + if (this.isMatchSelected(match)) { + this._selectedMatch = match; + } + } + }); + }); + this._findMatchDecorationModel?.setAllFindMatchesDecorations(matches); + this._onChange.fire({ forceUpdateModel: modelChange }); + } - const textSearchResults = editorMatchesToTextSearchResults(matches, this._model, this._previewOptions); + private updateMatches(matches: FindMatch[], modelChange: boolean, model: ITextModel): void { + const textSearchResults = editorMatchesToTextSearchResults(matches, model, this._previewOptions); textSearchResults.forEach(textSearchResult => { textSearchResultToMatches(textSearchResult, this).forEach(match => { if (!this._removedMatches.has(match.id())) { @@ -351,7 +476,7 @@ export class FileMatch extends Disposable implements IFileMatch { }); this.addContext( - addContextToEditorMatches(textSearchResults, this._model, this.parent().parent().query!) + addContextToEditorMatches(textSearchResults, model, this.parent().parent().query!) .filter((result => !resultIsMatch(result)) as ((a: any) => a is ITextSearchContext)) .map(context => ({ ...context, lineNumber: context.lineNumber + 1 }))); @@ -483,9 +608,43 @@ export class FileMatch extends Disposable implements IFileMatch { override dispose(): void { this.setSelectedMatch(null); this.unbindModel(); + this.unbindNotebookEditorWidget(); + this._findMatchDecorationModel?.dispose(); this._onDispose.fire(); super.dispose(); } + + public async showMatch(match: NotebookMatch) { + const offset = await this.highlightCurrentFindMatchDecoration(match); + this.revealCellRange(match, offset); + } + + private async highlightCurrentFindMatchDecoration(match: NotebookMatch): Promise { + if (!this._findMatchDecorationModel) { + return null; + } + if (match.webviewIndex === undefined) { + return this._findMatchDecorationModel.highlightCurrentFindMatchDecorationInCell(match.cell, match.range()); + } else { + return this._findMatchDecorationModel.highlightCurrentFindMatchDecorationInWebview(match.cell, match.webviewIndex); + } + } + + private revealCellRange(match: NotebookMatch, outputOffset: number | null) { + if (!this._notebookEditorWidget) { + return; + } + if (match.webviewIndex) { + const index = this._notebookEditorWidget.getCellIndex(match.cell); + if (index !== undefined) { + this._notebookEditorWidget.revealCellOffsetInCenterAsync(match.cell, outputOffset ?? 0); + } + } else { + match.cell.updateEditState(CellEditState.Editing, 'focusNotebookCell'); + this._notebookEditorWidget.setCellEditorSelection(match.cell, match.range()); + this._notebookEditorWidget.revealRangeInCenterIfOutsideViewportAsync(match.cell, match.range()); + } + } } export interface IChangeEvent { @@ -581,6 +740,33 @@ export class FolderMatch extends Disposable { } } + async bindNotebookEditorWidget(editor: NotebookEditorWidget, resource: URI) { + const fileMatch = this._fileMatches.get(resource); + + if (fileMatch) { + await fileMatch.bindNotebookEditorWidget(editor); + } else { + const folderMatches = this.folderMatchesIterator(); + for (const elem of folderMatches) { + await elem.bindNotebookEditorWidget(editor, resource); + } + } + } + + unbindNotebookEditorWidget(editor: NotebookEditorWidget, resource: URI) { + const fileMatch = this._fileMatches.get(resource); + + if (fileMatch) { + fileMatch.unbindNotebookEditorWidget(editor); + } else { + const folderMatches = this.folderMatchesIterator(); + for (const elem of folderMatches) { + elem.unbindNotebookEditorWidget(editor, resource); + } + } + + } + public createIntermediateFolderMatch(resource: URI, id: string, index: number, query: ITextQuery, baseWorkspaceFolder: FolderMatchWorkspaceRoot): FolderMatchWithResource { const folderMatch = this.instantiationService.createInstance(FolderMatchWithResource, resource, id, index, query, this, this._searchModel, baseWorkspaceFolder); this.configureIntermediateMatch(folderMatch); @@ -1018,6 +1204,10 @@ export function searchMatchComparer(elementA: RenderableMatch, elementB: Rendera } } + if (elementA instanceof NotebookMatch && elementB instanceof NotebookMatch) { + return compareNotebookPos(elementA, elementB); + } + if (elementA instanceof Match && elementB instanceof Match) { return Range.compareRangesUsingStarts(elementA.range(), elementB.range()); } @@ -1025,6 +1215,27 @@ export function searchMatchComparer(elementA: RenderableMatch, elementB: Rendera return 0; } +export function compareNotebookPos(match1: NotebookMatch, match2: NotebookMatch): number { + if (match1.cellIndex === match2.cellIndex) { + if (match1.matchStartIndex === match2.matchStartIndex) { + if (match1.matchEndIndex === match2.matchEndIndex) { + return 0; + } else if (match1.matchEndIndex < match2.matchEndIndex) { + return -1; + } else { + return 1; + } + } else if (match1.matchStartIndex < match2.matchStartIndex) { + return -1; + } else { + return 1; + } + } else if (match1.cellIndex < match2.cellIndex) { + return -1; + } else { + return 1; + } +} export function searchComparer(elementA: RenderableMatch, elementB: RenderableMatch, sortOrder: SearchSortOrder = SearchSortOrder.Default): number { const elemAParents = createParentList(elementA); const elemBParents = createParentList(elementB); @@ -1071,11 +1282,10 @@ export class SearchResult extends Disposable { private _folderMatchesMap: TernarySearchTree = TernarySearchTree.forUris(key => this.uriIdentityService.extUri.ignorePathCasing(key)); private _showHighlights: boolean = false; private _query: ITextQuery | null = null; - private _rangeHighlightDecorations: RangeHighlightDecorations; private disposePastResults: () => void = () => { }; - private _isDirty = false; + private _onWillChangeModelListener: IDisposable | undefined; constructor( private _searchModel: SearchModel, @@ -1083,12 +1293,23 @@ export class SearchResult extends Disposable { @IInstantiationService private readonly instantiationService: IInstantiationService, @IModelService private readonly modelService: IModelService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @INotebookEditorService private readonly notebookEditorService: INotebookEditorService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); this._rangeHighlightDecorations = this.instantiationService.createInstance(RangeHighlightDecorations); this._register(this.modelService.onModelAdded(model => this.onModelAdded(model))); + const experimentalNotebooksEnabled = this.configurationService.getValue('search').experimental.notebookSearch; + if (experimentalNotebooksEnabled) { + this._register(this.notebookEditorService.onDidAddNotebookEditor(widget => { + if (widget instanceof NotebookEditorWidget) { + this.onDidAddNotebookEditorWidget(widget); + } + })); + } + this._register(this.onChange(e => { if (e.removed) { this._isDirty = !this.isEmpty(); @@ -1191,11 +1412,42 @@ export class SearchResult extends Disposable { return retEvent; } + + private onDidAddNotebookEditorWidget(widget: NotebookEditorWidget): void { + this._onWillChangeModelListener?.dispose(); + this._onWillChangeModelListener = widget.onWillChangeModel( + (model) => { + if (model) { + this.onNotebookEditorWidgetRemoved(widget, model?.uri); + } + } + ); + + widget.onDidChangeModel( + (model) => { + if (model) { + this.onNotebookEditorWidgetAdded(widget, model?.uri); + } + } + ); + + } + private onModelAdded(model: ITextModel): void { const folderMatch = this._folderMatchesMap.findSubstr(model.uri); folderMatch?.bindModel(model); } + private async onNotebookEditorWidgetAdded(editor: NotebookEditorWidget, resource: URI): Promise { + const folderMatch = this._folderMatchesMap.findSubstr(resource); + await folderMatch?.bindNotebookEditorWidget(editor, resource); + } + + private onNotebookEditorWidgetRemoved(editor: NotebookEditorWidget, resource: URI): void { + const folderMatch = this._folderMatchesMap.findSubstr(resource); + folderMatch?.unbindNotebookEditorWidget(editor, resource); + } + private _createBaseFolderMatch(resource: URI | null, id: string, index: number, query: ITextQuery): FolderMatch { let folderMatch; if (resource) { @@ -1388,6 +1640,7 @@ export class SearchResult extends Disposable { } override dispose(): void { + this._onWillChangeModelListener?.dispose(); this.disposePastResults(); this.disposeMatches(); this._rangeHighlightDecorations.dispose(); @@ -1416,7 +1669,9 @@ export class SearchModel extends Disposable { @ISearchService private readonly searchService: ISearchService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @INotebookEditorService private readonly notebookEditorService: INotebookEditorService, ) { super(); this._searchResult = this.instantiationService.createInstance(SearchResult, this); @@ -1458,6 +1713,84 @@ export class SearchModel extends Disposable { return this._searchResult; } + private async getLocalNotebookResults(query: ITextQuery, token: CancellationToken): Promise<{ results: ResourceMap; limitHit: boolean }> { + const localResults = new ResourceMap(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: true, + includeMarkupPreview: false, + includeCodeInput: true, + includeOutput: false, + }, token); + + + if (matches.length) { + if (askMax && matches.length >= askMax) { + limitHit = true; + matches = matches.slice(0, askMax - 1); + } + const fileMatch = { resource: widget.viewModel.uri, results: notebookEditorMatchesToTextSearchResults(matches, query.previewOptions) }; + 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 { + const localResults = await this.getLocalNotebookResults(query, token); + + if (onProgress) { + arrays.coalesce([...localResults.results.values()]).forEach(onProgress); + } + + return { + messages: [], + limitHit: localResults.limitHit, + results: arrays.coalesce([...localResults.results.values()]) + }; + } + + private async doSearch(query: ITextQuery, progressEmitter: Emitter, searchQuery: ITextQuery, onProgress?: (result: ISearchProgressItem) => void): Promise { + const tokenSource = this.currentCancelTokenSource = new CancellationTokenSource(); + const onProgressCall = (p: ISearchProgressItem) => { + progressEmitter.fire(); + this.onSearchProgress(p); + + onProgress?.(p); + }; + const experimentalNotebooksEnabled = this.configurationService.getValue('search').experimental.notebookSearch; + + const notebookResult = experimentalNotebooksEnabled ? await this.notebookSearch(query, this.currentCancelTokenSource.token, onProgressCall) : { messages: [], results: [] }; + const currentResult = await this.searchService.textSearch( + searchQuery, + this.currentCancelTokenSource.token, onProgressCall, + new ResourceSet(notebookResult.results.map(r => r.resource, this.uriIdentityService.extUri.ignorePathCasing), uri => this.uriIdentityService.extUri.getComparisonKey(uri)) + ); + tokenSource.dispose(); + return { ...currentResult, ...notebookResult }; + } + async search(query: ITextQuery, onProgress?: (result: ISearchProgressItem) => void): Promise { this.cancelSearch(true); @@ -1474,16 +1807,7 @@ export class SearchModel extends Disposable { // In search on type case, delay the streaming of results just a bit, so that we don't flash the only "local results" fast path this._startStreamDelay = new Promise(resolve => setTimeout(resolve, this.searchConfig.searchOnType ? 150 : 0)); - const tokenSource = this.currentCancelTokenSource = new CancellationTokenSource(); - const currentRequest = this.searchService.textSearch(this._searchQuery, this.currentCancelTokenSource.token, p => { - progressEmitter.fire(); - this.onSearchProgress(p); - - onProgress?.(p); - }); - - const dispose = () => tokenSource.dispose(); - currentRequest.then(dispose, dispose); + const currentRequest = this.doSearch(query, progressEmitter, this._searchQuery, onProgress); const start = Date.now(); @@ -1724,6 +2048,21 @@ function textSearchResultToMatches(rawMatch: ITextSearchMatch, fileMatch: FileMa } } +function textSearchResultToNotebookMatches(rawMatch: NotebookTextSearchMatch, fileMatch: FileMatch): NotebookMatch[] { + const previewLines = rawMatch.preview.text.split('\n'); + if (Array.isArray(rawMatch.ranges)) { + + return rawMatch.ranges.map((r, i) => { + const previewRange: ISearchRange = (rawMatch.preview.matches)[i]; + return new NotebookMatch(fileMatch, previewLines, previewRange, r, rawMatch.notebookMatchInfo); + }); + } else { + const previewRange = rawMatch.preview.matches; + const match = new NotebookMatch(fileMatch, previewLines, previewRange, rawMatch.ranges, rawMatch.notebookMatchInfo); + return [match]; + } +} + export function arrayContainsElementOrParent(element: RenderableMatch, testArray: RenderableMatch[]): boolean { do { if (testArray.includes(element)) { diff --git a/src/vs/workbench/contrib/search/browser/searchNotebookHelpers.ts b/src/vs/workbench/contrib/search/browser/searchNotebookHelpers.ts new file mode 100644 index 0000000000000..84c48e8a41279 --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchNotebookHelpers.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CellFindMatchWithIndex, ICellViewModel, CellWebviewFindMatch } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; + +import { ISearchRange, ITextSearchPreviewOptions, TextSearchMatch } from 'vs/workbench/services/search/common/search'; +import { Range } from 'vs/editor/common/core/range'; + +export interface NotebookMatchInfo { + cellIndex: number; + matchStartIndex: number; + matchEndIndex: number; + cell: ICellViewModel; + webviewMatchInfo?: { + index: number; + }; +} + +interface CellFindMatchInfoForTextModel { + notebookMatchInfo: NotebookMatchInfo; + matches: FindMatch[] | CellWebviewFindMatch; +} + +export class NotebookTextSearchMatch extends TextSearchMatch { + constructor(text: string, range: ISearchRange | ISearchRange[], public notebookMatchInfo: NotebookMatchInfo, previewOptions?: ITextSearchPreviewOptions) { + super(text, range, previewOptions); + } +} + +function notebookEditorMatchToTextSearchResult(cellInfo: CellFindMatchInfoForTextModel, previewOptions?: ITextSearchPreviewOptions): NotebookTextSearchMatch | undefined { + const matches = cellInfo.matches; + + if (Array.isArray(matches)) { + if (matches.length > 0) { + const lineTexts: string[] = []; + const firstLine = matches[0].range.startLineNumber; + const lastLine = matches[matches.length - 1].range.endLineNumber; + for (let i = firstLine; i <= lastLine; i++) { + lineTexts.push(cellInfo.notebookMatchInfo.cell.textBuffer.getLineContent(i)); + } + + return new NotebookTextSearchMatch( + lineTexts.join('\n') + '\n', + matches.map(m => new Range(m.range.startLineNumber - 1, m.range.startColumn - 1, m.range.endLineNumber - 1, m.range.endColumn - 1)), + cellInfo.notebookMatchInfo, + previewOptions); + } + } + else { + // TODO: this is a placeholder for webview matches + const searchPreviewInfo = matches.searchPreviewInfo ?? { + line: '', range: { start: 0, end: 0 } + }; + + return new NotebookTextSearchMatch( + searchPreviewInfo.line, + new Range(0, searchPreviewInfo.range.start, 0, searchPreviewInfo.range.end), + cellInfo.notebookMatchInfo, + previewOptions); + } + return undefined; +} +export function notebookEditorMatchesToTextSearchResults(cellFindMatches: CellFindMatchWithIndex[], previewOptions?: ITextSearchPreviewOptions): NotebookTextSearchMatch[] { + let previousEndLine = -1; + const groupedMatches: CellFindMatchInfoForTextModel[] = []; + let currentMatches: FindMatch[] = []; + let startIndexOfCurrentMatches = 0; + + + cellFindMatches.forEach((cellFindMatch) => { + const cellIndex = cellFindMatch.index; + cellFindMatch.contentMatches.forEach((match, index) => { + if (match.range.startLineNumber !== previousEndLine) { + if (currentMatches.length > 0) { + groupedMatches.push({ matches: [...currentMatches], notebookMatchInfo: { cellIndex, matchStartIndex: startIndexOfCurrentMatches, matchEndIndex: index, cell: cellFindMatch.cell } }); + currentMatches = []; + } + startIndexOfCurrentMatches = cellIndex + 1; + } + + currentMatches.push(match); + previousEndLine = match.range.endLineNumber; + }); + + if (currentMatches.length > 0) { + groupedMatches.push({ matches: [...currentMatches], notebookMatchInfo: { cellIndex, matchStartIndex: startIndexOfCurrentMatches, matchEndIndex: cellFindMatch.contentMatches.length - 1, cell: cellFindMatch.cell } }); + currentMatches = []; + } + + cellFindMatch.webviewMatches.forEach((match, index) => { + groupedMatches.push({ matches: match, notebookMatchInfo: { cellIndex, matchStartIndex: index, matchEndIndex: index, cell: cellFindMatch.cell, webviewMatchInfo: { index: match.index } } }); + }); + }); + + return groupedMatches.map(sameLineMatches => { + return notebookEditorMatchToTextSearchResult(sameLineMatches, previewOptions); + }).filter((elem): elem is NotebookTextSearchMatch => !!elem); +} diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index f6b6a913faf66..11b30513aad88 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -18,7 +18,7 @@ import { ISearchConfigurationProperties } from 'vs/workbench/services/search/com import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; -import { FileMatch, Match, RenderableMatch, SearchModel, FolderMatch, FolderMatchNoRoot, FolderMatchWorkspaceRoot } from 'vs/workbench/contrib/search/common/searchModel'; +import { FileMatch, Match, RenderableMatch, SearchModel, FolderMatch, FolderMatchNoRoot, FolderMatchWorkspaceRoot } from 'vs/workbench/contrib/search/browser/searchModel'; import { isEqual } from 'vs/base/common/resources'; import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index baa6cf937e77f..eee8b95ed8906 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -21,6 +21,7 @@ import * as env from 'vs/base/common/platform'; import * as strings from 'vs/base/common/strings'; import { withNullAsUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; +import * as network from 'vs/base/common/network'; import 'vs/css!./media/searchview'; import { getCodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; @@ -60,7 +61,6 @@ import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/vie import { IEditorPane } from 'vs/workbench/common/editor'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IViewDescriptorService } from 'vs/workbench/common/views'; -import { NotebookFindContrib } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { ExcludePatternInputWidget, IncludePatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; import { appendKeyBindingLabel } from 'vs/workbench/contrib/search/browser/searchActionsBase'; @@ -70,10 +70,10 @@ import { renderSearchMessage } from 'vs/workbench/contrib/search/browser/searchM import { FileMatchRenderer, FolderMatchRenderer, MatchRenderer, SearchAccessibilityProvider, SearchDelegate } from 'vs/workbench/contrib/search/browser/searchResultsView'; import { SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; -import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; +import { IReplaceService } from 'vs/workbench/contrib/search/browser/replace'; import { getOutOfWorkspaceEditorResources, SearchStateKey, SearchUIState } from 'vs/workbench/contrib/search/common/search'; import { ISearchHistoryService, ISearchHistoryValues } from 'vs/workbench/contrib/search/common/searchHistoryService'; -import { FileMatch, FileMatchOrMatch, FolderMatch, FolderMatchWithResource, IChangeEvent, ISearchWorkbenchService, Match, RenderableMatch, searchMatchComparer, SearchModel, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; +import { FileMatch, FileMatchOrMatch, FolderMatch, FolderMatchWithResource, IChangeEvent, ISearchWorkbenchService, Match, NotebookMatch, RenderableMatch, searchMatchComparer, SearchModel, SearchResult } from 'vs/workbench/contrib/search/browser/searchModel'; import { createEditorFromSearchResult } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IPreferencesService, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences'; @@ -81,6 +81,7 @@ import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/services/se import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, SearchCompletionExitCode, SearchSortOrder, TextSearchCompleteMessageType, ViewMode } from 'vs/workbench/services/search/common/search'; import { TextSearchCompleteMessage } from 'vs/workbench/services/search/common/searchExtTypes'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; const $ = dom.$; @@ -186,6 +187,7 @@ export class SearchView extends ViewPane { @IStorageService storageService: IStorageService, @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, + @INotebookService private readonly notebookService: INotebookService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); @@ -1764,17 +1766,27 @@ export class SearchView extends ViewPane { this.currentSelectedFileMatch = undefined; } + private shouldOpenInNotebookEditor(match: Match, uri: URI): boolean { + // Untitled files will return a false positive for getContributedNotebookTypes. + // Since untitled files are already open, then untitled notebooks should return NotebookMatch results. + + // notebookMatch are only created when search.experimental.notebookSearch is enabled, so this should never return true if experimental flag is disabled. + return match instanceof NotebookMatch || (uri.scheme !== network.Schemas.untitled && this.notebookService.getContributedNotebookTypes(uri).length > 0); + } + private onFocus(lineMatch: Match, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise { const useReplacePreview = this.configurationService.getValue().search.useReplacePreview; - return (useReplacePreview && this.viewModel.isReplaceActive() && !!this.viewModel.replaceString) ? + + const resource = lineMatch instanceof Match ? lineMatch.parent().resource : (lineMatch).resource; + return (useReplacePreview && this.viewModel.isReplaceActive() && !!this.viewModel.replaceString && !(this.shouldOpenInNotebookEditor(lineMatch, resource))) ? this.replaceService.openReplacePreview(lineMatch, preserveFocus, sideBySide, pinned) : - this.open(lineMatch, preserveFocus, sideBySide, pinned); + this.open(lineMatch, preserveFocus, sideBySide, pinned, resource); } - async open(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): Promise { + async open(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean, resourceInput?: URI): Promise { const selection = this.getSelectionFrom(element); - const resource = element instanceof Match ? element.parent().resource : (element).resource; - + const oldParentMatches = element instanceof Match ? element.parent().matches() : []; + const resource = resourceInput ?? (element instanceof Match ? element.parent().resource : (element).resource); let editor: IEditorPane | undefined; try { editor = await this.editorService.openEditor({ @@ -1802,9 +1814,32 @@ export class SearchView extends ViewPane { } if (editor instanceof NotebookEditor) { - const controller = editor.getControl()?.getContribution(NotebookFindContrib.id); - const matchIndex = element instanceof Match ? element.parent().matches().findIndex(e => e.id() === element.id()) : undefined; - controller?.show(this.searchWidget.searchInput.getValue(), { matchIndex, focus: false }); + if (element instanceof Match) { + if (element instanceof NotebookMatch) { + element.parent().showMatch(element); + } else { + const editorWidget = editor.getControl(); + if (editorWidget) { + // Ensure that the editor widget is binded. If if is, then this should return immediately. + // Otherwise, it will bind the widget. + await element.parent().bindNotebookEditorWidget(editorWidget); + + const matchIndex = oldParentMatches.findIndex(e => e.id() === element.id()); + const matches = element.parent().matches(); + const match = matchIndex >= matches.length ? matches[matches.length - 1] : matches[matchIndex]; + + if (match instanceof NotebookMatch) { + element.parent().showMatch(match); + } + + if (!this.tree.getFocus().includes(match) || !this.tree.getSelection().includes(match)) { + this.tree.setSelection([match], getSelectionKeyboardEvent()); + } + } + + } + } + } } diff --git a/src/vs/workbench/contrib/search/test/browser/mockSearchTree.ts b/src/vs/workbench/contrib/search/test/browser/mockSearchTree.ts index 681dae8164cc8..31effdd2358f1 100644 --- a/src/vs/workbench/contrib/search/test/browser/mockSearchTree.ts +++ b/src/vs/workbench/contrib/search/test/browser/mockSearchTree.ts @@ -6,7 +6,7 @@ import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree'; import { Emitter } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { RenderableMatch } from 'vs/workbench/contrib/search/common/searchModel'; +import { RenderableMatch } from 'vs/workbench/contrib/search/browser/searchModel'; const someEvent = new Emitter().event; diff --git a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts index 9d77f760ab0b6..f27540fe7c831 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts @@ -16,11 +16,15 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; import { IFileMatch, QueryType } from 'vs/workbench/services/search/common/search'; import { getElementToFocusAfterRemoved, getLastNodeFromSameType } from 'vs/workbench/contrib/search/browser/searchActionsRemoveReplace'; -import { FileMatch, FileMatchOrMatch, FolderMatch, Match, SearchModel } from 'vs/workbench/contrib/search/common/searchModel'; +import { FileMatch, FileMatchOrMatch, FolderMatch, Match, SearchModel } from 'vs/workbench/contrib/search/browser/searchModel'; import { MockObjectTree } from 'vs/workbench/contrib/search/test/browser/mockSearchTree'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { ILabelService } from 'vs/platform/label/common/label'; +import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; +import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { TestEditorGroupsService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('Search Actions', () => { @@ -30,6 +34,7 @@ suite('Search Actions', () => { setup(() => { instantiationService = new TestInstantiationService(); instantiationService.stub(IModelService, stubModelService(instantiationService)); + instantiationService.stub(INotebookEditorService, stubNotebookEditorService(instantiationService)); instantiationService.stub(IKeybindingService, {}); instantiationService.stub(ILabelService, { getUriBasenameLabel: (uri: URI) => '' }); instantiationService.stub(IKeybindingService, 'resolveKeybinding', (keybinding: Keybinding) => USLayoutResolvedKeybinding.resolveKeybinding(keybinding, OS)); @@ -169,8 +174,15 @@ suite('Search Actions', () => { } function stubModelService(instantiationService: TestInstantiationService): IModelService { - instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(IThemeService, new TestThemeService()); + const config = new TestConfigurationService(); + config.setUserConfiguration('search', { searchOnType: true, experimental: { notebookSearch: false } }); + instantiationService.stub(IConfigurationService, config); return instantiationService.createInstance(ModelService); } + + function stubNotebookEditorService(instantiationService: TestInstantiationService): INotebookEditorService { + instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService()); + return instantiationService.createInstance(NotebookEditorWidgetService); + } }); diff --git a/src/vs/workbench/contrib/search/test/common/searchModel.test.ts b/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts similarity index 93% rename from src/vs/workbench/contrib/search/test/common/searchModel.test.ts rename to src/vs/workbench/contrib/search/test/browser/searchModel.test.ts index 5c9caf163a18b..ca89c3854ae3e 100644 --- a/src/vs/workbench/contrib/search/test/common/searchModel.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchModel.test.ts @@ -16,7 +16,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { IFileMatch, 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 { SearchModel } from 'vs/workbench/contrib/search/common/searchModel'; +import { SearchModel } from 'vs/workbench/contrib/search/browser/searchModel'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { FileService } from 'vs/platform/files/common/fileService'; @@ -25,6 +25,10 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { isWindows } from 'vs/base/common/platform'; import { ILabelService } from 'vs/platform/label/common/label'; +import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { TestEditorGroupsService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl'; const nullEvent = new class { id: number = -1; @@ -75,14 +79,11 @@ suite('SearchModel', () => { instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(ILabelService, { getUriBasenameLabel: (uri: URI) => '' }); instantiationService.stub(IModelService, stubModelService(instantiationService)); + instantiationService.stub(INotebookEditorService, stubNotebookEditorService(instantiationService)); instantiationService.stub(ISearchService, {}); instantiationService.stub(ISearchService, 'textSearch', Promise.resolve({ results: [] })); instantiationService.stub(IUriIdentityService, new UriIdentityService(new FileService(new NullLogService()))); instantiationService.stub(ILogService, new NullLogService()); - - const config = new TestConfigurationService(); - config.setUserConfiguration('search', { searchOnType: true }); - instantiationService.stub(IConfigurationService, config); }); teardown(() => { @@ -171,9 +172,8 @@ suite('SearchModel', () => { await testObject.search({ contentPattern: { pattern: 'somestring' }, type: QueryType.Text, folderQueries }); assert.ok(target.calledThrice); - const data = target.args[2]; - data[1].duration = -1; - assert.deepStrictEqual(['searchResultsFirstRender', { duration: -1 }], data); + assert.ok(target.calledWith('searchResultsFirstRender')); + assert.ok(target.calledWith('searchResultsFinished')); }); test('Search Model: Search reports timed telemetry on search when progress is not called', () => { @@ -355,9 +355,16 @@ suite('SearchModel', () => { } function stubModelService(instantiationService: TestInstantiationService): IModelService { - instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(IThemeService, new TestThemeService()); + const config = new TestConfigurationService(); + config.setUserConfiguration('search', { searchOnType: true, experimental: { notebookSearch: false } }); + instantiationService.stub(IConfigurationService, config); return instantiationService.createInstance(ModelService); } + function stubNotebookEditorService(instantiationService: TestInstantiationService): INotebookEditorService { + instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService()); + return instantiationService.createInstance(NotebookEditorWidgetService); + } + }); diff --git a/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts new file mode 100644 index 0000000000000..96c3c4b59f800 --- /dev/null +++ b/src/vs/workbench/contrib/search/test/browser/searchNotebookHelpers.test.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Range } from 'vs/editor/common/core/range'; +import { FindMatch, IReadonlyTextBuffer } from 'vs/editor/common/model'; +import { notebookEditorMatchesToTextSearchResults } from 'vs/workbench/contrib/search/browser/searchNotebookHelpers'; +import { ISearchRange } from 'vs/workbench/services/search/common/search'; +import { CellFindMatchWithIndex, ICellViewModel, CellWebviewFindMatch } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +suite('searchNotebookHelpers', () => { + setup(() => { + }); + suite('notebookEditorMatchesToTextSearchResults', () => { + + function assertRangesEqual(actual: ISearchRange | ISearchRange[], expected: ISearchRange[]) { + if (!Array.isArray(actual)) { + // All of these tests are for arrays... + throw new Error('Expected array of ranges'); + } + + assert.strictEqual(actual.length, expected.length); + + // These are sometimes Range, sometimes SearchRange + actual.forEach((r, i) => { + const expectedRange = expected[i]; + assert.deepStrictEqual( + { startLineNumber: r.startLineNumber, startColumn: r.startColumn, endLineNumber: r.endLineNumber, endColumn: r.endColumn }, + { startLineNumber: expectedRange.startLineNumber, startColumn: expectedRange.startColumn, endLineNumber: expectedRange.endLineNumber, endColumn: expectedRange.endColumn }); + }); + } + + test('simple', () => { + const cell = { + cellKind: CellKind.Code, textBuffer: { + getLineContent(lineNumber: number): string { + return 'test'; + } + } + } as ICellViewModel; + + const findMatch = new FindMatch(new Range(5, 1, 5, 2), null); + const cellFindMatchWithIndex: CellFindMatchWithIndex = { + cell, + index: 0, + length: 5, + getMatch(index: number): FindMatch | CellWebviewFindMatch { + return findMatch; + }, + contentMatches: [findMatch], + webviewMatches: [] + }; + + const results = notebookEditorMatchesToTextSearchResults([cellFindMatchWithIndex]); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].preview.text, 'test\n'); + assertRangesEqual(results[0].preview.matches, [new Range(0, 0, 0, 1)]); + assertRangesEqual(results[0].ranges, [new Range(4, 0, 4, 1)]); + }); + + }); +}); diff --git a/src/vs/workbench/contrib/search/test/common/searchResult.test.ts b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts similarity index 90% rename from src/vs/workbench/contrib/search/test/common/searchResult.test.ts rename to src/vs/workbench/contrib/search/test/browser/searchResult.test.ts index 522da80b2b5c1..8ba2465a21a56 100644 --- a/src/vs/workbench/contrib/search/test/common/searchResult.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchResult.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { Match, FileMatch, SearchResult, SearchModel, FolderMatch } from 'vs/workbench/contrib/search/common/searchModel'; +import { Match, FileMatch, SearchResult, SearchModel, FolderMatch } from 'vs/workbench/contrib/search/browser/searchModel'; import { URI } from 'vs/base/common/uri'; import { IFileMatch, TextSearchMatch, OneLineRange, ITextSearchMatch, QueryType } from 'vs/workbench/services/search/common/search'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -15,7 +15,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { ModelService } from 'vs/editor/common/services/modelService'; import { IModelService } from 'vs/editor/common/services/model'; -import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; +import { IReplaceService } from 'vs/workbench/contrib/search/browser/replace'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; @@ -25,6 +25,13 @@ import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { ILabelService } from 'vs/platform/label/common/label'; import { MockLabelService } from 'vs/workbench/services/label/test/common/mockLabelService'; import { isWindows } from 'vs/base/common/platform'; +import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { TestEditorGroupsService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl'; +import { NotebookTextSearchMatch } from 'vs/workbench/contrib/search/browser/searchNotebookHelpers'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; const lineOneRange = new OneLineRange(1, 0, 1); @@ -36,6 +43,7 @@ suite('SearchResult', () => { instantiationService = new TestInstantiationService(); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IModelService, stubModelService(instantiationService)); + instantiationService.stub(INotebookEditorService, stubNotebookEditorService(instantiationService)); instantiationService.stub(IUriIdentityService, new UriIdentityService(new FileService(new NullLogService()))); instantiationService.stubPromise(IReplaceService, {}); instantiationService.stub(IReplaceService, 'replace', () => Promise.resolve(null)); @@ -215,6 +223,42 @@ suite('SearchResult', () => { assert.ok(new Range(2, 1, 2, 2).equalsRange(actuaMatches[0].range())); }); + test('Adding multiple raw notebook matches', function () { + const testObject = aSearchResult(); + + const modelTarget = instantiationService.spy(IModelService, 'getModel'); + const cell = { cellKind: CellKind.Code } as ICellViewModel; + const target = [ + aRawMatch('/1', + new NotebookTextSearchMatch('preview 1', new OneLineRange(1, 1, 4), { + cellIndex: 0, + matchStartIndex: 0, + matchEndIndex: 1, + cell, + }), + new NotebookTextSearchMatch('preview 1', new OneLineRange(1, 4, 11), { + cellIndex: 0, + matchStartIndex: 0, + matchEndIndex: 1, + cell, + })), + aRawMatch('/2', + new NotebookTextSearchMatch('preview 2', lineOneRange, { + cellIndex: 0, + matchStartIndex: 0, + matchEndIndex: 1, + cell, + }))]; + + testObject.add(target); + assert.strictEqual(3, testObject.count()); + + // when a model is binded, the results are queried once again. + assert.ok(modelTarget.calledTwice); + assert.ok(modelTarget.calledWith(testObject.matches()[0].resource)); + assert.ok(modelTarget.calledWith(testObject.matches()[1].resource)); + }); + test('Dispose disposes matches', function () { const target1 = sinon.spy(); const target2 = sinon.spy(); @@ -268,21 +312,6 @@ suite('SearchResult', () => { assert.deepStrictEqual([{ elements: arrayToRemove, removed: true }], target.args[0]); }); - test('remove triggers change event', function () { - const target = sinon.spy(); - const testObject = aSearchResult(); - testObject.add([ - aRawMatch('/1', - new TextSearchMatch('preview 1', lineOneRange))]); - const objectToRemove = testObject.matches()[0]; - testObject.onChange(target); - - testObject.remove(objectToRemove); - - assert.ok(target.calledOnce); - assert.deepStrictEqual([{ elements: [objectToRemove], removed: true }], target.args[0]); - }); - test('Removing all line matches and adding back will add file back to result', function () { const testObject = aSearchResult(); testObject.add([ @@ -516,11 +545,18 @@ suite('SearchResult', () => { } function stubModelService(instantiationService: TestInstantiationService): IModelService { - instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(IThemeService, new TestThemeService()); + const config = new TestConfigurationService(); + config.setUserConfiguration('search', { searchOnType: true, experimental: { notebookSearch: false } }); + instantiationService.stub(IConfigurationService, config); return instantiationService.createInstance(ModelService); } + function stubNotebookEditorService(instantiationService: TestInstantiationService): INotebookEditorService { + instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService()); + return instantiationService.createInstance(NotebookEditorWidgetService); + } + function getPopulatedSearchResult() { const testObject = aSearchResult(); diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index 986e61e433be7..1da5e284d825e 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -21,10 +21,14 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity' import { UriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentityService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; -import { FileMatch, FolderMatch, Match, searchComparer, searchMatchComparer, SearchModel, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; +import { FileMatch, FolderMatch, Match, searchComparer, searchMatchComparer, SearchModel, SearchResult } from 'vs/workbench/contrib/search/browser/searchModel'; import { MockLabelService } from 'vs/workbench/services/label/test/common/mockLabelService'; import { IFileMatch, ITextSearchMatch, OneLineRange, QueryType, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; +import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { TestEditorGroupsService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { NotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorServiceImpl'; suite('Search - Viewlet', () => { let instantiation: TestInstantiationService; @@ -33,6 +37,8 @@ suite('Search - Viewlet', () => { instantiation = new TestInstantiationService(); instantiation.stub(ILanguageConfigurationService, TestLanguageConfigurationService); instantiation.stub(IModelService, stubModelService(instantiation)); + instantiation.stub(INotebookEditorService, stubNotebookEditorService(instantiation)); + instantiation.set(IWorkspaceContextService, new TestContextService(TestWorkspace)); instantiation.stub(IUriIdentityService, new UriIdentityService(new FileService(new NullLogService()))); instantiation.stub(ILabelService, new MockLabelService()); @@ -194,11 +200,20 @@ suite('Search - Viewlet', () => { } function stubModelService(instantiationService: TestInstantiationService): IModelService { - instantiationService.stub(IConfigurationService, new TestConfigurationService()); instantiationService.stub(IThemeService, new TestThemeService()); + + const config = new TestConfigurationService(); + config.setUserConfiguration('search', { searchOnType: true, experimental: { notebookSearch: false } }); + instantiationService.stub(IConfigurationService, config); + return instantiationService.createInstance(ModelService); } + function stubNotebookEditorService(instantiationService: TestInstantiationService): INotebookEditorService { + instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService()); + return instantiationService.createInstance(NotebookEditorWidgetService); + } + function createFileUriFromPathFromRoot(path?: string): URI { const rootName = getRootName(); if (path) { diff --git a/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts b/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts index 8168d0f831e53..346fa88d3cbc9 100644 --- a/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts +++ b/src/vs/workbench/contrib/search/test/electron-browser/textsearch.perf.integrationTest.ts @@ -34,7 +34,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { testWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; import 'vs/workbench/contrib/search/browser/search.contribution'; // load contributions import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder'; -import { SearchModel } from 'vs/workbench/contrib/search/common/searchModel'; +import { SearchModel } from 'vs/workbench/contrib/search/browser/searchModel'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 77127304454ab..2c70a3b137e57 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -44,7 +44,7 @@ import { SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; import { InputBoxFocusedKey } from 'vs/workbench/contrib/search/common/constants'; import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/services/search/common/queryBuilder'; import { getOutOfWorkspaceEditorResources } from 'vs/workbench/contrib/search/common/search'; -import { SearchModel, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; +import { SearchModel, SearchResult } from 'vs/workbench/contrib/search/browser/searchModel'; import { InSearchEditor, SearchEditorID, SearchEditorInputTypeId } from 'vs/workbench/contrib/searchEditor/browser/constants'; import type { SearchConfiguration, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { serializeSearchResultForEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index b5a7b7c9980fb..b2a7f050b20b1 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -17,7 +17,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { EditorsOrder } from 'vs/workbench/common/editor'; import { IViewsService } from 'vs/workbench/common/views'; import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; -import { SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; +import { SearchResult } from 'vs/workbench/contrib/search/browser/searchModel'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; import { OpenSearchEditorArgs } from 'vs/workbench/contrib/searchEditor/browser/searchEditor.contribution'; import { getOrMakeSearchEditorInput, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts index a931eb0a9070c..de40b0c6636b4 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorSerialization.ts @@ -10,7 +10,7 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { Range } from 'vs/editor/common/core/range'; import type { ITextModel } from 'vs/editor/common/model'; import { localize } from 'vs/nls'; -import { FileMatch, Match, searchMatchComparer, SearchResult, FolderMatch } from 'vs/workbench/contrib/search/common/searchModel'; +import { FileMatch, Match, searchMatchComparer, SearchResult, FolderMatch } from 'vs/workbench/contrib/search/browser/searchModel'; import type { SearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { ITextQuery, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index ecda55c219eff..ad3553fba7cb8 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -41,7 +41,7 @@ export const ISearchService = createDecorator('searchService'); */ export interface ISearchService { readonly _serviceBrand: undefined; - textSearch(query: ITextQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): Promise; + textSearch(query: ITextQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void, notebookURIs?: Set): Promise; fileSearch(query: IFileQuery, token?: CancellationToken): Promise; clearCache(cacheKey: string): Promise; registerSearchResultProvider(scheme: string, type: SearchProviderType, provider: ISearchResultProvider): IDisposable; @@ -173,6 +173,7 @@ export interface ISearchRange { export interface ITextSearchResultPreview { text: string; matches: ISearchRange | ISearchRange[]; + cellFragment?: string; } export interface ITextSearchMatch { @@ -398,6 +399,9 @@ export interface ISearchConfigurationProperties { badges: boolean; }; defaultViewMode: ViewMode; + experimental: { + notebookSearch: boolean; + }; } export interface ISearchConfiguration extends IFilesConfiguration { diff --git a/src/vs/workbench/services/search/common/searchService.ts b/src/vs/workbench/services/search/common/searchService.ts index 874de93dad5e7..63b1c192a8668 100644 --- a/src/vs/workbench/services/search/common/searchService.ts +++ b/src/vs/workbench/services/search/common/searchService.ts @@ -8,7 +8,7 @@ import { DeferredPromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { CancellationError } from 'vs/base/common/errors'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ResourceMap } from 'vs/base/common/map'; +import { ResourceMap, ResourceSet } from 'vs/base/common/map'; import { Schemas } from 'vs/base/common/network'; import { StopWatch } from 'vs/base/common/stopwatch'; import { isNumber } from 'vs/base/common/types'; @@ -73,7 +73,7 @@ export class SearchService extends Disposable implements ISearchService { }); } - async textSearch(query: ITextQuery, token?: CancellationToken, onProgress?: (item: ISearchProgressItem) => void): Promise { + async textSearch(query: ITextQuery, token?: CancellationToken, onProgress?: (item: ISearchProgressItem) => void, notebookURIs?: ResourceSet): Promise { // Get local results from dirty/untitled const localResults = this.getLocalResults(query); @@ -84,7 +84,7 @@ export class SearchService extends Disposable implements ISearchService { const onProviderProgress = (progress: ISearchProgressItem) => { if (isFileMatch(progress)) { // Match - if (!localResults.results.has(progress.resource) && onProgress) { // don't override local results + if (!localResults.results.has(progress.resource) && !(notebookURIs && notebookURIs.has(progress.resource)) && onProgress) { // don't override local results onProgress(progress); } } else if (onProgress) {