diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 32bf845aa31..370cb790ec6 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1363,6 +1363,11 @@ declare module 'vscode' { delete(index: number): void; } + export interface NotebookCellRange { + readonly start: number; + readonly end: number; + } + export interface NotebookEditor { /** * The document associated with this notebook editor. @@ -1374,6 +1379,12 @@ declare module 'vscode' { */ readonly selection?: NotebookCell; + + /** + * The current visible ranges in the editor (vertically). + */ + readonly visibleRanges: NotebookCellRange[]; + /** * The column in which this editor shows. */ @@ -1485,6 +1496,11 @@ declare module 'vscode' { readonly selection?: NotebookCell; } + export interface NotebookEditorVisibleRangesChangeEvent { + readonly notebookEditor: NotebookEditor; + readonly visibleRanges: ReadonlyArray; + } + export interface NotebookCellData { readonly cellKind: CellKind; readonly source: string; @@ -1707,6 +1723,7 @@ declare module 'vscode' { export const activeNotebookEditor: NotebookEditor | undefined; export const onDidChangeActiveNotebookEditor: Event; export const onDidChangeNotebookEditorSelection: Event; + export const onDidChangeNotebookEditorVisibleRanges: Event; export const onDidChangeNotebookCells: Event; export const onDidChangeCellOutputs: Event; export const onDidChangeCellLanguage: Event; diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index dd8b3f29585..4cca5edccdb 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -60,7 +60,7 @@ class DocumentAndEditorState { const apiEditors = []; for (let id in after.textEditors) { const editor = after.textEditors.get(id)!; - apiEditors.push({ id, documentUri: editor.uri!, selections: editor!.textModel!.selections }); + apiEditors.push({ id, documentUri: editor.uri!, selections: editor!.textModel!.selections, visibleRanges: editor.visibleRanges }); } return { @@ -74,7 +74,8 @@ class DocumentAndEditorState { const addedAPIEditors = editorDelta.added.map(add => ({ id: add.getId(), documentUri: add.uri!, - selections: add.textModel!.selections || [] + selections: add.textModel!.selections || [], + visibleRanges: add.visibleRanges })); const removedAPIEditors = editorDelta.removed.map(removed => removed.getId()); @@ -137,6 +138,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo private _toDisposeOnEditorRemove = new Map(); private _currentState?: DocumentAndEditorState; private _editorEventListenersMapping: Map = new Map(); + private _documentEventListenersMapping: Map = new Map(); private readonly _cellStatusBarEntries: Map = new Map(); constructor( @@ -166,9 +168,9 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo async removeNotebookTextModel(uri: URI): Promise { // TODO@rebornix, remove cell should use emitDelta as well to ensure document/editor events are sent together this._proxy.$acceptDocumentAndEditorsDelta({ removedDocuments: [uri] }); - let textModelDisposableStore = this._editorEventListenersMapping.get(uri.toString()); + let textModelDisposableStore = this._documentEventListenersMapping.get(uri.toString()); textModelDisposableStore?.dispose(); - this._editorEventListenersMapping.delete(URI.from(uri).toString()); + this._documentEventListenersMapping.delete(URI.from(uri).toString()); } private _isDeltaEmpty(delta: INotebookDocumentsAndEditorsDelta) { @@ -228,38 +230,67 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } })); + const notebookEditorAddedHandler = (editor: IEditor) => { + if (!this._editorEventListenersMapping.has(editor.getId())) { + const disposableStore = new DisposableStore(); + disposableStore.add(editor.onDidChangeVisibleRanges(() => { + this._proxy.$acceptEditorPropertiesChanged(editor.getId(), { visibleRanges: { ranges: editor.visibleRanges } }); + })); + + this._editorEventListenersMapping.set(editor.getId(), disposableStore); + } + }; + this._register(this._notebookService.onNotebookEditorAdd(editor => { + notebookEditorAddedHandler(editor); this._addNotebookEditor(editor); })); this._register(this._notebookService.onNotebookEditorsRemove(editors => { this._removeNotebookEditor(editors); + + editors.forEach(editor => { + this._editorEventListenersMapping.get(editor.getId())?.dispose(); + this._editorEventListenersMapping.delete(editor.getId()); + }); })); + this._notebookService.listNotebookEditors().forEach(editor => { + notebookEditorAddedHandler(editor); + }); + + const notebookDocumentAddedHandler = (doc: URI) => { + if (!this._editorEventListenersMapping.has(doc.toString())) { + const disposableStore = new DisposableStore(); + const textModel = this._notebookService.getNotebookTextModel(doc); + disposableStore.add(textModel!.onDidModelChangeProxy(e => { + this._proxy.$acceptModelChanged(textModel!.uri, e, textModel!.isDirty); + this._proxy.$acceptDocumentPropertiesChanged(doc, { selections: { selections: textModel!.selections }, metadata: null }); + })); + disposableStore.add(textModel!.onDidSelectionChange(e => { + const selectionsChange = e ? { selections: e } : null; + this._proxy.$acceptDocumentPropertiesChanged(doc, { selections: selectionsChange, metadata: null }); + })); + + this._editorEventListenersMapping.set(textModel!.uri.toString(), disposableStore); + } + }; + this._register(this._notebookService.onNotebookDocumentAdd((documents) => { documents.forEach(doc => { - if (!this._editorEventListenersMapping.has(doc.toString())) { - const disposableStore = new DisposableStore(); - const textModel = this._notebookService.getNotebookTextModel(doc); - disposableStore.add(textModel!.onDidModelChangeProxy(e => { - this._proxy.$acceptModelChanged(textModel!.uri, e, textModel!.isDirty); - this._proxy.$acceptEditorPropertiesChanged(doc, { selections: { selections: textModel!.selections }, metadata: null }); - })); - disposableStore.add(textModel!.onDidSelectionChange(e => { - const selectionsChange = e ? { selections: e } : null; - this._proxy.$acceptEditorPropertiesChanged(doc, { selections: selectionsChange, metadata: null }); - })); - - this._editorEventListenersMapping.set(textModel!.uri.toString(), disposableStore); - } + notebookDocumentAddedHandler(doc); }); this._updateState(); })); + this._notebookService.listNotebookDocuments().forEach((doc) => { + notebookDocumentAddedHandler(doc.uri); + }); + this._register(this._notebookService.onNotebookDocumentRemove((documents) => { documents.forEach(doc => { - this._editorEventListenersMapping.get(doc.toString())?.dispose(); - this._editorEventListenersMapping.delete(doc.toString()); + this._documentEventListenersMapping.get(doc.toString())?.dispose(); + this._documentEventListenersMapping.delete(doc.toString()); }); this._updateState(); @@ -420,7 +451,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo textModel.insertTemplateCell(mainCell); } - this._proxy.$acceptEditorPropertiesChanged(textModel.uri, { selections: null, metadata: textModel.metadata }); + this._proxy.$acceptDocumentPropertiesChanged(textModel.uri, { selections: null, metadata: textModel.metadata }); return; }, resolveNotebookEditor: async (viewType: string, uri: URI, editorId: string) => { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index d9943430885..ab29d86e094 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -981,6 +981,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeNotebookEditorSelection(listener, thisArgs, disposables); }, + onDidChangeNotebookEditorVisibleRanges(listener, thisArgs?, disposables?) { + checkProposedApiEnabled(extension); + return extHostNotebook.onDidChangeNotebookEditorVisibleRanges(listener, thisArgs, disposables); + }, onDidChangeCellOutputs(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeCellOutputs(listener, thisArgs, disposables); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 25182aee42f..d4bdfcfa5c2 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -51,7 +51,7 @@ import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; -import { IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, INotebookKernelInfoDto2, TransientMetadata, INotebookCellStatusBarEntry } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, INotebookKernelInfoDto2, TransientMetadata, INotebookCellStatusBarEntry, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; @@ -1624,9 +1624,22 @@ export interface INotebookSelectionChangeEvent { selections: number[]; } +export interface INotebookCellVisibleRange { + start: number; + end: number; +} + +export interface INotebookVisibleRangesEvent { + ranges: INotebookCellVisibleRange[]; +} + export interface INotebookEditorPropertiesChangeData { - selections: INotebookSelectionChangeEvent | null; + visibleRanges: INotebookVisibleRangesEvent | null; +} + +export interface INotebookDocumentPropertiesChangeData { metadata: NotebookDocumentMetadata | null; + selections: INotebookSelectionChangeEvent | null; } export interface INotebookModelAddedData { @@ -1636,13 +1649,14 @@ export interface INotebookModelAddedData { cells: IMainCellDto[], viewType: string; metadata?: NotebookDocumentMetadata; - attachedEditor?: { id: string; selections: number[]; } + attachedEditor?: { id: string; selections: number[]; visibleRanges: ICellRange[] } } export interface INotebookEditorAddData { id: string; documentUri: UriComponents; selections: number[]; + visibleRanges: ICellRange[]; } export interface INotebookDocumentsAndEditorsDelta { @@ -1670,7 +1684,8 @@ export interface ExtHostNotebookShape { $onDidReceiveMessage(editorId: string, rendererId: string | undefined, message: unknown): void; $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent, isDirty: boolean): void; $acceptModelSaved(uriComponents: UriComponents): void; - $acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void; + $acceptEditorPropertiesChanged(id: string, data: INotebookEditorPropertiesChangeData): void; + $acceptDocumentPropertiesChanged(uriComponents: UriComponents, data: INotebookDocumentPropertiesChangeData): void; $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): void; $undoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise; $redoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise; diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 379d6c285bd..8aeeb8e0b98 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -14,7 +14,7 @@ import { ISplice } from 'vs/base/common/sequence'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { CellKind, ExtHostNotebookShape, ICommandDto, IMainContext, IModelAddedData, INotebookDocumentsAndEditorsDelta, INotebookEditorPropertiesChangeData, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol'; +import { CellKind, ExtHostNotebookShape, ICommandDto, IMainContext, IModelAddedData, INotebookDocumentPropertiesChangeData, INotebookDocumentsAndEditorsDelta, INotebookEditorPropertiesChangeData, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol'; import { ILogService } from 'vs/platform/log/common/log'; import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDocumentsAndEditors, IExtHostModelAddedData } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; @@ -633,6 +633,20 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook selection?: vscode.NotebookCell; + private _visibleRanges: vscode.NotebookCellRange[] = []; + + get visibleRanges() { + return this._visibleRanges; + } + + set visibleRanges(_range: vscode.NotebookCellRange[]) { + throw readonly('visibleRanges'); + } + + _acceptVisibleRanges(value: vscode.NotebookCellRange[]): void { + this._visibleRanges = value; + } + private _active: boolean = false; get active(): boolean { return this._active; @@ -886,6 +900,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private readonly _commandsConverter: CommandsConverter; private readonly _onDidChangeNotebookEditorSelection = new Emitter(); readonly onDidChangeNotebookEditorSelection = this._onDidChangeNotebookEditorSelection.event; + private readonly _onDidChangeNotebookEditorVisibleRanges = new Emitter(); + readonly onDidChangeNotebookEditorVisibleRanges = this._onDidChangeNotebookEditorVisibleRanges.event; private readonly _onDidChangeNotebookCells = new Emitter(); readonly onDidChangeNotebookCells = this._onDidChangeNotebookCells.event; private readonly _onDidChangeCellOutputs = new Emitter(); @@ -1284,8 +1300,31 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } } - $acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void { - this.logService.debug('ExtHostNotebook#$acceptEditorPropertiesChanged', uriComponents.path, data); + $acceptEditorPropertiesChanged(id: string, data: INotebookEditorPropertiesChangeData): void { + this.logService.debug('ExtHostNotebook#$acceptEditorPropertiesChanged', id, data); + + let editor: { editor: ExtHostNotebookEditor; } | undefined; + this._editors.forEach(e => { + if (e.editor.id === id) { + editor = e; + } + }); + + if (!editor) { + return; + } + + if (data.visibleRanges) { + editor.editor._acceptVisibleRanges(data.visibleRanges.ranges); + this._onDidChangeNotebookEditorVisibleRanges.fire({ + notebookEditor: editor.editor, + visibleRanges: editor.editor.visibleRanges + }); + } + } + + $acceptDocumentPropertiesChanged(uriComponents: UriComponents, data: INotebookDocumentPropertiesChangeData): void { + this.logService.debug('ExtHostNotebook#$acceptDocumentPropertiesChanged', uriComponents.path, data); const editor = this._getEditorFromURI(uriComponents); if (!editor) { @@ -1306,6 +1345,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }); } + if (data.metadata) { editor.editor.notebookData.notebookDocument.metadata = { ...notebookDocumentMetadataDefaults, @@ -1314,7 +1354,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } } - private _createExtHostEditor(document: ExtHostNotebookDocument, editorId: string, selections: number[]) { + private _createExtHostEditor(document: ExtHostNotebookDocument, editorId: string, selections: number[], visibleRanges: vscode.NotebookCellRange[]) { const revivedUri = document.uri; let webComm = this._webviewComm.get(editorId); @@ -1339,6 +1379,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN editor.selection = undefined; } + editor._acceptVisibleRanges(visibleRanges); + this._editors.get(editorId)?.editor.dispose(); this._editors.set(editorId, { editor }); } @@ -1423,7 +1465,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN // create editor if populated if (modelData.attachedEditor) { - this._createExtHostEditor(document, modelData.attachedEditor.id, modelData.attachedEditor.selections); + this._createExtHostEditor(document, modelData.attachedEditor.id, modelData.attachedEditor.selections, modelData.attachedEditor.visibleRanges); editorChanged = true; } } @@ -1445,7 +1487,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const document = this._documents.get(revivedUri); if (document) { - this._createExtHostEditor(document, editorModelData.id, editorModelData.selections); + this._createExtHostEditor(document, editorModelData.id, editorModelData.selections, editorModelData.visibleRanges); editorChanged = true; } } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts index 6b24b18e9a2..aa7711d6817 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { INotebookEditor, INotebookEditorMouseEvent, ICellRange, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, INotebookEditorMouseEvent, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import * as DOM from 'vs/base/browser/dom'; import { CellFoldingState, FoldingModel } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts index fade3589aa0..3675729c2dd 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts @@ -8,9 +8,8 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { TrackedRangeStickiness } from 'vs/editor/common/model'; import { FoldingRegion, FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; import { IFoldingRangeData, sanitizeRanges } from 'vs/editor/contrib/folding/syntaxRangeProvider'; -import { ICellRange } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; type RegionFilter = (r: FoldingRegion) => boolean; type RegionFilterWithLevel = (r: FoldingRegion, level: number) => boolean; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index a3b61443510..05194d40170 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -22,7 +22,7 @@ import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/outpu import { RunStateRenderer, TimerRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, IProcessedOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata, INotebookKernelInfo, IEditor, INotebookKernelInfo2, IInsetRenderOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, IProcessedOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata, INotebookKernelInfo, IEditor, INotebookKernelInfo2, IInsetRenderOutput, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IMenu } from 'vs/platform/actions/common/actions'; @@ -465,6 +465,8 @@ export interface INotebookCellList { onDidScroll: Event; onDidChangeFocus: Event>; onDidChangeContentHeight: Event; + onDidChangeVisibleRanges: Event; + visibleRanges: ICellRange[]; scrollTop: number; scrollHeight: number; scrollLeft: number; @@ -517,6 +519,7 @@ export interface BaseCellRenderTemplate { contextKeyService: IContextKeyService; container: HTMLElement; cellContainer: HTMLElement; + decorationContainer: HTMLElement; toolbar: ToolBar; deleteToolbar: ToolBar; betweenCellToolbar: ToolBar; @@ -622,19 +625,20 @@ export interface CellViewModelStateChangeEvent { outputIsHoveredChanged?: boolean; } -/** - * [start, end] - */ -export interface ICellRange { - /** - * zero based index - */ - start: number; +export function cellRangesEqual(a: ICellRange[], b: ICellRange[]) { + a = reduceCellRanges(a); + b = reduceCellRanges(b); + if (a.length !== b.length) { + return false; + } - /** - * zero based index - */ - end: number; + for (let i = 0; i < a.length; i++) { + if (a[i].start !== b[i].start || a[i].end !== b[i].end) { + return false; + } + } + + return true; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index f12b346c3e6..e65eca7edf8 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -38,7 +38,7 @@ import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { PANEL_BORDER } from 'vs/workbench/common/theme'; import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugToolBar'; import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, INotebookCellList, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFocusMode, ICellViewModel, INotebookCellList, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { NotebookKernelProviderAssociations, notebookKernelProviderAssociationsSettingId } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; @@ -51,7 +51,7 @@ import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewMod import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, CellToolbarLocKey, IInsetRenderOutput, INotebookKernelInfo, INotebookKernelInfo2, INotebookKernelInfoDto, IProcessedOutput, NotebookCellRunState, NotebookRunState, ShowCellStatusbarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellToolbarLocKey, ICellRange, IInsetRenderOutput, INotebookKernelInfo, INotebookKernelInfo2, INotebookKernelInfoDto, IProcessedOutput, NotebookCellRunState, NotebookRunState, ShowCellStatusbarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { editorGutterModifiedBackground } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; @@ -211,6 +211,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._cursorNavigationMode = v; } + private readonly _onDidChangeVisibleRanges = this._register(new Emitter()); + onDidChangeVisibleRanges: Event = this._onDidChangeVisibleRanges.event; + + get visibleRanges() { + return this._list?.visibleRanges || []; + } + readonly isEmbedded: boolean; constructor( @@ -521,6 +528,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._onDidScroll.fire(e); })); + this._register(this._list.onDidChangeVisibleRanges(() => { + this._onDidChangeVisibleRanges.fire(); + })); + const widgetFocusTracker = DOM.trackFocus(this.getDomNode()); this._register(widgetFocusTracker); this._register(widgetFocusTracker.onDidFocus(() => this._onDidFocusEmitter.fire())); diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 76c404fe169..01984839042 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -19,9 +19,9 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellRange, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState, CellFocusMode, BaseCellRenderTemplate, NOTEBOOK_CELL_LIST_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState, CellFocusMode, BaseCellRenderTemplate, NOTEBOOK_CELL_LIST_FOCUSED, cellRangesEqual } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { diff, IProcessedOutput, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { diff, IProcessedOutput, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { clamp } from 'vs/base/common/numbers'; import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; @@ -54,12 +54,32 @@ export class NotebookCellList extends WorkbenchList implements ID private _hiddenRangeIds: string[] = []; private hiddenRangesPrefixSum: PrefixSumComputer | null = null; + private readonly _onDidChangeVisibleRanges = new Emitter(); + + onDidChangeVisibleRanges: Event = this._onDidChangeVisibleRanges.event; + private _visibleRanges: ICellRange[] = []; + + get visibleRanges() { + return this._visibleRanges; + } + + set visibleRanges(ranges: ICellRange[]) { + if (cellRangesEqual(this._visibleRanges, ranges)) { + return; + } + + this._visibleRanges = ranges; + this._onDidChangeVisibleRanges.fire(); + } + private _isDisposed = false; get isDisposed() { return this._isDisposed; } + private _isInLayout: boolean = false; + private readonly _focusNextPreviousDelegate: IFocusNextPreviousDelegate; constructor( @@ -152,6 +172,86 @@ export class NotebookCellList extends WorkbenchList implements ID focus.focusMode = CellFocusMode.Editor; } })); + + // update visibleRanges + const updateVisibleRanges = () => { + if (!this.view.length) { + return; + } + + const top = this.getViewScrollTop(); + const bottom = this.getViewScrollBottom(); + const topViewIndex = clamp(this.view.indexAt(top), 0, this.view.length - 1); + const topElement = this.view.element(topViewIndex); + const topModelIndex = this._viewModel!.getCellIndex(topElement); + const bottomViewIndex = clamp(this.view.indexAt(bottom), 0, this.view.length - 1); + const bottomElement = this.view.element(bottomViewIndex); + const bottomModelIndex = this._viewModel!.getCellIndex(bottomElement); + + if (bottomModelIndex - topModelIndex === bottomViewIndex - topViewIndex) { + this.visibleRanges = [{ start: topModelIndex, end: bottomModelIndex }]; + } else { + let stack: number[] = []; + const ranges: ICellRange[] = []; + // there are hidden ranges + let index = topViewIndex; + let modelIndex = topModelIndex; + + while (index <= bottomViewIndex) { + const accu = this.hiddenRangesPrefixSum!.getAccumulatedValue(index); + if (accu === modelIndex + 1) { + // no hidden area after it + if (stack.length) { + if (stack[stack.length - 1] === modelIndex - 1) { + ranges.push({ start: stack[stack.length - 1], end: modelIndex }); + } else { + ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] }); + } + } + + stack.push(modelIndex); + index++; + modelIndex++; + } else { + // there are hidden ranges after it + if (stack.length) { + if (stack[stack.length - 1] === modelIndex - 1) { + ranges.push({ start: stack[stack.length - 1], end: modelIndex }); + } else { + ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] }); + } + } + + stack.push(modelIndex); + index++; + modelIndex = accu; + } + } + + if (stack.length) { + ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] }); + } + + this.visibleRanges = reduceCellRanges(ranges); + } + }; + + this._localDisposableStore.add(this.view.onDidChangeContentHeight(() => { + if (this._isInLayout) { + DOM.scheduleAtNextAnimationFrame(() => { + updateVisibleRanges(); + }); + } + updateVisibleRanges(); + })); + this._localDisposableStore.add(this.view.onDidScroll(() => { + if (this._isInLayout) { + DOM.scheduleAtNextAnimationFrame(() => { + updateVisibleRanges(); + }); + } + updateVisibleRanges(); + })); } elementAt(position: number): ICellViewModel | undefined { @@ -933,6 +1033,12 @@ export class NotebookCellList extends WorkbenchList implements ID } } + layout(height?: number, width?: number): void { + this._isInLayout = true; + super.layout(height, width); + this._isInLayout = false; + } + dispose() { this._isDisposed = true; this._viewModelStore.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index d5aeb2571af..9be48cf297a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -383,7 +383,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR const container = DOM.append(rootContainer, DOM.$('.cell-inner-container')); const disposables = new DisposableStore(); const contextKeyService = disposables.add(this.contextKeyServiceProvider(container)); - + const decorationContainer = DOM.append(container, $('.cell-decoration')); const titleToolbarContainer = DOM.append(container, $('.cell-title-toolbar')); const toolbar = disposables.add(this.createToolbar(titleToolbarContainer)); const deleteToolbar = disposables.add(this.createToolbar(titleToolbarContainer, 'cell-delete-toolbar')); @@ -412,6 +412,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR expandButton, contextKeyService, container, + decorationContainer, cellContainer: innerContent, editorPart, editorContainer, @@ -631,7 +632,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const container = DOM.append(rootContainer, DOM.$('.cell-inner-container')); const disposables = new DisposableStore(); const contextKeyService = disposables.add(this.contextKeyServiceProvider(container)); - + const decorationContainer = DOM.append(container, $('.cell-decoration')); DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-top')); const titleToolbarContainer = DOM.append(container, $('.cell-title-toolbar')); const toolbar = disposables.add(this.createToolbar(titleToolbarContainer)); @@ -693,6 +694,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende expandButton, contextKeyService, container, + decorationContainer, cellContainer, cellRunState, progressBar, diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 0c02d11cf09..64db881ca8e 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -17,13 +17,13 @@ import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { CellEditState, CellFindMatch, ICellRange, ICellViewModel, NotebookLayoutInfo, IEditableCellViewModel, INotebookDeltaDecoration } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFindMatch, ICellViewModel, NotebookLayoutInfo, IEditableCellViewModel, INotebookDeltaDecoration } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { NotebookEventDispatcher, NotebookMetadataChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellFoldingState, EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, NotebookCellMetadata, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, INotebookSearchOptions, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer'; diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 8300e9f3950..c6be8ff48ab 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -685,10 +685,27 @@ export interface NotebookDocumentBackupData { readonly mtime?: number; } +/** + * [start, end] + */ +export interface ICellRange { + /** + * zero based index + */ + start: number; + + /** + * zero based index + */ + end: number; +} + export interface IEditor extends editorCommon.ICompositeCodeEditor { readonly onDidChangeModel: Event; readonly onDidFocusEditorWidget: Event; + readonly onDidChangeVisibleRanges: Event; isNotebookEditor: boolean; + visibleRanges: ICellRange[]; uri?: URI; textModel?: NotebookTextModel; getId(): string; diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index 70d9b950c3f..b45c690f1af 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -6,14 +6,14 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, NotebookCellMetadata, diff } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, diff, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebook, TestCell, NotebookEditorTestModel, setupInstantiationService } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { TrackedRangeStickiness } from 'vs/editor/common/model'; -import { reduceCellRanges, ICellRange } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { reduceCellRanges } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; suite('NotebookViewModel', () => { diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 7953687f0ce..3bb3c92a63d 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -12,13 +12,13 @@ import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { Range } from 'vs/editor/common/core/range'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { EditorModel } from 'vs/workbench/common/editor'; -import { ICellRange, ICellViewModel, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, INotebookDeltaDecoration, INotebookEditorCreationOptions, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellViewModel, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, INotebookDeltaDecoration, INotebookEditorCreationOptions, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, CellUri, INotebookEditorModel, IProcessedOutput, NotebookCellMetadata, INotebookKernelInfo, IInsetRenderOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellUri, INotebookEditorModel, IProcessedOutput, NotebookCellMetadata, INotebookKernelInfo, IInsetRenderOutput, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { ICompositeCodeEditor, IEditor } from 'vs/editor/common/editorCommon'; import { NotImplementedError } from 'vs/base/common/errors'; @@ -65,6 +65,7 @@ export class TestNotebookEditor implements INotebookEditor { constructor( ) { } + setOptions(options: NotebookEditorOptions | undefined): Promise { throw new Error('Method not implemented.'); } @@ -78,7 +79,8 @@ export class TestNotebookEditor implements INotebookEditor { onDidChangeActiveCell: Event = new Emitter().event; onDidScroll = new Emitter().event; onWillDispose = new Emitter().event; - + onDidChangeVisibleRanges: Event = new Emitter().event; + visibleRanges: ICellRange[] = []; uri?: URI | undefined; textModel?: NotebookTextModel | undefined; diff --git a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts index 2911cf0ab90..70070cd869b 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts @@ -81,7 +81,8 @@ suite('NotebookCell#Document', function () { addedEditors: [{ documentUri: notebookUri, id: '_notebook_editor_0', - selections: [0] + selections: [0], + visibleRanges: [] }] }); extHostNotebooks.$acceptDocumentAndEditorsDelta({ newActiveEditor: '_notebook_editor_0' }); diff --git a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts index a25ee16c3f4..f2ca94c32d5 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts @@ -74,7 +74,8 @@ suite('NotebookConcatDocument', function () { { documentUri: notebookUri, id: '_notebook_editor_0', - selections: [0] + selections: [0], + visibleRanges: [] } ] });