From 49eedd7d5119b6752ee4eda47d3047122e9276f8 Mon Sep 17 00:00:00 2001 From: Michael Lively Date: Mon, 27 May 2024 22:25:23 -0700 Subject: [PATCH] Add Search in Cell Selection to Notebook Find Widget (#213409) * all search scope functionality except cell decorations * fix filter icon showing fake useage * decorations + css tweaking + PR feedback --- .../editor/contrib/find/browser/findWidget.ts | 2 +- .../browser/contrib/find/findFilters.ts | 44 ++++++++- .../browser/contrib/find/findModel.ts | 14 +-- .../browser/contrib/find/notebookFind.ts | 13 ++- .../contrib/find/notebookFindReplaceWidget.ts | 94 +++++++++++++++++-- .../contrib/find/notebookFindWidget.ts | 3 + .../notebook/browser/media/notebook.css | 6 ++ .../notebook/browser/notebook.contribution.ts | 9 +- .../notebook/browser/notebookEditorWidget.ts | 21 ++++- .../view/cellParts/cellFocusIndicator.ts | 2 +- .../browser/view/cellParts/markupCell.ts | 3 + .../view/renderers/backLayerWebView.ts | 10 +- .../browser/view/renderers/webviewMessages.ts | 2 +- .../browser/view/renderers/webviewPreloads.ts | 6 +- .../viewModel/notebookViewModelImpl.ts | 13 ++- .../contrib/notebook/common/notebookCommon.ts | 5 +- .../contrib/search/browser/searchWidget.ts | 4 +- 17 files changed, 212 insertions(+), 39 deletions(-) diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index d3949c02e726c..f536964cb8664 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -47,10 +47,10 @@ import { createInstantHoverDelegate, getDefaultHoverDelegate } from 'vs/base/bro import { IHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegate'; import { IHoverService } from 'vs/platform/hover/browser/hover'; -const findSelectionIcon = registerIcon('find-selection', Codicon.selection, nls.localize('findSelectionIcon', 'Icon for \'Find in Selection\' in the editor find widget.')); const findCollapsedIcon = registerIcon('find-collapsed', Codicon.chevronRight, nls.localize('findCollapsedIcon', 'Icon to indicate that the editor find widget is collapsed.')); const findExpandedIcon = registerIcon('find-expanded', Codicon.chevronDown, nls.localize('findExpandedIcon', 'Icon to indicate that the editor find widget is expanded.')); +export const findSelectionIcon = registerIcon('find-selection', Codicon.selection, nls.localize('findSelectionIcon', 'Icon for \'Find in Selection\' in the editor find widget.')); export const findReplaceIcon = registerIcon('find-replace', Codicon.replace, nls.localize('findReplaceIcon', 'Icon for \'Replace\' in the editor find widget.')); export const findReplaceAllIcon = registerIcon('find-replace-all', Codicon.replaceAll, nls.localize('findReplaceAllIcon', 'Icon for \'Replace All\' in the editor find widget.')); export const findPreviousMatchIcon = registerIcon('find-previous-match', Codicon.arrowUp, nls.localize('findPreviousMatchIcon', 'Icon for \'Find Previous\' in the editor find widget.')); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/findFilters.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/findFilters.ts index a4bf2fd2d9fec..0901d295edd06 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findFilters.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findFilters.ts @@ -5,17 +5,19 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; -export interface INotebookFindFiltersChangeEvent { +export interface INotebookFindChangeEvent { markupInput?: boolean; markupPreview?: boolean; codeInput?: boolean; codeOutput?: boolean; + searchInRanges?: boolean; } export class NotebookFindFilters extends Disposable { - private readonly _onDidChange: Emitter = this._register(new Emitter()); - readonly onDidChange: Event = this._onDidChange.event; + private readonly _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; private _markupInput: boolean = true; @@ -68,17 +70,44 @@ export class NotebookFindFilters extends Disposable { } } + private _searchInRanges: boolean = false; + + get searchInRanges(): boolean { + return this._searchInRanges; + } + + set searchInRanges(value: boolean) { + if (this._searchInRanges !== value) { + this._searchInRanges = value; + this._onDidChange.fire({ searchInRanges: value }); + } + } + + private _selectedRanges: ICellRange[] = []; + + get selectedRanges(): ICellRange[] { + return this._selectedRanges; + } + + set selectedRanges(value: ICellRange[]) { + if (this._selectedRanges !== value) { + this._selectedRanges = value; + this._onDidChange.fire({ searchInRanges: this._searchInRanges }); + } + } + private readonly _initialMarkupInput: boolean; private readonly _initialMarkupPreview: boolean; private readonly _initialCodeInput: boolean; private readonly _initialCodeOutput: boolean; - constructor( markupInput: boolean, markupPreview: boolean, codeInput: boolean, - codeOutput: boolean + codeOutput: boolean, + searchInRanges: boolean, + selectedRanges: ICellRange[] ) { super(); @@ -86,6 +115,8 @@ export class NotebookFindFilters extends Disposable { this._markupPreview = markupPreview; this._codeInput = codeInput; this._codeOutput = codeOutput; + this._searchInRanges = searchInRanges; + this._selectedRanges = selectedRanges; this._initialMarkupInput = markupInput; this._initialMarkupPreview = markupPreview; @@ -94,6 +125,7 @@ export class NotebookFindFilters extends Disposable { } isModified(): boolean { + // do not include searchInRanges or selectedRanges in the check. This will incorrectly mark the filter icon as modified return ( this._markupInput !== this._initialMarkupInput || this._markupPreview !== this._initialMarkupPreview @@ -107,5 +139,7 @@ export class NotebookFindFilters extends Disposable { this._markupPreview = v.markupPreview; this._codeInput = v.codeInput; this._codeOutput = v.codeOutput; + this._searchInRanges = v.searchInRanges; + this._selectedRanges = v.selectedRanges; } } 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 6c0cca31b7516..de522b344a5eb 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts @@ -115,11 +115,7 @@ export class FindModel extends Disposable { } private _updateCellStates(e: FindReplaceStateChangedEvent) { - if (!this._state.filters?.markupInput) { - return; - } - - if (!this._state.filters?.markupPreview) { + if (!this._state.filters?.markupInput || !this._state.filters?.markupPreview || !this._state.filters?.searchInRanges || !this._state.filters?.selectedRanges) { return; } @@ -139,7 +135,9 @@ export class FindModel extends Disposable { includeMarkupInput: true, includeCodeInput: false, includeMarkupPreview: false, - includeOutput: false + includeOutput: false, + searchInRanges: this._state.filters?.searchInRanges, + selectedRanges: this._state.filters?.selectedRanges }; const contentMatches = viewModel.find(this._state.searchString, options); @@ -486,7 +484,9 @@ export class FindModel extends Disposable { includeMarkupInput: this._state.filters?.markupInput ?? true, includeCodeInput: this._state.filters?.codeInput ?? true, includeMarkupPreview: !!this._state.filters?.markupPreview, - includeOutput: !!this._state.filters?.codeOutput + includeOutput: !!this._state.filters?.codeOutput, + searchInRanges: this._state.filters?.searchInRanges, + selectedRanges: this._state.filters?.selectedRanges }; ret = await this._notebookEditor.find(val, options, token); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts index f91d2ed8863b1..fd39a06a3f720 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts @@ -25,6 +25,7 @@ import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INTERACTIVE_WINDOW_IS_ACTIVE_EDITOR, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { INotebookCommandContext, NotebookMultiCellAction } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; registerNotebookContribution(NotebookFindContrib.id, NotebookFindContrib); @@ -55,7 +56,7 @@ registerAction2(class extends Action2 { } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookMultiCellAction { constructor() { super({ id: 'notebook.find', @@ -68,7 +69,7 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor): Promise { + async runWithContext(accessor: ServicesAccessor, context: INotebookCommandContext): Promise { const editorService = accessor.get(IEditorService); const editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); @@ -77,7 +78,12 @@ registerAction2(class extends Action2 { } const controller = editor.getContribution(NotebookFindContrib.id); - controller.show(); + + if (context.selectedCells.length > 1) { + controller.show(undefined, { searchInRanges: true, selectedRanges: editor.getSelections() }); + } else { + controller.show(undefined, { searchInRanges: false, selectedRanges: [] }); + } } }); @@ -200,4 +206,3 @@ StartFindReplaceAction.addImplementation(100, (accessor: ServicesAccessor, codeE return false; }); - diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts index d616161b30996..7b261f5dc0e39 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -13,7 +13,7 @@ import { Delayer } from 'vs/base/common/async'; import { KeyCode } from 'vs/base/common/keyCodes'; import 'vs/css!./notebookFindReplaceWidget'; import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/browser/findState'; -import { findNextMatchIcon, findPreviousMatchIcon, findReplaceAllIcon, findReplaceIcon, SimpleButton } from 'vs/editor/contrib/find/browser/findWidget'; +import { findNextMatchIcon, findPreviousMatchIcon, findReplaceAllIcon, findReplaceIcon, findSelectionIcon, SimpleButton } from 'vs/editor/contrib/find/browser/findWidget'; import * as nls from 'vs/nls'; import { ContextScopedReplaceInput, registerAndCreateHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -35,19 +35,23 @@ import { filterIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIc import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; import { isSafari } from 'vs/base/common/platform'; import { ISashEvent, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; -import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookDeltaDecoration, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { defaultInputBoxStyles, defaultProgressBarStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { IToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; +import { IToggleStyles, Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { Disposable } from 'vs/base/common/lifecycle'; import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { asCssVariable, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; + const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find"); const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "Previous Match"); // const NLS_FILTER_BTN_LABEL = nls.localize('label.findFilterButton', "Search in View"); const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next Match"); +const NLS_FIND_IN_CELL_SELECTION_BTN_LABEL = nls.localize('label.findInCellSelectionButton', "Find in Cell Selection"); const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close"); const NLS_TOGGLE_REPLACE_MODE_BTN_LABEL = nls.localize('label.toggleReplaceButton', "Toggle Replace"); const NLS_REPLACE_INPUT_LABEL = nls.localize('label.replace', "Replace"); @@ -314,6 +318,10 @@ export abstract class SimpleFindReplaceWidget extends Widget { private _filters: NotebookFindFilters; + private readonly inSelectionToggle: Toggle; + private searchInSelectionEnabled: boolean; + private selectionDecorationIds: string[] = []; + constructor( @IContextViewService private readonly _contextViewService: IContextViewService, @IContextKeyService contextKeyService: IContextKeyService, @@ -326,14 +334,14 @@ export abstract class SimpleFindReplaceWidget extends Widget { ) { super(); - const findScope = this._configurationService.getValue<{ + const findFilters = this._configurationService.getValue<{ markupSource: boolean; markupPreview: boolean; codeSource: boolean; codeOutput: boolean; - }>(NotebookSetting.findScope) ?? { markupSource: true, markupPreview: true, codeSource: true, codeOutput: true }; + }>(NotebookSetting.findFilters) ?? { markupSource: true, markupPreview: true, codeSource: true, codeOutput: true }; - this._filters = new NotebookFindFilters(findScope.markupSource, findScope.markupPreview, findScope.codeSource, findScope.codeOutput); + this._filters = new NotebookFindFilters(findFilters.markupSource, findFilters.markupPreview, findFilters.codeSource, findFilters.codeOutput, false, []); this._state.change({ filters: this._filters }, false); this._filters.onDidChange(() => { @@ -430,7 +438,6 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._findInput.setWholeWords(this._state.wholeWord); this._findInput.setCaseSensitive(this._state.matchCase); this._replaceInput.setPreserveCase(this._state.preserveCase); - this.findFirst(); })); this._matchesCount = document.createElement('div'); @@ -453,6 +460,27 @@ export abstract class SimpleFindReplaceWidget extends Widget { } }, hoverService)); + this.inSelectionToggle = this._register(new Toggle({ + icon: findSelectionIcon, + title: NLS_FIND_IN_CELL_SELECTION_BTN_LABEL, + isChecked: false, + inputActiveOptionBackground: asCssVariable(inputActiveOptionBackground), + inputActiveOptionBorder: asCssVariable(inputActiveOptionBorder), + inputActiveOptionForeground: asCssVariable(inputActiveOptionForeground), + })); + + this.inSelectionToggle.onChange(() => { + const checked = this.inSelectionToggle.checked; + this._filters.searchInRanges = checked; + if (checked) { + this._filters.selectedRanges = this._notebookEditor.getSelections(); + this.setCellSelectionDecorations(); + } else { + this._filters.selectedRanges = []; + this.clearCellSelectionDecorations(); + } + }); + const closeBtn = this._register(new SimpleButton({ label: NLS_CLOSE_BTN_LABEL, icon: widgetClose, @@ -465,8 +493,25 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._innerFindDomNode.appendChild(this._matchesCount); this._innerFindDomNode.appendChild(this.prevBtn.domNode); this._innerFindDomNode.appendChild(this.nextBtn.domNode); + this._innerFindDomNode.appendChild(this.inSelectionToggle.domNode); this._innerFindDomNode.appendChild(closeBtn.domNode); + this.searchInSelectionEnabled = this._configurationService.getValue(NotebookSetting.findScope); + this.inSelectionToggle.domNode.style.display = this.searchInSelectionEnabled ? 'inline' : 'none'; + + this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(NotebookSetting.findScope)) { + this.searchInSelectionEnabled = this._configurationService.getValue(NotebookSetting.findScope); + if (this.searchInSelectionEnabled) { + this.inSelectionToggle.domNode.style.display = 'inline'; + } else { + this.inSelectionToggle.domNode.style.display = 'none'; + this.inSelectionToggle.checked = false; + this.clearCellSelectionDecorations(); + } + } + }); + // _domNode wraps _innerDomNode, ensuring that this._domNode.appendChild(this._innerFindDomNode); @@ -599,7 +644,6 @@ export abstract class SimpleFindReplaceWidget extends Widget { protected abstract onInputChanged(): boolean; protected abstract find(previous: boolean): void; - protected abstract findFirst(): void; protected abstract replaceOne(): void; protected abstract replaceAll(): void; protected abstract onFocusTrackerFocus(): void; @@ -647,6 +691,26 @@ export abstract class SimpleFindReplaceWidget extends Widget { this.updateButtons(this.foundMatch); } + private setCellSelectionDecorations() { + const cellHandles: number[] = []; + this._notebookEditor.getSelectionViewModels().forEach(viewModel => { + cellHandles.push(viewModel.handle); + }); + + const decorations: INotebookDeltaDecoration[] = []; + for (const handle of cellHandles) { + decorations.push({ + handle: handle, + options: { className: 'nb-multiCellHighlight', outputClassName: 'nb-multiCellHighlight' } + } satisfies INotebookDeltaDecoration); + } + this.selectionDecorationIds = this._notebookEditor.deltaCellDecorations([], decorations); + } + + private clearCellSelectionDecorations() { + this._notebookEditor.deltaCellDecorations(this.selectionDecorationIds, []); + } + protected _updateMatchesCount(): void { } @@ -686,11 +750,20 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._findInput.focus(); } - public show(initialInput?: string, options?: { focus?: boolean }): void { + public show(initialInput?: string, options?: { focus?: boolean; searchInRanges?: boolean; selectedRanges?: ICellRange[] }): void { if (initialInput) { this._findInput.setValue(initialInput); } + if (this.searchInSelectionEnabled && options?.searchInRanges !== undefined) { + this._filters.searchInRanges = options.searchInRanges; + this.inSelectionToggle.checked = options.searchInRanges; + if (options.searchInRanges && options.selectedRanges) { + this._filters.selectedRanges = options.selectedRanges; + this.setCellSelectionDecorations(); + } + } + this._isVisible = true; setTimeout(() => { @@ -738,6 +811,9 @@ export abstract class SimpleFindReplaceWidget extends Widget { public hide(): void { if (this._isVisible) { + this.inSelectionToggle.checked = false; + this._notebookEditor.deltaCellDecorations(this.selectionDecorationIds, []); + this._domNode.classList.remove('visible-transition'); this._domNode.setAttribute('aria-hidden', 'true'); // Need to delay toggling visibility until after Transition, then visibility hidden - removes from tabIndex list diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts index 7c92a6b057669..51a075fe24a61 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts @@ -26,6 +26,7 @@ import { FindModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/fi import { SimpleFindReplaceWidget } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget'; import { CellEditState, ICellViewModel, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; const FIND_HIDE_TRANSITION = 'find-hide-transition'; const FIND_SHOW_TRANSITION = 'find-show-transition'; @@ -39,6 +40,8 @@ export interface IShowNotebookFindWidgetOptions { matchIndex?: number; focus?: boolean; searchStringSeededFrom?: { cell: ICellViewModel; range: Range }; + searchInRanges?: boolean; + selectedRanges?: ICellRange[]; } export class NotebookFindContrib extends Disposable implements INotebookEditorContribution { diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 93989cbc04f31..d659b3cf277de 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -446,6 +446,12 @@ background-color: var(--vscode-notebook-symbolHighlightBackground) !important; } +/** Cell Search Range selection highlight */ +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.nb-multiCellHighlight .cell-focus-indicator, +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row.nb-multiCellHighlight { + background-color: var(--vscode-notebook-symbolHighlightBackground) !important; +} + /** Cell focused editor border */ .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-editor-focus .cell-editor-part:before { outline: solid 1px var(--vscode-notebook-focusedEditorBorder); diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 67c09c2746337..0f9026c0dde80 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -1026,8 +1026,8 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: true }, - [NotebookSetting.findScope]: { - markdownDescription: nls.localize('notebook.findScope', "Customize the Find Widget behavior for searching within notebook cells. When both markup source and markup preview are enabled, the Find Widget will search either the source code or preview based on the current state of the cell."), + [NotebookSetting.findFilters]: { + markdownDescription: nls.localize('notebook.findFilters', "Customize the Find Widget behavior for searching within notebook cells. When both markup source and markup preview are enabled, the Find Widget will search either the source code or preview based on the current state of the cell."), type: 'object', properties: { markupSource: { @@ -1055,6 +1055,11 @@ configurationRegistry.registerConfiguration({ }, tags: ['notebookLayout'] }, + [NotebookSetting.findScope]: { + markdownDescription: nls.localize('notebook.experimental.find.scope.enabled', "Enables the user to search within a selection of cells in the notebook. When enabled, the user will see a \"Find in Cell Selection\" icon in the notebook find widget."), + type: 'boolean', + default: false, + }, [NotebookSetting.remoteSaving]: { markdownDescription: nls.localize('notebook.remoteSaving', "Enables the incremental saving of notebooks between processes and across Remote connections. When enabled, only the changes to the notebook are sent to the extension host, improving performance for large notebooks and slow network connections."), type: 'boolean', diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 05156eebb7299..2f83ab690ff5a 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -80,7 +80,7 @@ import { INotebookExecutionService } from 'vs/workbench/contrib/notebook/common/ import { INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookOptions, OutputInnerContainerTopPadding } from 'vs/workbench/contrib/notebook/browser/notebookOptions'; -import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; +import { cellRangesToIndexes, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; @@ -835,6 +835,18 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } `); + styleSheets.push(` + .notebookOverlay .monaco-list .monaco-list-row.code-cell-row.nb-multiCellHighlight:has(+ .monaco-list-row.nb-multiCellHighlight) .cell-focus-indicator-bottom { + height: ${bottomToolbarGap + cellBottomMargin}px; + background-color: var(--vscode-notebook-symbolHighlightBackground) !important; + } + + .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row.nb-multiCellHighlight:has(+ .monaco-list-row.nb-multiCellHighlight) .cell-focus-indicator-bottom { + height: ${bottomToolbarGap + cellBottomMargin - 6}px; + background-color: var(--vscode-notebook-symbolHighlightBackground) !important; + } + `); + styleSheets.push(` .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .input-collapse-container .cell-collapse-preview { @@ -2537,7 +2549,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } } - return Promise.all(requests); } @@ -2576,7 +2587,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return []; } - const webviewMatches = await this._webview.find(query, { caseSensitive: options.caseSensitive, wholeWord: options.wholeWord, includeMarkup: !!options.includeMarkupPreview, includeOutput: !!options.includeOutput, shouldGetSearchPreviewInfo, ownerID }); + const selectedRanges = options.selectedRanges?.map(range => this._notebookViewModel?.validateRange(range)).filter(range => !!range); + const selectedIndexes = cellRangesToIndexes(selectedRanges ?? []); + const findIds: string[] = selectedIndexes.map(index => this._notebookViewModel?.viewCells[index].id ?? ''); + + const webviewMatches = await this._webview.find(query, { caseSensitive: options.caseSensitive, wholeWord: options.wholeWord, includeMarkup: !!options.includeMarkupPreview, includeOutput: !!options.includeOutput, shouldGetSearchPreviewInfo, ownerID, findIds: options.searchInRanges ? findIds : [] }); if (token.isCancellationRequested) { return []; diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts index 7549fdeaab827..189ef50f1665c 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellFocusIndicator.ts @@ -77,7 +77,7 @@ export class CellFocusIndicator extends CellContentPart { override updateInternalLayoutNow(element: ICellViewModel): void { if (element.cellKind === CellKind.Markup) { const indicatorPostion = this.notebookEditor.notebookOptions.computeIndicatorPosition(element.layoutInfo.totalHeight, (element as MarkupCellViewModel).layoutInfo.foldHintHeight, this.notebookEditor.textModel?.viewType); - this.bottom.domNode.style.transform = `translateY(${indicatorPostion.bottomIndicatorTop}px)`; + this.bottom.domNode.style.transform = `translateY(${indicatorPostion.bottomIndicatorTop + 6}px)`; this.left.setHeight(indicatorPostion.verticalIndicatorHeight); this.right.setHeight(indicatorPostion.verticalIndicatorHeight); this.codeFocusIndicator.setHeight(indicatorPostion.verticalIndicatorHeight - this.getIndicatorTopMargin() * 2 - element.layoutInfo.chatHeight); diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts index d334bb1db3f20..b1a3e8b3e9374 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/markupCell.ts @@ -228,12 +228,14 @@ export class MarkupCell extends Disposable { e.added.forEach(options => { if (options.className) { this.notebookEditor.deltaCellContainerClassNames(this.viewCell.id, [options.className], []); + this.templateData.rootContainer.classList.add(options.className); } }); e.removed.forEach(options => { if (options.className) { this.notebookEditor.deltaCellContainerClassNames(this.viewCell.id, [], [options.className]); + this.templateData.rootContainer.classList.remove(options.className); } }); })); @@ -241,6 +243,7 @@ export class MarkupCell extends Disposable { this.viewCell.getCellDecorations().forEach(options => { if (options.className) { this.notebookEditor.deltaCellContainerClassNames(this.viewCell.id, [options.className], []); + this.templateData.rootContainer.classList.add(options.className); } }); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 702182063b361..9cc82bc223a00 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -401,6 +401,14 @@ export class BackLayerWebView extends Themable { background-color: var(--theme-notebook-symbol-highlight-background); } + #container .markup > div.nb-multiCellHighlight { + background-color: var(--theme-notebook-symbol-highlight-background); + } + + #container .nb-multiCellHighlight .output_container .output { + background-color: var(--theme-notebook-symbol-highlight-background); + } + #container .nb-chatGenerationHighlight .output_container .output { background-color: var(--vscode-notebook-selectedCellBackground); } @@ -1772,7 +1780,7 @@ export class BackLayerWebView extends Themable { }); } - async find(query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean; ownerID: string }): Promise { + async find(query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean; ownerID: string; findIds: string[] }): Promise { if (query === '') { this._sendMessageToWebview({ type: 'findStop', 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 5d0e13ad3b25b..a59abdada13bc 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -399,7 +399,7 @@ export interface ITokenizedStylesChangedMessage { export interface IFindMessage { readonly type: 'find'; readonly query: string; - readonly options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean; ownerID: string }; + readonly options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean; ownerID: string; findIds: string[] }; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 6e4aed1f573eb..4ded337182e92 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -1425,9 +1425,9 @@ async function webviewPreloads(ctx: PreloadContext) { return offset + getSelectionOffsetRelativeTo(parentElement, currentNode.parentNode); } - const find = (query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean; ownerID: string }) => { + const find = (query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean; ownerID: string; findIds: string[] }) => { let find = true; - const matches: IFindMatch[] = []; + let matches: IFindMatch[] = []; const range = document.createRange(); range.selectNodeContents(window.document.getElementById('findStart')!); @@ -1553,6 +1553,8 @@ async function webviewPreloads(ctx: PreloadContext) { console.log(e); } + + matches = matches.filter(match => options.findIds.length ? options.findIds.includes(match.cellId) : true); _highlighter.addHighlights(matches, options.ownerID); window.document.getSelection()?.removeAllRanges(); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts index b7fa0bc7ecfe6..8a0f837901156 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl.ts @@ -910,7 +910,18 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD //#region Find find(value: string, options: INotebookSearchOptions): CellFindMatchWithIndex[] { const matches: CellFindMatchWithIndex[] = []; - this._viewCells.forEach((cell, index) => { + let findCells: CellViewModel[] = []; + + const selectedRanges = options.selectedRanges?.map(range => this.validateRange(range)).filter(range => !!range); + + if (options.searchInRanges && selectedRanges) { + const selectedIndexes = cellRangesToIndexes(selectedRanges); + findCells = selectedIndexes.map(index => this._viewCells[index]); + } else { + findCells = this._viewCells; + } + + findCells.forEach((cell, index) => { const cellMatches = cell.startFind(value, options); if (cellMatches) { matches.push(new CellFindMatchModel( diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 7828091e5982c..33c85b2742097 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -827,6 +827,8 @@ export interface INotebookSearchOptions { includeMarkupPreview?: boolean; includeCodeInput?: boolean; includeOutput?: boolean; + searchInRanges?: boolean; + selectedRanges?: ICellRange[]; } export interface INotebookExclusiveDocumentFilter { @@ -951,7 +953,8 @@ export const NotebookSetting = { outputFontSize: 'notebook.output.fontSize', outputFontFamilyDeprecated: 'notebook.outputFontFamily', outputFontFamily: 'notebook.output.fontFamily', - findScope: 'notebook.find.scope', + findFilters: 'notebook.find.filters', + findScope: 'notebook.experimental.find.scope.enabled', logging: 'notebook.logging', confirmDeleteRunningCell: 'notebook.confirmDeleteRunningCell', remoteSaving: 'notebook.experimental.remoteSave', diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index 9b55d52a07cf1..cf86112b1c2ba 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -204,7 +204,9 @@ export class SearchWidget extends Widget { notebookOptions.isInNotebookMarkdownInput, notebookOptions.isInNotebookMarkdownPreview, notebookOptions.isInNotebookCellInput, - notebookOptions.isInNotebookCellOutput + notebookOptions.isInNotebookCellOutput, + false, + [] )); this._register(