diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts index 38a4b9da69..07f1261abe 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts @@ -91,7 +91,6 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { private _editor: QueryTextEditor; private _editorInput: UntitledTextEditorInput; private _editorModel: ITextModel; - private _model: NotebookModel; private _activeCellId: string; private _layoutEmitter = new Emitter(); diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.ts index 1f92599d4f..4e9b428db8 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/codeCell.component.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { nb } from 'azdata'; -import { OnInit, Component, Input, Inject, forwardRef, ChangeDetectorRef, SimpleChange, OnChanges, HostListener, ViewChildren, QueryList } from '@angular/core'; +import { OnInit, Component, Input, Inject, forwardRef, ChangeDetectorRef, SimpleChange, OnChanges, HostListener, ViewChildren, QueryList, ViewChild } from '@angular/core'; import { CellView } from 'sql/workbench/contrib/notebook/browser/cellViews/interfaces'; import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; import { Deferred } from 'sql/base/common/promise'; import { ICellEditorProvider } from 'sql/workbench/services/notebook/browser/notebookService'; import { CodeComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/code.component'; -import { OutputComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/output.component'; +import { OutputAreaComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/outputArea.component'; export const CODE_SELECTOR: string = 'code-cell-component'; @@ -23,7 +23,7 @@ export const CODE_SELECTOR: string = 'code-cell-component'; export class CodeCellComponent extends CellView implements OnInit, OnChanges { @ViewChildren(CodeComponent) private codeCells: QueryList; - @ViewChildren(OutputComponent) private outputCells: QueryList; + @ViewChild(OutputAreaComponent) private outputAreaCell: OutputAreaComponent; @Input() cellModel: ICellModel; @Input() set model(value: NotebookModel) { this._model = value; @@ -38,7 +38,6 @@ export class CodeCellComponent extends CellView implements OnInit, OnChanges { this._model.updateActiveCell(undefined); } - private _model: NotebookModel; private _activeCellId: string; public inputDeferred: Deferred; @@ -82,8 +81,8 @@ export class CodeCellComponent extends CellView implements OnInit, OnChanges { if (this.codeCells) { editors.push(...this.codeCells.toArray()); } - if (this.outputCells) { - editors.push(...this.outputCells.toArray()); + if (this.outputAreaCell) { + editors.push(...this.outputAreaCell.cellEditors); } return editors; } diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/interfaces.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/interfaces.ts index 46244856a2..8ab8e4ab89 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/interfaces.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/interfaces.ts @@ -3,15 +3,30 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { OnDestroy } from '@angular/core'; +import { OnDestroy, ElementRef } from '@angular/core'; +import * as Mark from 'mark.js'; + import { AngularDisposable } from 'sql/base/browser/lifecycle'; -import { ICellEditorProvider, NotebookRange } from 'sql/workbench/services/notebook/browser/notebookService'; +import { ICellEditorProvider, INotebookService, NotebookRange } from 'sql/workbench/services/notebook/browser/notebookService'; import { MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; import { nb } from 'azdata'; +import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; +import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput'; +export const findHighlightClass = 'rangeHighlight'; +export const findRangeSpecificClass = 'rangeSpecificHighlight'; export abstract class CellView extends AngularDisposable implements OnDestroy, ICellEditorProvider { + + protected isFindActive: boolean = false; + protected highlightRange: NotebookRange; + protected output: ElementRef; + protected notebookService: INotebookService; + protected _model: NotebookModel; + isCellOutput: boolean = false; + protected searchTerm: string; + constructor() { super(); } @@ -29,8 +44,122 @@ export abstract class CellView extends AngularDisposable implements OnDestroy, I public abstract cellGuid(): string; public deltaDecorations(newDecorationsRange: NotebookRange | NotebookRange[], oldDecorationsRange: NotebookRange | NotebookRange[]): void { - + if (newDecorationsRange) { + this.isFindActive = true; + if (Array.isArray(newDecorationsRange)) { + this.highlightAllMatches(); + } else { + this.highlightRange = newDecorationsRange; + this.addDecoration(newDecorationsRange); + } + } + if (oldDecorationsRange) { + if (Array.isArray(oldDecorationsRange)) { + this.removeDecoration(); + this.isFindActive = false; + } else { + this.highlightRange = oldDecorationsRange === this.highlightRange ? undefined : this.highlightRange; + this.removeDecoration(oldDecorationsRange); + } + } } + + protected addDecoration(range?: NotebookRange): void { + range = range ?? this.highlightRange; + if (this.output && this.output.nativeElement) { + this.highlightAllMatches(); + if (range) { + let elements = this.getHtmlElements(); + if (elements?.length >= range.startLineNumber) { + let elementContainingText = elements[range.startLineNumber - 1]; + let markCurrent = new Mark(elementContainingText); // to highlight the current item of them all. + + markCurrent.markRanges([{ + start: range.startColumn - 1, //subtracting 1 since markdown html is 0 indexed. + length: range.endColumn - range.startColumn + }], { + className: findRangeSpecificClass, + each: function (node, range) { + // node is the marked DOM element + node.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }); + } + } + } + } + + protected highlightAllMatches(): void { + if (this.output && this.output.nativeElement) { + let markAllOccurances = new Mark(this.output.nativeElement); // to highlight all occurances in the element. + let editor = this.notebookService.findNotebookEditor(this._model.notebookUri); + if (editor) { + let findModel = (editor.notebookParams.input as NotebookInput).notebookFindModel; + if (findModel?.findMatches?.length > 0) { + let searchString = findModel.findExpression; + markAllOccurances.mark(searchString, { + className: findHighlightClass + }); + } + } + } + } + + protected removeDecoration(range?: NotebookRange): void { + if (this.output && this.output.nativeElement) { + if (range) { + let elements = this.getHtmlElements(); + let elementContainingText = elements[range.startLineNumber - 1]; + let markCurrent = new Mark(elementContainingText); + markCurrent.unmark({ acrossElements: true, className: findRangeSpecificClass }); + } else { + let markAllOccurances = new Mark(this.output.nativeElement); + markAllOccurances.unmark({ acrossElements: true, className: findHighlightClass }); + markAllOccurances.unmark({ acrossElements: true, className: findRangeSpecificClass }); + this.highlightRange = undefined; + } + } + } + + + protected getHtmlElements(): any[] { + let hostElem = this.output?.nativeElement; + let children = []; + if (hostElem) { + for (let element of hostElem.children) { + if (element.nodeName.toLowerCase() === 'table') { + // add table header and table rows. + if (element.children.length > 0) { + children.push(element.children[0]); + if (element.children.length > 1) { + for (let trow of element.children[1].children) { + children.push(trow); + } + } + } + } else if (element.children.length > 1) { + children = children.concat(this.getChildren(element)); + } else { + children.push(element); + } + } + } + return children; + } + + + protected getChildren(parent: any): any[] { + let children: any = []; + if (parent.children.length > 1 && parent.nodeName.toLowerCase() !== 'li' && parent.nodeName.toLowerCase() !== 'p') { + for (let child of parent.children) { + children = children.concat(this.getChildren(child)); + } + } else { + return [parent]; + } + return children; + } + } export interface IMarkdownStringWithCellAttachments extends IMarkdownString { diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/output.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/output.component.ts index 99ea80b0aa..594e724adf 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/output.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/output.component.ts @@ -6,6 +6,7 @@ import 'vs/css!./code'; import 'vs/css!./media/output'; import { OnInit, Component, Input, Inject, ElementRef, ViewChild, SimpleChange, AfterViewInit, forwardRef, ChangeDetectorRef, ComponentRef, ComponentFactoryResolver } from '@angular/core'; +import * as Mark from 'mark.js'; import { Event } from 'vs/base/common/event'; import { nb } from 'azdata'; import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; @@ -19,11 +20,13 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { localize } from 'vs/nls'; import * as types from 'vs/base/common/types'; import { getErrorMessage } from 'vs/base/common/errors'; -import { CellView } from 'sql/workbench/contrib/notebook/browser/cellViews/interfaces'; +import { CellView, findHighlightClass, findRangeSpecificClass } from 'sql/workbench/contrib/notebook/browser/cellViews/interfaces'; +import { INotebookService, NotebookRange } from 'sql/workbench/services/notebook/browser/notebookService'; +import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput'; export const OUTPUT_SELECTOR: string = 'output-component'; const USER_SELECT_CLASS = 'actionselect'; - +const GRID_CLASS = '[class="grid-canvas"]'; const componentRegistry = Registry.as(Extensions.MimeComponentContribution); @Component({ @@ -31,7 +34,7 @@ const componentRegistry = Registry.as(Extensions.MimeCom templateUrl: decodeURI(require.toUrl('./output.component.html')) }) export class OutputComponent extends CellView implements OnInit, AfterViewInit { - @ViewChild('output', { read: ElementRef }) private outputElement: ElementRef; + @ViewChild('output', { read: ElementRef }) override output: ElementRef; @ViewChild(ComponentHostDirective) componentHost: ComponentHostDirective; @Input() cellOutput: nb.ICellOutput; @Input() cellModel: ICellModel; @@ -46,7 +49,8 @@ export class OutputComponent extends CellView implements OnInit, AfterViewInit { @Inject(IThemeService) private _themeService: IThemeService, @Inject(forwardRef(() => ChangeDetectorRef)) private _changeref: ChangeDetectorRef, @Inject(forwardRef(() => ElementRef)) private _ref: ElementRef, - @Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver + @Inject(forwardRef(() => ComponentFactoryResolver)) private _componentFactoryResolver: ComponentFactoryResolver, + @Inject(INotebookService) override notebookService: INotebookService ) { super(); } @@ -84,7 +88,7 @@ export class OutputComponent extends CellView implements OnInit, AfterViewInit { } private get nativeOutputElement() { - return this.outputElement ? this.outputElement.nativeElement : undefined; + return this.output ? this.output.nativeElement : undefined; } public layout(): void { @@ -187,4 +191,117 @@ export class OutputComponent extends CellView implements OnInit, AfterViewInit { public cellGuid(): string { return this.cellModel.cellGuid; } + + override isCellOutput = true; + + getCellModel(): ICellModel { + return this.cellModel; + } + + protected override addDecoration(range?: NotebookRange): void { + range = range ?? this.highlightRange; + if (this.output && this.output.nativeElement) { + this.highlightAllMatches(); + if (range) { + let elements = this.getHtmlElements(); + if (elements.length === 1 && elements[0].nodeName === 'MIME-OUTPUT') { + let markCurrent = new Mark(elements[0]); + markCurrent.markRanges([{ + start: range.startColumn - 1, //subtracting 1 since markdown html is 0 indexed. + length: range.endColumn - range.startColumn + }], { + className: findRangeSpecificClass, + each: function (node, range) { + // node is the marked DOM element + node.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }); + } else if (elements?.length >= range.startLineNumber) { + let elementContainingText = elements[range.startLineNumber - 1]; + let markCurrent = new Mark(elementContainingText); // to highlight the current item of them all. + if (elementContainingText.children.length > 0) { + markCurrent = new Mark(elementContainingText.children[range.startColumn]); + markCurrent?.mark(this.searchTerm, { + className: findRangeSpecificClass, + each: function (node, range) { + // node is the marked DOM element + node.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }); + } + } + } + } + } + + protected override highlightAllMatches(): void { + if (this.output && this.output.nativeElement) { + let markAllOccurances = new Mark(this.output.nativeElement); // to highlight all occurances in the element. + if (!this._model) { + this._model = this.getCellModel().notebookModel; + } + let editor = this.notebookService.findNotebookEditor(this._model?.notebookUri); + if (editor) { + let findModel = (editor.notebookParams.input as NotebookInput).notebookFindModel; + if (findModel?.findMatches?.length > 0) { + this.searchTerm = findModel.findExpression; + markAllOccurances.mark(this.searchTerm, { + className: findHighlightClass, + separateWordSearch: true, + }); + // if there are grids + let grids = document.querySelectorAll(GRID_CLASS); + grids?.forEach(g => { + markAllOccurances = new Mark(g); + markAllOccurances.mark(this.searchTerm, { + className: findHighlightClass + }); + }); + } + } + } + } + + protected override removeDecoration(range?: NotebookRange): void { + if (this.output && this.output.nativeElement) { + if (range) { + let elements = this.getHtmlElements(); + let elementContainingText = elements[range.startLineNumber - 1]; + if (elements.length === 1 && elements[0].nodeName === 'MIME-OUTPUT') { + elementContainingText = elements[0]; + } + let markCurrent = new Mark(elementContainingText); + markCurrent.unmark({ acrossElements: true, className: findRangeSpecificClass }); + } else { + let markAllOccurances = new Mark(this.output.nativeElement); + markAllOccurances.unmark({ acrossElements: true, className: findHighlightClass }); + markAllOccurances.unmark({ acrossElements: true, className: findRangeSpecificClass }); + this.highlightRange = undefined; + // if there is a grid + let grids = document.querySelectorAll(GRID_CLASS); + grids?.forEach(g => { + markAllOccurances = new Mark(g); + markAllOccurances.unmark({ acrossElements: true, className: findHighlightClass }); + markAllOccurances.unmark({ acrossElements: true, className: findRangeSpecificClass }); + }); + } + } + } + + protected override getHtmlElements(): any[] { + let children = []; + let slickGrids = this.output.nativeElement.querySelectorAll(GRID_CLASS); + if (slickGrids.length > 0) { + slickGrids.forEach(grid => { + children.push(...grid.children); + }); + } else { + // if the decoration range belongs to code cell output and output is a stream of data + // it's in tag of the output. + let outputMessages = this.output.nativeElement.querySelectorAll('mime-output'); + children.push(...outputMessages); + } + return children; + } + } diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/outputArea.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/outputArea.component.ts index 4da99c5f0c..138299340f 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/outputArea.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/outputArea.component.ts @@ -4,13 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./code'; import 'vs/css!./outputArea'; -import { OnInit, Component, Input, Inject, ElementRef, ViewChild, forwardRef, ChangeDetectorRef } from '@angular/core'; -import { AngularDisposable } from 'sql/base/browser/lifecycle'; +import { OnInit, Component, Input, Inject, ElementRef, ViewChild, forwardRef, ChangeDetectorRef, ViewChildren, QueryList } from '@angular/core'; import { ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import * as themeColors from 'vs/workbench/common/theme'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { URI } from 'vs/base/common/uri'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; +import { CellView } from 'sql/workbench/contrib/notebook/browser/cellViews/interfaces'; +import { OutputComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/output.component'; +import { ICellEditorProvider } from 'sql/workbench/services/notebook/browser/notebookService'; export const OUTPUT_AREA_SELECTOR: string = 'output-area-component'; @@ -18,8 +20,10 @@ export const OUTPUT_AREA_SELECTOR: string = 'output-area-component'; selector: OUTPUT_AREA_SELECTOR, templateUrl: decodeURI(require.toUrl('./outputArea.component.html')) }) -export class OutputAreaComponent extends AngularDisposable implements OnInit { +export class OutputAreaComponent extends CellView implements OnInit { @ViewChild('outputarea', { read: ElementRef }) private outputArea: ElementRef; + @ViewChildren(OutputComponent) private outputCells: QueryList; + @Input() cellModel: ICellModel; private _activeCellId: string; @@ -75,4 +79,19 @@ export class OutputAreaComponent extends AngularDisposable implements OnInit { let outputElement = this.outputArea.nativeElement; outputElement.style.borderTopColor = theme.getColor(themeColors.SIDE_BAR_BACKGROUND, true).toString(); } + + public cellGuid(): string { + return this.cellModel.cellGuid; + } + + public layout() { + } + + public get cellEditors(): ICellEditorProvider[] { + let editors: ICellEditorProvider[] = []; + if (this.outputCells) { + editors.push(...this.outputCells.toArray()); + } + return editors; + } } diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/placeholderCell.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/placeholderCell.component.ts index 0c640ec781..4e0c007221 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/placeholderCell.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/placeholderCell.component.ts @@ -25,8 +25,6 @@ export class PlaceholderCellComponent extends CellView implements OnInit, OnChan this._model = value; } - private _model: NotebookModel; - constructor( @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, ) { 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 a4d04cd84a..7b25278415 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.ts @@ -8,7 +8,6 @@ import 'vs/css!./media/highlight'; import * as DOM from 'vs/base/browser/dom'; import { OnInit, Component, Input, Inject, forwardRef, ElementRef, ChangeDetectorRef, ViewChild, OnChanges, SimpleChange, HostListener, ViewChildren, QueryList } from '@angular/core'; -import * as Mark from 'mark.js'; import { localize } from 'vs/nls'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; @@ -27,23 +26,21 @@ import { ICaretPosition, CellEditModes, ICellModel } from 'sql/workbench/service import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; import { ISanitizer, defaultSanitizer } from 'sql/workbench/services/notebook/browser/outputs/sanitizer'; import { CodeComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/code.component'; -import { NotebookRange, ICellEditorProvider, INotebookService } from 'sql/workbench/services/notebook/browser/notebookService'; +import { ICellEditorProvider, INotebookService } from 'sql/workbench/services/notebook/browser/notebookService'; import { HTMLMarkdownConverter } from 'sql/workbench/contrib/notebook/browser/htmlMarkdownConverter'; -import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput'; import { highlightSelectedText } from 'sql/workbench/contrib/notebook/browser/utils'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; export const TEXT_SELECTOR: string = 'text-cell-component'; const USER_SELECT_CLASS = 'actionselect'; -const findHighlightClass = 'rangeHighlight'; -const findRangeSpecificClass = 'rangeSpecificHighlight'; + @Component({ selector: TEXT_SELECTOR, templateUrl: decodeURI(require.toUrl('./textCell.component.html')) }) export class TextCellComponent extends CellView implements OnInit, OnChanges { - @ViewChild('preview', { read: ElementRef }) private output: ElementRef; + @ViewChild('preview', { read: ElementRef }) override output: ElementRef; @ViewChildren(CodeComponent) private markdowncodeCell: QueryList; @Input() cellModel: ICellModel; @@ -115,7 +112,6 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { private _previewMode: boolean = true; private _markdownMode: boolean; private _sanitizer: ISanitizer; - private _model: NotebookModel; private _activeCellId: string; private readonly _onDidClickLink = this._register(new Emitter()); private markdownRenderer: NotebookMarkdownRenderer; @@ -125,8 +121,6 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { public readonly onDidClickLink = this._onDidClickLink.event; public previewFeaturesEnabled: boolean = false; public doubleClickEditEnabled: boolean; - private _highlightRange: NotebookRange; - private _isFindActive: boolean = false; private _editorHeight: number; private readonly _markdownMaxHeight = 4000; @@ -138,7 +132,7 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { @Inject(IInstantiationService) private _instantiationService: IInstantiationService, @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService, @Inject(IConfigurationService) private _configurationService: IConfigurationService, - @Inject(INotebookService) private _notebookService: INotebookService + @Inject(INotebookService) override notebookService: INotebookService ) { super(); this.markdownRenderer = this._instantiationService.createInstance(NotebookMarkdownRenderer); @@ -342,7 +336,7 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { outputElement.style.lineHeight = this.markdownPreviewLineHeight.toString(); this.cellModel.renderedOutputTextContent = this.getRenderedTextOutput(); outputElement.focus(); - if (this._isFindActive) { + if (this.isFindActive) { this.addDecoration(); } } @@ -545,121 +539,6 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { return this.cellModel && this.cellModel.id === this.activeCellId; } - public override deltaDecorations(newDecorationsRange: NotebookRange | NotebookRange[], oldDecorationsRange: NotebookRange | NotebookRange[]): void { - if (newDecorationsRange) { - this._isFindActive = true; - if (Array.isArray(newDecorationsRange)) { - this.highlightAllMatches(); - } else { - this._highlightRange = newDecorationsRange; - this.addDecoration(newDecorationsRange); - } - } - if (oldDecorationsRange) { - if (Array.isArray(oldDecorationsRange)) { - this.removeDecoration(); - this._isFindActive = false; - } else { - this._highlightRange = oldDecorationsRange === this._highlightRange ? undefined : this._highlightRange; - this.removeDecoration(oldDecorationsRange); - } - } - } - - private addDecoration(range?: NotebookRange): void { - range = range ?? this._highlightRange; - if (this.output && this.output.nativeElement) { - this.highlightAllMatches(); - if (range) { - let elements = this.getHtmlElements(); - if (elements?.length >= range.startLineNumber) { - let elementContainingText = elements[range.startLineNumber - 1]; - let markCurrent = new Mark(elementContainingText); // to highlight the current item of them all. - - markCurrent.markRanges([{ - start: range.startColumn - 1, //subtracting 1 since markdown html is 0 indexed. - length: range.endColumn - range.startColumn - }], { - className: findRangeSpecificClass, - each: function (node, range) { - // node is the marked DOM element - node.scrollIntoView({ behavior: 'smooth', block: 'center' }); - } - }); - } - } - } - } - - private highlightAllMatches(): void { - if (this.output && this.output.nativeElement) { - let markAllOccurances = new Mark(this.output.nativeElement); // to highlight all occurances in the element. - let editor = this._notebookService.findNotebookEditor(this.model.notebookUri); - if (editor) { - let findModel = (editor.notebookParams.input as NotebookInput).notebookFindModel; - if (findModel?.findMatches?.length > 0) { - let searchString = findModel.findExpression; - markAllOccurances.mark(searchString, { - className: findHighlightClass - }); - } - } - } - } - - private removeDecoration(range?: NotebookRange): void { - if (this.output && this.output.nativeElement) { - if (range) { - let elements = this.getHtmlElements(); - let elementContainingText = elements[range.startLineNumber - 1]; - let markCurrent = new Mark(elementContainingText); - markCurrent.unmark({ acrossElements: true, className: findRangeSpecificClass }); - } else { - let markAllOccurances = new Mark(this.output.nativeElement); - markAllOccurances.unmark({ acrossElements: true, className: findHighlightClass }); - markAllOccurances.unmark({ acrossElements: true, className: findRangeSpecificClass }); - this._highlightRange = undefined; - } - } - } - - private getHtmlElements(): any[] { - let hostElem = this.output?.nativeElement; - let children = []; - if (hostElem) { - for (let element of hostElem.children) { - if (element.nodeName.toLowerCase() === 'table') { - // add table header and table rows. - if (element.children.length > 0) { - children.push(element.children[0]); - if (element.children.length > 1) { - for (let trow of element.children[1].children) { - children.push(trow); - } - } - } - } else if (element.children.length > 1) { - children = children.concat(this.getChildren(element)); - } else { - children.push(element); - } - } - } - return children; - } - - private getChildren(parent: any): any[] { - let children: any = []; - if (parent.children.length > 1 && parent.nodeName.toLowerCase() !== 'li' && parent.nodeName.toLowerCase() !== 'p') { - for (let child of parent.children) { - children = children.concat(this.getChildren(child)); - } - } else { - return parent; - } - return children; - } - private getRenderedTextOutput(): string[] { let textOutput: string[] = []; let elements = this.getHtmlElements(); diff --git a/src/sql/workbench/contrib/notebook/browser/find/notebookFindDecorations.ts b/src/sql/workbench/contrib/notebook/browser/find/notebookFindDecorations.ts index 98694166a8..d78e62e5c9 100644 --- a/src/sql/workbench/contrib/notebook/browser/find/notebookFindDecorations.ts +++ b/src/sql/workbench/contrib/notebook/browser/find/notebookFindDecorations.ts @@ -52,7 +52,7 @@ export class NotebookFindDecorations implements IDisposable { } public getCount(): number { - return this._decorations.length; + return this._findScopeDecorationIds.length; } public getFindScope(): NotebookRange | null { @@ -125,6 +125,16 @@ export class NotebookFindDecorations implements IDisposable { break; } } + if (matchPosition === 0) { + for (let i = 0, len = this._findScopeDecorationIds.length; i < len; i++) { + let range = this._editor.notebookFindModel.getDecorationRange(this._findScopeDecorationIds[i]); + if (nextMatch.equalsRange(range)) { + newCurrentDecorationId = this._findScopeDecorationIds[i]; + matchPosition = (i + 1); + break; + } + } + } } if (this._highlightedDecorationId !== null || newCurrentDecorationId !== null) { @@ -164,14 +174,14 @@ export class NotebookFindDecorations implements IDisposable { private removeLastDecoration(): void { if (this._currentMatch && this._currentMatch.cell) { - let prevEditor = this._currentMatch.cell.cellType === 'markdown' && !this._currentMatch.isMarkdownSourceCell ? undefined : this._editor.getCellEditor(this._currentMatch.cell.cellGuid); + let prevEditor = (this._currentMatch.cell.cellType === 'markdown' && !this._currentMatch.isMarkdownSourceCell) || this._currentMatch.outputComponentIndex >= 0 ? undefined : this._editor.getCellEditor(this._currentMatch.cell.cellGuid); if (prevEditor) { prevEditor.getControl().changeDecorations((changeAccessor: IModelDecorationsChangeAccessor) => { changeAccessor.removeDecoration(this._rangeHighlightDecorationId); this._rangeHighlightDecorationId = null; }); } else { - if (this._currentMatch.cell.cellType === 'markdown') { + if (this._currentMatch.cell.cellType === 'markdown' || this._currentMatch.outputComponentIndex >= 0) { this._editor.updateDecorations(undefined, this._currentMatch); } } @@ -189,43 +199,53 @@ export class NotebookFindDecorations implements IDisposable { } public checkValidEditor(range: NotebookRange): boolean { - return range && range.cell && !!(this._editor.getCellEditor(range.cell.cellGuid)) && (range.cell.cellType === 'code' || range.isMarkdownSourceCell); + return range && range.cell && range.outputComponentIndex === -1 && !!(this._editor.getCellEditor(range.cell.cellGuid)) && (range.cell.cellType === 'code' || range.isMarkdownSourceCell); } public set(findMatches: NotebookFindMatch[], findScopes: NotebookRange[] | null): void { if (findScopes) { - this._editor.updateDecorations(findScopes, undefined); - - this._editor.changeDecorations((accessor) => { - let findMatchesOptions = NotebookFindDecorations._FIND_MATCH_NO_OVERVIEW_DECORATION; - // Find matches - let newFindMatchesDecorations: IModelDeltaDecoration[] = new Array(findMatches.length); - for (let i = 0, len = findMatches.length; i < len; i++) { - newFindMatchesDecorations[i] = { - range: findMatches[i].range, - options: findMatchesOptions - }; - } - this._decorations = accessor.deltaDecorations(this._decorations, newFindMatchesDecorations); - - // Find scope - if (this._findScopeDecorationIds.length) { - this._findScopeDecorationIds.forEach(findScopeDecorationId => accessor.removeDecoration(findScopeDecorationId)); - this._findScopeDecorationIds = []; - } - if (findScopes.length) { - this._findScopeDecorationIds = findScopes.map(findScope => accessor.addDecoration(findScope, NotebookFindDecorations._FIND_SCOPE_DECORATION)); - } + let markdownFindScopes = findScopes.filter((c, i, ranges) => { + return ranges.indexOf(ranges.find(t => t.cell.cellGuid === c.cell.cellGuid && (t.cell.cellType === 'markdown' || t.outputComponentIndex >= 0))) === i; }); + this._editor.updateDecorations(markdownFindScopes, undefined); + + let codeCellFindScopes = findScopes.filter((c, i, ranges) => { + return ranges.indexOf(ranges.find(t => t.cell.cellGuid === c.cell.cellGuid && t.cell.cellType === 'code' && t.outputComponentIndex === -1)) === i; + }); + if (codeCellFindScopes) { + this._editor.changeDecorations((accessor) => { + let findMatchesOptions = NotebookFindDecorations._FIND_MATCH_NO_OVERVIEW_DECORATION; + // filter code cell find matches + findMatches = findMatches.filter(m => m.range.cell.cellType === 'code' && m.range.outputComponentIndex === -1); + let newFindMatchesDecorations: IModelDeltaDecoration[] = new Array(findMatches.length); + for (let i = 0, len = findMatches.length; i < len; i++) { + newFindMatchesDecorations[i] = { + range: findMatches[i].range, + options: findMatchesOptions + }; + } + this._decorations = accessor.deltaDecorations(this._decorations, newFindMatchesDecorations); + + // Find scope + if (this._findScopeDecorationIds.length) { + this._findScopeDecorationIds.forEach(findScopeDecorationId => accessor.removeDecoration(findScopeDecorationId)); + this._findScopeDecorationIds = []; + } + if (findScopes.length) { + this._findScopeDecorationIds = findScopes.map(findScope => accessor.addDecoration(findScope, NotebookFindDecorations._FIND_SCOPE_DECORATION)); + } + }); + + this.setCodeCellDecorations(findMatches, codeCellFindScopes); + } - this.setCodeCellDecorations(findMatches, findScopes); } } private setCodeCellDecorations(findMatches: NotebookFindMatch[], findScopes: NotebookRange[] | null): void { //get all code cells which have matches const codeCellsFindMatches = findScopes.filter((c, i, ranges) => { - return ranges.indexOf(ranges.find(t => t.cell.cellGuid === c.cell.cellGuid && t.cell.cellType === 'code')) === i; + return ranges.indexOf(ranges.find(t => t.cell.cellGuid === c.cell.cellGuid && t.cell.cellType === 'code' && t.outputComponentIndex === -1)) === i; }); codeCellsFindMatches.forEach(findMatch => { this._editor.getCellEditor(findMatch.cell.cellGuid)?.getControl().changeDecorations((accessor) => { @@ -233,7 +253,7 @@ export class NotebookFindDecorations implements IDisposable { let findMatchesOptions: ModelDecorationOptions = NotebookFindDecorations._RANGE_HIGHLIGHT_DECORATION; let newOverviewRulerApproximateDecorations: IModelDeltaDecoration[] = []; - let cellFindScopes = findScopes.filter(f => f.cell.cellGuid === findMatch.cell.cellGuid); + let cellFindScopes = findScopes.filter(f => f.cell.cellGuid === findMatch.cell.cellGuid && f.outputComponentIndex === -1); let findMatchesInCell = findMatches?.filter(m => m.range.cell.cellGuid === findMatch.cell.cellGuid) || []; let _cellFindScopeDecorationIds: string[] = []; if (findMatchesInCell.length > 1000) { diff --git a/src/sql/workbench/contrib/notebook/browser/find/notebookFindModel.ts b/src/sql/workbench/contrib/notebook/browser/find/notebookFindModel.ts index cb5a3d50d5..5023f642a2 100644 --- a/src/sql/workbench/contrib/notebook/browser/find/notebookFindModel.ts +++ b/src/sql/workbench/contrib/notebook/browser/find/notebookFindModel.ts @@ -25,6 +25,7 @@ import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ActiveEditorContext } from 'vs/workbench/common/editor'; import { NotebookRange } from 'sql/workbench/services/notebook/browser/notebookService'; +import { nb } from 'azdata'; function _normalizeOptions(options: model.IModelDecorationOptions): ModelDecorationOptions { if (options instanceof ModelDecorationOptions) { @@ -570,6 +571,66 @@ export class NotebookFindModel extends Disposable implements INotebookFindModel } } } + if (cell.cellType === 'code' && cell.outputs.length > 0) { + // i = output element index. + let i: number = 0; + cell.outputs.forEach(output => { + let findStartResults: number[] = []; + switch (output.output_type) { + case 'stream': + let cellValFormatted = output as nb.IStreamResult; + findStartResults = this.search(cellValFormatted.text.toString(), exp, matchCase, wholeWord, maxMatches - findResults.length); + findStartResults?.forEach(start => { + let range = new NotebookRange(cell, i + 1, start, i + 1, start + exp.length, false, i); + findResults.push(range); + }); + i++; + break; + case 'error': + let error = output as nb.IErrorResult; + let errorValue = error.traceback?.length > 0 ? error.traceback.toString() : error.evalue; + findStartResults = this.search(errorValue, exp, matchCase, wholeWord, maxMatches - findResults.length); + findStartResults.forEach(start => { + let range = new NotebookRange(cell, i + 1, start, i + 1, start + exp.length, false, i); + findResults.push(range); + }); + i++; + break; + case 'display_data': + let displayValue = output as nb.IDisplayData; + findStartResults = this.search(JSON.parse(JSON.stringify(displayValue.data))['text/html'], exp, matchCase, wholeWord, maxMatches - findResults.length); + findStartResults.forEach(start => { + let range = new NotebookRange(cell, i + 1, start, i + 1, start + exp.length, false, i); + findResults.push(range); + }); + i++; + break; + case 'execute_result': + // When result is a table + let executeResult = output as nb.IExecuteResult; + const result = JSON.parse(JSON.stringify(executeResult.data)); + const data = result['application/vnd.dataresource+json'].data; + if (data.length > 0) { + for (let row = 0; row < data.length; row++) { + let rowData = data[row]; + let j: number = 0; + for (const key in rowData) { + let findStartResults = this.search(rowData[key].toString(), exp, matchCase, wholeWord, maxMatches - findResults.length); + if (findStartResults.length) { + let range = new NotebookRange(cell, row + 1, j + 1, row + 1, j + 1, false, i); + findResults.push(range); + } + j++; + } + } + i++; + } + break; + default: i++; + break; + } + }); + } return findResults; } diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts index 792011f9d2..a5629ba081 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts @@ -195,15 +195,23 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe public deltaDecorations(newDecorationsRange: NotebookRange | NotebookRange[], oldDecorationsRange: NotebookRange | NotebookRange[]): void { if (oldDecorationsRange) { if (Array.isArray(oldDecorationsRange)) { + // markdown cells let cells = [...new Set(oldDecorationsRange.map(item => item.cell))].filter(c => c.cellType === 'markdown'); cells.forEach(cell => { let cellOldDecorations = oldDecorationsRange.filter(r => r.cell === cell); let cellEditor = this.cellEditors.find(c => c.cellGuid() === cell.cellGuid); cellEditor.deltaDecorations(undefined, cellOldDecorations); }); + // code cell outputs + let codeCells = [...new Set(oldDecorationsRange.map(item => item.cell))].filter(c => c.cellType === 'code'); + codeCells.forEach(cell => { + let cellOldDecorations = oldDecorationsRange.filter(r => r.outputComponentIndex >= 0 && cell.cellGuid === r.cell.cellGuid); + let cellEditors = this.cellEditors.filter(c => c.cellGuid() === cell.cellGuid && c.isCellOutput); + cellEditors.forEach(cellEditor => cellEditor.deltaDecorations(undefined, cellOldDecorations)); + }); } else { - if (oldDecorationsRange.cell.cellType === 'markdown') { - let cell = this.cellEditors.find(c => c.cellGuid() === oldDecorationsRange.cell.cellGuid); + if (oldDecorationsRange.cell.cellType === 'markdown' || oldDecorationsRange.outputComponentIndex >= 0) { + let cell = oldDecorationsRange.outputComponentIndex >= 0 ? this.cellEditors.filter(c => c.cellGuid() === oldDecorationsRange.cell.cellGuid && c.isCellOutput)[oldDecorationsRange.outputComponentIndex] : this.cellEditors.find(c => c.cellGuid() === oldDecorationsRange.cell.cellGuid); cell.deltaDecorations(undefined, oldDecorationsRange); } } @@ -216,9 +224,16 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe let cellEditor = this.cellEditors.find(c => c.cellGuid() === cell.cellGuid); cellEditor.deltaDecorations(cellNewDecorations, undefined); }); + // code cell outputs + let codeCells = [...new Set(newDecorationsRange.map(item => item.cell))].filter(c => c.cellType === 'code'); + codeCells.forEach(cell => { + let cellNewDecorations = newDecorationsRange.filter(r => r.outputComponentIndex >= 0 && cell.cellGuid === r.cell.cellGuid); + let cellEditors = this.cellEditors.filter(c => c.cellGuid() === cell.cellGuid && c.isCellOutput); + cellEditors.forEach(cellEditor => cellEditor.deltaDecorations(cellNewDecorations, undefined)); + }); } else { - if (newDecorationsRange.cell.cellType === 'markdown') { - let cell = this.cellEditors.find(c => c.cellGuid() === newDecorationsRange.cell.cellGuid); + if (newDecorationsRange.cell.cellType === 'markdown' || newDecorationsRange.outputComponentIndex >= 0) { + let cell = newDecorationsRange.outputComponentIndex >= 0 ? this.cellEditors.filter(c => c.cellGuid() === newDecorationsRange.cell.cellGuid && c.isCellOutput)[newDecorationsRange.outputComponentIndex] : this.cellEditors.find(c => c.cellGuid() === newDecorationsRange.cell.cellGuid); cell.deltaDecorations(newDecorationsRange, undefined); } } diff --git a/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts b/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts index 6280474c6b..7cf15f4626 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts @@ -84,7 +84,7 @@ export class NotebookEditor extends EditorPane implements IFindNotebookControlle } private get _findDecorations(): NotebookFindDecorations { - return this.notebookInput.notebookFindModel.findDecorations; + return this.notebookInput?.notebookFindModel?.findDecorations; } public getPosition(): NotebookRange { @@ -432,9 +432,11 @@ export class NotebookEditor extends EditorPane implements IFindNotebookControlle public async findNext(): Promise { try { const p = await this.notebookFindModel.findNext(); - this.setSelection(p); - this._updateFinderMatchState(); - this._setCurrentFindMatch(p); + if (p !== this._currentMatch) { + this.setSelection(p); + this._updateFinderMatchState(); + this._setCurrentFindMatch(p); + } } catch (er) { onUnexpectedError(er); } @@ -443,9 +445,11 @@ export class NotebookEditor extends EditorPane implements IFindNotebookControlle public async findPrevious(): Promise { try { const p = await this.notebookFindModel.findPrevious(); - this.setSelection(p); - this._updateFinderMatchState(); - this._setCurrentFindMatch(p); + if (p !== this._currentMatch) { + this.setSelection(p); + this._updateFinderMatchState(); + this._setCurrentFindMatch(p); + } } catch (er) { onUnexpectedError(er); } diff --git a/src/sql/workbench/contrib/notebook/browser/notebookStyles.ts b/src/sql/workbench/contrib/notebook/browser/notebookStyles.ts index aacb31a2bf..aae8faea0d 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookStyles.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookStyles.ts @@ -250,7 +250,7 @@ export function registerNotebookThemes(overrideEditorThemeSetting: boolean, conf collector.addRule(`.notebook-preview .rangeHighlight { background-color: ${notebookFindMatchHighlightColor};}`); } if (notebookFindRangeHighlightColor) { - collector.addRule(`.notebook-preview .rangeSpecificHighlight { background-color: ${notebookFindRangeHighlightColor}!important;}`); + collector.addRule(`mark .rangeSpecificHighlight { background-color: ${notebookFindRangeHighlightColor}!important;}`); } }); } 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 94a5adb7e3..bdaaf32cb8 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 @@ -401,6 +401,166 @@ suite('Notebook Find Model', function (): void { assert.strictEqual(notebookFindModel.findMatches.length, 2, 'Find failed on markdown edit'); }); + test('Should find results in the output of the code cell when output is stream', async function (): Promise { + let codeCellOutput: nb.IStreamResult = { + output_type: 'stream', + name: 'stdout', + text: 'trace\nhello world\n.local\n' + }; + let cellContent: nb.INotebookContents = { + cells: [{ + cell_type: CellTypes.Markdown, + source: ['Hello World'], + metadata: { language: 'python' }, + execution_count: 1 + }, + { + cell_type: 'code', + source: [ + 'print(\'trace\')\n', + 'print(\'hello world\')\n', + 'print(\'.local\')' + ], + metadata: { language: 'python' }, + outputs: [ + codeCellOutput + ], + execution_count: 1 + }], + metadata: { + kernelspec: { + name: 'mssql', + language: 'sql', + display_name: 'SQL' + } + }, + nbformat: 4, + nbformat_minor: 5 + }; + await initNotebookModel(cellContent); + + // Need to set rendered text content for 1st cell + setRenderedTextContent(0); + + let notebookFindModel = new NotebookFindModel(model); + + await notebookFindModel.find('trace', false, false, max_find_count); + assert.strictEqual(notebookFindModel.findMatches.length, 2, 'Find failed on code cell and its output'); + + await notebookFindModel.find('hello', false, false, max_find_count); + assert.strictEqual(notebookFindModel.findMatches.length, 3, 'Find failed on code cell output'); + }); + + test('Should find results in the output of the code cell when output is executeResult', async function (): Promise { + let codeCellOutput: nb.IExecuteResult = { + output_type: 'execute_result', + execution_count: null, + data: { + 'application/vnd.dataresource+json': { + 'schema': { + 'fields': [ + { + 'name': 'ContactTypeID' + }, + { + 'name': 'Name' + }, + { + 'name': 'ModifiedDate' + } + ] + }, + 'data': [ + { + '0': '1', + '1': 'Accounting Manager', + '2': '2008-04-30 00:00:00.000' + }, + { + '0': '2', + '1': 'Assistant Sales Agent', + '2': '2008-04-30 00:00:00.000' + }, + { + '0': '3', + '1': 'Assistant Sales Representative', + '2': '2008-04-30 00:00:00.000' + }, + { + '0': '4', + '1': 'Coordinator Foreign Markets', + '2': '2008-04-30 00:00:00.000' + }, + { + '0': '5', + '1': 'Export Administrator', + '2': '2008-04-30 00:00:00.000' + }, + { + '0': '6', + '1': 'International Marketing Manager', + '2': '2008-04-30 00:00:00.000' + }, + { + '0': '7', + '1': 'Marketing Assistant', + '2': '2008-04-30 00:00:00.000' + }, + { + '0': '8', + '1': 'Marketing Manager', + '2': '2008-04-30 00:00:00.000' + }, + { + '0': '9', + '1': 'Marketing Representative', + '2': '2008-04-30 00:00:00.000' + }, + { + '0': '10', + '1': 'Order Administrator', + '2': '2008-04-30 00:00:00.000' + } + ] + } + } + }; + let cellContent: nb.INotebookContents = { + cells: [ + { + cell_type: 'code', + source: [ + 'Select top 10 * from Person.ContactType\n', ' -- Assistant' + ], + metadata: { language: 'sql' }, + outputs: [ + codeCellOutput + ], + execution_count: 1 + }], + metadata: { + kernelspec: { + name: 'mssql', + language: 'sql', + display_name: 'SQL' + } + }, + nbformat: 4, + nbformat_minor: 5 + }; + max_find_count = 4; + await initNotebookModel(cellContent); + + // Need to set rendered text content for 1st cell + setRenderedTextContent(0); + + let notebookFindModel = new NotebookFindModel(model); + await notebookFindModel.find('Assistant', false, false, max_find_count); + + assert.strictEqual(notebookFindModel.getFindCount(), 4, 'Find failed on executed code cell output'); + }); + + test('Find next/previous should return the correct find index', async function (): Promise { // Need to set rendered text content for 2nd cell setRenderedTextContent(1); diff --git a/src/sql/workbench/contrib/notebook/test/stubs.ts b/src/sql/workbench/contrib/notebook/test/stubs.ts index d858178fcf..550973a06f 100644 --- a/src/sql/workbench/contrib/notebook/test/stubs.ts +++ b/src/sql/workbench/contrib/notebook/test/stubs.ts @@ -751,6 +751,7 @@ export class NotebookEditorStub implements INotebookEditor { } export class CellEditorProviderStub implements ICellEditorProvider { + isCellOutput = false; hasEditor(): boolean { throw new Error('Method not implemented.'); } diff --git a/src/sql/workbench/services/notebook/browser/notebookService.ts b/src/sql/workbench/services/notebook/browser/notebookService.ts index d20166bdfa..55fb8f985c 100644 --- a/src/sql/workbench/services/notebook/browser/notebookService.ts +++ b/src/sql/workbench/services/notebook/browser/notebookService.ts @@ -188,6 +188,7 @@ export interface INotebookSection { export interface ICellEditorProvider { hasEditor(): boolean; + isCellOutput: boolean; cellGuid(): string; getEditor(): BaseTextEditor; deltaDecorations(newDecorationsRange: NotebookRange | NotebookRange[], oldDecorationsRange: NotebookRange | NotebookRange[]): void; @@ -199,11 +200,13 @@ export class NotebookRange extends Range { } cell: ICellModel; isMarkdownSourceCell: boolean; + outputComponentIndex: number; - constructor(cell: ICellModel, startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, markdownEditMode?: boolean) { + constructor(cell: ICellModel, startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, markdownEditMode?: boolean, outputIndex?: number) { super(startLineNumber, startColumn, endLineNumber, endColumn); this.updateActiveCell(cell); this.isMarkdownSourceCell = markdownEditMode ? markdownEditMode : false; + this.outputComponentIndex = outputIndex >= 0 ? outputIndex : -1; } }