diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.ts index e4474ec47b..b58d8d8fee 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.ts @@ -25,8 +25,7 @@ import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/no import { ISanitizer, defaultSanitizer } from 'sql/workbench/services/notebook/browser/outputs/sanitizer'; import { CellToggleMoreActions } from 'sql/workbench/contrib/notebook/browser/cellToggleMoreActions'; import { CodeComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/code.component'; -import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { NotebookRange } from 'sql/workbench/services/notebook/browser/notebookService'; +import { NotebookRange, ICellEditorProvider } from 'sql/workbench/services/notebook/browser/notebookService'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; export const TEXT_SELECTOR: string = 'text-cell-component'; @@ -106,6 +105,14 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { })); } + public get cellEditors(): ICellEditorProvider[] { + let editors: ICellEditorProvider[] = []; + if (this.markdowncodeCell) { + editors.push(...this.markdowncodeCell.toArray()); + } + return editors; + } + //Gets sanitizer from ISanitizer interface private get sanitizer(): ISanitizer { if (this._sanitizer) { @@ -121,15 +128,6 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { get activeCellId(): string { return this._activeCellId; } - /** - * Returns the code editor of makrdown cell in edit mode. - */ - getEditor(): BaseTextEditor | undefined { - if (this.markdowncodeCell.length > 0) { - return this.markdowncodeCell.first.getEditor(); - } - return undefined; - } private setLoading(isLoading: boolean): void { this.cellModel.loaded = !isLoading; @@ -231,6 +229,7 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { public toggleEditMode(editMode?: boolean): void { this.isEditMode = editMode !== undefined ? editMode : !this.isEditMode; + this.cellModel.isEditMode = this.isEditMode; this.updateMoreActions(); this.updatePreview(); this._changeRef.detectChanges(); diff --git a/src/sql/workbench/contrib/notebook/browser/models/notebookFindModel.ts b/src/sql/workbench/contrib/notebook/browser/models/notebookFindModel.ts index f120241c0e..8748becadf 100644 --- a/src/sql/workbench/contrib/notebook/browser/models/notebookFindModel.ts +++ b/src/sql/workbench/contrib/notebook/browser/models/notebookFindModel.ts @@ -48,4 +48,6 @@ export interface INotebookFindModel { findExpression: string; /** Emit event when the find count changes */ onFindCountChange: Event; + /** Get the find index when range is given*/ + getIndexByRange(range: NotebookRange): number; } diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts index 15b67828c5..f76429eb05 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts @@ -71,7 +71,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe @ViewChild('bookNav', { read: ElementRef }) private bookNav: ElementRef; @ViewChildren(CodeCellComponent) private codeCells: QueryList; - @ViewChildren(TextCellComponent) private textCells: QueryList; + @ViewChildren(TextCellComponent) private textCells: QueryList; private _model: NotebookModel; protected _actionBar: Taskbar; @@ -150,6 +150,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe this.codeCells.toArray().forEach(cell => editors.push(...cell.cellEditors)); } if (this.textCells) { + this.textCells.toArray().forEach(cell => editors.push(...cell.cellEditors)); editors.push(...this.textCells.toArray()); } return editors; @@ -157,12 +158,12 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe public deltaDecorations(newDecorationRange: NotebookRange, oldDecorationRange: NotebookRange): void { if (newDecorationRange && newDecorationRange.cell && newDecorationRange.cell.cellType === 'markdown') { - let cell = this.cellEditors.filter(c => c.cellGuid() === newDecorationRange.cell.cellGuid)[0]; - cell.deltaDecorations(newDecorationRange, undefined); + let cell = this.cellEditors.filter(c => c.cellGuid() === newDecorationRange.cell.cellGuid); + cell[cell.length - 1].deltaDecorations(newDecorationRange, undefined); } if (oldDecorationRange && oldDecorationRange.cell && oldDecorationRange.cell.cellType === 'markdown') { - let cell = this.cellEditors.filter(c => c.cellGuid() === oldDecorationRange.cell.cellGuid)[0]; - cell.deltaDecorations(undefined, oldDecorationRange); + let cell = this.cellEditors.filter(c => c.cellGuid() === oldDecorationRange.cell.cellGuid); + cell[cell.length - 1].deltaDecorations(undefined, oldDecorationRange); } } diff --git a/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts b/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts index fdff70be4e..b75bf77ba3 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts @@ -94,7 +94,9 @@ export class NotebookEditor extends BaseEditor implements IFindNotebookControlle let editorImpl = this._notebookService.findNotebookEditor(this.notebookInput.notebookUri); if (editorImpl) { let cellEditorProvider = editorImpl.cellEditors.filter(c => c.cellGuid() === cellGuid)[0]; - return cellEditorProvider ? cellEditorProvider.getEditor() : undefined; + if (cellEditorProvider) { + return cellEditorProvider.getEditor(); + } } return undefined; } @@ -269,6 +271,7 @@ export class NotebookEditor extends BaseEditor implements IFindNotebookControlle } if (this._findCountChangeListener === undefined && this._notebookModel) { this._findCountChangeListener = this.notebookInput.notebookFindModel.onFindCountChange(() => this._updateFinderMatchState()); + this.registerModelChanges(); } if (e.isRevealed) { if (this._findState.isRevealed) { @@ -281,14 +284,20 @@ export class NotebookEditor extends BaseEditor implements IFindNotebookControlle this._finder.getDomNode().style.visibility = 'hidden'; this._findDecorations.clearDecorations(); } + } else { + if (!this._findState.isRevealed) { + this._finder.getDomNode().style.visibility = 'hidden'; + this._findDecorations.clearDecorations(); + } } if (e.searchString || e.matchCase || e.wholeWord) { this._findDecorations.clearDecorations(); + // if the search scope changes remove the prev if (this._notebookModel) { if (this._findState.searchString) { let findScope = this._findDecorations.getFindScope(); - if (this._findState.searchString === this.notebookFindModel.findExpression && findScope !== null && !e.matchCase && !e.wholeWord) { + if (this._findState.searchString === this.notebookFindModel.findExpression && findScope !== null && !e.matchCase && !e.wholeWord && !e.searchScope) { if (findScope) { this._updateFinderMatchState(); this._findState.changeMatchInfo( @@ -328,6 +337,46 @@ export class NotebookEditor extends BaseEditor implements IFindNotebookControlle } } } + if (e.searchScope) { + await this.notebookInput.notebookFindModel.find(this._findState.searchString, this._findState.matchCase, this._findState.wholeWord, NOTEBOOK_MAX_MATCHES).then(findRange => { + this._findDecorations.set(this.notebookFindModel.findMatches, this._currentMatch); + this._findState.changeMatchInfo( + this.notebookFindModel.getIndexByRange(this._currentMatch), + this._findDecorations.getCount(), + this._currentMatch + ); + if (this._finder.getDomNode().style.visibility === 'visible') { + this._setCurrentFindMatch(this._currentMatch); + } + }); + } + } + + private registerModelChanges(): void { + let changeEvent: FindReplaceStateChangedEvent = { + moveCursor: true, + updateHistory: true, + searchString: false, + replaceString: false, + isRevealed: false, + isReplaceRevealed: false, + isRegex: false, + wholeWord: false, + matchCase: false, + preserveCase: false, + searchScope: true, + matchesPosition: false, + matchesCount: false, + currentMatch: false + }; + this._notebookModel.cells.forEach(cell => { + this._register(cell.onCellModeChanged((state) => { + this._onFindStateChange(changeEvent).catch(onUnexpectedError); + })); + }); + this._register(this._notebookModel.contentChanged(e => { + this._onFindStateChange(changeEvent).catch(onUnexpectedError); + })); } public setSelection(range: NotebookRange): void { diff --git a/src/sql/workbench/contrib/notebook/find/notebookFindDecorations.ts b/src/sql/workbench/contrib/notebook/find/notebookFindDecorations.ts index 749aedc714..5b5ee1c324 100644 --- a/src/sql/workbench/contrib/notebook/find/notebookFindDecorations.ts +++ b/src/sql/workbench/contrib/notebook/find/notebookFindDecorations.ts @@ -128,9 +128,9 @@ export class NotebookFindDecorations implements IDisposable { private removePrevDecorations(): void { if (this._currentMatch && this._currentMatch.cell) { - let pevEditor = this._currentMatch.cell.cellType === 'markdown' ? undefined : this._editor.getCellEditor(this._currentMatch.cell.cellGuid); - if (pevEditor) { - pevEditor.getControl().changeDecorations((changeAccessor: IModelDecorationsChangeAccessor) => { + let prevEditor = this._currentMatch.cell.cellType === 'markdown' && !this._currentMatch.isMarkdownSourceCell ? undefined : this._editor.getCellEditor(this._currentMatch.cell.cellGuid); + if (prevEditor) { + prevEditor.getControl().changeDecorations((changeAccessor: IModelDecorationsChangeAccessor) => { changeAccessor.removeDecoration(this._rangeHighlightDecorationId); this._rangeHighlightDecorationId = null; }); @@ -153,7 +153,7 @@ export class NotebookFindDecorations implements IDisposable { } public checkValidEditor(range: NotebookRange): boolean { - return range && range.cell && range.cell.cellType === 'code' && !!(this._editor.getCellEditor(range.cell.cellGuid)); + return range && range.cell && !!(this._editor.getCellEditor(range.cell.cellGuid)) && (range.cell.cellType === 'code' || range.isMarkdownSourceCell); } public set(findMatches: NotebookFindMatch[], findScope: NotebookRange | null): void { diff --git a/src/sql/workbench/contrib/notebook/find/notebookFindModel.ts b/src/sql/workbench/contrib/notebook/find/notebookFindModel.ts index 9bef6634d1..f8c13d5a55 100644 --- a/src/sql/workbench/contrib/notebook/find/notebookFindModel.ts +++ b/src/sql/workbench/contrib/notebook/find/notebookFindModel.ts @@ -406,7 +406,8 @@ export class NotebookFindModel extends Disposable implements INotebookFindModel if (oldDecorationIndex < oldDecorationsLen) { // (1) get ourselves an old node do { - node = this._decorations[oldDecorationsIds[oldDecorationIndex++]].node; + let decorationNode = this._decorations[oldDecorationsIds[oldDecorationIndex++]]; + node = decorationNode?.node; } while (!node && oldDecorationIndex < oldDecorationsLen); // (2) remove the node from the tree (if it exists) @@ -522,11 +523,29 @@ export class NotebookFindModel extends Disposable implements INotebookFindModel } public get findArray(): NotebookRange[] { - return this.findArray; + return this._findArray; + } + + getIndexByRange(range: NotebookRange): number { + let index = this.findArray.findIndex(r => r.cell.cellGuid === range.cell.cellGuid && r.startColumn === range.startColumn && r.endColumn === range.endColumn && r.startLineNumber === range.startLineNumber && r.endLineNumber === range.endLineNumber && r.isMarkdownSourceCell === range.isMarkdownSourceCell); + this._findIndex = index > -1 ? index : this._findIndex; + // _findIndex is the 0 based index, return index + 1 for the actual count on UI + return this._findIndex + 1; } private searchFn(cell: ICellModel, exp: string, matchCase: boolean = false, wholeWord: boolean = false, maxMatches?: number): NotebookRange[] { let findResults: NotebookRange[] = []; + if (cell.cellType === 'markdown' && cell.isEditMode && typeof cell.source !== 'string') { + let cellSource = cell.source; + for (let j = 0; j < cellSource.length; j++) { + let findStartResults = this.search(cellSource[j], exp, matchCase, wholeWord, maxMatches - findResults.length); + findStartResults.forEach(start => { + // lineNumber: j+1 since notebook editors aren't zero indexed. + let range = new NotebookRange(cell, j + 1, start, j + 1, start + exp.length, true); + findResults.push(range); + }); + } + } let cellVal = cell.cellType === 'markdown' ? cell.renderedOutputTextContent : cell.source; if (cellVal) { if (typeof cellVal === 'string') { @@ -650,10 +669,10 @@ export class NotebookFindModel extends Disposable implements INotebookFindModel } -export class NotebookIntervalNode { +export class NotebookIntervalNode extends IntervalNode { constructor(public node: IntervalNode, public cell: ICellModel) { - + super(node.id, node.start, node.end); } } diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/cell.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/cell.test.ts index e0e77cccca..a8af67243f 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/cell.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/cell.test.ts @@ -833,4 +833,39 @@ suite('Cell Model', function (): void { assert.strictEqual(actualMsg, testMsg); }); }); + + test('Should emit event on markdown cell edit', async function (): Promise { + let notebookModel = new NotebookModelStub({ + name: '', + version: '', + mimetype: '' + }); + let contents: nb.ICellContents = { + cell_type: CellTypes.Markdown, + source: '' + }; + let model = factory.createCell(contents, { notebook: notebookModel, isTrusted: false }); + assert(!model.isEditMode); + + let createCellModePromise = () => { + return new Promise((resolve, reject) => { + setTimeout((error) => reject(error), 2000); + model.onCellModeChanged(isEditMode => { + resolve(isEditMode); + }); + }); + }; + + assert(!model.isEditMode); + let cellModePromise = createCellModePromise(); + model.isEditMode = true; + let isEditMode = await cellModePromise; + assert(isEditMode); + + cellModePromise = createCellModePromise(); + model.isEditMode = false; + isEditMode = await cellModePromise; + assert(!isEditMode); + }); + }); diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts index fabca4ccd7..d441ad38ca 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts @@ -323,6 +323,41 @@ suite('Notebook Find Model', function (): void { assert.equal(notebookFindModel.findMatches.length, 0, 'Find failed to apply match whole word for //'); }); + test('Should find results in the code cell on markdown edit', async function (): Promise { + let markdownContent: nb.INotebookContents = { + cells: [{ + cell_type: CellTypes.Markdown, + source: ['SOP067 - INTERNAL - Install azdata CLI - release candidate', '==========================================================', 'Steps', '-----', '### Parameters'], + metadata: { language: 'python' }, + execution_count: 1 + }], + metadata: { + kernelspec: { + name: 'mssql', + language: 'sql' + } + }, + nbformat: 4, + nbformat_minor: 5 + }; + await initNotebookModel(markdownContent); + + // Need to set rendered text content for 1st cell + setRenderedTextContent(0); + + let notebookFindModel = new NotebookFindModel(model); + await notebookFindModel.find('SOP', false, false, max_find_count); + + assert.equal(notebookFindModel.findMatches.length, 1, 'Find failed on markdown'); + + // fire the edit mode on cell + model.cells[0].isEditMode = true; + notebookFindModel = new NotebookFindModel(model); + await notebookFindModel.find('SOP', false, false, max_find_count); + + assert.equal(notebookFindModel.findMatches.length, 2, 'Find failed on markdown edit'); + }); + async function initNotebookModel(contents: nb.INotebookContents): Promise { let mockContentManager = TypeMoq.Mock.ofType(NotebookEditorContentManager); diff --git a/src/sql/workbench/contrib/notebook/test/stubs.ts b/src/sql/workbench/contrib/notebook/test/stubs.ts index 47fbf0ecd2..06ecd6c44c 100644 --- a/src/sql/workbench/contrib/notebook/test/stubs.ts +++ b/src/sql/workbench/contrib/notebook/test/stubs.ts @@ -123,7 +123,6 @@ export class NotebookModelStub implements INotebookModel { } export class NotebookFindModelStub implements INotebookFindModel { - getFindCount(): number { throw new Error('Method not implemented.'); } @@ -158,6 +157,9 @@ export class NotebookFindModelStub implements INotebookFindModel { findMatches: NotebookFindMatch[]; findExpression: string; onFindCountChange: vsEvent.Event; + getIndexByRange(range: NotebookRange): number { + throw new Error('Method not implemented.'); + } } export class NotebookManagerStub implements INotebookManager { diff --git a/src/sql/workbench/services/notebook/browser/models/cell.ts b/src/sql/workbench/services/notebook/browser/models/cell.ts index 136d413781..e3cd0e7122 100644 --- a/src/sql/workbench/services/notebook/browser/models/cell.ts +++ b/src/sql/workbench/services/notebook/browser/models/cell.ts @@ -108,6 +108,10 @@ export class CellModel implements ICellModel { return this._onOutputsChanged.event; } + public get onCellModeChanged(): Event { + return this._onCellModeChanged.event; + } + public get isEditMode(): boolean { return this._isEditMode; } diff --git a/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts b/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts index b4f42eb12c..a778f9bd82 100644 --- a/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts +++ b/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts @@ -484,6 +484,7 @@ export interface ICellModel { readonly onLoaded: Event; isCollapsed: boolean; readonly onCollapseStateChanged: Event; + readonly onCellModeChanged: Event; modelContentChangedEvent: IModelContentChangedEvent; isEditMode: boolean; readonly ariaLabel: string; diff --git a/src/sql/workbench/services/notebook/browser/notebookService.ts b/src/sql/workbench/services/notebook/browser/notebookService.ts index b5c4804d83..03359db6db 100644 --- a/src/sql/workbench/services/notebook/browser/notebookService.ts +++ b/src/sql/workbench/services/notebook/browser/notebookService.ts @@ -179,10 +179,12 @@ export class NotebookRange extends Range { this.cell = cell; } cell: ICellModel; + isMarkdownSourceCell: boolean; - constructor(cell: ICellModel, startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number) { + constructor(cell: ICellModel, startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, markdownEditMode?: boolean) { super(startLineNumber, startColumn, endLineNumber, endColumn); this.updateActiveCell(cell); + this.isMarkdownSourceCell = markdownEditMode ? markdownEditMode : false; } }