From 33ba5864757eece2e8a6336b229f335a662ee956 Mon Sep 17 00:00:00 2001 From: Maddy <12754347+MaddyDev@users.noreply.github.com> Date: Tue, 21 Dec 2021 11:56:33 -0800 Subject: [PATCH] enable find in cell output when output is a data stream (#17759) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial changes * add isCodeOutput and apply decorations on output * Add extension gallery update reminder action (#17644) * Fix extensionsGallery.json name (#17646) * Remove galleries list action (#17648) * multi-level table support (#17638) * multi-level table support * comments * address comments * add period to end of sentence. * Show connection string example for add sql binding quickpick (#17650) * add connection string example * reorder * Resolves same-origin-policy violation when ADS web is running in a container (#17555) * Stops appending port to authority for web mode * Clarifies comment * Adds missing sql carbon edit tag * vbump STS (#17653) * use latest STS (#17658) * Refactoring readProjFile() (#17637) * move reading project parts to different helper functions * cleanup * remove comment * addressing comments * Corrects Web Build Pipeline ENOENT Error (#17656) * Checks for successful directory creation * Revert "Checks for successful directory creation" This reverts commit 372409ef323f0d82e11992bc7bc33d607a7d5581. * Checks for the existence of the logs directory before accessing. * Adds SQL carbon edit comment * Removing call to copy from non-existing directory. * Removes unneeded import * Checks for file existence before copying. * Provides explanation for modification * Replaces file existence check with exception handling. * Bump tools service (#17671) * Apply changes from Remote Database to sqlproj - mssql changes (#17655) * update project from database * update project from database * update project from database * Re-adding schemaComparePublishChanges for temporary backcompat * Adding comment for keeping enum values in sync * Correcting enum value Co-authored-by: Noureldine Yehia * update add file/folder for msbuild sdk style projects (#17660) * update add file for msbuild sdk style projects * also handle add folder * fix comment * fix issue reported by component governance (#17678) * update json-schema version * remove unused packages * update package.json * SQL Binding: Give default connection setting name (#17659) * show sqlconnectionstring in quickpick * fix duplicate sqlconnectionstring setting * add (new) * add sqlconnectionstring as default setting name * check if sqlconnectionstring already exists * Provide aria-labels for node checkboxes in the tree view. (#17676) During accessibility testing, it was discovered that screen reader does not announce what checkboxes in the tree view represent. It was merely announcing "checkbox unchecked", so it was not clear without visuals which checkbox the focus is on. This change sets an `aria-label` of the checkbox elements to match the label of the owning tree node. This way the announcement becomes "My Node; checkbox; unchecked". This is fine as a quick solution to the problem, but in the future we may want to consider adding additional checkbox label property to the nodes exposed by the tree provider, so that each checkbox can announce additional information, if needed. * Respect ARIA label specified int he tree component options. (#17674) During accessibility testing it was discovered that tree view in our wizard reads "Tree Node tree view" instead of the proper label that is specified. It turned out to be the problem with the tree component, where `ariaLabel` was hardcoded to "Tree Node", instead of the one provided in the options. This change addresses the problem by passing through `ariaLabel` from the options object to the underlying tree control. I also removed the default `Tree Node` hardcoded label, as it didn't make much sense. This does mean that all tree-views that do not explicitly specify their aria-label will now get an empty label. I think this is better than having unrelated, unlocalized `Tree Node`. I'm also worried about changes to the `ariaLabel` property after the component was initialized. I updated the code to propagate the value to the underlying tree view in the `setProperties` override of the tree component and hope that it will take care of it. * Fix sql projects net6 warnings (#17673) * fix .net 6 error showing on startup * fix double warning * addressing comments * update key string * undo adding space in net core sdk location setting (#17684) * update names for msbuild sdk style projects (#17677) * update names for msbuild sdk style projects * remove msbuild from names * update comments * Designer: property descriptions (#17668) * format * added strings * format doc * use codicon instead * show descriptions in property pane only * fix ssdt string bug * fix overflow option * review comments * review comments * changes * sts 156 vbump (#17683) * Sql Binding: Add "Check out pane for more details" when nuget package download fails (#17680) * check output for more details * detail error * wait for result from showerrormessage * [Loc] Update to sql-database-projects and sql.xlf (#17687) * update the vmImage for build jobs (#17689) * update vmImage for windows build * update tar command * use specific macos version * support building msbuild sdk style projects (#17675) * support building msbuild sdk style projects * fixes after merge * add foreign keys and constraints (#17697) * foreign keys and constraints * refactoring * fix issues * properties pane improvements (#17700) * [Loc] update to tabledesignercomponentinput (#17704) * Add instructions for developing VS Code version of sql-database-projects (#17705) * Adding UI for deploying a db proj to docker (#17495) * code refactoring (#17706) * Improve accessibility for wizard steps navigation (#17669) Our extension is relying on the wizard dialog. During accessibility testing it was discovered that wizard step buttons are being reported as links by the screen reader (NVDA, JAWS). Claimed expected behavior by the tester is that they should be announced as buttons. I discussed this issue with accessibility SMEs and they said it is perfectly fine to keep them as links. They did mention that they would probably design the UX differently from the start, but given that we already have it this way, links are fine. They did suggest to add few additional ARIA attributes to the link elements: - `aria-current="step"` if the link is for the currently active step. This literally just announces "current step" at the end, when you focus on a link - `aria-disabled="true"` makes it say "**unavailable**; link; **" when in NVDA "browse" mode and move to the grayed-out link. So this change implements the said improvements. * bump sts version to 159 (#17709) * LEGO: check in for main to temporary branch. (#17699) * LEGO: check in for main to temporary branch. (#17702) Co-authored-by: Alex Ma * [Loc] update to sql-database-projects (#17713) * LEGO: check in for main to temporary branch. (#17715) * Register additional editor overrides when adding new notebook file types (#17708) * Also standardized file extension contributions to always start with a period, and to always do lower case string comparisons for file extensions. * input width in designer (#17714) * Add additional properties to wizard page navigation events (#17716) * Fix open external not working (#17717) * save password checkbox fix (#17718) * save password checkbox fix * remove code to reset the checkbox value * allow build to continue when cache task fails (#17720) * cache task should not fail the build * update cachesalt * Fix language flavor change on connection when in sqlcmd mode (#17719) * Fix language flavor change on connection when in sqlcmd mode * comment + fix * LEGO: check in for main to temporary branch. (#17722) * LEGO: check in for main to temporary branch. (#17724) Co-authored-by: Alex Ma * Update one more variable name for sdk style projects (#17710) * LEGO: check in for main to temporary branch. (#17725) Co-authored-by: Alex Ma * LEGO: check in for main to temporary branch. (#17726) Co-authored-by: Alex Ma * LEGO: check in for main to temporary branch. (#17730) * Fix "unsupported version" error when adding sql binding package (#17721) * Apply changes from remote database to sqlproj - schema-compare changes (#17679) * update project from database * update project from database * Merge from main * Removing dupe test stub * PR feedback * cleanup * PR feedback * Fixing tests, adding stubs to update sqlproj as schema compare target * updating code comment Co-authored-by: Noureldine Yehia * [Loc] Update to schema-compare XLF (#17733) * Enabled deployment of Azure Arc data controllers in directly connected mode (#17707) * Added fields for connectivity mode, custom loc, auto-metrics, auto-logs, and the dynamic enablement of such fields. * Changed the description of the data controller details page. * Change notebook params to work for direct mode * Added login to dc deployment notebooks * Fixed auto upload metrics and logs true/false, separated login into another cell. * Removed localization of indirect and direct connectivity labels. * Fix ordering of reading sqlproj Build Includes and Removes (#17712) * evaluate includes and removes in order in sqlproj * fix after merge * fix comment * update comment * Add resource deployment samples for (#17734) * Update remove file for sdk style sql projects (#17688) * add support for removing file in new style project * fix test * only load files, not whole project when checking if a needs to be added * merge changes * fixes after merge * [Loc] update to Arc XLF (#17737) * Bump to latest version of azdata (#17735) * Remove .net 6 version cutoff for building sql projects (#17736) * remove .net 6 version cutoff for building sql projects * Revert "Warning when .NET 6 SDK is detected (#17422)" This reverts commit 2ed8aeb56590bb0404cf3118e8f958b2370d9c3b. * add back skipVersionSupportedCheck * add back return false * addressing comments * [Loc] update to sql-database-projects xlf (#17743) * Add additional notebook tests for handling relative links. (#17739) * [Loc] Fix for duplicate strings in LCL files (#17756) * WIP spanish lcl duplicate removal * Revert "WIP spanish lcl duplicate removal" This reverts commit 5f943153ec8980849a045c8bf7256d852571a778. * fix for duplicate strings * removed spaces * Fixes ADS Web bug around copying user codes and opening a browser tab when adding an Azure Account. (#17760) * Fixes bug around copying user codes and opening a browser tab. * Code review changes * Additional review changes. * Unnecessary import removed * Editing pipeline ACR service connection endpoint to the latest one created (#17767) * using the new registry endpoint * updating the service connection * Changing the name of acr service endpoint to SqlToolsContainer * Updating web build acr endpoint as well. * Notebooks: Add Tooltips for Link/Image Buttons on Markdown Toolbar (#17763) * Add title for tooltips * Tweak tooltips * Added UI for user to accept EULA when deploying sql proj to docker container (#17762) * fix sorting bug (#17769) * fix sorting bug * comments * Set default radio button selection, evaluate default component popula… (#17764) * Set default radio button selection, evaluate default component population off of selection instead of unprocessed input * rename var * if -> switches * Remove project radio buttons because they require commands that aren't yet checked in (and fail as a result) * Added fix for Publish Target Label Position (#17771) * Added fix for Publish Target Label Position * renamed checkbox * LEGO: check in for main to temporary branch. (#17775) * LEGO: check in for main to temporary branch. (#17778) Co-authored-by: Alex Ma * update dashboard taskbar separator (#17779) * remove comments * fix applying decorations * add test * enable find in sql result set * add tests and update sql results highlight logic * calculate the outputComponent index * fix editor issues later * remove newline replace on search * address comments * fix highlight issue Co-authored-by: Charles Gagnon Co-authored-by: Alan Ren Co-authored-by: Lucy Zhang Co-authored-by: Lewis Sanchez <87730006+lewis-sanchez@users.noreply.github.com> Co-authored-by: Kim Santiago <31145923+kisantia@users.noreply.github.com> Co-authored-by: Benjin Dubishar Co-authored-by: Noureldine Yehia Co-authored-by: Alexander Ivanov Co-authored-by: Aditya Bist Co-authored-by: Sai Avishkar Sreerama <74571829+ssreerama@users.noreply.github.com> Co-authored-by: Alex Ma Co-authored-by: Leila Lali Co-authored-by: csigs Co-authored-by: Cory Rivera Co-authored-by: Candice Ye Co-authored-by: Aasim Khan Co-authored-by: Chris LaFreniere <40371649+chlafreniere@users.noreply.github.com> --- .../browser/cellViews/code.component.ts | 1 - .../browser/cellViews/codeCell.component.ts | 11 +- .../notebook/browser/cellViews/interfaces.ts | 135 ++++++++++++++- .../browser/cellViews/output.component.ts | 127 +++++++++++++- .../browser/cellViews/outputArea.component.ts | 25 ++- .../cellViews/placeholderCell.component.ts | 2 - .../browser/cellViews/textCell.component.ts | 131 +------------- .../browser/find/notebookFindDecorations.ts | 78 +++++---- .../browser/find/notebookFindModel.ts | 61 +++++++ .../notebook/browser/notebook.component.ts | 23 ++- .../notebook/browser/notebookEditor.ts | 18 +- .../notebook/browser/notebookStyles.ts | 2 +- .../notebookFindModel.test.ts | 160 ++++++++++++++++++ .../workbench/contrib/notebook/test/stubs.ts | 1 + .../notebook/browser/notebookService.ts | 5 +- 15 files changed, 592 insertions(+), 188 deletions(-) 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; } }