diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index d4f775ccc45..0b792160eab 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1294,11 +1294,17 @@ declare module 'vscode' { * The range will always be revealed in the center of the viewport. */ InCenter = 1, + /** * If the range is outside the viewport, it will be revealed in the center of the viewport. * Otherwise, it will be revealed with as little scrolling as possible. */ InCenterIfOutsideViewport = 2, + + /** + * The range will always be revealed at the top of the viewport. + */ + AtTop = 3 } export interface NotebookEditor { diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 4e361ea1fc4..0b4b7f3f55d 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -646,14 +646,13 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo switch (revealType) { case NotebookEditorRevealType.Default: - notebookEditor.revealInView(cell); - break; + return notebookEditor.revealCellRangeInView(range); case NotebookEditorRevealType.InCenter: - notebookEditor.revealInCenter(cell); - break; + return notebookEditor.revealInCenter(cell); case NotebookEditorRevealType.InCenterIfOutsideViewport: - notebookEditor.revealInCenterIfOutsideViewport(cell); - break; + return notebookEditor.revealInCenterIfOutsideViewport(cell); + case NotebookEditorRevealType.AtTop: + return notebookEditor.revealInViewAtTop(cell); default: break; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a5f6d3ed535..4f24459ce50 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -754,6 +754,7 @@ export enum NotebookEditorRevealType { Default = 0, InCenter = 1, InCenterIfOutsideViewport = 2, + AtTop = 3 } export interface INotebookDocumentShowOptions { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index e153af8aa46..fbd6592c661 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2858,7 +2858,8 @@ export enum NotebookCellStatusBarAlignment { export enum NotebookEditorRevealType { Default = 0, InCenter = 1, - InCenterIfOutsideViewport = 2 + InCenterIfOutsideViewport = 2, + AtTop = 3 } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index d06ec99a6b2..b5ed5c132a1 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -500,11 +500,21 @@ export interface INotebookEditor extends IEditor, ICommonNotebookEditor { */ triggerScroll(event: IMouseWheelEvent): void; + /** + * The range will be revealed with as little scrolling as possible. + */ + revealCellRangeInView(range: ICellRange): void; + /** * Reveal cell into viewport. */ revealInView(cell: ICellViewModel): void; + /** + * Reveal cell into the top of viewport. + */ + revealInViewAtTop(cell: ICellViewModel): void; + /** * Reveal cell into viewport center. */ @@ -614,7 +624,9 @@ export interface INotebookCellList { focusElement(element: ICellViewModel): void; selectElement(element: ICellViewModel): void; getFocusedElements(): ICellViewModel[]; + revealElementsInView(range: ICellRange): void; revealElementInView(element: ICellViewModel): void; + revealElementInViewAtTop(element: ICellViewModel): void; revealElementInCenterIfOutsideViewport(element: ICellViewModel): void; revealElementInCenter(element: ICellViewModel): void; revealElementInCenterIfOutsideViewportAsync(element: ICellViewModel): Promise; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index f0f93c38438..036f4cb0f25 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -1226,10 +1226,18 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor // this.viewModel!.selectionHandles = [cell.handle]; } + revealCellRangeInView(range: ICellRange) { + return this._list.revealElementsInView(range); + } + revealInView(cell: ICellViewModel) { this._list.revealElementInView(cell); } + revealInViewAtTop(cell: ICellViewModel) { + this._list.revealElementInViewAtTop(cell); + } + revealInCenterIfOutsideViewport(cell: ICellViewModel) { this._list.revealElementInCenterIfOutsideViewport(cell); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 8de660ff484..8d0c6a5422a 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -547,6 +547,22 @@ export class NotebookCellList extends WorkbenchList implements ID return viewIndexInfo.index; } + private _getViewIndexUpperBound2(modelIndex: number) { + if (!this.hiddenRangesPrefixSum) { + return modelIndex; + } + + const viewIndexInfo = this.hiddenRangesPrefixSum.getIndexOf(modelIndex); + + if (viewIndexInfo.remainder !== 0) { + if (modelIndex >= this.hiddenRangesPrefixSum.getTotalValue()) { + return modelIndex - (this.hiddenRangesPrefixSum.getTotalValue() - this.hiddenRangesPrefixSum.getCount()); + } + } + + return viewIndexInfo.index; + } + focusElement(cell: ICellViewModel) { const index = this._getViewIndexUpperBound(cell); @@ -587,6 +603,46 @@ export class NotebookCellList extends WorkbenchList implements ID super.setFocus(indexes, browserEvent); } + revealElementsInView(range: ICellRange) { + const startIndex = this._getViewIndexUpperBound2(range.start); + + if (startIndex < 0) { + return; + } + + const endIndex = this._getViewIndexUpperBound2(range.end); + + const scrollTop = this.getViewScrollTop(); + const wrapperBottom = this.getViewScrollBottom(); + const elementTop = this.view.elementTop(startIndex); + if (elementTop >= scrollTop + && elementTop < wrapperBottom) { + // start element is visible + // check end + + const endElementTop = this.view.elementTop(endIndex); + const endElementHeight = this.view.elementHeight(endIndex); + + if (endElementTop >= wrapperBottom) { + return this._revealInternal(startIndex, false, CellRevealPosition.Top); + } + + if (endElementTop < wrapperBottom) { + // end element partially visible + if (endElementTop + endElementHeight - wrapperBottom < elementTop - scrollTop) { + // there is enough space to just scroll up a little bit to make the end element visible + return this.view.setScrollTop(scrollTop + endElementTop + endElementHeight - wrapperBottom); + } else { + // don't even try it + return this._revealInternal(startIndex, false, CellRevealPosition.Top); + } + } + } + + + this._revealInView(startIndex); + } + revealElementInView(cell: ICellViewModel) { const index = this._getViewIndexUpperBound(cell); @@ -595,6 +651,14 @@ export class NotebookCellList extends WorkbenchList implements ID } } + revealElementInViewAtTop(cell: ICellViewModel) { + const index = this._getViewIndexUpperBound(cell); + + if (index >= 0) { + this._revealInternal(index, false, CellRevealPosition.Top); + } + } + revealElementInCenterIfOutsideViewport(cell: ICellViewModel) { const index = this._getViewIndexUpperBound(cell); diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index ff58b2ef751..9ee463274b7 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -275,7 +275,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const mainCells = cells.map(cell => { const cellHandle = this._cellhandlePool++; - const cellUri = CellUri.generate(this.uri, cellHandle); + const cellUri = CellUri.generate(this.uri, this.viewType, cellHandle); return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata, this.transientOptions, this._modelService); }); @@ -425,7 +425,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel // prepare add const cells = cellDtos.map(cellDto => { const cellHandle = this._cellhandlePool++; - const cellUri = CellUri.generate(this.uri, cellHandle); + const cellUri = CellUri.generate(this.uri, this.viewType, cellHandle); const cell = new NotebookCellTextModel( cellUri, cellHandle, cellDto.source, cellDto.language, cellDto.cellKind, cellDto.outputs || [], cellDto.metadata, this.transientOptions, diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index a2f99da121d..cbc5b9938ee 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -467,7 +467,7 @@ export function getCellUndoRedoComparisonKey(uri: URI) { return uri.toString(); } - return data.notebook.toString(); + return `vt=${data.viewType}&uri=data.notebook.toString()`; } @@ -477,10 +477,11 @@ export namespace CellUri { const _regex = /^ch(\d{7,})/; - export function generate(notebook: URI, handle: number): URI { + export function generate(notebook: URI, viewType: string, handle: number): URI { return notebook.with({ scheme, - fragment: `ch${handle.toString().padStart(7, '0')}${notebook.scheme !== Schemas.file ? notebook.scheme : ''}` + fragment: `ch${handle.toString().padStart(7, '0')}${notebook.scheme !== Schemas.file ? notebook.scheme : ''}`, + query: `vt=${viewType}` }); } @@ -492,7 +493,7 @@ export namespace CellUri { }); } - export function parse(cell: URI): { notebook: URI, handle: number } | undefined { + export function parse(cell: URI): { notebook: URI, handle: number, viewType: string } | undefined { if (cell.scheme !== scheme) { return undefined; } @@ -505,8 +506,10 @@ export namespace CellUri { handle, notebook: cell.with({ scheme: cell.fragment.substr(match[0].length) || Schemas.file, - fragment: null - }) + fragment: null, + query: null + }), + viewType: cell.query.substr('vt='.length) }; } } diff --git a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts index 38a91a236ce..2104ef1586d 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts @@ -336,11 +336,12 @@ suite('CellUri', function () { const nb = URI.parse('foo:///bar/følder/file.nb'); const id = 17; - const data = CellUri.generate(nb, id); + const data = CellUri.generate(nb, 'test', id); const actual = CellUri.parse(data); assert.ok(Boolean(actual)); assert.equal(actual?.handle, id); assert.equal(actual?.notebook.toString(), nb.toString()); + assert.equal(actual?.viewType, 'test'); }); test('parse, generate (foo-scheme)', function () { @@ -348,10 +349,11 @@ suite('CellUri', function () { const nb = URI.parse('foo:///bar/følder/file.nb'); const id = 17; - const data = CellUri.generate(nb, id); + const data = CellUri.generate(nb, 'test', id); const actual = CellUri.parse(data); assert.ok(Boolean(actual)); assert.equal(actual?.handle, id); assert.equal(actual?.notebook.toString(), nb.toString()); + assert.equal(actual?.viewType, 'test'); }); }); diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index fa820f8fb9d..0d12ec2f4b6 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -47,7 +47,7 @@ export class TestCell extends NotebookCellTextModel { outputs: IProcessedOutput[], modelService: ITextModelService ) { - super(CellUri.generate(URI.parse('test:///fake/notebook'), handle), handle, source, language, cellKind, outputs, undefined, { transientMetadata: {}, transientOutputs: false }, modelService); + super(CellUri.generate(URI.parse('test:///fake/notebook'), viewType, handle), handle, source, language, cellKind, outputs, undefined, { transientMetadata: {}, transientOutputs: false }, modelService); } } diff --git a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts index 9b3a4c2ec2a..e2d51f7cceb 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts @@ -62,7 +62,7 @@ suite('NotebookCell#Document', function () { versionId: 0, cells: [{ handle: 0, - uri: CellUri.generate(notebookUri, 0), + uri: CellUri.generate(notebookUri, 'test', 0), source: ['### Heading'], eol: '\n', language: 'markdown', @@ -70,7 +70,7 @@ suite('NotebookCell#Document', function () { outputs: [], }, { handle: 1, - uri: CellUri.generate(notebookUri, 1), + uri: CellUri.generate(notebookUri, 'test', 1), source: ['console.log("aaa")', 'console.log("bbb")'], eol: '\n', language: 'javascript', @@ -167,7 +167,7 @@ suite('NotebookCell#Document', function () { kind: NotebookCellsChangeType.ModelChange, changes: [[0, 0, [{ handle: 2, - uri: CellUri.generate(notebookUri, 2), + uri: CellUri.generate(notebookUri, 'test', 2), source: ['Hello', 'World', 'Hello World!'], eol: '\n', language: 'test', @@ -175,7 +175,7 @@ suite('NotebookCell#Document', function () { outputs: [], }, { handle: 3, - uri: CellUri.generate(notebookUri, 3), + uri: CellUri.generate(notebookUri, 'test', 3), source: ['Hallo', 'Welt', 'Hallo Welt!'], eol: '\n', language: 'test', @@ -284,7 +284,7 @@ suite('NotebookCell#Document', function () { kind: NotebookCellsChangeType.ModelChange, changes: [[0, 0, [{ handle: 2, - uri: CellUri.generate(notebookUri, 2), + uri: CellUri.generate(notebookUri, 'test', 2), source: ['Hello', 'World', 'Hello World!'], eol: '\n', language: 'test', @@ -292,7 +292,7 @@ suite('NotebookCell#Document', function () { outputs: [], }, { handle: 3, - uri: CellUri.generate(notebookUri, 3), + uri: CellUri.generate(notebookUri, 'test', 3), source: ['Hallo', 'Welt', 'Hallo Welt!'], eol: '\n', language: 'test', diff --git a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts index e49a0c930ef..776543c4fda 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts @@ -61,7 +61,7 @@ suite('NotebookConcatDocument', function () { viewType: 'test', cells: [{ handle: 0, - uri: CellUri.generate(notebookUri, 0), + uri: CellUri.generate(notebookUri, 'test', 0), source: ['### Heading'], eol: '\n', language: 'markdown', @@ -122,8 +122,8 @@ suite('NotebookConcatDocument', function () { test('contains', function () { - const cellUri1 = CellUri.generate(notebook.uri, 1); - const cellUri2 = CellUri.generate(notebook.uri, 2); + const cellUri1 = CellUri.generate(notebook.uri, 'test', 1); + const cellUri2 = CellUri.generate(notebook.uri, 'test', 2); extHostNotebooks.$acceptModelChanged(notebookUri, { versionId: notebook.notebookDocument.version + 1, @@ -169,7 +169,7 @@ suite('NotebookConcatDocument', function () { kind: NotebookCellsChangeType.ModelChange, changes: [[0, 0, [{ handle: 1, - uri: CellUri.generate(notebook.uri, 1), + uri: CellUri.generate(notebook.uri, 'test', 1), source: ['Hello', 'World', 'Hello World!'], eol: '\n', language: 'test', @@ -177,7 +177,7 @@ suite('NotebookConcatDocument', function () { outputs: [], }, { handle: 2, - uri: CellUri.generate(notebook.uri, 2), + uri: CellUri.generate(notebook.uri, 'test', 2), source: ['Hallo', 'Welt', 'Hallo Welt!'], eol: '\n', language: 'test', @@ -214,7 +214,7 @@ suite('NotebookConcatDocument', function () { kind: NotebookCellsChangeType.ModelChange, changes: [[0, 0, [{ handle: 1, - uri: CellUri.generate(notebook.uri, 1), + uri: CellUri.generate(notebook.uri, 'test', 1), source: ['Hello', 'World', 'Hello World!'], eol: '\n', language: 'test', @@ -241,7 +241,7 @@ suite('NotebookConcatDocument', function () { kind: NotebookCellsChangeType.ModelChange, changes: [[1, 0, [{ handle: 2, - uri: CellUri.generate(notebook.uri, 2), + uri: CellUri.generate(notebook.uri, 'test', 2), source: ['Hallo', 'Welt', 'Hallo Welt!'], eol: '\n', language: 'test', @@ -292,7 +292,7 @@ suite('NotebookConcatDocument', function () { kind: NotebookCellsChangeType.ModelChange, changes: [[0, 0, [{ handle: 1, - uri: CellUri.generate(notebook.uri, 1), + uri: CellUri.generate(notebook.uri, 'test', 1), source: ['Hello', 'World', 'Hello World!'], eol: '\n', language: 'test', @@ -300,7 +300,7 @@ suite('NotebookConcatDocument', function () { outputs: [], }, { handle: 2, - uri: CellUri.generate(notebook.uri, 2), + uri: CellUri.generate(notebook.uri, 'test', 2), source: ['Hallo', 'Welt', 'Hallo Welt!'], eol: '\n', language: 'test', @@ -350,7 +350,7 @@ suite('NotebookConcatDocument', function () { kind: NotebookCellsChangeType.ModelChange, changes: [[0, 0, [{ handle: 1, - uri: CellUri.generate(notebook.uri, 1), + uri: CellUri.generate(notebook.uri, 'test', 1), source: ['fooLang-document'], eol: '\n', language: 'fooLang', @@ -358,7 +358,7 @@ suite('NotebookConcatDocument', function () { outputs: [], }, { handle: 2, - uri: CellUri.generate(notebook.uri, 2), + uri: CellUri.generate(notebook.uri, 'test', 2), source: ['barLang-document'], eol: '\n', language: 'barLang', @@ -384,7 +384,7 @@ suite('NotebookConcatDocument', function () { kind: NotebookCellsChangeType.ModelChange, changes: [[2, 0, [{ handle: 3, - uri: CellUri.generate(notebook.uri, 3), + uri: CellUri.generate(notebook.uri, 'test', 3), source: ['barLang-document2'], eol: '\n', language: 'barLang', @@ -422,7 +422,7 @@ suite('NotebookConcatDocument', function () { kind: NotebookCellsChangeType.ModelChange, changes: [[0, 0, [{ handle: 1, - uri: CellUri.generate(notebook.uri, 1), + uri: CellUri.generate(notebook.uri, 'test', 1), source: ['Hello', 'World', 'Hello World!'], eol: '\n', language: 'test', @@ -430,7 +430,7 @@ suite('NotebookConcatDocument', function () { outputs: [], }, { handle: 2, - uri: CellUri.generate(notebook.uri, 2), + uri: CellUri.generate(notebook.uri, 'test', 2), source: ['Hallo', 'Welt', 'Hallo Welt!'], eol: '\n', language: 'test', @@ -479,7 +479,7 @@ suite('NotebookConcatDocument', function () { kind: NotebookCellsChangeType.ModelChange, changes: [[0, 0, [{ handle: 1, - uri: CellUri.generate(notebook.uri, 1), + uri: CellUri.generate(notebook.uri, 'test', 1), source: ['Hello', 'World', 'Hello World!'], eol: '\n', language: 'test', @@ -487,7 +487,7 @@ suite('NotebookConcatDocument', function () { outputs: [], }, { handle: 2, - uri: CellUri.generate(notebook.uri, 2), + uri: CellUri.generate(notebook.uri, 'test', 2), source: ['Hallo', 'Welt', 'Hallo Welt!'], eol: '\n', language: 'test', @@ -520,7 +520,7 @@ suite('NotebookConcatDocument', function () { kind: NotebookCellsChangeType.ModelChange, changes: [[0, 0, [{ handle: 1, - uri: CellUri.generate(notebook.uri, 1), + uri: CellUri.generate(notebook.uri, 'test', 1), source: ['Hello', 'World', 'Hello World!'], eol: '\n', language: 'test', @@ -528,7 +528,7 @@ suite('NotebookConcatDocument', function () { outputs: [], }, { handle: 2, - uri: CellUri.generate(notebook.uri, 2), + uri: CellUri.generate(notebook.uri, 'test', 2), source: ['Hallo', 'Welt', 'Hallo Welt!'], eol: '\n', language: 'test', @@ -558,7 +558,7 @@ suite('NotebookConcatDocument', function () { kind: NotebookCellsChangeType.ModelChange, changes: [[0, 0, [{ handle: 1, - uri: CellUri.generate(notebook.uri, 1), + uri: CellUri.generate(notebook.uri, 'test', 1), source: ['Hello', 'World', 'Hello World!'], eol: '\n', language: 'test', @@ -566,7 +566,7 @@ suite('NotebookConcatDocument', function () { outputs: [], }, { handle: 2, - uri: CellUri.generate(notebook.uri, 2), + uri: CellUri.generate(notebook.uri, 'test', 2), source: ['Hallo', 'Welt', 'Hallo Welt!'], eol: '\n', language: 'test',