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 46ff30bbc2348..bf30ba0fe1962 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -38,6 +38,7 @@ import { ISashEvent, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; import { 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 { Disposable } from 'vs/base/common/lifecycle'; const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find"); @@ -150,31 +151,26 @@ class NotebookFindFilterActionViewItem extends DropdownMenuActionViewItem { } } -class NotebookFindInput extends FindInput { +export class NotebookFindInputFilterButton extends Disposable { private _filterButtonContainer: HTMLElement; private _actionbar: ActionBar | null = null; - private _filterChecked: boolean = false; private _filtersAction: IAction; private _toggleStyles: IToggleStyles; constructor( readonly filters: NotebookFindFilters, - contextKeyService: IContextKeyService, readonly contextMenuService: IContextMenuService, readonly instantiationService: IInstantiationService, - parent: HTMLElement | null, - contextViewProvider: IContextViewProvider, - options: IFindInputOptions + options: IFindInputOptions, + tooltip: string = NOTEBOOK_FIND_FILTERS, ) { - super(parent, contextViewProvider, options); + super(); this._toggleStyles = options.toggleStyles; - this._register(registerAndCreateHistoryNavigationContext(contextKeyService, this.inputBox)); - this._filtersAction = new Action('notebookFindFilterAction', NOTEBOOK_FIND_FILTERS, 'notebook-filters ' + ThemeIcon.asClassName(filterIcon)); + this._filtersAction = new Action('notebookFindFilterAction', tooltip, 'notebook-filters ' + ThemeIcon.asClassName(filterIcon)); this._filtersAction.checked = false; this._filterButtonContainer = dom.$('.find-filter-button'); - this.controls.appendChild(this._filterButtonContainer); this.createFilters(this._filterButtonContainer); this._register(this.filters.onDidChange(() => { @@ -184,14 +180,24 @@ class NotebookFindInput extends FindInput { this._filtersAction.checked = false; } })); + } - this.inputBox.paddingRight = (this.caseSensitive?.width() ?? 0) + (this.wholeWords?.width() ?? 0) + (this.regex?.width() ?? 0) + this.getFilterWidth(); + get container() { + return this._filterButtonContainer; } - private getFilterWidth() { + get width() { return 2 /*margin left*/ + 2 /*border*/ + 2 /*padding*/ + 16 /* icon width */; } + applyStyles(filterChecked: boolean): void { + const toggleStyles = this._toggleStyles; + + this._filterButtonContainer.style.borderColor = (filterChecked && toggleStyles.inputActiveOptionBorder) || ''; + this._filterButtonContainer.style.color = (filterChecked && toggleStyles.inputActiveOptionForeground) || 'inherit'; + this._filterButtonContainer.style.backgroundColor = (filterChecked && toggleStyles.inputActiveOptionBackground) || ''; + } + private createFilters(container: HTMLElement): void { this._actionbar = this._register(new ActionBar(container, { actionViewItemProvider: action => { @@ -203,6 +209,29 @@ class NotebookFindInput extends FindInput { })); this._actionbar.push(this._filtersAction, { icon: true, label: false }); } +} + +export class NotebookFindInput extends FindInput { + private _findFilter: NotebookFindInputFilterButton; + private _filterChecked: boolean = false; + + constructor( + readonly filters: NotebookFindFilters, + contextKeyService: IContextKeyService, + readonly contextMenuService: IContextMenuService, + readonly instantiationService: IInstantiationService, + parent: HTMLElement | null, + contextViewProvider: IContextViewProvider, + options: IFindInputOptions, + ) { + super(parent, contextViewProvider, options); + + this._register(registerAndCreateHistoryNavigationContext(contextKeyService, this.inputBox)); + this._findFilter = this._register(new NotebookFindInputFilterButton(filters, contextMenuService, instantiationService, options)); + + this.inputBox.paddingRight = (this.caseSensitive?.width() ?? 0) + (this.wholeWords?.width() ?? 0) + (this.regex?.width() ?? 0) + this._findFilter.width; + this.controls.appendChild(this._findFilter.container); + } override setEnabled(enabled: boolean) { super.setEnabled(enabled); @@ -226,15 +255,7 @@ class NotebookFindInput extends FindInput { this.regex.domNode.classList.toggle('disabled', false); } } - this.applyStyles(); - } - - private applyStyles(): void { - const toggleStyles = this._toggleStyles; - - this._filterButtonContainer.style.borderColor = (this._filterChecked && toggleStyles.inputActiveOptionBorder) || ''; - this._filterButtonContainer.style.color = (this._filterChecked && toggleStyles.inputActiveOptionForeground) || 'inherit'; - this._filterButtonContainer.style.backgroundColor = (this._filterChecked && toggleStyles.inputActiveOptionBackground) || ''; + this._findFilter.applyStyles(this._filterChecked); } getCellToolbarActions(menu: IMenu): { primary: IAction[]; secondary: IAction[] } { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 7b81535766333..6a7b6cce56fdf 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -2423,7 +2423,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD return Promise.all(requests); } - async find(query: string, options: INotebookSearchOptions, token: CancellationToken, skipWarmup: boolean = false): Promise { + async find(query: string, options: INotebookSearchOptions, token: CancellationToken, skipWarmup: boolean = false, shouldGetSearchPreviewInfo = false): Promise { if (!this._notebookViewModel) { return []; } @@ -2459,7 +2459,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD await this._warmupAll(!!options.includeOutput); const end = Date.now(); this.logService.debug('Find', `Warmup time: ${end - start}ms`); - const webviewMatches = await this._webview.find(query, { caseSensitive: options.caseSensitive, wholeWord: options.wholeWord, includeMarkup: !!options.includeMarkupPreview, includeOutput: !!options.includeOutput }); + const webviewMatches = await this._webview.find(query, { caseSensitive: options.caseSensitive, wholeWord: options.wholeWord, includeMarkup: !!options.includeMarkupPreview, includeOutput: !!options.includeOutput, shouldGetSearchPreviewInfo }); // attach webview matches to model find matches webviewMatches.forEach(match => { if (!options.includeMarkupPreview && match.type === 'preview') { 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 efc86df776155..d1bf32e261213 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -1545,7 +1545,7 @@ export class BackLayerWebView extends Themable { }); } - async find(query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean }): Promise { + async find(query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean }): Promise { if (query === '') { return []; } 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 a1d281dcd85e2..405ab9162ec13 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -396,7 +396,7 @@ export interface ITokenizedStylesChangedMessage { export interface IFindMessage { readonly type: 'find'; readonly query: string; - readonly options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean }; + readonly options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean }; } 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 f8e4857eb6099..cbc9dfe09247f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -869,9 +869,18 @@ async function webviewPreloads(ctx: PreloadContext) { container: Node; originalRange: Range; isShadow: boolean; + searchPreviewInfo?: ISearchPreviewInfo; highlightResult?: IHighlightResult; } + interface ISearchPreviewInfo { + line: string; + range: { + start: number; + end: number; + }; + } + interface IHighlighter { highlightCurrentMatch(index: number): void; unHighlightCurrentMatch(index: number): void; @@ -1003,7 +1012,82 @@ async function webviewPreloads(ctx: PreloadContext) { } } - const find = (query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean }) => { + function extractSelectionLine(selection: Selection): ISearchPreviewInfo { + const range = selection.getRangeAt(0); + + // we need to keep a reference to the old selection range to re-apply later + const oldRange = range.cloneRange(); + const captureLength = selection.toString().length; + + // use selection API to modify selection to get entire line (the first line if multi-select) + + // collapse selection to start so that the cursor position is at beginning of match + selection.collapseToStart(); + + // extend selection in both directions to select the line + selection.modify('move', 'backward', 'lineboundary'); + selection.modify('extend', 'forward', 'lineboundary'); + + const line = selection.toString(); + + // using the original range and the new range, we can find the offset of the match from the line start. + const rangeStart = getStartOffset(selection.getRangeAt(0), oldRange); + + // line range for match + const lineRange = { + start: rangeStart, + end: rangeStart + captureLength, + }; + + // re-add the old range so that the selection is restored + selection.removeAllRanges(); + selection.addRange(oldRange); + + return { line, range: lineRange }; + } + + function getStartOffset(lineRange: Range, originalRange: Range) { + // sometimes, the old and new range are in different DOM elements (ie: when the match is inside of ) + // so we need to find the first common ancestor DOM element and find the positions of the old and new range relative to that. + const firstCommonAncestor = findFirstCommonAncestor(lineRange.startContainer, originalRange.startContainer); + + const selectionOffset = getSelectionOffsetRelativeTo(firstCommonAncestor, lineRange.startContainer) + lineRange.startOffset; + const textOffset = getSelectionOffsetRelativeTo(firstCommonAncestor, originalRange.startContainer) + originalRange.startOffset; + return textOffset - selectionOffset; + } + + // modified from https://stackoverflow.com/a/68583466/16253823 + function findFirstCommonAncestor(nodeA: Node, nodeB: Node) { + const range = new Range(); + range.setStart(nodeA, 0); + range.setEnd(nodeB, 0); + return range.commonAncestorContainer; + } + + // modified from https://stackoverflow.com/a/48812529/16253823 + function getSelectionOffsetRelativeTo(parentElement: Node, currentNode: Node | null): number { + if (!currentNode) { + return 0; + } + let offset = 0; + + if (currentNode === parentElement || !parentElement.contains(currentNode)) { + return offset; + } + + + // count the number of chars before the current dom elem and the start of the dom + let prevSibling = currentNode.previousSibling; + while (prevSibling) { + const nodeContent = prevSibling.nodeValue || ''; + offset += nodeContent.length; + prevSibling = prevSibling.previousSibling; + } + + return offset + getSelectionOffsetRelativeTo(parentElement, currentNode.parentNode); + } + + const find = (query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; shouldGetSearchPreviewInfo: boolean }) => { let find = true; const matches: IFindMatch[] = []; @@ -1048,7 +1132,8 @@ async function webviewPreloads(ctx: PreloadContext) { cellId: preview.id, container: preview, isShadow: true, - originalRange: shadowSelection.getRangeAt(0) + originalRange: shadowSelection.getRangeAt(0), + searchPreviewInfo: options.shouldGetSearchPreviewInfo ? extractSelectionLine(shadowSelection) : undefined, }); } } @@ -1068,7 +1153,8 @@ async function webviewPreloads(ctx: PreloadContext) { cellId: cellId, container: outputNode, isShadow: true, - originalRange: shadowSelection.getRangeAt(0) + originalRange: shadowSelection.getRangeAt(0), + searchPreviewInfo: options.shouldGetSearchPreviewInfo ? extractSelectionLine(shadowSelection) : undefined, }); } } @@ -1086,7 +1172,8 @@ async function webviewPreloads(ctx: PreloadContext) { cellId: lastEl.cellId, container: lastEl.container, isShadow: false, - originalRange: selection.getRangeAt(0) + originalRange: selection.getRangeAt(0), + searchPreviewInfo: options.shouldGetSearchPreviewInfo ? extractSelectionLine(selection) : undefined, }); } else { @@ -1106,7 +1193,8 @@ async function webviewPreloads(ctx: PreloadContext) { cellId: cellId, container: node, isShadow: false, - originalRange: selection.getRangeAt(0) + originalRange: selection.getRangeAt(0), + searchPreviewInfo: options.shouldGetSearchPreviewInfo ? extractSelectionLine(selection) : undefined, }); } break; @@ -1142,7 +1230,8 @@ async function webviewPreloads(ctx: PreloadContext) { type: match.type, id: match.id, cellId: match.cellId, - index + index, + searchPreviewInfo: match.searchPreviewInfo, })) }); }; diff --git a/src/vs/workbench/contrib/search/browser/media/searchview.css b/src/vs/workbench/contrib/search/browser/media/searchview.css index 2b87730ba8c1a..2c434917669a0 100644 --- a/src/vs/workbench/contrib/search/browser/media/searchview.css +++ b/src/vs/workbench/contrib/search/browser/media/searchview.css @@ -376,3 +376,22 @@ .monaco-workbench .search-view .monaco-list.element-focused .monaco-list-row.focused.selected:not(.highlighted) .action-label:focus { outline-color: var(--vscode-list-activeSelectionForeground) } + +.monaco-workbench .search-container .monaco-custom-toggle.disabled { + opacity: 0.3; + cursor: default; + user-select: none; + -webkit-user-select: none; + pointer-events: none; + background-color: inherit !important; +} + +.monaco-workbench .search-container .find-filter-button { + color: inherit; + margin-left: 2px; + float: left; + cursor: pointer; + box-sizing: border-box; + user-select: none; + -webkit-user-select: none; +} diff --git a/src/vs/workbench/contrib/search/browser/replaceService.ts b/src/vs/workbench/contrib/search/browser/replaceService.ts index c991021c2b9bc..1d1e46802eda3 100644 --- a/src/vs/workbench/contrib/search/browser/replaceService.ts +++ b/src/vs/workbench/contrib/search/browser/replaceService.ts @@ -6,7 +6,7 @@ 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 { Disposable, IReference } from 'vs/base/common/lifecycle'; 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'; @@ -27,7 +27,7 @@ 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 { CellUri, IResolvedNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; const REPLACE_PREVIEW = 'replacePreview'; @@ -116,10 +116,13 @@ export class ReplaceService implements IReplaceService { 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(); + let ref: IReference | undefined; + try { + ref = await this.notebookEditorModelResolverService.resolve(notebookResource); + await ref.object.save({ source: ReplaceService.REPLACE_SAVE_SOURCE }); + } finally { + ref?.dispose(); + } } return; } else { @@ -196,8 +199,11 @@ export class ReplaceService implements IReplaceService { if (arg instanceof Match) { if (arg instanceof NotebookMatch) { - const match = arg; - edits.push(this.createEdit(match, match.replaceString, match.cell.uri)); + if (!arg.isWebviewMatch()) { + // only apply edits if it's not a webview match, since webview matches are read-only + const match = arg; + edits.push(this.createEdit(match, match.replaceString, match.cell.uri)); + } } else { const match = arg; edits.push(this.createEdit(match, match.replaceString, resource)); @@ -212,13 +218,12 @@ 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, (match instanceof NotebookMatch) ? match.cell.uri : resource) + edits.push(...fileMatch.matches().flatMap( + match => this.createEdits(match, resource) )); } }); } - return edits; } diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 6b810e6420008..9ee1b0b89e901 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -353,7 +353,7 @@ configurationRegistry.registerConfiguration({ }, 'search.experimental.notebookSearch': { type: 'boolean', - description: nls.localize('search.experimental.notebookSearch', "Controls whether to use the experimental notebook search in the global search. Please refresh your search for changes to this setting to take effect."), + description: nls.localize('search.experimental.notebookSearch', "Controls whether to use the experimental notebook search in the global search. Please reload your VS Code instance for changes to this setting to take effect."), default: typeof product.quality !== 'string' ? product.quality !== 'stable' : false, // only enable as default in insiders }, } diff --git a/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts b/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts index 761884194839e..c9e36f7d55e11 100644 --- a/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts +++ b/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts @@ -156,20 +156,20 @@ registerAction2(class ReplaceAction extends Action2 { category, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.MatchFocusKey), + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.MatchFocusKey, Constants.IsEditableItemKey), primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, }, icon: searchReplaceIcon, menu: [ { id: MenuId.SearchContext, - when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.MatchFocusKey), + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.MatchFocusKey, Constants.IsEditableItemKey), group: 'search', order: 1 }, { id: MenuId.SearchActionMenu, - when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.MatchFocusKey), + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.MatchFocusKey, Constants.IsEditableItemKey), group: 'inline', order: 1 } @@ -195,7 +195,7 @@ registerAction2(class ReplaceAllAction extends Action2 { category, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FileFocusKey), + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FileFocusKey, Constants.IsEditableItemKey), primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter], }, @@ -203,13 +203,13 @@ registerAction2(class ReplaceAllAction extends Action2 { menu: [ { id: MenuId.SearchContext, - when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FileFocusKey), + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FileFocusKey, Constants.IsEditableItemKey), group: 'search', order: 1 }, { id: MenuId.SearchActionMenu, - when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FileFocusKey), + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FileFocusKey, Constants.IsEditableItemKey), group: 'inline', order: 1 } @@ -234,7 +234,7 @@ registerAction2(class ReplaceAllInFolderAction extends Action2 { category, keybinding: { weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FolderFocusKey), + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FolderFocusKey, Constants.IsEditableItemKey), primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter], }, @@ -242,13 +242,13 @@ registerAction2(class ReplaceAllInFolderAction extends Action2 { menu: [ { id: MenuId.SearchContext, - when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FolderFocusKey), + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FolderFocusKey, Constants.IsEditableItemKey), group: 'search', order: 1 }, { id: MenuId.SearchActionMenu, - when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FolderFocusKey), + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FolderFocusKey, Constants.IsEditableItemKey), group: 'inline', order: 1 } diff --git a/src/vs/workbench/contrib/search/browser/searchFindInput.ts b/src/vs/workbench/contrib/search/browser/searchFindInput.ts new file mode 100644 index 0000000000000..250018083bf72 --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchFindInput.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; +import { IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { ContextScopedFindInput } from 'vs/platform/history/browser/contextScopedHistoryWidget'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; +import { NotebookFindInputFilterButton } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget'; +import * as nls from 'vs/nls'; + +export class SearchFindInput extends ContextScopedFindInput { + private _findFilter: NotebookFindInputFilterButton; + private _filterChecked: boolean = false; + private _visible: boolean = false; + + constructor( + container: HTMLElement | null, + contextViewProvider: IContextViewProvider, + options: IFindInputOptions, + contextKeyService: IContextKeyService, + readonly contextMenuService: IContextMenuService, + readonly instantiationService: IInstantiationService, + readonly filters: NotebookFindFilters, + filterStartVisiblitity: boolean + ) { + super(container, contextViewProvider, options, contextKeyService); + this._findFilter = this._register( + new NotebookFindInputFilterButton( + filters, + contextMenuService, + instantiationService, + options, + nls.localize('searchFindInputNotebookFilter.label', "Notebook Find Filters") + )); + this.inputBox.paddingRight = (this.caseSensitive?.width() ?? 0) + (this.wholeWords?.width() ?? 0) + (this.regex?.width() ?? 0) + this._findFilter.width; + this.controls.appendChild(this._findFilter.container); + this._findFilter.container.classList.add('monaco-custom-toggle'); + + this.filterVisible = filterStartVisiblitity; + } + + set filterVisible(show: boolean) { + this._findFilter.container.style.display = show ? '' : 'none'; + this._visible = show; + this.updateStyles(); + } + + override setEnabled(enabled: boolean) { + super.setEnabled(enabled); + if (enabled && (!this._filterChecked || !this._visible)) { + this.regex?.enable(); + } else { + this.regex?.disable(); + } + } + + updateStyles() { + this._filterChecked = this.filters.markupPreview || this.filters.codeOutput; + if (this.regex) { + if (this._filterChecked && this._visible) { + this.regex.disable(); + this.regex.domNode.tabIndex = -1; + this.regex.domNode.classList.toggle('disabled', true); + } else { + this.regex.enable(); + this.regex.domNode.tabIndex = 0; + this.regex.domNode.classList.toggle('disabled', false); + } + } + this._findFilter.applyStyles(this._filterChecked); + } +} diff --git a/src/vs/workbench/contrib/search/browser/searchModel.ts b/src/vs/workbench/contrib/search/browser/searchModel.ts index 06e7a0e3d17a7..c5ed43d8fb682 100644 --- a/src/vs/workbench/contrib/search/browser/searchModel.ts +++ b/src/vs/workbench/contrib/search/browser/searchModel.ts @@ -310,6 +310,9 @@ export class FileMatch extends Disposable implements IFileMatch { return this._closestRoot; } + hasWebviewMatches(): boolean { + return this.matches().some(m => m instanceof NotebookMatch && m.isWebviewMatch()); + } private async createMatches(): Promise { const model = this.modelService.getModel(this._resource); const experimentalNotebooksEnabled = this.configurationService.getValue('search').experimental.notebookSearch; @@ -360,6 +363,9 @@ export class FileMatch extends Disposable implements IFileMatch { async bindNotebookEditorWidget(widget: NotebookEditorWidget) { if (this._notebookEditorWidget === widget) { + // ensure that the matches are up to date, but everything else should be configured already. + // TODO: try to reduce calls that occur when the notebook hasn't loaded yet + await this.updateMatchesForEditorWidget(); return; } @@ -418,11 +424,11 @@ export class FileMatch extends Disposable implements IFileMatch { wholeWord: this._query.isWordMatch, caseSensitive: this._query.isCaseSensitive, wordSeparators: wordSeparators ?? undefined, - includeMarkupInput: true, - includeMarkupPreview: false, - includeCodeInput: true, - includeOutput: false, - }, CancellationToken.None, true); + includeMarkupInput: this._query.notebookInfo?.isInNotebookMarkdownInput, + includeMarkupPreview: !this._query.notebookInfo?.isInNotebookMarkdownInput, + includeCodeInput: this._query.notebookInfo?.isInNotebookCellInput, + includeOutput: this._query.notebookInfo?.isInNotebookCellOutput, + }, CancellationToken.None, false, true); this.updateNotebookMatches(allMatches, true); } @@ -634,7 +640,7 @@ export class FileMatch extends Disposable implements IFileMatch { if (!this._notebookEditorWidget) { return; } - if (match.webviewIndex) { + if (match.webviewIndex !== undefined) { const index = this._notebookEditorWidget.getCellIndex(match.cell); if (index !== undefined) { this._notebookEditorWidget.revealCellOffsetInCenterAsync(match.cell, outputOffset ?? 0); @@ -795,7 +801,7 @@ export class FolderMatch extends Disposable { replace(match: FileMatch): Promise { return this.replaceService.replace([match]).then(() => { - this.doRemoveFile([match]); + this.doRemoveFile([match], true, true, true); }); } @@ -947,7 +953,7 @@ export class FolderMatch extends Disposable { const allMatches = getFileMatches(matches); await this.replaceService.replace(allMatches); - this.doRemoveFile(allMatches, true, true); + this.doRemoveFile(allMatches, true, true, true); } public onFileChange(fileMatch: FileMatch, removed = false): void { @@ -978,11 +984,14 @@ export class FolderMatch extends Disposable { this._onChange.fire(event); } - private doRemoveFile(fileMatches: FileMatch[], dispose: boolean = true, trigger: boolean = true): void { + private doRemoveFile(fileMatches: FileMatch[], dispose: boolean = true, trigger: boolean = true, keepReadonly = false): void { const removed = []; for (const match of fileMatches as FileMatch[]) { if (this._fileMatches.get(match.resource)) { + if (keepReadonly && match.hasWebviewMatches()) { + continue; + } this._fileMatches.delete(match.resource); if (dispose) { match.dispose(); @@ -1414,6 +1423,12 @@ export class SearchResult extends Disposable { } private onDidAddNotebookEditorWidget(widget: NotebookEditorWidget): void { + const experimentalNotebooksEnabled = this.configurationService.getValue('search').experimental.notebookSearch; + + if (!experimentalNotebooksEnabled) { + return; + } + this._onWillChangeModelListener?.dispose(); this._onWillChangeModelListener = widget.onWillChangeModel( (model) => { @@ -1731,11 +1746,11 @@ export class SearchModel extends Disposable { regex: query.contentPattern.isRegExp, wholeWord: query.contentPattern.isWordMatch, caseSensitive: query.contentPattern.isCaseSensitive, - includeMarkupInput: true, - includeMarkupPreview: false, - includeCodeInput: true, - includeOutput: false, - }, token); + includeMarkupInput: query.contentPattern.notebookInfo?.isInNotebookMarkdownInput, + includeMarkupPreview: !query.contentPattern.notebookInfo?.isInNotebookMarkdownInput, + includeCodeInput: query.contentPattern.notebookInfo?.isInNotebookCellInput, + includeOutput: query.contentPattern.notebookInfo?.isInNotebookCellOutput, + }, token, false, true); if (matches.length) { diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index 11b30513aad88..910ed817280af 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/browser/searchModel'; +import { FileMatch, Match, RenderableMatch, SearchModel, FolderMatch, FolderMatchNoRoot, FolderMatchWorkspaceRoot, NotebookMatch } 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'; @@ -28,7 +28,7 @@ import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/br import { ISearchActionContext } from 'vs/workbench/contrib/search/browser/searchActionsRemoveReplace'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { FileFocusKey, FolderFocusKey, MatchFocusKey } from 'vs/workbench/contrib/search/common/constants'; +import { IsEditableItemKey, FileFocusKey, FolderFocusKey, MatchFocusKey } from 'vs/workbench/contrib/search/common/constants'; import { defaultCountBadgeStyles } from 'vs/platform/theme/browser/defaultStyles'; interface IFolderMatchTemplate { @@ -56,6 +56,7 @@ interface IMatchTemplate { lineNumber: HTMLElement; actions: MenuWorkbenchToolBar; disposables: DisposableStore; + contextKeyService: IContextKeyService; } export class SearchDelegate implements IListVirtualDelegate { @@ -289,8 +290,12 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender const disposables = new DisposableStore(); - const contextKeyService = this.contextKeyService.createOverlay([[MatchFocusKey.key, true], [FileFocusKey.key, false], [FolderFocusKey.key, false]]); - const instantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])); + const contextKeyServiceMain = disposables.add(this.contextKeyService.createScoped(container)); + MatchFocusKey.bindTo(contextKeyServiceMain).set(true); + FileFocusKey.bindTo(contextKeyServiceMain).set(false); + FolderFocusKey.bindTo(contextKeyServiceMain).set(false); + + const instantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyServiceMain])); const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, { menuOptions: { shouldForwardArgs: true @@ -310,13 +315,14 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender lineNumber, actions, disposables, + contextKeyService: contextKeyServiceMain }; } renderElement(node: ITreeNode, index: number, templateData: IMatchTemplate): void { const match = node.element; const preview = match.preview(); - const replace = this.searchModel.isReplaceActive() && !!this.searchModel.replaceString; + const replace = this.searchModel.isReplaceActive() && !!this.searchModel.replaceString && !(match instanceof NotebookMatch && match.isWebviewMatch()); templateData.before.textContent = preview.before; templateData.match.textContent = preview.inside; @@ -325,6 +331,8 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender templateData.after.textContent = preview.after; templateData.parent.title = (preview.before + (replace ? match.replaceString : preview.inside) + preview.after).trim().substr(0, 999); + IsEditableItemKey.bindTo(templateData.contextKeyService).set(!(match instanceof NotebookMatch && match.isWebviewMatch())); + const numLines = match.range().endLineNumber - match.range().startLineNumber; const extraLinesStr = numLines > 0 ? `+${numLines}` : ''; diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index aac35eb8b2524..78f76daf4a94a 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -114,6 +114,7 @@ export class SearchView extends ViewPane { private folderMatchFocused: IContextKey; private folderMatchWithResourceFocused: IContextKey; private matchFocused: IContextKey; + private isEditableItem: IContextKey; private hasSearchResultsKey: IContextKey; private lastFocusState: 'input' | 'tree' = 'input'; @@ -203,6 +204,7 @@ export class SearchView extends ViewPane { this.fileMatchFocused = Constants.FileFocusKey.bindTo(this.contextKeyService); this.folderMatchFocused = Constants.FolderFocusKey.bindTo(this.contextKeyService); this.folderMatchWithResourceFocused = Constants.ResourceFolderFocusKey.bindTo(this.contextKeyService); + this.isEditableItem = Constants.IsEditableItemKey.bindTo(this.contextKeyService); this.hasSearchResultsKey = Constants.HasSearchResults.bindTo(this.contextKeyService); this.matchFocused = Constants.MatchFocusKey.bindTo(this.contextKeyService); this.searchStateKey = SearchStateKey.bindTo(this.contextKeyService); @@ -461,6 +463,11 @@ export class SearchView extends ViewPane { const showReplace = typeof this.viewletState['view.showReplace'] === 'boolean' ? this.viewletState['view.showReplace'] : true; const preserveCase = this.viewletState['query.preserveCase'] === true; + const isInNotebookMarkdownInput = this.viewletState['query.isInNotebookMarkdownInput']; + const isInNotebookCellInput = this.viewletState['query.isInNotebookCellInput']; + const isInNotebookCellOutput = this.viewletState['query.isInNotebookCellOutput']; + + this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, container, { value: contentPattern, replaceValue: replaceText, @@ -471,7 +478,12 @@ export class SearchView extends ViewPane { replaceHistory: replaceHistory, preserveCase: preserveCase, inputBoxStyles: defaultInputBoxStyles, - toggleStyles: defaultToggleStyles + toggleStyles: defaultToggleStyles, + notebookOptions: { + isInNotebookMarkdownInput, + isInNotebookCellInput, + isInNotebookCellOutput, + } })); if (showReplace) { @@ -481,6 +493,7 @@ export class SearchView extends ViewPane { this._register(this.searchWidget.onSearchSubmit(options => this.triggerQueryChange(options))); this._register(this.searchWidget.onSearchCancel(({ focus }) => this.cancelSearch(focus))); this._register(this.searchWidget.searchInput.onDidOptionChange(() => this.triggerQueryChange())); + this._register(this.searchWidget.getFilters().onDidChange(() => this.triggerQueryChange())); const updateHasPatternKey = () => this.hasSearchPatternKey.set(this.searchWidget.searchInput.getValue().length > 0); updateHasPatternKey(); @@ -813,18 +826,20 @@ export class SearchView extends ViewPane { })); this._register(Event.any(this.tree.onDidFocus, this.tree.onDidChangeFocus)(() => { - if (this.tree.isDOMFocused()) { - const focus = this.tree.getFocus()[0]; - this.firstMatchFocused.set(this.tree.navigate().first() === focus); - this.fileMatchOrMatchFocused.set(!!focus); - this.fileMatchFocused.set(focus instanceof FileMatch); - this.folderMatchFocused.set(focus instanceof FolderMatch); - this.matchFocused.set(focus instanceof Match); - this.fileMatchOrFolderMatchFocus.set(focus instanceof FileMatch || focus instanceof FolderMatch); - this.fileMatchOrFolderMatchWithResourceFocus.set(focus instanceof FileMatch || focus instanceof FolderMatchWithResource); - this.folderMatchWithResourceFocused.set(focus instanceof FolderMatchWithResource); - this.lastFocusState = 'tree'; - } + const focus = this.tree.getFocus()[0]; + this.firstMatchFocused.set(this.tree.navigate().first() === focus); + this.fileMatchOrMatchFocused.set(!!focus); + this.fileMatchFocused.set(focus instanceof FileMatch); + this.folderMatchFocused.set(focus instanceof FolderMatch); + + // we don't need to check experimental flag here because NotebookMatches only exist when the flag is enabled + const editable = (!(focus instanceof NotebookMatch)) || !focus.isWebviewMatch(); + this.isEditableItem.set(editable); + this.matchFocused.set(focus instanceof Match); + this.fileMatchOrFolderMatchFocus.set(focus instanceof FileMatch || focus instanceof FolderMatch); + this.fileMatchOrFolderMatchWithResourceFocus.set(focus instanceof FileMatch || focus instanceof FolderMatchWithResource); + this.folderMatchWithResourceFocused.set(focus instanceof FolderMatchWithResource); + this.lastFocusState = 'tree'; })); this._register(this.tree.onDidBlur(() => { @@ -836,6 +851,7 @@ export class SearchView extends ViewPane { this.fileMatchOrFolderMatchFocus.reset(); this.fileMatchOrFolderMatchWithResourceFocus.reset(); this.folderMatchWithResourceFocused.reset(); + this.isEditableItem.reset(); })); } @@ -1379,6 +1395,10 @@ export class SearchView extends ViewPane { } const isRegex = this.searchWidget.searchInput.getRegex(); + const isInNotebookMarkdownInput = this.searchWidget.getFilters().markupInput; + const isInNotebookCellInput = this.searchWidget.getFilters().codeInput; + const isInNotebookCellOutput = this.searchWidget.getFilters().codeOutput; + const isWholeWords = this.searchWidget.searchInput.getWholeWords(); const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive(); const contentPattern = this.searchWidget.searchInput.getValue(); @@ -1397,7 +1417,12 @@ export class SearchView extends ViewPane { pattern: contentPattern, isRegExp: isRegex, isCaseSensitive: isCaseSensitive, - isWordMatch: isWholeWords + isWordMatch: isWholeWords, + notebookInfo: { + isInNotebookMarkdownInput, + isInNotebookCellInput, + isInNotebookCellOutput + } }; const excludePattern = this.inputPatternExcludes.getValue(); @@ -1954,6 +1979,7 @@ export class SearchView extends ViewPane { } public override saveState(): void { + const patternExcludes = this.inputPatternExcludes?.getValue().trim() ?? ''; const patternIncludes = this.inputPatternIncludes?.getValue().trim() ?? ''; const onlyOpenEditors = this.inputPatternIncludes?.onlySearchInOpenEditors() ?? false; @@ -1966,10 +1992,18 @@ export class SearchView extends ViewPane { const isCaseSensitive = this.searchWidget.searchInput.getCaseSensitive(); const contentPattern = this.searchWidget.searchInput.getValue(); + const isInNotebookCellInput = this.searchWidget.getFilters().codeInput; + const isInNotebookCellOutput = this.searchWidget.getFilters().codeOutput; + const isInNotebookMarkdownInput = this.searchWidget.getFilters().markupInput; + this.viewletState['query.contentPattern'] = contentPattern; this.viewletState['query.regex'] = isRegex; this.viewletState['query.wholeWords'] = isWholeWords; this.viewletState['query.caseSensitive'] = isCaseSensitive; + + this.viewletState['query.isInNotebookMarkdownInput'] = isInNotebookMarkdownInput; + this.viewletState['query.isInNotebookCellInput'] = isInNotebookCellInput; + this.viewletState['query.isInNotebookCellOutput'] = isInNotebookCellOutput; } this.viewletState['query.folderExclusions'] = patternExcludes; diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index 6732fde2c4a11..833f081f721a9 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -20,7 +20,7 @@ import * as nls from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; @@ -36,6 +36,12 @@ import { searchReplaceAllIcon, searchHideReplaceIcon, searchShowContextIcon, sea import { ToggleSearchEditorContextLinesCommandId } from 'vs/workbench/contrib/searchEditor/browser/constants'; import { showHistoryKeybindingHint } from 'vs/platform/history/browser/historyWidgetKeybindingHint'; import { defaultInputBoxStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; +import { GroupModelChangeKind } from 'vs/workbench/common/editor'; +import { SearchFindInput } from 'vs/workbench/contrib/search/browser/searchFindInput'; /** Specified in searchview.css */ const SingleLineInputHeight = 26; @@ -53,6 +59,11 @@ export interface ISearchWidgetOptions { showContextToggle?: boolean; inputBoxStyles: IInputBoxStyles; toggleStyles: IToggleStyles; + notebookOptions?: { + isInNotebookMarkdownInput: boolean; + isInNotebookCellInput: boolean; + isInNotebookCellOutput: boolean; + }; } class ReplaceAllAction extends Action { @@ -93,6 +104,7 @@ function stopPropagationForMultiLineDownwards(event: IKeyboardEvent, value: stri } } + export class SearchWidget extends Widget { private static readonly INPUT_MAX_HEIGHT = 134; @@ -153,6 +165,8 @@ export class SearchWidget extends Widget { private showContextToggle!: Toggle; public contextLinesInput!: InputBox; + private _notebookFilters: NotebookFindFilters; + constructor( container: HTMLElement, options: ISearchWidgetOptions, @@ -161,13 +175,38 @@ export class SearchWidget extends Widget { @IKeybindingService private readonly keybindingService: IKeybindingService, @IClipboardService private readonly clipboardServce: IClipboardService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IEditorService private readonly editorService: IEditorService, ) { super(); this.replaceActive = Constants.ReplaceActiveKey.bindTo(this.contextKeyService); this.searchInputBoxFocused = Constants.SearchInputBoxFocusedKey.bindTo(this.contextKeyService); this.replaceInputBoxFocused = Constants.ReplaceInputBoxFocusedKey.bindTo(this.contextKeyService); + this._notebookFilters = this._register( + new NotebookFindFilters( + options.notebookOptions?.isInNotebookMarkdownInput ?? true, + !options.notebookOptions?.isInNotebookMarkdownInput ?? false, + options.notebookOptions?.isInNotebookCellInput ?? true, + options.notebookOptions?.isInNotebookCellOutput ?? false + )); + + this._register( + this._notebookFilters.onDidChange(() => { + if (this.searchInput instanceof SearchFindInput) { + this.searchInput.updateStyles(); + } + })); + this._register(this.editorService.onDidEditorsChange((e) => { + if (this.searchInput instanceof SearchFindInput && + e.event.editor instanceof NotebookEditorInput && + (e.event.kind === GroupModelChangeKind.EDITOR_OPEN || e.event.kind === GroupModelChangeKind.EDITOR_CLOSE)) { + this.searchInput.filterVisible = this._hasNotebookOpen(); + } + })); + this._replaceHistoryDelayer = new Delayer(500); this.render(container, options); @@ -177,10 +216,20 @@ export class SearchWidget extends Widget { this.updateAccessibilitySupport(); } }); + this.accessibilityService.onDidChangeScreenReaderOptimized(() => this.updateAccessibilitySupport()); this.updateAccessibilitySupport(); } + private _hasNotebookOpen(): boolean { + const editors = this.editorService.editors; + return editors.some(editor => editor instanceof NotebookEditorInput); + } + + getFilters() { + return this._notebookFilters; + } + focus(select: boolean = true, focusReplace: boolean = false, suppressGlobalSearchBuffer = false): void { this.ignoreGlobalFindBufferOnNextFocus = suppressGlobalSearchBuffer; @@ -326,7 +375,13 @@ export class SearchWidget extends Widget { }; const searchInputContainer = dom.append(parent, dom.$('.search-container.input-box')); - this.searchInput = this._register(new ContextScopedFindInput(searchInputContainer, this.contextViewService, inputOptions, this.contextKeyService)); + + const experimentalNotebooksEnabled = this.configurationService.getValue('search').experimental.notebookSearch; + if (experimentalNotebooksEnabled) { + this.searchInput = this._register(new SearchFindInput(searchInputContainer, this.contextViewService, inputOptions, this.contextKeyService, this.contextMenuService, this.instantiationService, this._notebookFilters, this._hasNotebookOpen())); + } else { + this.searchInput = this._register(new ContextScopedFindInput(searchInputContainer, this.contextViewService, inputOptions, this.contextKeyService)); + } this.searchInput.onKeyDown((keyboardEvent: IKeyboardEvent) => this.onSearchInputKeyDown(keyboardEvent)); this.searchInput.setValue(options.value || ''); this.searchInput.setRegex(!!options.isRegex); diff --git a/src/vs/workbench/contrib/search/common/constants.ts b/src/vs/workbench/contrib/search/common/constants.ts index f0700ce6a992f..2b81f549838b3 100644 --- a/src/vs/workbench/contrib/search/common/constants.ts +++ b/src/vs/workbench/contrib/search/common/constants.ts @@ -64,6 +64,7 @@ export const FileMatchOrFolderMatchWithResourceFocusKey = new RawContextKey('fileMatchFocus', false); export const FolderFocusKey = new RawContextKey('folderMatchFocus', false); export const ResourceFolderFocusKey = new RawContextKey('folderMatchWithResourceFocus', false); +export const IsEditableItemKey = new RawContextKey('isEditableItem', true); export const MatchFocusKey = new RawContextKey('matchFocus', false); export const ViewHasSearchPatternKey = new RawContextKey('viewHasSearchPattern', false); export const ViewHasReplacePatternKey = new RawContextKey('viewHasReplacePattern', false); diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index ad3553fba7cb8..2a33627a41e2e 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -145,6 +145,13 @@ export interface IPatternInfo { isMultiline?: boolean; isUnicode?: boolean; isCaseSensitive?: boolean; + notebookInfo?: INotebookPatternInfo; +} + +export interface INotebookPatternInfo { + isInNotebookMarkdownInput?: boolean; + isInNotebookCellInput?: boolean; + isInNotebookCellOutput?: boolean; } export interface IExtendedExtensionSearchOptions {