From f88bef8b4ccc9be9632a978c364f132403e952eb Mon Sep 17 00:00:00 2001 From: Maddy <12754347+MaddyDev@users.noreply.github.com> Date: Wed, 6 Oct 2021 23:24:22 -0700 Subject: [PATCH] save richTextCursorPosition/markdownCursorPosition (#17180) * save richTextCursorPosition/markdownCursorPosition * rename variables * check to see if the nodes exist * added comments * add comments * check if valid offset * pr comments * sqllint error fix --- .../browser/cellViews/code.component.ts | 4 + .../browser/cellViews/textCell.component.ts | 80 ++++++++++++++++++- .../services/notebook/browser/models/cell.ts | 5 +- .../browser/models/modelInterfaces.ts | 10 +++ 4 files changed, 97 insertions(+), 2 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 613bc09430..2dec5f2d19 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts @@ -282,6 +282,10 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { DOM.getContentHeight(this.codeElement.nativeElement))); this._editor.setHeightToScrollHeight(false, this._cellModel.isCollapsed); this.horizontalScrollbar(); + // Move cursor to the last known location + if (this.cellModel.markdownCursorPosition) { + this._editor.getControl().setPosition(this.cellModel.markdownCursorPosition); + } } /** 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 792c06c375..866f0a857c 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/textCell.component.ts @@ -23,7 +23,7 @@ import { IMarkdownRenderResult } from 'vs/editor/browser/core/markdownRenderer'; import { NotebookMarkdownRenderer } from 'sql/workbench/contrib/notebook/browser/outputs/notebookMarkdown'; import { CellView } from 'sql/workbench/contrib/notebook/browser/cellViews/interfaces'; -import { CellEditModes, ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; +import { ICaretPosition, CellEditModes, ICellModel } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; 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'; @@ -211,10 +211,48 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { this._changeRef.detectChanges(); })); this._register(this.cellModel.onCellPreviewModeChanged(preview => { + // On preview mode change, get the cursor position (get the position only when the selection node is a text node) + if (window.getSelection() && window.getSelection().focusNode?.nodeName === '#text' && window.getSelection().getRangeAt(0)) { + let selection = window.getSelection().getRangeAt(0); + // Check to see if the last cursor position is still the same and skip + if (selection.startOffset !== this.cellModel.richTextCursorPosition?.startOffset) { + // window.getSelection gives the exact html element and offsets of cursor location + // Since we only have the output element reference which is the parent of all html nodes + // we iterate through it's child nodes until we get the selection element and store the node indexes + // in the startElementNodes and endElementNodes and their offsets respectively. + let startElementNodes = []; + let startNode = selection.startContainer; + let endNode = selection.endContainer; + while (startNode !== this.output.nativeElement) { + startElementNodes.push(this.getNodeIndex(startNode)); + startNode = startNode.parentNode; + } + let endElementNodes = []; + while (endNode !== this.output.nativeElement) { + endElementNodes.push(this.getNodeIndex(endNode)); + endNode = endNode.parentNode; + } + // Create cursor position + let cursorPosition: ICaretPosition = { + startElementNodes: startElementNodes, + startOffset: selection.startOffset, + endElementNodes: endElementNodes, + endOffset: selection.endOffset + }; + this.cellModel.richTextCursorPosition = cursorPosition; + } + } this.previewMode = preview; this.focusIfPreviewMode(); })); this._register(this.cellModel.onCellMarkdownModeChanged(markdown => { + if (!markdown) { + let editorControl = this.cellEditors.length > 0 ? this.cellEditors[0].getEditor().getControl() : undefined; + if (editorControl) { + let selection = editorControl.getSelection(); + this.cellModel.markdownCursorPosition = selection?.getPosition(); + } + } this.markdownMode = markdown; this.focusIfPreviewMode(); })); @@ -236,6 +274,17 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { } } + getNodeIndex(n) { + let i = 0; + // walk up the node to the top and get it's index + n = n.previousSibling; + while (n) { + i++; + n = n.previousSibling; + } + return i; + } + public cellGuid(): string { return this.cellModel.cellGuid; } @@ -452,6 +501,35 @@ export class TextCellComponent extends CellView implements OnInit, OnChanges { } else { this.setSplitViewHeight(); } + // Move cursor to the richTextCursorPosition + // We iterate through the output element childnodes to get to the element of cursor location + // If the elements exist, we set the selection, else the cursor defaults to beginning. + if (!this.markdownMode && this.cellModel.richTextCursorPosition) { + let selection = window.getSelection(); + let htmlNodes = this.cellModel.richTextCursorPosition.startElementNodes; + let depthToNode = htmlNodes.length; + let startNodeElement: any = this.output.nativeElement; + while (depthToNode-- && startNodeElement) { + startNodeElement = startNodeElement.childNodes[htmlNodes[depthToNode]]; + } + htmlNodes = this.cellModel.richTextCursorPosition.endElementNodes; + depthToNode = htmlNodes.length; + let endNodeElement: any = this.output.nativeElement; + while (depthToNode-- && endNodeElement) { + endNodeElement = endNodeElement?.childNodes[htmlNodes[depthToNode]]; + } + // check to see if the nodes exist and set the cursor + if (startNodeElement && endNodeElement) { + // check the offset is still valid (element's text updates can make it invalid) + if (startNodeElement.length >= this.cellModel.richTextCursorPosition.startOffset && endNodeElement.length >= this.cellModel.richTextCursorPosition.endOffset) { + let range = document.createRange(); + range.setStart(startNodeElement, this.cellModel.richTextCursorPosition.startOffset); + range.setEnd(endNodeElement, this.cellModel.richTextCursorPosition.endOffset); + selection.removeAllRanges(); + selection.addRange(range); + } + } + } } else { this.setMarkdownEditorHeight(this._markdownMaxHeight); } diff --git a/src/sql/workbench/services/notebook/browser/models/cell.ts b/src/sql/workbench/services/notebook/browser/models/cell.ts index 7e17bf78db..72a3415383 100644 --- a/src/sql/workbench/services/notebook/browser/models/cell.ts +++ b/src/sql/workbench/services/notebook/browser/models/cell.ts @@ -12,7 +12,7 @@ import { localize } from 'vs/nls'; import * as notebookUtils from 'sql/workbench/services/notebook/browser/models/notebookUtils'; import { CellTypes, CellType, NotebookChangeType, TextCellEditModes } from 'sql/workbench/services/notebook/common/contracts'; import { NotebookModel } from 'sql/workbench/services/notebook/browser/models/notebookModel'; -import { ICellModel, IOutputChangedEvent, CellExecutionState, ICellModelOptions, ITableUpdatedEvent, CellEditModes } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; +import { ICellModel, IOutputChangedEvent, CellExecutionState, ICellModelOptions, ITableUpdatedEvent, CellEditModes, ICaretPosition } from 'sql/workbench/services/notebook/browser/models/modelInterfaces'; import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -31,6 +31,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import { IInsightOptions } from 'sql/workbench/common/editor/query/chartState'; +import { IPosition } from 'vs/editor/common/core/position'; let modelId = 0; const ads_execute_command = 'ads_execute_command'; @@ -85,6 +86,8 @@ export class CellModel extends Disposable implements ICellModel { private _outputCounter = 0; // When re-executing the same cell, ensure that we apply chart options in the same order private _attachments: nb.ICellAttachments | undefined; private _preventNextChartCache: boolean = false; + public richTextCursorPosition: ICaretPosition | undefined; + public markdownCursorPosition: IPosition | undefined; constructor(cellData: nb.ICellContents, private _options: ICellModelOptions, diff --git a/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts b/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts index 53ce5c56bc..2dba892370 100644 --- a/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts +++ b/src/sql/workbench/services/notebook/browser/models/modelInterfaces.ts @@ -23,6 +23,7 @@ import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvent import type { FutureInternal } from 'sql/workbench/services/notebook/browser/interfaces'; import { ICellValue, ResultSetSummary } from 'sql/workbench/services/query/common/query'; import { QueryResultId } from 'sql/workbench/services/notebook/browser/models/cell'; +import { IPosition } from 'vs/editor/common/core/position'; export enum ViewMode { Notebook, @@ -548,6 +549,15 @@ export interface ICellModel { * Returns the name of the attachment added to metadata. */ addAttachment(mimeType: string, base64Encoding: string, name: string): string; + richTextCursorPosition: ICaretPosition; + markdownCursorPosition: IPosition; +} + +export interface ICaretPosition { + startElementNodes: number[]; + startOffset: number; + endElementNodes: number[]; + endOffset: number; } export interface IModelFactory {