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 be1e1c5c07d..3d63457554d 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/findModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/common/async'; -import { INotebookEditor, CellFindMatch, CellEditState, CellFindMatchWithIndex, OutputFindMatch, ICellModelDecorations, ICellModelDeltaDecorations, INotebookDeltaDecoration } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, CellFindMatch, CellEditState, CellFindMatchWithIndex, OutputFindMatch, ICellModelDecorations, ICellModelDeltaDecorations, INotebookDeltaDecoration, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { Range } from 'vs/editor/common/core/range'; import { FindDecorations } from 'vs/editor/contrib/find/browser/findDecorations'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; @@ -88,6 +88,34 @@ export class FindModel extends Disposable { }; } + refreshCurrentMatch(focus: { cell: ICellViewModel; range: Range }) { + const findMatchIndex = this.findMatches.findIndex(match => match.cell === focus.cell); + + if (findMatchIndex === -1) { + return; + } + + const findMatch = this.findMatches[findMatchIndex]; + const index = findMatch.matches.slice(0, findMatch.modelMatchCount).findIndex(match => (match as FindMatch).range.intersectRanges(focus.range) !== null); + + if (index === undefined) { + return; + } + + const matchesBefore = findMatchIndex === 0 ? 0 : (this._findMatchesStarts?.getPrefixSum(findMatchIndex - 1) ?? 0); + this._currentMatch = matchesBefore + index; + + this.highlightCurrentFindMatchDecoration(findMatchIndex, index).then(offset => { + this.revealCellRange(findMatchIndex, index, offset); + + this._state.changeMatchInfo( + this._currentMatch, + this._findMatches.reduce((p, c) => p + c.matches.length, 0), + undefined + ); + }); + } + find(option: { previous: boolean } | { index: number }) { if (!this.findMatches.length) { return; @@ -189,20 +217,28 @@ export class FindModel extends Disposable { return; } + const findFirstMatchAfterCellIndex = (cellIndex: number) => { + const matchAfterSelection = findFirstInSorted(findMatches.map(match => match.index), index => index >= cellIndex); + this._updateCurrentMatch(findMatches, this._matchesCountBeforeIndex(findMatches, matchAfterSelection)); + }; + if (this._currentMatch === -1) { // no active current match - this.set(findMatches, false); - return; + if (this._notebookEditor.getLength() === 0) { + this.set(findMatches, false); + return; + } else { + const focus = this._notebookEditor.getFocus().start; + findFirstMatchAfterCellIndex(focus); + this.set(findMatches, false); + return; + } } const oldCurrIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); const oldCurrCell = this._findMatches[oldCurrIndex.index].cell; const oldCurrMatchCellIndex = this._notebookEditor.getCellIndex(oldCurrCell); - const findFirstMatchAfterCellIndex = (cellIndex: number) => { - const matchAfterSelection = findFirstInSorted(findMatches.map(match => match.index), index => index >= cellIndex); - this._updateCurrentMatch(findMatches, this._matchesCountBeforeIndex(findMatches, matchAfterSelection)); - }; if (oldCurrMatchCellIndex < 0) { // the cell containing the active match is deleted 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 50fe4315bf0..e8f1ecef273 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFind.ts @@ -18,7 +18,7 @@ import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { NotebookFindWidget } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; +import { IShowNotebookFindWidgetOptions, NotebookFindWidget } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; import { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -92,21 +92,27 @@ function notebookContainsTextModel(uri: URI, textModel: ITextModel) { return false; } -function getSearchString(editor: ICodeEditor, opts: IFindStartOptions) { +function getSearchStringOptions(editor: ICodeEditor, opts: IFindStartOptions) { // Get the search string result, following the same logic in _start function in 'vs/editor/contrib/find/browser/findController' - let searchString = ''; if (opts.seedSearchStringFromSelection === 'single') { const selectionSearchString = getSelectionSearchString(editor, opts.seedSearchStringFromSelection, opts.seedSearchStringFromNonEmptySelection); if (selectionSearchString) { - searchString = selectionSearchString; + return { + searchString: selectionSearchString, + selection: editor.getSelection() + }; } } else if (opts.seedSearchStringFromSelection === 'multiple' && !opts.updateSearchScope) { const selectionSearchString = getSelectionSearchString(editor, opts.seedSearchStringFromSelection); if (selectionSearchString) { - searchString = selectionSearchString; + return { + searchString: selectionSearchString, + selection: editor.getSelection() + }; } } - return searchString; + + return undefined; } @@ -118,6 +124,10 @@ StartFindAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: return false; } + if (!codeEditor.hasModel()) { + return false; + } + if (!editor.hasEditorFocus() && !editor.hasWebviewFocus()) { const codeEditorService = accessor.get(ICodeEditorService); // check if the active pane contains the active text editor @@ -131,7 +141,7 @@ StartFindAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: const controller = editor.getContribution(NotebookFindWidget.id); - const searchString = getSearchString(codeEditor, { + const searchStringOptions = getSearchStringOptions(codeEditor, { forceRevealReplace: false, seedSearchStringFromSelection: codeEditor.getOption(EditorOption.find).seedSearchStringFromSelection !== 'never' ? 'single' : 'none', seedSearchStringFromNonEmptySelection: codeEditor.getOption(EditorOption.find).seedSearchStringFromSelection === 'selection', @@ -142,7 +152,19 @@ StartFindAction.addImplementation(100, (accessor: ServicesAccessor, codeEditor: loop: codeEditor.getOption(EditorOption.find).loop }); - controller.show(searchString); + let options: IShowNotebookFindWidgetOptions | undefined = undefined; + const uri = codeEditor.getModel().uri; + const data = CellUri.parse(uri); + if (searchStringOptions?.selection && data) { + const cell = editor.getCellByHandle(data.handle); + if (cell) { + options = { + searchStringSeededFrom: { cell, range: searchStringOptions.selection }, + }; + } + } + + controller.show(searchStringOptions?.searchString, options); return true; }); @@ -154,9 +176,13 @@ StartFindReplaceAction.addImplementation(100, (accessor: ServicesAccessor, codeE return false; } + if (!codeEditor.hasModel()) { + return false; + } + const controller = editor.getContribution(NotebookFindWidget.id); - const searchString = getSearchString(codeEditor, { + const searchStringOptions = getSearchStringOptions(codeEditor, { forceRevealReplace: false, seedSearchStringFromSelection: codeEditor.getOption(EditorOption.find).seedSearchStringFromSelection !== 'never' ? 'single' : 'none', seedSearchStringFromNonEmptySelection: codeEditor.getOption(EditorOption.find).seedSearchStringFromSelection === 'selection', @@ -168,7 +194,7 @@ StartFindReplaceAction.addImplementation(100, (accessor: ServicesAccessor, codeE }); if (controller) { - controller.replace(searchString); + controller.replace(searchStringOptions?.searchString); return true; } 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 9c38190c342..df3bc2f48ba 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts @@ -23,7 +23,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; import { FindModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; import { SimpleFindReplaceWidget } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget'; -import { CellEditState, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +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'; const FIND_HIDE_TRANSITION = 'find-hide-transition'; @@ -37,6 +37,7 @@ export interface IShowNotebookFindWidgetOptions { matchCase?: boolean; matchIndex?: number; focus?: boolean; + searchStringSeededFrom?: { cell: ICellViewModel; range: Range }; } export class NotebookFindWidget extends SimpleFindReplaceWidget implements INotebookEditorContribution { @@ -214,6 +215,7 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote protected onFindInputFocusTrackerBlur(): void { } override async show(initialInput?: string, options?: IShowNotebookFindWidgetOptions): Promise { + const searchStringUpdate = this._state.searchString !== initialInput; super.show(initialInput, options); this._state.change({ searchString: initialInput ?? '', isRevealed: true }, false); @@ -226,6 +228,10 @@ export class NotebookFindWidget extends SimpleFindReplaceWidget implements INote this._findInput.select(); } + if (!searchStringUpdate && options?.searchStringSeededFrom) { + this._findModel.refreshCurrentMatch(options.searchStringSeededFrom); + } + if (this._showTimeout === null) { if (this._hideTimeout !== null) { window.clearTimeout(this._hideTimeout); 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 6b1282d9afb..50aeeb9f290 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 @@ -67,13 +67,13 @@ suite('Notebook Find', () => { state.change({ searchString: '1' }, true); await found; assert.strictEqual(model.findMatches.length, 2); - assert.strictEqual(model.currentMatch, -1); - model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); model.find({ previous: false }); assert.strictEqual(model.currentMatch, 1); model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); + model.find({ previous: false }); + assert.strictEqual(model.currentMatch, 1); assert.strictEqual(editor.textModel.length, 3); @@ -88,7 +88,7 @@ suite('Notebook Find', () => { await found2; assert.strictEqual(editor.textModel.length, 4); assert.strictEqual(model.findMatches.length, 3); - assert.strictEqual(model.currentMatch, 0); + assert.strictEqual(model.currentMatch, 1); }); }); @@ -114,13 +114,13 @@ suite('Notebook Find', () => { await found; // find matches is not necessarily find results assert.strictEqual(model.findMatches.length, 4); - assert.strictEqual(model.currentMatch, -1); - model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); model.find({ previous: false }); assert.strictEqual(model.currentMatch, 1); model.find({ previous: false }); assert.strictEqual(model.currentMatch, 2); + model.find({ previous: false }); + assert.strictEqual(model.currentMatch, 3); const found2 = new Promise(resolve => state.onFindReplaceStateChange(e => { if (e.matchesCount) { resolve(true); } @@ -131,15 +131,15 @@ suite('Notebook Find', () => { await found2; assert.strictEqual(model.findMatches.length, 3); - assert.strictEqual(model.currentMatch, 2); + assert.strictEqual(model.currentMatch, 0); model.find({ previous: true }); - assert.strictEqual(model.currentMatch, 1); - model.find({ previous: false }); - assert.strictEqual(model.currentMatch, 2); - model.find({ previous: false }); assert.strictEqual(model.currentMatch, 3); model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); + model.find({ previous: false }); + assert.strictEqual(model.currentMatch, 1); + model.find({ previous: false }); + assert.strictEqual(model.currentMatch, 2); }); }); @@ -165,7 +165,7 @@ suite('Notebook Find', () => { await found; // find matches is not necessarily find results assert.strictEqual(model.findMatches.length, 4); - assert.strictEqual(model.currentMatch, -1); + assert.strictEqual(model.currentMatch, 0); model.find({ previous: true }); assert.strictEqual(model.currentMatch, 4); @@ -207,11 +207,11 @@ suite('Notebook Find', () => { await found; // find matches is not necessarily find results assert.strictEqual(model.findMatches.length, 4); - assert.strictEqual(model.currentMatch, -1); + assert.strictEqual(model.currentMatch, 0); model.find({ previous: false }); model.find({ previous: false }); model.find({ previous: false }); - assert.strictEqual(model.currentMatch, 2); + assert.strictEqual(model.currentMatch, 3); const found2 = new Promise(resolve => state.onFindReplaceStateChange(e => { if (e.matchesCount) { resolve(true); } })); @@ -243,13 +243,13 @@ suite('Notebook Find', () => { state.change({ searchString: '1' }, true); await found; assert.strictEqual(model.findMatches.length, 2); - assert.strictEqual(model.currentMatch, -1); - model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); model.find({ previous: false }); assert.strictEqual(model.currentMatch, 1); model.find({ previous: false }); assert.strictEqual(model.currentMatch, 0); + model.find({ previous: false }); + assert.strictEqual(model.currentMatch, 1); assert.strictEqual(editor.textModel.length, 3);