diff --git a/.github/commands.yml b/.github/commands.yml index d2bac8edb1fb3..64fdf683bfe99 100644 --- a/.github/commands.yml +++ b/.github/commands.yml @@ -1,154 +1,12 @@ -# { -# perform: true, -# commands: [ -# { -# type: 'comment', -# name: 'findDuplicates', -# allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'], -# action: 'comment', -# comment: "Potential duplicates:\n${potentialDuplicates}" -# } -# ] -# } - { perform: true, commands: [ - { - type: 'comment', - name: 'question', - allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'], - action: 'updateLabels', - addLabel: '*question' - }, - { - type: 'label', - name: '*question', - allowTriggerByBot: true, - action: 'close', - comment: "Please ask your question on [StackOverflow](https://aka.ms/vscodestackoverflow). We have a great community over [there](https://aka.ms/vscodestackoverflow). They have already answered thousands of questions and are happy to answer yours as well. See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" - }, - { - type: 'label', - name: '*dev-question', - allowTriggerByBot: true, - action: 'close', - comment: "We have a great developer community [over on slack](https://aka.ms/vscode-dev-community) where extension authors help each other. This is a great place for you to ask questions and find support.\n\nHappy Coding!" - }, - { - type: 'label', - name: '*extension-candidate', - allowTriggerByBot: true, - action: 'close', - comment: "We try to keep VS Code lean and we think the functionality you're asking for is great for a VS Code extension. Maybe you can already find one that suits you in the [VS Code Marketplace](https://aka.ms/vscodemarketplace). Just in case, in a few simple steps you can get started [writing your own extension](https://aka.ms/vscodewritingextensions). See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" - }, - { - type: 'label', - name: '*not-reproducible', - allowTriggerByBot: true, - action: 'close', - comment: "We closed this issue because we are unable to reproduce the problem with the steps you describe. Chances are we've already fixed your problem in a recent version of VS Code. If not, please ask us to reopen the issue and provide us with more detail. Our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines might help you with that.\n\nHappy Coding!" - }, - { - type: 'label', - name: '*out-of-scope', - allowTriggerByBot: true, - action: 'close', - comment: "We closed this issue because we don't plan to address it in the foreseeable future. You can find more detailed information about our decision-making process [here](https://aka.ms/vscode-out-of-scope). If you disagree and feel that this issue is crucial: We are happy to listen and to reconsider.\n\nIf you wonder what we are up to, please see our [roadmap](https://aka.ms/vscoderoadmap) and [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nThanks for your understanding and happy coding!" - }, - { - type: 'comment', - name: 'causedByExtension', - allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'], - action: 'updateLabels', - addLabel: '*caused-by-extension' - }, - { - type: 'label', - name: '*caused-by-extension', - allowTriggerByBot: true, - action: 'close', - comment: "This issue is caused by an extension, please file it with the repository (or contact) the extension has linked in its overview in VS Code or the [marketplace](https://aka.ms/vscodemarketplace) for VS Code. See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" - }, - { - type: 'label', - name: '*as-designed', - allowTriggerByBot: true, - action: 'close', - comment: "The described behavior is how it is expected to work. If you disagree, please explain what is expected and what is not in more detail. See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" - }, - { - type: 'label', - name: '*english-please', - allowTriggerByBot: true, - action: 'close', - comment: "This issue is being closed because its description is not in English, that makes it hard for us to work on it. Please open a new issue with an English description. You might find [Bing Translator](https://www.bing.com/translator) useful." - }, - { - type: 'comment', - name: 'duplicate', - allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'], - action: 'updateLabels', - addLabel: '*duplicate' - }, - { - type: 'label', - name: '*duplicate', - allowTriggerByBot: true, - action: 'close', - comment: "Thanks for creating this issue! We figured it's covering the same as another one we already have. Thus, we closed this one as a duplicate. You can search for existing issues [here](https://aka.ms/vscodeissuesearch). See also our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" - }, - { - type: 'comment', - name: 'confirm', - allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'], - action: 'updateLabels', - addLabel: 'confirmed', - removeLabel: 'confirmation-pending' - }, - { - type: 'comment', - name: 'confirmationPending', - allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'], - action: 'updateLabels', - addLabel: 'confirmation-pending', - removeLabel: 'confirmed' - }, { type: 'comment', name: 'findDuplicates', allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'], action: 'comment', comment: "Potential duplicates:\n${potentialDuplicates}" - }, - { - type: 'comment', - name: 'needsMoreInfo', - allowUsers: ['cleidigh', 'usernamehw', 'gjsjohnmurray', 'IllusionMH'], - action: 'updateLabels', - addLabel: 'needs more info', - comment: "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines. Please take the time to review these and update the issue.\n\nHappy Coding!" - }, - { - type: 'label', - name: '~needs more info', - action: 'updateLabels', - addLabel: 'needs more info', - removeLabel: '~needs more info', - comment: "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines. Please take the time to review these and update the issue.\n\nHappy Coding!" - }, - { - type: 'comment', - name: 'a11ymas', - allowUsers: ['AccessibilityTestingTeam-TCS', 'dixitsonali95', 'Mohini78', 'ChitrarupaSharma', 'mspatil110', 'umasarath52', 'v-umnaik'], - action: 'updateLabels', - addLabel: 'a11ymas' - }, - { - type: 'label', - name: '*off-topic', - action: 'close', - comment: "Thanks for creating this issue. We think this issue is unactionable or unrelated to the goals of this project. Please follow our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" } ] } diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 35974e435355c..9a31b98bfe200 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -14,8 +14,8 @@ jobs: with: repository: 'JacksonKearl/vscode-triage-github-actions' ref: v2 - # - name: Run Commands - # uses: ./commands - # with: - # token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - # config-path: commands + - name: Run Commands + uses: ./commands + with: + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} + config-path: commands diff --git a/.github/workflows/copycat.yml b/.github/workflows/copycat.yml index 34fb291329a52..c4b706759f938 100644 --- a/.github/workflows/copycat.yml +++ b/.github/workflows/copycat.yml @@ -12,15 +12,15 @@ jobs: with: repository: 'JacksonKearl/vscode-triage-github-actions' ref: v2 - # - name: Run CopyCat (JacksonKearl/testissues) - # uses: ./copycat - # with: - # token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - # owner: JacksonKearl - # repo: testissues - # - name: Run CopyCat (chrmarti/testissues) - # uses: ./copycat - # with: - # token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} - # owner: chrmarti - # repo: testissues + - name: Run CopyCat (JacksonKearl/testissues) + uses: ./copycat + with: + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} + owner: JacksonKearl + repo: testissues + - name: Run CopyCat (chrmarti/testissues) + uses: ./copycat + with: + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} + owner: chrmarti + repo: testissues diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 3494753b30a15..df81f2c39084e 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -122,6 +122,10 @@ "name": "vs/workbench/contrib/preferences", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/notebook", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/quickaccess", "project": "vscode-workbench" diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 59a6281e372c5..0e00e4723974d 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -279,18 +279,32 @@ export class ListView implements ISpliceable, IDisposable { this.scrollableElement.triggerScrollFromMouseWheelEvent(browserEvent); } - updateElementHeight(index: number, size: number): void { + updateElementHeight(index: number, size: number, anchorIndex: number | null): void { if (this.items[index].size === size) { return; } const lastRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); - const heightDiff = index < lastRenderRange.start ? size - this.items[index].size : 0; + let heightDiff = 0; + + if (index < lastRenderRange.start) { + // do not scroll the viewport if resized element is out of viewport + heightDiff = size - this.items[index].size; + } else { + if (anchorIndex !== null && anchorIndex > index && anchorIndex <= lastRenderRange.end) { + // anchor in viewport + // resized elemnet in viewport and above the anchor + heightDiff = size - this.items[index].size; + } else { + heightDiff = 0; + } + } + this.rangeMap.splice(index, 1, [{ size: size }]); this.items[index].size = size; - this.render(lastRenderRange, this.lastRenderTop + heightDiff, this.lastRenderHeight, undefined, undefined, true); + this.render(lastRenderRange, Math.max(0, this.lastRenderTop + heightDiff), this.lastRenderHeight, undefined, undefined, true); this.eventuallyUpdateScrollDimensions(); @@ -1134,6 +1148,10 @@ export class ListView implements ISpliceable, IDisposable { return 0; } + if (!!this.virtualDelegate.hasDynamicHeight && !this.virtualDelegate.hasDynamicHeight(item.element)) { + return 0; + } + const size = item.size; if (!this.setRowHeight && item.row && item.row.domNode) { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index b2ee2f104f8b3..4d129c4bdb4a3 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -1314,7 +1314,7 @@ export class List implements ISpliceable, IDisposable { } updateElementHeight(index: number, size: number): void { - this.view.updateElementHeight(index, size); + this.view.updateElementHeight(index, size, null); } rerender(): void { diff --git a/src/vs/base/common/iterator.ts b/src/vs/base/common/iterator.ts index 537a1ac9028fb..7586b3c8507da 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -49,7 +49,15 @@ export namespace Iterable { return false; } - export function* map(iterable: Iterable, fn: (t: T) => R): IterableIterator { + export function* filter(iterable: Iterable, predicate: (t: T) => boolean): Iterable { + for (const element of iterable) { + if (predicate(element)) { + return yield element; + } + } + } + + export function* map(iterable: Iterable, fn: (t: T) => R): Iterable { for (const element of iterable) { return yield fn(element); } diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 3b9f5d03e2437..2d69767a7e7de 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -1013,6 +1013,16 @@ export function isDiffEditor(thing: any): thing is IDiffEditor { } } +/** + *@internal + */ +export function isCompositeEditor(thing: any): thing is editorCommon.ICompositeCodeEditor { + return thing + && typeof thing === 'object' + && typeof (thing).onDidChangeActiveEditor === 'function'; + +} + /** *@internal */ diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index e176e90ede30a..b236bb7fe7e83 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -5,6 +5,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ConfigurationChangedEvent, IComputedEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IPosition, Position } from 'vs/editor/common/core/position'; @@ -529,6 +530,24 @@ export interface IDiffEditor extends IEditor { getModifiedEditor(): IEditor; } +/** + * @internal + */ +export interface ICompositeCodeEditor { + + /** + * An event that signals that the active editor has changed + */ + readonly onDidChangeActiveEditor: Event; + + /** + * The active code editor iff any + */ + readonly activeCodeEditor: IEditor | undefined; + // readonly editors: readonly ICodeEditor[] maybe supported with uris +} + + /** * An editor contribution that gets created every time a new editor gets created and gets disposed when the editor gets disposed. */ diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 5b6764ea1b143..a74dc9216dfd5 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -627,6 +627,12 @@ export interface ITextModel { */ equalsTextBuffer(other: ITextBuffer): boolean; + /** + * Get the underling text buffer. + * @internal + */ + getTextBuffer(): ITextBuffer; + /** * Get the text in a certain range. * @param range The range describing what text to get. diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index f8d97c2b59826..59e611258450a 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -384,6 +384,11 @@ export class TextModel extends Disposable implements model.ITextModel { return this._buffer.equals(other); } + public getTextBuffer(): model.ITextBuffer { + this._assertNotDisposed(); + return this._buffer; + } + private _emitContentChangedEvent(rawChange: ModelRawContentChangedEvent, change: IModelContentChangedEvent): void { if (this._isDisposing) { // Do not confuse listeners by emitting any event after disposing diff --git a/src/vs/editor/contrib/find/findDecorations.ts b/src/vs/editor/contrib/find/findDecorations.ts index ee93bccc3cf96..5d4422e3bef07 100644 --- a/src/vs/editor/contrib/find/findDecorations.ts +++ b/src/vs/editor/contrib/find/findDecorations.ts @@ -261,7 +261,7 @@ export class FindDecorations implements IDisposable { return result; } - private static readonly _CURRENT_FIND_MATCH_DECORATION = ModelDecorationOptions.register({ + public static readonly _CURRENT_FIND_MATCH_DECORATION = ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, zIndex: 13, className: 'currentFindMatch', @@ -276,7 +276,7 @@ export class FindDecorations implements IDisposable { } }); - private static readonly _FIND_MATCH_DECORATION = ModelDecorationOptions.register({ + public static readonly _FIND_MATCH_DECORATION = ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'findMatch', showIfCollapsed: true, @@ -290,7 +290,7 @@ export class FindDecorations implements IDisposable { } }); - private static readonly _FIND_MATCH_NO_OVERVIEW_DECORATION = ModelDecorationOptions.register({ + public static readonly _FIND_MATCH_NO_OVERVIEW_DECORATION = ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, className: 'findMatch', showIfCollapsed: true diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index b407cfbc7d3d8..f5a1a2e00bc5a 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -437,14 +437,14 @@ export function registerAction2(ctor: { new(): Action2 }): IDisposable { KeybindingsRegistry.registerKeybindingRule({ ...item, id: command.id, - when: ContextKeyExpr.and(command.precondition, item.when) + when: command.precondition ? ContextKeyExpr.and(command.precondition, item.when) : item.when }); } } else if (keybinding) { KeybindingsRegistry.registerKeybindingRule({ ...keybinding, id: command.id, - when: ContextKeyExpr.and(command.precondition, keybinding.when) + when: command.precondition ? ContextKeyExpr.and(command.precondition, keybinding.when) : keybinding.when }); } diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index b6ff5e5057b9d..2a5cfda1b32f7 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -62,6 +62,7 @@ export interface IProgressNotificationOptions extends IProgressOptions { readonly primaryActions?: ReadonlyArray; readonly secondaryActions?: ReadonlyArray; readonly delay?: number; + readonly silent?: boolean; } export interface IProgressWindowOptions extends IProgressOptions { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 9f92e5eff2c19..720a82e06103f 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1577,6 +1577,125 @@ declare module 'vscode' { //#endregion + //#region Peng: Notebook + + export enum CellKind { + Markdown = 1, + Code = 2 + } + + export enum CellOutputKind { + Text = 1, + Error = 2, + Rich = 3 + } + + export interface CellStreamOutput { + outputKind: CellOutputKind.Text; + text: string; + } + + export interface CellErrorOutput { + outputKind: CellOutputKind.Error; + /** + * Exception Name + */ + ename: string; + /** + * Exception Value + */ + evalue: string; + /** + * Exception call stack + */ + traceback: string[]; + } + + export interface CellDisplayOutput { + outputKind: CellOutputKind.Rich; + /** + * { mime_type: value } + * + * Example: + * ```json + * { + * "outputKind": vscode.CellOutputKind.Rich, + * "data": { + * "text/html": [ + * "

Hello

" + * ], + * "text/plain": [ + * "" + * ] + * } + * } + */ + data: { [key: string]: any }; + } + + export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput; + + export interface NotebookCell { + readonly uri: Uri; + handle: number; + language: string; + cellKind: CellKind; + outputs: CellOutput[]; + getContent(): string; + } + + export interface NotebookDocument { + readonly uri: Uri; + readonly fileName: string; + readonly isDirty: boolean; + languages: string[]; + cells: NotebookCell[]; + displayOrder?: GlobPattern[]; + } + + export interface NotebookEditor { + readonly document: NotebookDocument; + viewColumn?: ViewColumn; + /** + * Create a notebook cell. The cell is not inserted into current document when created. Extensions should insert the cell into the document by [TextDocument.cells](#TextDocument.cells) + */ + createCell(content: string, language: string, type: CellKind, outputs: CellOutput[]): NotebookCell; + } + + export interface NotebookProvider { + resolveNotebook(editor: NotebookEditor): Promise; + executeCell(document: NotebookDocument, cell: NotebookCell | undefined): Promise; + save(document: NotebookDocument): Promise; + } + + export interface NotebookOutputSelector { + type: string; + subTypes?: string[]; + } + + export interface NotebookOutputRenderer { + /** + * + * @returns HTML fragment. We can probably return `CellOutput` instead of string ? + * + */ + render(document: NotebookDocument, cell: NotebookCell, output: CellOutput, mimeType: string): string; + preloads?: Uri[]; + } + + namespace window { + export function registerNotebookProvider( + notebookType: string, + provider: NotebookProvider + ): Disposable; + + export function registerNotebookOutputRenderer(type: string, outputSelector: NotebookOutputSelector, renderer: NotebookOutputRenderer): Disposable; + + export let activeNotebookDocument: NotebookDocument | undefined; + } + + //#endregion + //#region color theme access /** diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index e69aa80159daa..3f2de2c738009 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -56,6 +56,7 @@ import './mainThreadWindow'; import './mainThreadWebview'; import './mainThreadWorkspace'; import './mainThreadComments'; +import './mainThreadNotebook'; import './mainThreadTask'; import './mainThreadLabelService'; import './mainThreadTunnelService'; diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts new file mode 100644 index 0000000000000..9330413e36a86 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -0,0 +1,255 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext } from '../common/extHost.protocol'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellsSplice, NotebookCellOutputsSplice, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; + +export class MainThreadNotebookDocument extends Disposable { + private _textModel: NotebookTextModel; + + get textModel() { + return this._textModel; + } + + constructor( + private readonly _proxy: ExtHostNotebookShape, + public handle: number, + public viewType: string, + public uri: URI + ) { + super(); + this._textModel = new NotebookTextModel(handle, viewType, uri); + } + + async deleteCell(uri: URI, index: number): Promise { + let deleteExtHostCell = await this._proxy.$deleteCell(this.viewType, uri, index); + if (deleteExtHostCell) { + this._textModel.removeCell(index); + return true; + } + + return false; + } + + dispose() { + this._textModel.dispose(); + super.dispose(); + } +} + +@extHostNamedCustomer(MainContext.MainThreadNotebook) +export class MainThreadNotebooks extends Disposable implements MainThreadNotebookShape { + private readonly _notebookProviders = new Map(); + private readonly _proxy: ExtHostNotebookShape; + + constructor( + extHostContext: IExtHostContext, + @INotebookService private _notebookService: INotebookService, + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + super(); + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook); + this.registerListeners(); + } + + registerListeners() { + this._register(this._notebookService.onDidChangeActiveEditor(e => { + this._proxy.$updateActiveEditor(e.viewType, e.uri); + })); + + let userOrder = this.configurationService.getValue('notebook.displayOrder'); + this._proxy.$acceptDisplayOrder({ + defaultOrder: NOTEBOOK_DISPLAY_ORDER, + userOrder: userOrder + }); + + this.configurationService.onDidChangeConfiguration(e => { + if (e.affectedKeys.indexOf('notebook.displayOrder') >= 0) { + let userOrder = this.configurationService.getValue('notebook.displayOrder'); + + this._proxy.$acceptDisplayOrder({ + defaultOrder: NOTEBOOK_DISPLAY_ORDER, + userOrder: userOrder + }); + } + }); + } + + async $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise { + this._notebookService.registerNotebookRenderer(handle, extension, type, selectors, preloads.map(uri => URI.revive(uri))); + } + + async $unregisterNotebookRenderer(handle: number): Promise { + this._notebookService.unregisterNotebookRenderer(handle); + } + + async $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string): Promise { + let controller = new MainThreadNotebookController(this._proxy, this, viewType); + this._notebookProviders.set(viewType, controller); + this._notebookService.registerNotebookController(viewType, extension, controller); + return; + } + + async $unregisterNotebookProvider(viewType: string): Promise { + this._notebookProviders.delete(viewType); + this._notebookService.unregisterNotebookProvider(viewType); + return; + } + + async $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise { + let controller = this._notebookProviders.get(viewType); + + if (controller) { + controller.createNotebookDocument(handle, viewType, resource); + } + + return; + } + + async $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise { + let controller = this._notebookProviders.get(viewType); + + if (controller) { + controller.updateLanguages(resource, languages); + } + } + + async resolveNotebook(viewType: string, uri: URI): Promise { + let handle = await this._proxy.$resolveNotebook(viewType, uri); + return handle; + } + + async $spliceNotebookCells(viewType: string, resource: UriComponents, splices: NotebookCellsSplice[], renderers: number[]): Promise { + let controller = this._notebookProviders.get(viewType); + controller?.spliceNotebookCells(resource, splices, renderers); + } + + async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise { + let controller = this._notebookProviders.get(viewType); + controller?.spliceNotebookCellOutputs(resource, cellHandle, splices, renderers); + } + + async executeNotebook(viewType: string, uri: URI): Promise { + return this._proxy.$executeNotebook(viewType, uri, undefined); + } +} + +export class MainThreadNotebookController implements IMainNotebookController { + private _mapping: Map = new Map(); + + constructor( + private readonly _proxy: ExtHostNotebookShape, + private _mainThreadNotebook: MainThreadNotebooks, + private _viewType: string + ) { + } + + async resolveNotebook(viewType: string, uri: URI): Promise { + // TODO: resolve notebook should wait for all notebook document destory operations to finish. + let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); + + if (mainthreadNotebook) { + return mainthreadNotebook.textModel; + } + + let notebookHandle = await this._mainThreadNotebook.resolveNotebook(viewType, uri); + if (notebookHandle !== undefined) { + mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); + return mainthreadNotebook?.textModel; + } + + return undefined; + } + + spliceNotebookCells(resource: UriComponents, splices: NotebookCellsSplice[], renderers: number[]): void { + let mainthreadNotebook = this._mapping.get(URI.from(resource).toString()); + mainthreadNotebook?.textModel.updateRenderers(renderers); + mainthreadNotebook?.textModel.$spliceNotebookCells(splices); + } + + spliceNotebookCellOutputs(resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): void { + let mainthreadNotebook = this._mapping.get(URI.from(resource).toString()); + mainthreadNotebook?.textModel.updateRenderers(renderers); + mainthreadNotebook?.textModel.$spliceNotebookCellOutputs(cellHandle, splices); + } + + async executeNotebook(viewType: string, uri: URI): Promise { + this._mainThreadNotebook.executeNotebook(viewType, uri); + } + + // Methods for ExtHost + async createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise { + let document = new MainThreadNotebookDocument(this._proxy, handle, viewType, URI.revive(resource)); + this._mapping.set(URI.revive(resource).toString(), document); + } + + updateLanguages(resource: UriComponents, languages: string[]) { + let document = this._mapping.get(URI.from(resource).toString()); + document?.textModel.updateLanguages(languages); + } + + updateNotebookRenderers(resource: UriComponents, renderers: number[]): void { + let document = this._mapping.get(URI.from(resource).toString()); + document?.textModel.updateRenderers(renderers); + } + + updateNotebookActiveCell(uri: URI, cellHandle: number): void { + let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); + mainthreadNotebook?.textModel.updateActiveCell(cellHandle); + } + + async createRawCell(uri: URI, index: number, language: string, type: CellKind): Promise { + let cell = await this._proxy.$createEmptyCell(this._viewType, uri, index, language, type); + if (cell) { + let mainCell = new NotebookCellTextModel(URI.revive(cell.uri), cell.handle, cell.source, cell.language, cell.cellKind, cell.outputs); + return mainCell; + } + + return undefined; + } + + async deleteCell(uri: URI, index: number): Promise { + let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); + + if (mainthreadNotebook) { + return mainthreadNotebook.deleteCell(uri, index); + } + + return false; + } + + executeNotebookActiveCell(uri: URI): void { + let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); + + if (mainthreadNotebook && mainthreadNotebook.textModel.activeCell) { + this._proxy.$executeNotebook(this._viewType, uri, mainthreadNotebook.textModel.activeCell.handle); + } + } + + async destoryNotebookDocument(notebook: INotebookTextModel): Promise { + let document = this._mapping.get(URI.from(notebook.uri).toString()); + + if (!document) { + return; + } + + let removeFromExtHost = await this._proxy.$destoryNotebookDocument(this._viewType, notebook.uri); + if (removeFromExtHost) { + document.dispose(); + this._mapping.delete(URI.from(notebook.uri).toString()); + } + } + + async save(uri: URI): Promise { + return this._proxy.$saveNotebook(this._viewType, uri); + } +} diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index e92023f36406b..e7a3f40a091f8 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -67,6 +67,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming'; import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; @@ -129,7 +130,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostComment = rpcProtocol.set(ExtHostContext.ExtHostComments, new ExtHostComments(rpcProtocol, extHostCommands, extHostDocuments)); const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol)); const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress))); - const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHostLabelService, new ExtHostLabelService(rpcProtocol)); + const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHosLabelService, new ExtHostLabelService(rpcProtocol)); + const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostDocumentsAndEditors)); const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands)); @@ -598,6 +600,15 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I createInputBox(): vscode.InputBox { return extHostQuickOpen.createInputBox(extension.identifier); }, + registerNotebookProvider: (viewType: string, provider: vscode.NotebookProvider) => { + return extHostNotebook.registerNotebookProvider(extension, viewType, provider); + }, + registerNotebookOutputRenderer: (type: string, outputFilter: vscode.NotebookOutputSelector, renderer: vscode.NotebookOutputRenderer) => { + return extHostNotebook.registerNotebookOutputRenderer(type, extension, outputFilter, renderer); + }, + get activeNotebookDocument(): vscode.NotebookDocument | undefined { + return extHostNotebook.activeNotebookDocument; + }, get activeColorTheme(): vscode.ColorTheme { checkProposedApiEnabled(extension); return extHostTheming.activeColorTheme; @@ -1018,7 +1029,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I WebviewContentState: extHostTypes.WebviewContentState, UIKind: UIKind, ColorThemeKind: extHostTypes.ColorThemeKind, - TimelineItem: extHostTypes.TimelineItem + TimelineItem: extHostTypes.TimelineItem, + CellKind: extHostTypes.CellKind, + CellOutputKind: extHostTypes.CellOutputKind }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d01f93391782a..7bf429a3d82a9 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -51,6 +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 { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; @@ -577,6 +578,16 @@ export interface WebviewExtensionDescription { readonly location: UriComponents; } +export interface NotebookExtensionDescription { + readonly id: ExtensionIdentifier; + readonly location: UriComponents; +} + +export enum WebviewEditorCapabilities { + Editable, + SupportsHotExit, +} + export interface CustomTextEditorCapabilities { readonly supportsMove?: boolean; } @@ -636,6 +647,49 @@ export interface ExtHostWebviewsShape { $onMoveCustomEditor(handle: WebviewPanelHandle, newResource: UriComponents, viewType: string): Promise; } +export enum CellKind { + Markdown = 1, + Code = 2 +} + +export enum CellOutputKind { + Text = 1, + Error = 2, + Rich = 3 +} + +export interface ICellDto { + handle: number; + uri: UriComponents, + source: string[]; + language: string; + cellKind: CellKind; + outputs: IOutput[]; +} + +export type NotebookCellsSplice = [ + number /* start */, + number /* delete count */, + ICellDto[] +]; + +export type NotebookCellOutputsSplice = [ + number /* start */, + number /* delete count */, + IOutput[] +]; + +export interface MainThreadNotebookShape extends IDisposable { + $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string): Promise; + $unregisterNotebookProvider(viewType: string): Promise; + $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise; + $unregisterNotebookRenderer(handle: number): Promise; + $createNotebookDocument(handle: number, viewType: string, resource: UriComponents): Promise; + $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise; + $spliceNotebookCells(viewType: string, resource: UriComponents, splices: NotebookCellsSplice[], renderers: number[]): Promise; + $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise; +} + export interface MainThreadUrlsShape extends IDisposable { $registerUriHandler(handle: number, extensionId: ExtensionIdentifier): Promise; $unregisterUriHandler(handle: number): Promise; @@ -1464,6 +1518,17 @@ export interface ExtHostCommentsShape { $toggleReaction(commentControllerHandle: number, threadHandle: number, uri: UriComponents, comment: modes.Comment, reaction: modes.CommentReaction): Promise; } +export interface ExtHostNotebookShape { + $resolveNotebook(viewType: string, uri: UriComponents): Promise; + $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise; + $createEmptyCell(viewType: string, uri: UriComponents, index: number, language: string, type: CellKind): Promise; + $deleteCell(viewType: string, uri: UriComponents, index: number): Promise; + $saveNotebook(viewType: string, uri: UriComponents): Promise; + $updateActiveEditor(viewType: string, uri: UriComponents): Promise; + $destoryNotebookDocument(viewType: string, uri: UriComponents): Promise; + $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void; +} + export interface ExtHostStorageShape { $acceptValue(shared: boolean, key: string, value: object | undefined): void; } @@ -1529,6 +1594,7 @@ export const MainContext = { MainThreadTask: createMainId('MainThreadTask'), MainThreadWindow: createMainId('MainThreadWindow'), MainThreadLabelService: createMainId('MainThreadLabelService'), + MainThreadNotebook: createMainId('MainThreadNotebook'), MainThreadTheming: createMainId('MainThreadTheming'), MainThreadTunnelService: createMainId('MainThreadTunnelService'), MainThreadTimeline: createMainId('MainThreadTimeline') @@ -1565,7 +1631,8 @@ export const ExtHostContext = { ExtHostStorage: createMainId('ExtHostStorage'), ExtHostUrls: createExtId('ExtHostUrls'), ExtHostOutputService: createMainId('ExtHostOutputService'), - ExtHostLabelService: createMainId('ExtHostLabelService'), + ExtHosLabelService: createMainId('ExtHostLabelService'), + ExtHostNotebook: createMainId('ExtHostNotebook'), ExtHostTheming: createMainId('ExtHostTheming'), ExtHostTunnelService: createMainId('ExtHostTunnelService'), ExtHostAuthentication: createMainId('ExtHostAuthentication'), diff --git a/src/vs/workbench/api/common/extHostDocumentData.ts b/src/vs/workbench/api/common/extHostDocumentData.ts index cd871a8b382f7..a295f646dd347 100644 --- a/src/vs/workbench/api/common/extHostDocumentData.ts +++ b/src/vs/workbench/api/common/extHostDocumentData.ts @@ -238,7 +238,7 @@ export class ExtHostDocumentData extends MirrorTextModel { } } -class ExtHostDocumentLine implements vscode.TextLine { +export class ExtHostDocumentLine implements vscode.TextLine { private readonly _line: number; private readonly _text: string; diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts new file mode 100644 index 0000000000000..22df2ef1ddf40 --- /dev/null +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -0,0 +1,621 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { ExtHostNotebookShape, IMainContext, MainThreadNotebookShape, MainContext, ICellDto, NotebookCellsSplice, NotebookCellOutputsSplice, CellKind, CellOutputKind } from 'vs/workbench/api/common/extHost.protocol'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { Disposable as VSCodeDisposable } from './extHostTypes'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { readonly } from 'vs/base/common/errors'; +import { Emitter, Event } from 'vs/base/common/event'; +import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { INotebookDisplayOrder, ITransformedDisplayOutputDto, IOrderedMimeType, IStreamOutput, IErrorOutput, mimeTypeSupportedByCore, IOutput, sortMimeTypes, diff, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ISplice } from 'vs/base/common/sequence'; + +export class ExtHostCell implements vscode.NotebookCell { + + public source: string[]; + private _outputs: any[]; + private _onDidChangeOutputs = new Emitter[]>(); + onDidChangeOutputs: Event[]> = this._onDidChangeOutputs.event; + private _textDocument: vscode.TextDocument | undefined; + private _initalVersion: number = -1; + private _outputMapping = new Set(); + + constructor( + readonly handle: number, + readonly uri: URI, + private _content: string, + public cellKind: CellKind, + public language: string, + outputs: any[] + ) { + this.source = this._content.split(/\r|\n|\r\n/g); + this._outputs = outputs; + } + + get outputs() { + return this._outputs; + } + + set outputs(newOutputs: vscode.CellOutput[]) { + let diffs = diff(this._outputs || [], newOutputs || [], (a) => { + return this._outputMapping.has(a); + }); + + diffs.forEach(diff => { + for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { + this._outputMapping.delete(this._outputs[i]); + } + + diff.toInsert.forEach(output => { + this._outputMapping.add(output); + }); + }); + + this._outputs = newOutputs; + this._onDidChangeOutputs.fire(diffs); + } + + getContent(): string { + if (this._textDocument && this._initalVersion !== this._textDocument?.version) { + return this._textDocument.getText(); + } else { + return this.source.join('\n'); + } + } + + attachTextDocument(document: vscode.TextDocument) { + this._textDocument = document; + this._initalVersion = this._textDocument.version; + } + + detachTextDocument(document: vscode.TextDocument) { + if (this._textDocument && this._textDocument.version !== this._initalVersion) { + this.source = this._textDocument.getText().split(/\r|\n|\r\n/g); + } + + this._textDocument = undefined; + this._initalVersion = -1; + } +} + +export class ExtHostNotebookDocument extends Disposable implements vscode.NotebookDocument { + private static _handlePool: number = 0; + readonly handle = ExtHostNotebookDocument._handlePool++; + + private _cells: ExtHostCell[] = []; + + private _cellDisposableMapping = new Map(); + + get cells() { + return this._cells; + } + + set cells(newCells: ExtHostCell[]) { + let diffs = diff(this._cells, newCells, (a) => { + return this._cellDisposableMapping.has(a.handle); + }); + + diffs.forEach(diff => { + for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { + this._cellDisposableMapping.get(this._cells[i].handle)?.clear(); + this._cellDisposableMapping.delete(this._cells[i].handle); + } + + diff.toInsert.forEach(cell => { + this._cellDisposableMapping.set(cell.handle, new DisposableStore()); + this._cellDisposableMapping.get(cell.handle)?.add(cell.onDidChangeOutputs((outputDiffs) => { + this.eventuallyUpdateCellOutputs(cell, outputDiffs); + })); + }); + }); + + this._cells = newCells; + this.eventuallyUpdateCells(diffs); + } + + private _languages: string[] = []; + + get languages() { + return this._languages = []; + } + + set languages(newLanguages: string[]) { + this._languages = newLanguages; + this._proxy.$updateNotebookLanguages(this.viewType, this.uri, this._languages); + } + + private _displayOrder: string[] = []; + + get displayOrder() { + return this._displayOrder; + } + + set displayOrder(newOrder: string[]) { + this._displayOrder = newOrder; + } + + constructor( + private readonly _proxy: MainThreadNotebookShape, + public viewType: string, + public uri: URI, + public renderingHandler: ExtHostNotebookOutputRenderingHandler + ) { + super(); + } + + dispose() { + super.dispose(); + this._cellDisposableMapping.forEach(cell => cell.dispose()); + } + + get fileName() { return this.uri.fsPath; } + + get isDirty() { return false; } + + eventuallyUpdateCells(diffs: ISplice[]) { + let renderers = new Set(); + let diffDtos: NotebookCellsSplice[] = []; + + diffDtos = diffs.map(diff => { + let inserts = diff.toInsert; + + let cellDtos = inserts.map(cell => { + let outputs: IOutput[] = []; + if (cell.outputs.length) { + outputs = cell.outputs.map(output => { + if (output.outputKind === CellOutputKind.Rich) { + const ret = this.transformMimeTypes(cell, output); + + if (ret.orderedMimeTypes[ret.pickedMimeTypeIndex].isResolved) { + renderers.add(ret.orderedMimeTypes[ret.pickedMimeTypeIndex].rendererId!); + } + return ret; + } else { + return output as IStreamOutput | IErrorOutput; + } + }); + } + + return { + uri: cell.uri, + handle: cell.handle, + source: cell.source, + language: cell.language, + cellKind: cell.cellKind, + outputs: outputs, + isDirty: false + }; + }); + + return [diff.start, diff.deleteCount, cellDtos]; + }); + + this._proxy.$spliceNotebookCells( + this.viewType, + this.uri, + diffDtos, + Array.from(renderers) + ); + } + + eventuallyUpdateCellOutputs(cell: ExtHostCell, diffs: ISplice[]) { + let renderers = new Set(); + let outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => { + let outputs = diff.toInsert; + + let transformedOutputs = outputs.map(output => { + if (output.outputKind === CellOutputKind.Rich) { + const ret = this.transformMimeTypes(cell, output); + + if (ret.orderedMimeTypes[ret.pickedMimeTypeIndex].isResolved) { + renderers.add(ret.orderedMimeTypes[ret.pickedMimeTypeIndex].rendererId!); + } + return ret; + } else { + return output as IStreamOutput | IErrorOutput; + } + }); + + return [diff.start, diff.deleteCount, transformedOutputs]; + }); + + this._proxy.$spliceNotebookCellOutputs(this.viewType, this.uri, cell.handle, outputDtos, Array.from(renderers)); + } + + insertCell(index: number, cell: ExtHostCell) { + this.cells.splice(index, 0, cell); + + if (!this._cellDisposableMapping.has(cell.handle)) { + this._cellDisposableMapping.set(cell.handle, new DisposableStore()); + } + + let store = this._cellDisposableMapping.get(cell.handle)!; + + store.add(cell.onDidChangeOutputs((diffs) => { + this.eventuallyUpdateCellOutputs(cell, diffs); + })); + } + + deleteCell(index: number): boolean { + if (index >= this.cells.length) { + return false; + } + + let cell = this.cells[index]; + this._cellDisposableMapping.get(cell.handle)?.dispose(); + this._cellDisposableMapping.delete(cell.handle); + + this.cells.splice(index, 1); + return true; + } + + + transformMimeTypes(cell: ExtHostCell, output: vscode.CellDisplayOutput): ITransformedDisplayOutputDto { + let mimeTypes = Object.keys(output.data); + + // TODO@rebornix, the document display order might be assigned a bit later. We need to postpone sending the outputs to the core side. + let coreDisplayOrder = this.renderingHandler.outputDisplayOrder; + const sorted = sortMimeTypes(mimeTypes, coreDisplayOrder?.userOrder || [], this._displayOrder, coreDisplayOrder?.defaultOrder || []); + + let orderMimeTypes: IOrderedMimeType[] = []; + + sorted.forEach(mimeType => { + let handlers = this.renderingHandler.findBestMatchedRenderer(mimeType); + + if (handlers.length) { + let renderedOutput = handlers[0].render(this, cell, output, mimeType); + + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: true, + rendererId: handlers[0].handle, + output: renderedOutput + }); + + for (let i = 1; i < handlers.length; i++) { + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: false, + rendererId: handlers[i].handle + }); + } + + if (mimeTypeSupportedByCore(mimeType)) { + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: false, + rendererId: -1 + }); + } + } else { + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: false + }); + } + }); + + return { + outputKind: output.outputKind, + data: output.data, + orderedMimeTypes: orderMimeTypes, + pickedMimeTypeIndex: 0 + }; + } + + getCell(cellHandle: number) { + return this.cells.find(cell => cell.handle === cellHandle); + } + + attachCellTextDocument(textDocument: vscode.TextDocument) { + let cell = this.cells.find(cell => cell.uri.toString() === textDocument.uri.toString()); + if (cell) { + cell.attachTextDocument(textDocument); + } + } + + detachCellTextDocument(textDocument: vscode.TextDocument) { + let cell = this.cells.find(cell => cell.uri.toString() === textDocument.uri.toString()); + if (cell) { + cell.detachTextDocument(textDocument); + } + } +} + +export class ExtHostNotebookEditor extends Disposable implements vscode.NotebookEditor { + private _viewColumn: vscode.ViewColumn | undefined; + private static _cellhandlePool: number = 0; + + constructor( + viewType: string, + readonly id: string, + public uri: URI, + public document: ExtHostNotebookDocument, + private _documentsAndEditors: ExtHostDocumentsAndEditors + ) { + super(); + this._register(this._documentsAndEditors.onDidAddDocuments(documents => { + for (const { document: textDocument } of documents) { + let data = CellUri.parse(textDocument.uri); + if (data) { + if (this.document.uri.toString() === data.notebook.toString()) { + document.attachCellTextDocument(textDocument); + } + } + } + })); + + this._register(this._documentsAndEditors.onDidRemoveDocuments(documents => { + for (const { document: textDocument } of documents) { + let data = CellUri.parse(textDocument.uri); + if (data) { + if (this.document.uri.toString() === data.notebook.toString()) { + document.detachCellTextDocument(textDocument); + } + } + } + })); + } + + createCell(content: string, language: string, type: CellKind, outputs: vscode.CellOutput[]): vscode.NotebookCell { + const handle = ExtHostNotebookEditor._cellhandlePool++; + const uri = CellUri.generate(this.document.uri, handle); + const cell = new ExtHostCell(handle, uri, content, type, language, outputs); + return cell; + } + + get viewColumn(): vscode.ViewColumn | undefined { + return this._viewColumn; + } + + set viewColumn(value) { + throw readonly('viewColumn'); + } +} + +export class ExtHostNotebookOutputRenderer { + private static _handlePool: number = 0; + readonly handle = ExtHostNotebookOutputRenderer._handlePool++; + + constructor( + public type: string, + public filter: vscode.NotebookOutputSelector, + public renderer: vscode.NotebookOutputRenderer + ) { + + } + + matches(mimeType: string): boolean { + if (this.filter.subTypes) { + if (this.filter.subTypes.indexOf(mimeType) >= 0) { + return true; + } + } + return false; + } + + render(document: ExtHostNotebookDocument, cell: ExtHostCell, output: vscode.CellOutput, mimeType: string): string { + let html = this.renderer.render(document, cell, output, mimeType); + + return html; + } +} + +export interface ExtHostNotebookOutputRenderingHandler { + outputDisplayOrder: INotebookDisplayOrder | undefined; + findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer[]; +} + +export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostNotebookOutputRenderingHandler { + private static _handlePool: number = 0; + + private readonly _proxy: MainThreadNotebookShape; + private readonly _notebookProviders = new Map(); + private readonly _documents = new Map(); + private readonly _editors = new Map(); + private readonly _notebookOutputRenderers = new Map(); + private _outputDisplayOrder: INotebookDisplayOrder | undefined; + + get outputDisplayOrder(): INotebookDisplayOrder | undefined { + return this._outputDisplayOrder; + } + + private _activeNotebookDocument: ExtHostNotebookDocument | undefined; + + get activeNotebookDocument() { + return this._activeNotebookDocument; + } + + constructor(mainContext: IMainContext, private _documentsAndEditors: ExtHostDocumentsAndEditors) { + this._proxy = mainContext.getProxy(MainContext.MainThreadNotebook); + } + + registerNotebookOutputRenderer( + type: string, + extension: IExtensionDescription, + filter: vscode.NotebookOutputSelector, + renderer: vscode.NotebookOutputRenderer + ): vscode.Disposable { + let extHostRenderer = new ExtHostNotebookOutputRenderer(type, filter, renderer); + this._notebookOutputRenderers.set(extHostRenderer.handle, extHostRenderer); + this._proxy.$registerNotebookRenderer({ id: extension.identifier, location: extension.extensionLocation }, type, filter, extHostRenderer.handle, renderer.preloads || []); + return new VSCodeDisposable(() => { + this._notebookOutputRenderers.delete(extHostRenderer.handle); + this._proxy.$unregisterNotebookRenderer(extHostRenderer.handle); + }); + } + + findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer[] { + let matches: ExtHostNotebookOutputRenderer[] = []; + for (let renderer of this._notebookOutputRenderers) { + if (renderer[1].matches(mimeType)) { + matches.push(renderer[1]); + } + } + + return matches; + } + + registerNotebookProvider( + extension: IExtensionDescription, + viewType: string, + provider: vscode.NotebookProvider, + ): vscode.Disposable { + + if (this._notebookProviders.has(viewType)) { + throw new Error(`Notebook provider for '${viewType}' already registered`); + } + + this._notebookProviders.set(viewType, { extension, provider }); + this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation }, viewType); + return new VSCodeDisposable(() => { + this._notebookProviders.delete(viewType); + this._proxy.$unregisterNotebookProvider(viewType); + }); + } + + async $resolveNotebook(viewType: string, uri: UriComponents): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + if (!this._documents.has(URI.revive(uri).toString())) { + let document = new ExtHostNotebookDocument(this._proxy, viewType, URI.revive(uri), this); + await this._proxy.$createNotebookDocument( + document.handle, + viewType, + uri + ); + + this._documents.set(URI.revive(uri).toString(), document); + } + + let editor = new ExtHostNotebookEditor( + viewType, + `${ExtHostNotebookController._handlePool++}`, + URI.revive(uri), + this._documents.get(URI.revive(uri).toString())!, + this._documentsAndEditors + ); + + this._editors.set(URI.revive(uri).toString(), editor); + await provider.provider.resolveNotebook(editor); + // await editor.document.$updateCells(); + return editor.document.handle; + } + + return Promise.resolve(undefined); + } + + async $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise { + let provider = this._notebookProviders.get(viewType); + + if (!provider) { + return; + } + + let document = this._documents.get(URI.revive(uri).toString()); + + if (!document) { + return; + } + + let cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; + return provider.provider.executeCell(document!, cell); + } + + async $createEmptyCell(viewType: string, uri: URI, index: number, language: string, type: CellKind): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + let editor = this._editors.get(URI.revive(uri).toString()); + let document = this._documents.get(URI.revive(uri).toString()); + + let rawCell = editor?.createCell('', language, type, []) as ExtHostCell; + document?.insertCell(index, rawCell!); + + let allDocuments = this._documentsAndEditors.allDocuments(); + for (let { document: textDocument } of allDocuments) { + let data = CellUri.parse(textDocument.uri); + if (data) { + if (uri.toString() === data.notebook.toString() && textDocument.uri.toString() === rawCell.uri.toString()) { + rawCell.attachTextDocument(textDocument); + } + } + } + return { + uri: rawCell.uri, + handle: rawCell.handle, + source: rawCell.source, + language: rawCell.language, + cellKind: rawCell.cellKind, + outputs: [] + }; + } + + return; + } + + async $deleteCell(viewType: string, uri: UriComponents, index: number): Promise { + let provider = this._notebookProviders.get(viewType); + + if (!provider) { + return false; + } + + let document = this._documents.get(URI.revive(uri).toString()); + + if (document) { + return document.deleteCell(index); + } + + return false; + } + + async $saveNotebook(viewType: string, uri: UriComponents): Promise { + let provider = this._notebookProviders.get(viewType); + let document = this._documents.get(URI.revive(uri).toString()); + + if (provider && document) { + return await provider.provider.save(document); + } + + return false; + } + + async $updateActiveEditor(viewType: string, uri: UriComponents): Promise { + this._activeNotebookDocument = this._documents.get(URI.revive(uri).toString()); + } + + async $destoryNotebookDocument(viewType: string, uri: UriComponents): Promise { + let provider = this._notebookProviders.get(viewType); + + if (!provider) { + return false; + } + + let document = this._documents.get(URI.revive(uri).toString()); + + if (document) { + document.dispose(); + this._documents.delete(URI.revive(uri).toString()); + } + + let editor = this._editors.get(URI.revive(uri).toString()); + + if (editor) { + editor.dispose(); + this._editors.delete(URI.revive(uri).toString()); + } + + return true; + } + + $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void { + this._outputDisplayOrder = displayOrder; + } +} diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 1dac637a08ff8..27e443c044041 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2559,6 +2559,21 @@ export enum ColorThemeKind { //#endregion Theming +//#region Notebook + +export enum CellKind { + Markdown = 1, + Code = 2 +} + +export enum CellOutputKind { + Text = 1, + Error = 2, + Rich = 3 +} + +//#endregion + //#region Timeline @es5ClassCompat diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.css b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.css new file mode 100644 index 0000000000000..d726db9d7bd19 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.css @@ -0,0 +1,147 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .simple-fr-find-part-wrapper { + overflow: hidden; + z-index: 10; + position: absolute; + top: -45px; + right: 18px; + width: 318px; + max-width: calc(100% - 28px - 28px - 8px); + pointer-events: none; + transition: top 200ms linear; + visibility: hidden; +} + +.monaco-workbench .simple-fr-find-part { + /* visibility: hidden; Use visibility to maintain flex layout while hidden otherwise interferes with transition */ + z-index: 10; + position: relative; + top: 0px; + display: flex; + padding: 4px; + align-items: center; + pointer-events: all; + margin: 0 0 0 17px; +} + +.monaco-workbench .simple-fr-replace-part { + /* visibility: hidden; Use visibility to maintain flex layout while hidden otherwise interferes with transition */ + z-index: 10; + position: relative; + top: 0px; + display: flex; + padding: 4px; + align-items: center; + pointer-events: all; + margin: 0 0 0 17px; +} + +.monaco-workbench .simple-fr-find-part-wrapper .find-replace-progress { + width: 100%; + height: 2px; + position: absolute; +} + +.monaco-workbench .simple-fr-find-part-wrapper .find-replace-progress .monaco-progress-container { + height: 2px; + top: 0px !important; + z-index: 100 !important; +} + +.monaco-workbench .simple-fr-find-part-wrapper .find-replace-progress .monaco-progress-container .progress-bit { + height: 2px; +} + +.monaco-workbench .simple-fr-find-part-wrapper .monaco-findInput { + width: 224px; +} + +.monaco-workbench .simple-fr-find-part-wrapper .button { + width: 20px; + height: 20px; + flex: initial; + margin-left: 3px; + background-position: 50%; + background-repeat: no-repeat; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; +} + +.monaco-workbench .simple-fr-find-part-wrapper.visible .simple-fr-find-part { + visibility: visible; +} + +.monaco-workbench .simple-fr-find-part-wrapper .toggle { + position: absolute; + top: 0; + width: 18px; + height: 100%; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + margin-left: 0px; + pointer-events: all; +} + +.monaco-workbench .simple-fr-find-part-wrapper.visible { + visibility: visible; +} + +.monaco-workbench .simple-fr-find-part-wrapper.visible-transition { + top: 0; +} + +.monaco-workbench .simple-fr-find-part .monaco-findInput { + flex: 1; +} + +.monaco-workbench .simple-fr-find-part .button { + min-width: 20px; + width: 20px; + height: 20px; + display: flex; + flex: initial; + margin-left: 3px; + background-position: center center; + background-repeat: no-repeat; + cursor: pointer; +} + +.monaco-workbench .simple-fr-find-part .button.previous { + background-image: url('images/chevron-previous-light.svg'); +} + +.monaco-workbench .simple-fr-find-part .button.next { + background-image: url('images/chevron-next-light.svg'); +} + +.monaco-workbench .simple-fr-find-part .button.close-fw { + background-image: url('images/close-light.svg'); +} + +.hc-black .monaco-workbench .simple-fr-find-part .button.previous, +.vs-dark .monaco-workbench .simple-fr-find-part .button.previous { + background-image: url('images/chevron-previous-dark.svg'); +} + +.hc-black .monaco-workbench .simple-fr-find-part .button.next, +.vs-dark .monaco-workbench .simple-fr-find-part .button.next { + background-image: url('images/chevron-next-dark.svg'); +} + +.hc-black .monaco-workbench .simple-fr-find-part .button.close-fw, +.vs-dark .monaco-workbench .simple-fr-find-part .button.close-fw { + background-image: url('images/close-dark.svg'); +} + +.monaco-workbench .simple-fr-find-part .button.disabled { + opacity: 0.3; + cursor: default; +} diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts new file mode 100644 index 0000000000000..1377d4d5bd097 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts @@ -0,0 +1,427 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./simpleFindReplaceWidget'; +import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; +import { FindInput, IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput'; +import { Widget } from 'vs/base/browser/ui/widget'; +import { Delayer } from 'vs/base/common/async'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/findState'; +import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; +import { SimpleButton } from 'vs/editor/contrib/find/findWidget'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, widgetShadow, editorWidgetForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IColorTheme, registerThemingParticipant, IThemeService } from 'vs/platform/theme/common/themeService'; +import { ContextScopedFindInput, ContextScopedReplaceInput } from 'vs/platform/browser/contextScopedHistoryWidget'; +import { ReplaceInput, IReplaceInputStyles } from 'vs/base/browser/ui/findinput/replaceInput'; +import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; +import { attachProgressBarStyler } from 'vs/platform/theme/common/styler'; + +const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); +const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find"); +const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "Previous match"); +const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next match"); +const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close"); +const NLS_TOGGLE_REPLACE_MODE_BTN_LABEL = nls.localize('label.toggleReplaceButton', "Toggle Replace mode"); +const NLS_REPLACE_INPUT_LABEL = nls.localize('label.replace', "Replace"); +const NLS_REPLACE_INPUT_PLACEHOLDER = nls.localize('placeholder.replace', "Replace"); +const NLS_REPLACE_BTN_LABEL = nls.localize('label.replaceButton', "Replace"); +const NLS_REPLACE_ALL_BTN_LABEL = nls.localize('label.replaceAllButton', "Replace All"); + +export abstract class SimpleFindReplaceWidget extends Widget { + protected readonly _findInput: FindInput; + private readonly _domNode: HTMLElement; + private readonly _innerFindDomNode: HTMLElement; + private readonly _focusTracker: dom.IFocusTracker; + private readonly _findInputFocusTracker: dom.IFocusTracker; + private readonly _updateHistoryDelayer: Delayer; + private readonly prevBtn: SimpleButton; + private readonly nextBtn: SimpleButton; + + private readonly _replaceInput!: ReplaceInput; + private readonly _innerReplaceDomNode!: HTMLElement; + private _toggleReplaceBtn!: SimpleButton; + private readonly _replaceInputFocusTracker!: dom.IFocusTracker; + private _replaceBtn!: SimpleButton; + private _replaceAllBtn!: SimpleButton; + + + private _isVisible: boolean = false; + private _isReplaceVisible: boolean = false; + private foundMatch: boolean = false; + + protected _progressBar!: ProgressBar; + + + constructor( + @IContextViewService private readonly _contextViewService: IContextViewService, + @IContextKeyService contextKeyService: IContextKeyService, + @IThemeService private readonly _themeService: IThemeService, + private readonly _state: FindReplaceState = new FindReplaceState(), + showOptionButtons?: boolean + ) { + super(); + + this._domNode = document.createElement('div'); + this._domNode.classList.add('simple-fr-find-part-wrapper'); + this._register(this._state.onFindReplaceStateChange((e) => this._onStateChanged(e))); + + let progressContainer = dom.$('.find-replace-progress'); + this._progressBar = new ProgressBar(progressContainer); + this._register(attachProgressBarStyler(this._progressBar, this._themeService)); + this._domNode.appendChild(progressContainer); + + // Toggle replace button + this._toggleReplaceBtn = this._register(new SimpleButton({ + label: NLS_TOGGLE_REPLACE_MODE_BTN_LABEL, + className: 'codicon toggle left', + onTrigger: () => { + this._isReplaceVisible = !this._isReplaceVisible; + this._state.change({ isReplaceRevealed: this._isReplaceVisible }, false); + if (this._isReplaceVisible) { + this._innerReplaceDomNode.style.display = 'flex'; + } else { + this._innerReplaceDomNode.style.display = 'none'; + } + } + })); + this._toggleReplaceBtn.toggleClass('codicon-chevron-down', this._isReplaceVisible); + this._toggleReplaceBtn.toggleClass('codicon-chevron-right', !this._isReplaceVisible); + this._toggleReplaceBtn.setExpanded(this._isReplaceVisible); + this._domNode.appendChild(this._toggleReplaceBtn.domNode); + + + this._innerFindDomNode = document.createElement('div'); + this._innerFindDomNode.classList.add('simple-fr-find-part'); + + this._findInput = this._register(new ContextScopedFindInput(null, this._contextViewService, { + label: NLS_FIND_INPUT_LABEL, + placeholder: NLS_FIND_INPUT_PLACEHOLDER, + validation: (value: string): InputBoxMessage | null => { + if (value.length === 0 || !this._findInput.getRegex()) { + return null; + } + try { + new RegExp(value); + return null; + } catch (e) { + this.foundMatch = false; + this.updateButtons(this.foundMatch); + return { content: e.message }; + } + } + }, contextKeyService, showOptionButtons)); + + // Find History with update delayer + this._updateHistoryDelayer = new Delayer(500); + + this.oninput(this._findInput.domNode, (e) => { + this.foundMatch = this.onInputChanged(); + this.updateButtons(this.foundMatch); + this._delayedUpdateHistory(); + }); + + this._findInput.setRegex(!!this._state.isRegex); + this._findInput.setCaseSensitive(!!this._state.matchCase); + this._findInput.setWholeWords(!!this._state.wholeWord); + + this._register(this._findInput.onDidOptionChange(() => { + this._state.change({ + isRegex: this._findInput.getRegex(), + wholeWord: this._findInput.getWholeWords(), + matchCase: this._findInput.getCaseSensitive() + }, true); + })); + + this._register(this._state.onFindReplaceStateChange(() => { + this._findInput.setRegex(this._state.isRegex); + this._findInput.setWholeWords(this._state.wholeWord); + this._findInput.setCaseSensitive(this._state.matchCase); + this.findFirst(); + })); + + this.prevBtn = this._register(new SimpleButton({ + label: NLS_PREVIOUS_MATCH_BTN_LABEL, + className: 'previous', + onTrigger: () => { + this.find(true); + } + })); + + this.nextBtn = this._register(new SimpleButton({ + label: NLS_NEXT_MATCH_BTN_LABEL, + className: 'next', + onTrigger: () => { + this.find(false); + } + })); + + const closeBtn = this._register(new SimpleButton({ + label: NLS_CLOSE_BTN_LABEL, + className: 'close-fw', + onTrigger: () => { + this.hide(); + } + })); + + this._innerFindDomNode.appendChild(this._findInput.domNode); + this._innerFindDomNode.appendChild(this.prevBtn.domNode); + this._innerFindDomNode.appendChild(this.nextBtn.domNode); + this._innerFindDomNode.appendChild(closeBtn.domNode); + + // _domNode wraps _innerDomNode, ensuring that + this._domNode.appendChild(this._innerFindDomNode); + + this.onkeyup(this._innerFindDomNode, e => { + if (e.equals(KeyCode.Escape)) { + this.hide(); + e.preventDefault(); + return; + } + }); + + this._focusTracker = this._register(dom.trackFocus(this._innerFindDomNode)); + this._register(this._focusTracker.onDidFocus(this.onFocusTrackerFocus.bind(this))); + this._register(this._focusTracker.onDidBlur(this.onFocusTrackerBlur.bind(this))); + + this._findInputFocusTracker = this._register(dom.trackFocus(this._findInput.domNode)); + this._register(this._findInputFocusTracker.onDidFocus(this.onFindInputFocusTrackerFocus.bind(this))); + this._register(this._findInputFocusTracker.onDidBlur(this.onFindInputFocusTrackerBlur.bind(this))); + + this._register(dom.addDisposableListener(this._innerFindDomNode, 'click', (event) => { + event.stopPropagation(); + })); + + // Replace + this._innerReplaceDomNode = document.createElement('div'); + this._innerReplaceDomNode.classList.add('simple-fr-replace-part'); + + this._replaceInput = this._register(new ContextScopedReplaceInput(null, undefined, { + label: NLS_REPLACE_INPUT_LABEL, + placeholder: NLS_REPLACE_INPUT_PLACEHOLDER, + history: [] + }, contextKeyService, false)); + this._innerReplaceDomNode.appendChild(this._replaceInput.domNode); + this._replaceInputFocusTracker = this._register(dom.trackFocus(this._replaceInput.domNode)); + this._register(this._replaceInputFocusTracker.onDidFocus(this.onReplaceInputFocusTrackerFocus.bind(this))); + this._register(this._replaceInputFocusTracker.onDidBlur(this.onReplaceInputFocusTrackerBlur.bind(this))); + + this._domNode.appendChild(this._innerReplaceDomNode); + + if (this._isReplaceVisible) { + this._innerReplaceDomNode.style.display = 'flex'; + } else { + this._innerReplaceDomNode.style.display = 'none'; + } + + this._replaceBtn = this._register(new SimpleButton({ + label: NLS_REPLACE_BTN_LABEL, + className: 'codicon codicon-replace', + onTrigger: () => { + this.replaceOne(); + } + })); + + // Replace all button + this._replaceAllBtn = this._register(new SimpleButton({ + label: NLS_REPLACE_ALL_BTN_LABEL, + className: 'codicon codicon-replace-all', + onTrigger: () => { + this.replaceAll(); + } + })); + + this._innerReplaceDomNode.appendChild(this._replaceBtn.domNode); + this._innerReplaceDomNode.appendChild(this._replaceAllBtn.domNode); + + + } + + protected abstract onInputChanged(): boolean; + protected abstract find(previous: boolean): void; + protected abstract findFirst(): void; + protected abstract replaceOne(): void; + protected abstract replaceAll(): void; + protected abstract onFocusTrackerFocus(): void; + protected abstract onFocusTrackerBlur(): void; + protected abstract onFindInputFocusTrackerFocus(): void; + protected abstract onFindInputFocusTrackerBlur(): void; + protected abstract onReplaceInputFocusTrackerFocus(): void; + protected abstract onReplaceInputFocusTrackerBlur(): void; + + protected get inputValue() { + return this._findInput.getValue(); + } + + protected get replaceValue() { + return this._replaceInput.getValue(); + } + + public get focusTracker(): dom.IFocusTracker { + return this._focusTracker; + } + + public updateTheme(theme: IColorTheme): void { + const inputStyles: IFindInputStyles = { + inputActiveOptionBorder: theme.getColor(inputActiveOptionBorder), + inputActiveOptionBackground: theme.getColor(inputActiveOptionBackground), + inputBackground: theme.getColor(inputBackground), + inputForeground: theme.getColor(inputForeground), + inputBorder: theme.getColor(inputBorder), + inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground), + inputValidationInfoForeground: theme.getColor(inputValidationInfoForeground), + inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder), + inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground), + inputValidationWarningForeground: theme.getColor(inputValidationWarningForeground), + inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder), + inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground), + inputValidationErrorForeground: theme.getColor(inputValidationErrorForeground), + inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder) + }; + this._findInput.style(inputStyles); + const replaceStyles: IReplaceInputStyles = { + inputActiveOptionBorder: theme.getColor(inputActiveOptionBorder), + inputActiveOptionBackground: theme.getColor(inputActiveOptionBackground), + inputBackground: theme.getColor(inputBackground), + inputForeground: theme.getColor(inputForeground), + inputBorder: theme.getColor(inputBorder), + inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground), + inputValidationInfoForeground: theme.getColor(inputValidationInfoForeground), + inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder), + inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground), + inputValidationWarningForeground: theme.getColor(inputValidationWarningForeground), + inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder), + inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground), + inputValidationErrorForeground: theme.getColor(inputValidationErrorForeground), + inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder) + }; + this._replaceInput.style(replaceStyles); + } + + private _onStateChanged(e: FindReplaceStateChangedEvent): void { + this._updateButtons(); + } + + private _updateButtons(): void { + this._findInput.setEnabled(this._isVisible); + this._replaceInput.setEnabled(this._isVisible && this._isReplaceVisible); + let findInputIsNonEmpty = (this._state.searchString.length > 0); + this._replaceBtn.setEnabled(this._isVisible && this._isReplaceVisible && findInputIsNonEmpty); + this._replaceAllBtn.setEnabled(this._isVisible && this._isReplaceVisible && findInputIsNonEmpty); + + dom.toggleClass(this._domNode, 'replaceToggled', this._isReplaceVisible); + this._toggleReplaceBtn.toggleClass('codicon-chevron-right', !this._isReplaceVisible); + this._toggleReplaceBtn.toggleClass('codicon-chevron-down', this._isReplaceVisible); + this._toggleReplaceBtn.setExpanded(this._isReplaceVisible); + } + + + dispose() { + super.dispose(); + + if (this._domNode && this._domNode.parentElement) { + this._domNode.parentElement.removeChild(this._domNode); + } + } + + public getDomNode() { + return this._domNode; + } + + public reveal(initialInput?: string): void { + if (initialInput) { + this._findInput.setValue(initialInput); + } + + if (this._isVisible) { + this._findInput.select(); + return; + } + + this._isVisible = true; + this.updateButtons(this.foundMatch); + + setTimeout(() => { + dom.addClass(this._domNode, 'visible'); + dom.addClass(this._domNode, 'visible-transition'); + this._domNode.setAttribute('aria-hidden', 'false'); + this._findInput.select(); + }, 0); + } + + public show(initialInput?: string): void { + if (initialInput && !this._isVisible) { + this._findInput.setValue(initialInput); + } + + this._isVisible = true; + + setTimeout(() => { + dom.addClass(this._domNode, 'visible'); + dom.addClass(this._domNode, 'visible-transition'); + this._domNode.setAttribute('aria-hidden', 'false'); + }, 0); + } + + public hide(): void { + if (this._isVisible) { + dom.removeClass(this._domNode, 'visible-transition'); + this._domNode.setAttribute('aria-hidden', 'true'); + // Need to delay toggling visibility until after Transition, then visibility hidden - removes from tabIndex list + setTimeout(() => { + this._isVisible = false; + this.updateButtons(this.foundMatch); + dom.removeClass(this._domNode, 'visible'); + }, 200); + } + } + + protected _delayedUpdateHistory() { + this._updateHistoryDelayer.trigger(this._updateHistory.bind(this)); + } + + protected _updateHistory() { + this._findInput.inputBox.addToHistory(); + } + + protected _getRegexValue(): boolean { + return this._findInput.getRegex(); + } + + protected _getWholeWordValue(): boolean { + return this._findInput.getWholeWords(); + } + + protected _getCaseSensitiveValue(): boolean { + return this._findInput.getCaseSensitive(); + } + + protected updateButtons(foundMatch: boolean) { + const hasInput = this.inputValue.length > 0; + this.prevBtn.setEnabled(this._isVisible && hasInput && foundMatch); + this.nextBtn.setEnabled(this._isVisible && hasInput && foundMatch); + } +} + +// theming +registerThemingParticipant((theme, collector) => { + const findWidgetBGColor = theme.getColor(editorWidgetBackground); + if (findWidgetBGColor) { + collector.addRule(`.monaco-workbench .simple-fr-find-part-wrapper { background-color: ${findWidgetBGColor} !important; }`); + } + + const widgetForeground = theme.getColor(editorWidgetForeground); + if (widgetForeground) { + collector.addRule(`.monaco-workbench .simple-fr-find-part-wrapper { color: ${widgetForeground}; }`); + } + + const widgetShadowColor = theme.getColor(widgetShadow); + if (widgetShadowColor) { + collector.addRule(`.monaco-workbench .simple-fr-find-part-wrapper { box-shadow: 0 2px 8px ${widgetShadowColor}; }`); + } +}); diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 6f93ec9f73127..7c3220d2e54e6 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -54,6 +54,7 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; import { StartDebugQuickAccessProvider } from 'vs/workbench/contrib/debug/browser/debugQuickAccess'; +import { DebugProgressContribution } from 'vs/workbench/contrib/debug/browser/debugProgress'; class OpenDebugViewletAction extends ShowViewletAction { public static readonly ID = VIEWLET_ID; @@ -298,6 +299,7 @@ configurationRegistry.registerConfiguration({ // Register Debug Status Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugStatusContribution, LifecyclePhase.Eventually); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DebugProgressContribution, LifecyclePhase.Eventually); // Debug toolbar diff --git a/src/vs/workbench/contrib/debug/browser/debugProgress.ts b/src/vs/workbench/contrib/debug/browser/debugProgress.ts new file mode 100644 index 0000000000000..c6c29122cd9f2 --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/debugProgress.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { IDebugService, VIEWLET_ID, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; + +export class DebugProgressContribution implements IWorkbenchContribution { + + private toDispose: IDisposable[] = []; + + constructor( + @IDebugService private readonly debugService: IDebugService, + @IProgressService private readonly progressService: IProgressService + ) { + let progressListener: IDisposable; + const onFocusSession = (session: IDebugSession | undefined) => { + if (progressListener) { + progressListener.dispose(); + } + if (session) { + progressListener = session.onDidProgressStart(async progressStartEvent => { + const promise = new Promise(r => { + // Show progress until a progress end event comes or the session ends + const listener = Event.any(Event.filter(session.onDidProgressEnd, e => e.body.progressId === progressStartEvent.body.progressId), + session.onDidEndAdapter)(() => { + listener.dispose(); + r(); + }); + }); + + this.progressService.withProgress({ location: VIEWLET_ID }, () => promise); + this.progressService.withProgress({ + location: ProgressLocation.Notification, + title: progressStartEvent.body.title, + cancellable: progressStartEvent.body.cancellable, + silent: true + }, () => promise, () => session.cancel(progressStartEvent.body.progressId)); + }); + } + }; + this.toDispose.push(this.debugService.getViewModel().onDidFocusSession(onFocusSession)); + onFocusSession(this.debugService.getViewModel().focusedSession); + } + + dispose(): void { + dispose(this.toDispose); + } +} diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index d91eb34c82d80..d330f6da6e784 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -5,11 +5,10 @@ import 'vs/css!./media/debugViewlet'; import * as nls from 'vs/nls'; -import { IAction, Action } from 'vs/base/common/actions'; +import { IAction } from 'vs/base/common/actions'; import * as DOM from 'vs/base/browser/dom'; -import { Event } from 'vs/base/common/event'; import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IDebugService, VIEWLET_ID, State, BREAKPOINTS_VIEW_ID, IDebugConfiguration, DEBUG_PANEL_ID, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, VIEWLET_ID, State, BREAKPOINTS_VIEW_ID, IDebugConfiguration, DEBUG_PANEL_ID, CONTEXT_DEBUG_UX, CONTEXT_DEBUG_UX_KEY } from 'vs/workbench/contrib/debug/common/debug'; import { StartAction, ConfigureAction, SelectAndStartAction, FocusSessionAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { StartDebugActionViewItem, FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -44,7 +43,6 @@ export class DebugViewPaneContainer extends ViewPaneContainer { private paneListeners = new Map(); private debugToolBarMenu: IMenu | undefined; private disposeOnTitleUpdate: IDisposable | undefined; - private progressEvents: { event: DebugProtocol.ProgressStartEvent, session: IDebugSession }[] = []; constructor( @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @@ -81,38 +79,6 @@ export class DebugViewPaneContainer extends ViewPaneContainer { this.updateTitleArea(); } })); - - let progressListener: IDisposable; - this._register(this.debugService.getViewModel().onDidFocusSession(session => { - if (progressListener) { - progressListener.dispose(); - } - if (session) { - progressListener = session.onDidProgressStart(async progressStartEvent => { - // Update title area to show the cancel progress action - this.progressEvents.push({ session: session, event: progressStartEvent }); - if (progressStartEvent.body.cancellable) { - this.cancelAction.tooltip = nls.localize('cancelProgress', "Cancel {0}", progressStartEvent.body.title); - this.updateTitleArea(); - } - await this.progressService.withProgress({ location: VIEWLET_ID }, () => { - return new Promise(r => { - // Show progress until a progress end event comes or the session ends - const listener = Event.any(Event.filter(session.onDidProgressEnd, e => e.body.progressId === progressStartEvent.body.progressId), - session.onDidEndAdapter)(() => { - listener.dispose(); - r(); - }); - }); - }); - this.progressEvents = this.progressEvents.filter(pe => pe.event.body.progressId !== progressStartEvent.body.progressId); - if (progressStartEvent.body.cancellable) { - this.cancelAction.tooltip = nls.localize('cancel', "Cancel"); - this.updateTitleArea(); - } - }); - } - })); } create(parent: HTMLElement): void { @@ -145,16 +111,6 @@ export class DebugViewPaneContainer extends ViewPaneContainer { return this._register(this.instantiationService.createInstance(OpenDebugPanelAction, OpenDebugPanelAction.ID, OpenDebugPanelAction.LABEL)); } - @memoize - private get cancelAction(): Action { - return this._register(new Action('debug.cancelProgress', nls.localize('cancel', "Cancel"), 'debug-action codicon codicon-stop', true, async () => { - const progressEvent = this.progressEvents.filter(e => e.event.body.cancellable).pop(); - if (progressEvent) { - await progressEvent.session.cancel(progressEvent.event.body.progressId); - } - })); - } - @memoize private get selectAndStartAction(): SelectAndStartAction { return this._register(this.instantiationService.createInstance(SelectAndStartAction, SelectAndStartAction.ID, nls.localize('startAdditionalSession', "Start Additional Session"))); @@ -165,7 +121,6 @@ export class DebugViewPaneContainer extends ViewPaneContainer { return []; } - let result: IAction[]; if (!this.showInitialDebugActions) { if (!this.debugToolBarMenu) { @@ -179,18 +134,14 @@ export class DebugViewPaneContainer extends ViewPaneContainer { } this.disposeOnTitleUpdate = disposable; - result = actions; - } else if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { - result = [this.toggleReplAction]; - } else { - result = [this.startAction, this.configureAction, this.toggleReplAction]; + return actions; } - if (this.progressEvents.filter(e => e.event.body.cancellable).length) { - result.unshift(this.cancelAction); + if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { + return [this.toggleReplAction]; } - return result; + return [this.startAction, this.configureAction, this.toggleReplAction]; } get showInitialDebugActions(): boolean { @@ -235,7 +186,7 @@ export class DebugViewPaneContainer extends ViewPaneContainer { } if (state === State.Initializing) { - this.progressService.withProgress({ location: VIEWLET_ID }, _progress => { + this.progressService.withProgress({ location: VIEWLET_ID, }, _progress => { return new Promise(resolve => this.progressResolve = resolve); }); } diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts new file mode 100644 index 0000000000000..fa48f36d05739 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const INSERT_CODE_CELL_ABOVE_COMMAND_ID = 'workbench.notebook.code.insertCellAbove'; +export const INSERT_CODE_CELL_BELOW_COMMAND_ID = 'workbench.notebook.code.insertCellBelow'; +export const INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID = 'workbench.notebook.markdown.insertCellAbove'; +export const INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID = 'workbench.notebook.markdown.insertCellAbove'; + +export const EDIT_CELL_COMMAND_ID = 'workbench.notebook.cell.edit'; +export const SAVE_CELL_COMMAND_ID = 'workbench.notebook.cell.save'; +export const DELETE_CELL_COMMAND_ID = 'workbench.notebook.cell.delete'; + +export const MOVE_CELL_UP_COMMAND_ID = 'workbench.notebook.cell.moveUp'; +export const MOVE_CELL_DOWN_COMMAND_ID = 'workbench.notebook.cell.moveDown'; +export const COPY_CELL_UP_COMMAND_ID = 'workbench.notebook.cell.copyUp'; +export const COPY_CELL_DOWN_COMMAND_ID = 'workbench.notebook.cell.copyDown'; + +export const EXECUTE_CELL_COMMAND_ID = 'workbench.notebook.cell.execute'; + +// Cell sizing related +export const CELL_MARGIN = 24; +export const EDITOR_TOP_PADDING = 8; +export const EDITOR_BOTTOM_PADDING = 8; +export const EDITOR_TOOLBAR_HEIGHT = 22; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts new file mode 100644 index 0000000000000..a38156a27f738 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookActions.ts @@ -0,0 +1,956 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { localize } from 'vs/nls'; +import { Action2, IAction2Options, MenuId, MenuItemAction, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { InputFocusedContext, InputFocusedContextKey, IsDevelopmentContext } from 'vs/platform/contextkey/common/contextkeys'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { DELETE_CELL_COMMAND_ID, EDIT_CELL_COMMAND_ID, INSERT_CODE_CELL_ABOVE_COMMAND_ID, INSERT_CODE_CELL_BELOW_COMMAND_ID, INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID, INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, MOVE_CELL_DOWN_COMMAND_ID, MOVE_CELL_UP_COMMAND_ID, SAVE_CELL_COMMAND_ID, COPY_CELL_UP_COMMAND_ID, COPY_CELL_DOWN_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/constants'; +import { INotebookEditor, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, NOTEBOOK_EDITOR_FOCUSED, ICellViewModel, CellState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.executeNotebookCell', + title: localize('notebookActions.execute', "Execute Notebook Cell"), + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext), + primary: KeyMod.WinCtrl | KeyCode.Enter, + win: { + primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Enter + }, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + runActiveCell(accessor); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.executeNotebookCellSelectBelow', + title: localize('notebookActions.executeAndSelectBelow', "Execute Notebook Cell and Select Below"), + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext), + primary: KeyMod.Shift | KeyCode.Enter, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const activeCell = runActiveCell(accessor); + if (!activeCell) { + return; + } + + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + const idx = editor.viewModel?.getViewCellIndex(activeCell); + if (typeof idx !== 'number') { + return; + } + + // Try to select below, fall back on inserting + const nextCell = editor.viewModel?.viewCells[idx + 1]; + if (nextCell) { + editor.focusNotebookCell(nextCell, false); + } else { + editor.insertNotebookCell(activeCell, CellKind.Code, 'below'); + } + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.executeNotebookCellInsertBelow', + title: localize('notebookActions.executeAndInsertBelow', "Execute Notebook Cell and Insert Below"), + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext), + primary: KeyMod.Alt | KeyCode.Enter, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const activeCell = runActiveCell(accessor); + if (!activeCell) { + return; + } + + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + editor.insertNotebookCell(activeCell, CellKind.Code, 'below'); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.executeNotebook', + title: localize('notebookActions.executeNotebook', "Execute Notebook") + }); + } + + async run(accessor: ServicesAccessor): Promise { + let editorService = accessor.get(IEditorService); + let notebookService = accessor.get(INotebookService); + + let resource = editorService.activeEditor?.resource; + + if (!resource) { + return; + } + + let notebookProviders = notebookService.getContributedNotebookProviders(resource!); + + if (notebookProviders.length > 0) { + let viewType = notebookProviders[0].id; + notebookService.executeNotebook(viewType, resource); + } + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.quitNotebookEdit', + title: localize('notebookActions.quitEditing', "Quit Notebook Cell Editing"), + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext), + primary: KeyCode.Escape, + weight: KeybindingWeight.EditorContrib - 5 + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + let editorService = accessor.get(IEditorService); + let editor = getActiveNotebookEditor(editorService); + + if (!editor) { + return; + } + + let activeCell = editor.getActiveCell(); + if (activeCell) { + if (activeCell.cellKind === CellKind.Markdown) { + activeCell.state = CellState.Preview; + } + + editor.focusNotebookCell(activeCell, false); + } + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.notebook.hideFind', + title: localize('notebookActions.hideFind', "Hide Find in Notebook"), + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED), + primary: KeyCode.Escape, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + let editorService = accessor.get(IEditorService); + let editor = getActiveNotebookEditor(editorService); + + editor?.hideFind(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.notebook.find', + title: localize('notebookActions.findInNotebook', "Find in Notebook"), + keybinding: { + when: NOTEBOOK_EDITOR_FOCUSED, + primary: KeyCode.KEY_F | KeyMod.CtrlCmd, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + let editorService = accessor.get(IEditorService); + let editor = getActiveNotebookEditor(editorService); + + editor?.showFind(); + } +}); + +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: 'workbench.action.executeNotebook', + title: localize('notebookActions.menu.executeNotebook', "Execute Notebook (Run all cells)"), + icon: { id: 'codicon/debug-start' } + }, + order: -1, + group: 'navigation', + when: NOTEBOOK_EDITOR_FOCUSED +}); + + +MenuRegistry.appendMenuItem(MenuId.EditorTitle, { + command: { + id: 'workbench.action.executeNotebookCell', + title: localize('notebookActions.menu.execute', "Execute Notebook Cell"), + icon: { id: 'codicon/debug-continue' } + }, + order: -1, + group: 'navigation', + when: NOTEBOOK_EDITOR_FOCUSED +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.changeCellToCode', + title: localize('notebookActions.changeCellToCode', "Change Cell to Code"), + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), + primary: KeyCode.KEY_Y, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + return changeActiveCellToKind(CellKind.Code, accessor); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.changeCellToMarkdown', + title: localize('notebookActions.changeCellToMarkdown', "Change Cell to Markdown"), + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), + primary: KeyCode.KEY_M, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + return changeActiveCellToKind(CellKind.Markdown, accessor); + } +}); + +function getActiveNotebookEditor(editorService: IEditorService): INotebookEditor | undefined { + // TODO can `isNotebookEditor` be on INotebookEditor to avoid a circular dependency? + const activeEditorPane = editorService.activeEditorPane as any | undefined; + return activeEditorPane?.isNotebookEditor ? activeEditorPane : undefined; +} + +function runActiveCell(accessor: ServicesAccessor): ICellViewModel | undefined { + const editorService = accessor.get(IEditorService); + const notebookService = accessor.get(INotebookService); + + const resource = editorService.activeEditor?.resource; + if (!resource) { + return; + } + + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + const notebookProviders = notebookService.getContributedNotebookProviders(resource); + if (!notebookProviders.length) { + return; + } + + const activeCell = editor.getActiveCell(); + if (!activeCell) { + return; + } + + const idx = editor.viewModel?.getViewCellIndex(activeCell); + if (typeof idx !== 'number') { + return; + } + + const viewType = notebookProviders[0].id; + notebookService.executeNotebookActiveCell(viewType, resource); + + return activeCell; +} + +function changeActiveCellToKind(kind: CellKind, accessor: ServicesAccessor): void { + const editorService = accessor.get(IEditorService); + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + const activeCell = editor.getActiveCell(); + if (!activeCell) { + return; + } + + if (activeCell.cellKind === kind) { + return; + } + + const text = activeCell.getText(); + editor.insertNotebookCell(activeCell, kind, 'below', text); + const idx = editor.viewModel?.getViewCellIndex(activeCell); + if (typeof idx !== 'number') { + return; + } + + const newCell = editor.viewModel?.viewCells[idx + 1]; + if (!newCell) { + return; + } + + editor.focusNotebookCell(newCell, false); + editor.deleteNotebookCell(activeCell); +} + +export interface INotebookCellActionContext { + cell: ICellViewModel; + notebookEditor: INotebookEditor; +} + +function getActiveCellContext(accessor: ServicesAccessor): INotebookCellActionContext | undefined { + const editorService = accessor.get(IEditorService); + + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + const activeCell = editor.getActiveCell(); + if (!activeCell) { + return; + } + + return { + cell: activeCell, + notebookEditor: editor + }; +} + +abstract class InsertCellCommand extends Action2 { + constructor( + desc: Readonly, + private kind: CellKind, + private direction: 'above' | 'below' + ) { + super(desc); + } + + async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { + if (!context) { + context = getActiveCellContext(accessor); + if (!context) { + return; + } + } + + context.notebookEditor.insertNotebookCell(context.cell, this.kind, this.direction); + } +} + +registerAction2(class extends InsertCellCommand { + constructor() { + super( + { + id: INSERT_CODE_CELL_ABOVE_COMMAND_ID, + title: localize('notebookActions.insertCodeCellAbove', "Insert Code Cell Above") + }, + CellKind.Code, + 'above'); + } +}); + +registerAction2(class extends InsertCellCommand { + constructor() { + super( + { + id: INSERT_CODE_CELL_BELOW_COMMAND_ID, + title: localize('notebookActions.insertCodeCellBelow', "Insert Code Cell Below") + }, + CellKind.Code, + 'below'); + } +}); + +registerAction2(class extends InsertCellCommand { + constructor() { + super( + { + id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, + title: localize('notebookActions.insertMarkdownCellAbove', "Insert Markdown Cell Above"), + }, + CellKind.Markdown, + 'above'); + } +}); + +registerAction2(class extends InsertCellCommand { + constructor() { + super( + { + id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, + title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"), + }, + CellKind.Code, + 'below'); + } +}); + +export class InsertCodeCellAboveAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: INSERT_CODE_CELL_ABOVE_COMMAND_ID, + title: localize('notebookActions.insertCodeCellAbove', "Insert Code Cell Above"), + icon: { id: 'codicon/add' } + }, + undefined, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + } +} + +export class InsertCodeCellBelowAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: INSERT_CODE_CELL_BELOW_COMMAND_ID, + title: localize('notebookActions.insertCodeCellBelow', "Insert Code Cell Below"), + icon: { id: 'codicon/add' } + }, + { + id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, + title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"), + icon: { id: 'codicon/add' } + }, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + } +} + +export class InsertMarkdownCellAboveAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID, + title: localize('notebookActions.insertMarkdownCellAbove', "Insert Markdown Cell Above"), + icon: { id: 'codicon/add' } + }, + undefined, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + } +} + +export class InsertMarkdownCellBelowAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, + title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"), + icon: { id: 'codicon/add' } + }, + undefined, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + } +} + +registerAction2(class extends Action2 { + constructor() { + super( + { + id: EDIT_CELL_COMMAND_ID, + title: localize('notebookActions.editCell', "Edit Cell"), + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), + primary: KeyCode.Enter, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { + if (!context) { + context = getActiveCellContext(accessor); + if (!context) { + return; + } + } + + return context.notebookEditor.editNotebookCell(context.cell); + } +}); + +export class EditCellAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: EDIT_CELL_COMMAND_ID, + title: localize('notebookActions.editCell', "Edit Cell"), + icon: { id: 'codicon/pencil' } + }, + undefined, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + } +} + +registerAction2(class extends Action2 { + constructor() { + super( + { + id: SAVE_CELL_COMMAND_ID, + title: localize('notebookActions.saveCell', "Save Cell") + }); + } + + run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { + if (!context) { + context = getActiveCellContext(accessor); + if (!context) { + return; + } + } + + return context.notebookEditor.saveNotebookCell(context.cell); + } +}); + +export class SaveCellAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: SAVE_CELL_COMMAND_ID, + title: localize('notebookActions.saveCell', "Save Cell"), + icon: { id: 'codicon/save' } + }, + undefined, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + } +} + +registerAction2(class extends Action2 { + constructor() { + super( + { + id: DELETE_CELL_COMMAND_ID, + title: localize('notebookActions.deleteCell', "Delete Cell") + }); + } + + run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { + if (!context) { + context = getActiveCellContext(accessor); + if (!context) { + return; + } + } + + return context.notebookEditor.deleteNotebookCell(context.cell); + } +}); + +export class DeleteCellAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: DELETE_CELL_COMMAND_ID, + title: localize('notebookActions.deleteCell', "Delete Cell"), + icon: { id: 'codicon/x' } + }, + undefined, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + + this.class = 'codicon-x'; + } +} + +async function moveCell(context: INotebookCellActionContext, direction: 'up' | 'down'): Promise { + direction === 'up' ? + context.notebookEditor.moveCellUp(context.cell) : + context.notebookEditor.moveCellDown(context.cell); +} + +async function copyCell(context: INotebookCellActionContext, direction: 'up' | 'down'): Promise { + const text = context.cell.getText(); + const newCellDirection = direction === 'up' ? 'above' : 'below'; + return context.notebookEditor.insertNotebookCell(context.cell, context.cell.cellKind, newCellDirection, text); +} + +registerAction2(class extends Action2 { + constructor() { + super( + { + id: MOVE_CELL_UP_COMMAND_ID, + title: localize('notebookActions.moveCellUp', "Move Cell Up") + }); + } + + async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { + if (!context) { + context = getActiveCellContext(accessor); + if (!context) { + return; + } + } + + return moveCell(context, 'up'); + } +}); + +export class MoveCellUpAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: MOVE_CELL_UP_COMMAND_ID, + title: localize('notebookActions.moveCellUp', "Move Cell Up"), + icon: { id: 'codicon/arrow-up' } + }, + { + id: COPY_CELL_UP_COMMAND_ID, + title: localize('notebookActions.copyCellUp', "Copy Cell Up"), + icon: { id: 'codicon/arrow-up' } + }, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + } +} + +registerAction2(class extends Action2 { + constructor() { + super( + { + id: MOVE_CELL_DOWN_COMMAND_ID, + title: localize('notebookActions.moveCellDown', "Move Cell Down") + }); + } + + async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { + if (!context) { + context = getActiveCellContext(accessor); + if (!context) { + return; + } + } + + return moveCell(context, 'down'); + } +}); + +export class MoveCellDownAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: MOVE_CELL_DOWN_COMMAND_ID, + title: localize('notebookActions.moveCellDown', "Move Cell Down"), + icon: { id: 'codicon/arrow-down' } + }, + { + id: COPY_CELL_DOWN_COMMAND_ID, + title: localize('notebookActions.copyCellDown', "Copy Cell Down"), + icon: { id: 'codicon/arrow-down' } + }, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + + this.class = 'codicon-arrow-down'; + } +} + +registerAction2(class extends Action2 { + constructor() { + super( + { + id: COPY_CELL_UP_COMMAND_ID, + title: localize('notebookActions.copyCellUp', "Copy Cell Up") + }); + } + + async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { + if (!context) { + context = getActiveCellContext(accessor); + if (!context) { + return; + } + } + + return copyCell(context, 'up'); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super( + { + id: COPY_CELL_DOWN_COMMAND_ID, + title: localize('notebookActions.copyCellDown', "Copy Cell Down") + }); + } + + async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { + if (!context) { + context = getActiveCellContext(accessor); + if (!context) { + return; + } + } + + return copyCell(context, 'down'); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.notebook.cursorDown', + title: 'Notebook Cursor Move Down', + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.has(InputFocusedContextKey), NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('top'), NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('none')), + primary: KeyCode.DownArrow, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { + if (!context) { + context = getActiveCellContext(accessor); + if (!context) { + return; + } + } + + const editor = context.notebookEditor; + const activeCell = context.cell; + + const idx = editor.viewModel?.getViewCellIndex(activeCell); + if (typeof idx !== 'number') { + return; + } + + const newCell = editor.viewModel?.viewCells[idx + 1]; + + if (!newCell) { + return; + } + + editor.focusNotebookCell(newCell, true); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.notebook.cursorUp', + title: 'Notebook Cursor Move Up', + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.has(InputFocusedContextKey), NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('bottom'), NOTEBOOK_EDITOR_CURSOR_BOUNDARY.notEqualsTo('none')), + primary: KeyCode.UpArrow, + weight: KeybindingWeight.WorkbenchContrib + }, + }); + } + + async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { + if (!context) { + context = getActiveCellContext(accessor); + if (!context) { + return; + } + } + + const editor = context.notebookEditor; + const activeCell = context.cell; + + const idx = editor.viewModel?.getViewCellIndex(activeCell); + if (typeof idx !== 'number') { + return; + } + + if (idx < 1) { + // we don't do loop + return; + } + + const newCell = editor.viewModel?.viewCells[idx - 1]; + + if (!newCell) { + return; + } + + editor.focusNotebookCell(newCell, true); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.notebook.undo', + title: 'Notebook Undo', + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), + primary: KeyMod.CtrlCmd | KeyCode.KEY_Z, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + const viewModel = editor.viewModel; + + if (!viewModel) { + return; + } + + viewModel.undo(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.notebook.redo', + title: 'Notebook Redo', + keybinding: { + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z, + weight: KeybindingWeight.WorkbenchContrib + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + const viewModel = editor.viewModel; + + if (!viewModel) { + return; + } + + viewModel.redo(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.notebook.testResize', + title: 'Notebook Test Cell Resize', + keybinding: { + when: IsDevelopmentContext, + primary: undefined, + weight: KeybindingWeight.WorkbenchContrib + }, + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const resource = editorService.activeEditor?.resource; + if (!resource) { + return; + } + + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + const cells = editor.viewModel?.viewCells; + + if (cells && cells.length) { + const firstCell = cells[0]; + editor.layoutNotebookCell(firstCell, 400); + } + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget.ts new file mode 100644 index 0000000000000..9036f508eee60 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget.ts @@ -0,0 +1,234 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED, INotebookEditor, CellFindMatch, CellState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { FindDecorations } from 'vs/editor/contrib/find/findDecorations'; +import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { ICellModelDeltaDecorations, ICellModelDecorations } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; +import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { SimpleFindReplaceWidget } from 'vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + +export class NotebookFindWidget extends SimpleFindReplaceWidget { + protected _findWidgetFocused: IContextKey; + private _findMatches: CellFindMatch[] = []; + protected _findMatchesStarts: PrefixSumComputer | null = null; + private _currentMatch: number = -1; + private _allMatchesDecorations: ICellModelDecorations[] = []; + private _currentMatchDecorations: ICellModelDecorations[] = []; + + constructor( + private readonly _notebookEditor: INotebookEditor, + @IContextViewService contextViewService: IContextViewService, + @IContextKeyService contextKeyService: IContextKeyService, + @IThemeService themeService: IThemeService, + + ) { + super(contextViewService, contextKeyService, themeService); + this._findWidgetFocused = KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED.bindTo(contextKeyService); + this._register(this._findInput.onKeyDown((e) => this._onFindInputKeyDown(e))); + } + + private _onFindInputKeyDown(e: IKeyboardEvent): void { + if (e.equals(KeyCode.Enter)) { + if (this._findMatches.length) { + this.set(this._findMatches); + + if (this._currentMatch !== -1) { + const nextIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); + this.revealCellRange(nextIndex.index, nextIndex.remainder); + } + } else { + this.set(null); + } + e.preventDefault(); + return; + } + } + + protected onInputChanged(): boolean { + const val = this.inputValue; + if (val) { + this._findMatches = this._notebookEditor.viewModel!.find(val).filter(match => match.matches.length > 0); + if (this._findMatches.length) { + return true; + } else { + return false; + } + } + + return false; + } + + protected find(previous: boolean): void { + if (!this._findMatches.length) { + return; + } + + if (!this._findMatchesStarts) { + this.set(this._findMatches); + } + + const totalVal = this._findMatchesStarts!.getTotalValue(); + const nextVal = (this._currentMatch + (previous ? -1 : 1) + totalVal) % totalVal; + this._currentMatch = nextVal; + + const nextIndex = this._findMatchesStarts!.getIndexOf(nextVal); + this.setCurrentFindMatchDecoration(nextIndex.index, nextIndex.remainder); + this.revealCellRange(nextIndex.index, nextIndex.remainder); + } + + protected replaceOne() { + if (!this._findMatches.length) { + return; + } + + if (!this._findMatchesStarts) { + this.set(this._findMatches); + } + + const nextIndex = this._findMatchesStarts!.getIndexOf(this._currentMatch); + const cell = this._findMatches[nextIndex.index].cell; + const match = this._findMatches[nextIndex.index].matches[nextIndex.remainder]; + + this._progressBar.infinite().show(); + + this._notebookEditor.viewModel!.replaceOne(cell, match.range, this.replaceValue).then(() => { + this._progressBar.stop(); + }); + } + + protected replaceAll() { + this._progressBar.infinite().show(); + + this._notebookEditor.viewModel!.replaceAll(this._findMatches, this.replaceValue).then(() => { + this._progressBar.stop(); + }); + } + + private revealCellRange(cellIndex: number, matchIndex: number) { + this._findMatches[cellIndex].cell.state = CellState.Editing; + this._notebookEditor.selectElement(this._findMatches[cellIndex].cell); + this._notebookEditor.setCellSelection(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range); + this._notebookEditor.revealRangeInCenterIfOutsideViewport(this._findMatches[cellIndex].cell, this._findMatches[cellIndex].matches[matchIndex].range); + } + + hide() { + super.hide(); + this.set([]); + } + + protected findFirst(): void { } + + protected onFocusTrackerFocus() { + this._findWidgetFocused.set(true); + } + + protected onFocusTrackerBlur() { + this._findWidgetFocused.reset(); + } + + protected onReplaceInputFocusTrackerFocus(): void { + // throw new Error('Method not implemented.'); + } + protected onReplaceInputFocusTrackerBlur(): void { + // throw new Error('Method not implemented.'); + } + + protected onFindInputFocusTrackerFocus(): void { } + protected onFindInputFocusTrackerBlur(): void { } + + private constructFindMatchesStarts() { + if (this._findMatches && this._findMatches.length) { + const values = new Uint32Array(this._findMatches.length); + for (let i = 0; i < this._findMatches.length; i++) { + values[i] = this._findMatches[i].matches.length; + } + + this._findMatchesStarts = new PrefixSumComputer(values); + } else { + this._findMatchesStarts = null; + } + } + + private set(cellFindMatches: CellFindMatch[] | null): void { + if (!cellFindMatches || !cellFindMatches.length) { + this._findMatches = []; + this.setAllFindMatchesDecorations([]); + + this.constructFindMatchesStarts(); + this._currentMatch = -1; + this.clearCurrentFindMatchDecoration(); + return; + } + + // all matches + this._findMatches = cellFindMatches; + this.setAllFindMatchesDecorations(cellFindMatches || []); + + // current match + this.constructFindMatchesStarts(); + this._currentMatch = 0; + this.setCurrentFindMatchDecoration(0, 0); + } + + private setCurrentFindMatchDecoration(cellIndex: number, matchIndex: number) { + this._notebookEditor.changeDecorations(accessor => { + const findMatchesOptions: ModelDecorationOptions = FindDecorations._CURRENT_FIND_MATCH_DECORATION; + + const cell = this._findMatches[cellIndex].cell; + const match = this._findMatches[cellIndex].matches[matchIndex]; + const decorations: IModelDeltaDecoration[] = [ + { range: match.range, options: findMatchesOptions } + ]; + const deltaDecoration: ICellModelDeltaDecorations = { + ownerId: cell.handle, + decorations: decorations + }; + + this._currentMatchDecorations = accessor.deltaDecorations(this._currentMatchDecorations, [deltaDecoration]); + }); + } + + private clearCurrentFindMatchDecoration() { + this._notebookEditor.changeDecorations(accessor => { + this._currentMatchDecorations = accessor.deltaDecorations(this._currentMatchDecorations, []); + }); + } + + private setAllFindMatchesDecorations(cellFindMatches: CellFindMatch[]) { + this._notebookEditor.changeDecorations((accessor) => { + + let findMatchesOptions: ModelDecorationOptions = FindDecorations._FIND_MATCH_DECORATION; + + let deltaDecorations: ICellModelDeltaDecorations[] = cellFindMatches.map(cellFindMatch => { + const findMatches = cellFindMatch.matches; + + // 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 + }; + } + + return { ownerId: cellFindMatch.cell.handle, decorations: newFindMatchesDecorations }; + }); + + this._allMatchesDecorations = accessor.deltaDecorations(this._allMatchesDecorations, deltaDecorations); + }); + } + + clear() { + this._currentMatch = -1; + this._findMatches = []; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts new file mode 100644 index 0000000000000..7dbcd235058c9 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts @@ -0,0 +1,119 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import * as nls from 'vs/nls'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { NotebookSelector } from 'vs/workbench/contrib/notebook/common/notebookProvider'; + +namespace NotebookEditorContribution { + export const viewType = 'viewType'; + export const displayName = 'displayName'; + export const selector = 'selector'; +} + +interface INotebookEditorContribution { + readonly [NotebookEditorContribution.viewType]: string; + readonly [NotebookEditorContribution.displayName]: string; + readonly [NotebookEditorContribution.selector]?: readonly NotebookSelector[]; +} + +namespace NotebookRendererContribution { + export const viewType = 'viewType'; + export const displayName = 'displayName'; + export const mimeTypes = 'mimeTypes'; +} + +interface INotebookRendererContribution { + readonly [NotebookRendererContribution.viewType]: string; + readonly [NotebookRendererContribution.displayName]: string; + readonly [NotebookRendererContribution.mimeTypes]?: readonly string[]; +} + + + +const notebookProviderContribution: IJSONSchema = { + description: nls.localize('contributes.notebook.provider', 'Contributes notebook document provider.'), + type: 'array', + defaultSnippets: [{ body: [{ viewType: '', displayName: '' }] }], + items: { + type: 'object', + required: [ + NotebookEditorContribution.viewType, + NotebookEditorContribution.displayName, + NotebookEditorContribution.selector, + ], + properties: { + [NotebookEditorContribution.viewType]: { + type: 'string', + description: nls.localize('contributes.notebook.provider.viewType', 'Unique identifier of the notebook.'), + }, + [NotebookEditorContribution.displayName]: { + type: 'string', + description: nls.localize('contributes.notebook.provider.displayName', 'Human readable name of the notebook.'), + }, + [NotebookEditorContribution.selector]: { + type: 'array', + description: nls.localize('contributes.notebook.provider.selector', 'Set of globs that the notebook is for.'), + items: { + type: 'object', + properties: { + filenamePattern: { + type: 'string', + description: nls.localize('contributes.notebook.provider.selector.filenamePattern', 'Glob that the notebook is enabled for.'), + }, + excludeFileNamePattern: { + type: 'string', + description: nls.localize('contributes.notebook.selector.provider.excludeFileNamePattern', 'Glob that the notebook is disabled for.') + } + } + } + } + } + } +}; + +const notebookRendererContribution: IJSONSchema = { + description: nls.localize('contributes.notebook.renderer', 'Contributes notebook output renderer provider.'), + type: 'array', + defaultSnippets: [{ body: [{ viewType: '', displayName: '', mimeTypes: [''] }] }], + items: { + type: 'object', + required: [ + NotebookRendererContribution.viewType, + NotebookRendererContribution.displayName, + NotebookRendererContribution.mimeTypes, + ], + properties: { + [NotebookRendererContribution.viewType]: { + type: 'string', + description: nls.localize('contributes.notebook.renderer.viewType', 'Unique identifier of the notebook output renderer.'), + }, + [NotebookRendererContribution.displayName]: { + type: 'string', + description: nls.localize('contributes.notebook.renderer.displayName', 'Human readable name of the notebook output renderer.'), + }, + [NotebookRendererContribution.mimeTypes]: { + type: 'array', + description: nls.localize('contributes.notebook.selector', 'Set of globs that the notebook is for.'), + items: { + type: 'string' + } + } + } + } +}; + +export const notebookProviderExtensionPoint = ExtensionsRegistry.registerExtensionPoint( + { + extensionPoint: 'notebookProvider', + jsonSchema: notebookProviderContribution + }); + +export const notebookRendererExtensionPoint = ExtensionsRegistry.registerExtensionPoint( + { + extensionPoint: 'notebookOutputRenderer', + jsonSchema: notebookRendererContribution + }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts new file mode 100644 index 0000000000000..4e3e2f82da425 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -0,0 +1,220 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; +import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { IEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, IEditorInputFactory, EditorInput } from 'vs/workbench/common/editor'; +import { NotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { INotebookService, NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService'; +import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ITextModel } from 'vs/editor/common/model'; +import { URI } from 'vs/base/common/uri'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { assertType } from 'vs/base/common/types'; +import { parse } from 'vs/base/common/marshalling'; +import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ResourceMap } from 'vs/base/common/map'; + +// Output renderers registration + +import 'vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform'; +import 'vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform'; +import 'vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform'; + +// Actions +import 'vs/workbench/contrib/notebook/browser/contrib/notebookActions'; +import { basename } from 'vs/base/common/resources'; +import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; + +Registry.as(EditorExtensions.Editors).registerEditor( + EditorDescriptor.create( + NotebookEditor, + NotebookEditor.ID, + 'Notebook Editor' + ), + [ + new SyncDescriptor(NotebookEditorInput) + ] +); + +Registry.as(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory( + NotebookEditorInput.ID, + class implements IEditorInputFactory { + canSerialize(): boolean { + return true; + } + serialize(input: EditorInput): string { + assertType(input instanceof NotebookEditorInput); + return JSON.stringify({ + resource: input.resource, + name: input.name, + viewType: input.viewType, + }); + } + deserialize(instantiationService: IInstantiationService, raw: string) { + type Data = { resource: URI, name: string, viewType: string }; + const data = parse(raw); + if (!data) { + return undefined; + } + const { resource, name, viewType } = data; + if (!data || !URI.isUri(resource) || typeof name !== 'string' || typeof viewType !== 'string') { + return undefined; + } + // TODO@joh,peng this is disabled because the note-editor isn't fit for being + // restorted (as it seems) + if ('true') { + return undefined; + } + return instantiationService.createInstance(NotebookEditorInput, resource, name, viewType); + } + } +); + +function getFirstNotebookInfo(notebookService: INotebookService, uri: URI): NotebookProviderInfo | undefined { + return notebookService.getContributedNotebookProviders(uri)[0]; +} + +export class NotebookContribution implements IWorkbenchContribution { + private _resourceMapping = new ResourceMap(); + + constructor( + @IEditorService private readonly editorService: IEditorService, + @INotebookService private readonly notebookService: INotebookService, + @IInstantiationService private readonly instantiationService: IInstantiationService + + ) { + this.editorService.overrideOpenEditor((editor, options, group) => this.onEditorOpening(editor, options, group)); + + this.editorService.onDidActiveEditorChange(() => { + if (this.editorService.activeEditor && this.editorService.activeEditor! instanceof NotebookEditorInput) { + let editorInput = this.editorService.activeEditor! as NotebookEditorInput; + this.notebookService.updateActiveNotebookDocument(editorInput.viewType!, editorInput.resource!); + } + }); + } + + private onEditorOpening(originalInput: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup): IOpenEditorOverride | undefined { + let resource = originalInput.resource; + if (!resource) { + return undefined; + } + + let info: NotebookProviderInfo | undefined; + const data = CellUri.parse(resource); + if (data && (info = getFirstNotebookInfo(this.notebookService, data.notebook))) { + // cell-uri -> open (container) notebook + const name = basename(data.notebook); + const input = this.instantiationService.createInstance(NotebookEditorInput, data.notebook, name, info.id); + this._resourceMapping.set(resource, input); + return { override: this.editorService.openEditor(input, new NotebookEditorOptions({ ...options, forceReload: true, cellOptions: { resource, options } }), group) }; + } + + info = getFirstNotebookInfo(this.notebookService, resource); + if (!info) { + return undefined; + } + + if (this._resourceMapping.has(resource)) { + const input = this._resourceMapping.get(resource); + + if (!input!.isDisposed()) { + return { override: this.editorService.openEditor(input!, new NotebookEditorOptions(options || {}).with({ ignoreOverrides: true }), group) }; + } + } + + const input = this.instantiationService.createInstance(NotebookEditorInput, resource, originalInput.getName(), info.id); + this._resourceMapping.set(resource, input); + + return { override: this.editorService.openEditor(input, options, group) }; + } +} + +class CellContentProvider implements ITextModelContentProvider { + + private readonly _registration: IDisposable; + + constructor( + @ITextModelService textModelService: ITextModelService, + @IModelService private readonly _modelService: IModelService, + @IModeService private readonly _modeService: IModeService, + @INotebookService private readonly _notebookService: INotebookService, + ) { + this._registration = textModelService.registerTextModelContentProvider('vscode-notebook', this); + } + + dispose(): void { + this._registration.dispose(); + } + + async provideTextContent(resource: URI): Promise { + const existing = this._modelService.getModel(resource); + if (existing) { + return existing; + } + const data = CellUri.parse(resource); + // const data = parseCellUri(resource); + if (!data) { + return null; + } + const info = getFirstNotebookInfo(this._notebookService, data.notebook); + if (!info) { + return null; + } + const notebook = await this._notebookService.resolveNotebook(info.id, data.notebook); + if (!notebook) { + return null; + } + for (let cell of notebook.cells) { + if (cell.uri.toString() === resource.toString()) { + let bufferFactory = cell.resolveTextBufferFactory(); + return this._modelService.createModel( + bufferFactory, + cell.language ? this._modeService.create(cell.language) : this._modeService.createByFilepathOrFirstLine(resource, cell.source[0]), + resource + ); + } + } + + return null; + } +} + +const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchContributionsRegistry.registerWorkbenchContribution(NotebookContribution, LifecyclePhase.Starting); +workbenchContributionsRegistry.registerWorkbenchContribution(CellContentProvider, LifecyclePhase.Starting); + +registerSingleton(INotebookService, NotebookService); + +const configurationRegistry = Registry.as(Extensions.Configuration); +configurationRegistry.registerConfiguration({ + id: 'notebook', + order: 100, + title: nls.localize('notebookConfigurationTitle', "Notebook"), + type: 'object', + properties: { + 'notebook.displayOrder': { + markdownDescription: nls.localize('notebook.displayOrder.description', "Priority list for output mime types"), + type: ['array'], + items: { + type: 'string' + }, + default: [] + } + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.css b/src/vs/workbench/contrib/notebook/browser/notebook.css new file mode 100644 index 0000000000000..d72a77ae50caa --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebook.css @@ -0,0 +1,294 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .part.editor > .content .notebook-editor { + box-sizing: border-box; + line-height: 22px; + user-select: initial; + -webkit-user-select: initial; + position: relative; +} + +.cell.markdown { + user-select: text; + -webkit-user-select: text; + white-space: initial; +} + +/* .monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > div.cell { + transform: translate3d(0, 0, 0); +} */ + +.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container { + position: relative; +} + +.monaco-workbench .part.editor > .content .notebook-editor .notebook-content-widgets { + position: absolute; + top: 0; + left: 0; + width: 100%; +} + +.monaco-workbench .part.editor > .content .notebook-editor .output { + padding-left: 8px; + padding-right: 8px; + user-select: text; + transform: translate3d(0px, 0px, 0px); + cursor: auto; + box-sizing: border-box; +} + +.monaco-workbench .part.editor > .content .notebook-editor .output p { + white-space: initial; + overflow-x: auto; + margin: 0px; +} + +.monaco-workbench .part.editor > .content .notebook-editor .output > div.foreground { + padding: 8px; + box-sizing: border-box; +} + +.monaco-workbench .part.editor > .content .notebook-editor .output .multi-mimetype-output { + position: absolute; + top: 4px; + left: -24px; + width: 16px; + height: 16px; + cursor: pointer; +} + +.monaco-workbench .part.editor > .content .notebook-editor .output .error_message { + color: red; +} + +.monaco-workbench .part.editor > .content .notebook-editor .output pre.traceback { + margin: 8px 0; +} + +.monaco-workbench .part.editor > .content .notebook-editor .output .traceback > span { + display: block; +} + +.monaco-workbench .part.editor > .content .notebook-editor .output .display img { + max-width: 100%; +} + + +.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row { + overflow: visible !important; +} + + +.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row:focus-within { + z-index: 10; +} + +.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .menu { + position: absolute; + left: 0; + top: 8px; + visibility: hidden; + width: 16px; + margin: auto; + padding-left: 4px; +} + + +.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .menu.mouseover, +.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row:hover .menu { + visibility: visible; +} + +/* .monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row:hover { + outline: none !important; +} */ + +/* .monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.focused { + outline: none !important; +} */ + +.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .menu.mouseover, +.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .menu:hover { + cursor: pointer; +} + +.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .monaco-toolbar { + visibility: hidden; + margin-right: 24px; +} + +.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.focused .monaco-toolbar, +.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row:hover .monaco-toolbar { + visibility: visible; +} + +.monaco-workbench .part.editor > .content .notebook-editor .monaco-tree.focused.no-focused-item:focus:before, +.monaco-workbench .part.editor > .content .notebook-editor .monaco-list:not(.element-focused):focus:before { + outline: none !important; +} + +.notebook-webview { + position: absolute; + z-index: 1000000; + left: 373px; + top: 0px; +} + +/* markdown */ + + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown img { + max-width: 100%; + max-height: 100%; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a { + text-decoration: none; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a:hover { + text-decoration: underline; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a:focus, +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown input:focus, +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown select:focus, +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown textarea:focus { + outline: 1px solid -webkit-focus-ring-color; + outline-offset: -1px; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown hr { + border: 0; + height: 2px; + border-bottom: 2px solid; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1 { + padding-bottom: 0.3em; + line-height: 1.2; + border-bottom-width: 1px; + border-bottom-style: solid; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1, +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h2, +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h3 { + font-weight: normal; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table { + border-collapse: collapse; + border-spacing: 0; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table th, +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table td { + border: 1px solid ; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th { + text-align: left; + border-bottom: 1px solid; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th, +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > td, +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > th, +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > td { + padding: 5px 10px; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr + tr > td { + border-top: 1px solid; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown blockquote { + margin: 0 7px 0 5px; + padding: 0 16px 0 10px; + border-left-width: 5px; + border-left-style: solid; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown code { + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; + font-size: 1em; + line-height: 1.357em; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown body.wordWrap pre { + white-space: pre-wrap; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre:not(.hljs), +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre.hljs code > div { + padding: 16px; + border-radius: 3px; + overflow: auto; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre code { + color: var(--vscode-editor-foreground); + tab-size: 4; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex-block { + display: block; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex { + vertical-align: middle; + display: inline-block; +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex img, +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex-block img { + filter: brightness(0) invert(0) +} + +.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex img, +.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex-block img { + filter: brightness(0) invert(1) +} + +/** Theming */ + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre { + background-color: rgba(220, 220, 220, 0.4); +} + +.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre { + background-color: rgba(10, 10, 10, 0.4); +} + +.hc-black .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre { + background-color: rgb(0, 0, 0); +} + +.hc-black .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1 { + border-color: rgb(0, 0, 0); +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th { + border-color: rgba(0, 0, 0, 0.18); +} + +.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th { + border-color: rgba(255, 255, 255, 0.18); +} + +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1, +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown hr, +.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > td { + border-color: rgba(0, 0, 0, 0.18); +} + +.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1, +.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown hr, +.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > td { + border-color: rgba(255, 255, 255, 0.18); +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts new file mode 100644 index 0000000000000..6c8a60969a48e --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -0,0 +1,255 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; +import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; +import { IOutput, CellKind, IRenderOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { NotebookViewModel, IModelDecorationsChangeAccessor } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { FindMatch } from 'vs/editor/common/model'; +import { Range } from 'vs/editor/common/core/range'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; + +export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); + +export const NOTEBOOK_EDITOR_FOCUSED = new RawContextKey('notebookEditorFocused', false); + +export interface NotebookLayoutInfo { + width: number; + height: number; + fontInfo: BareFontInfo; +} + +export interface ICellViewModel { + readonly id: string; + handle: number; + uri: URI; + cellKind: CellKind; + state: CellState; + focusMode: CellFocusMode; + getText(): string; +} + +export interface INotebookEditor { + + /** + * Notebook view model attached to the current editor + */ + viewModel: NotebookViewModel | undefined; + + /** + * Focus the notebook editor cell list + */ + focus(): void; + + /** + * Select & focus cell + */ + selectElement(cell: ICellViewModel): void; + + /** + * Layout info for the notebook editor + */ + getLayoutInfo(): NotebookLayoutInfo; + /** + * Fetch the output renderers for notebook outputs. + */ + getOutputRenderer(): OutputRenderer; + + /** + * Insert a new cell around `cell` + */ + insertNotebookCell(cell: ICellViewModel, type: CellKind, direction: 'above' | 'below', initialText?: string): Promise; + + /** + * Delete a cell from the notebook + */ + deleteNotebookCell(cell: ICellViewModel): void; + + /** + * Move a cell up one spot + */ + moveCellUp(cell: ICellViewModel): void; + + /** + * Move a cell down one spot + */ + moveCellDown(cell: ICellViewModel): void; + + /** + * Switch the cell into editing mode. + * + * For code cell, the monaco editor will be focused. + * For markdown cell, it will switch from preview mode to editing mode, which focuses the monaco editor. + */ + editNotebookCell(cell: ICellViewModel): void; + + /** + * Quit cell editing mode. + */ + saveNotebookCell(cell: ICellViewModel): void; + + /** + * Focus the container of a cell (the monaco editor inside is not focused). + */ + focusNotebookCell(cell: ICellViewModel, focusEditor: boolean): void; + + /** + * Get current active cell + */ + getActiveCell(): ICellViewModel | undefined; + + /** + * Layout the cell with a new height + */ + layoutNotebookCell(cell: ICellViewModel, height: number): void; + + /** + * Render the output in webview layer + */ + createInset(cell: ICellViewModel, output: IOutput, shadowContent: string, offset: number): void; + + /** + * Remove the output from the webview layer + */ + removeInset(output: IOutput): void; + + /** + * Trigger the editor to scroll from scroll event programmatically + */ + triggerScroll(event: IMouseWheelEvent): void; + + /** + * Reveal cell into viewport. + */ + revealInView(cell: ICellViewModel): void; + + /** + * Reveal cell into viewport center. + */ + revealInCenter(cell: ICellViewModel): void; + + /** + * Reveal cell into viewport center if cell is currently out of the viewport. + */ + revealInCenterIfOutsideViewport(cell: ICellViewModel): void; + + /** + * Reveal a line in notebook cell into viewport with minimal scrolling. + */ + revealLineInView(cell: ICellViewModel, line: number): void; + + /** + * Reveal a line in notebook cell into viewport center. + */ + revealLineInCenter(cell: ICellViewModel, line: number): void; + + /** + * Reveal a line in notebook cell into viewport center. + */ + revealLineInCenterIfOutsideViewport(cell: ICellViewModel, line: number): void; + + /** + * Reveal a range in notebook cell into viewport with minimal scrolling. + */ + revealRangeInView(cell: ICellViewModel, range: Range): void; + + /** + * Reveal a range in notebook cell into viewport center. + */ + revealRangeInCenter(cell: ICellViewModel, range: Range): void; + + /** + * Reveal a range in notebook cell into viewport center. + */ + revealRangeInCenterIfOutsideViewport(cell: ICellViewModel, range: Range): void; + + setCellSelection(cell: ICellViewModel, selection: Range): void; + + /** + * Change the decorations on cells. + * The notebook is virtualized and this method should be called to create/delete editor decorations safely. + */ + changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any; + + /** + * Show Find Widget. + * + * Currently Find is still part of the NotebookEditor core + */ + showFind(): void; + + /** + * Hide Find Widget + */ + hideFind(): void; +} + +export interface CellRenderTemplate { + container: HTMLElement; + cellContainer: HTMLElement; + menuContainer?: HTMLElement; + toolbar: ToolBar; + editingContainer?: HTMLElement; + outputContainer?: HTMLElement; + editor?: CodeEditorWidget; + disposables: DisposableStore; +} + +export interface IOutputTransformContribution { + /** + * Dispose this contribution. + */ + dispose(): void; + + render(output: IOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput; +} + +export interface CellFindMatch { + cell: CellViewModel; + matches: FindMatch[]; +} + +export enum CellRevealType { + Line, + Range +} + +export enum CellRevealPosition { + Top, + Center +} + +export enum CellState { + /** + * Default state. + * For markdown cell, it's Markdown preview. + * For code cell, the browser focus should be on the container instead of the editor + */ + Preview, + + + /** + * Eding mode. Source for markdown or code is rendered in editors and the state will be persistent. + */ + Editing +} + +export enum CellFocusMode { + Container, + Editor +} + +export enum CursorAtBoundary { + None, + Top, + Bottom, + Both +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts new file mode 100644 index 0000000000000..570cdb594cb28 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -0,0 +1,745 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getZoomLevel } from 'vs/base/browser/browser'; +import * as DOM from 'vs/base/browser/dom'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import 'vs/css!./notebook'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { contrastBorder, editorBackground, focusBorder, foreground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorOptions, IEditorMemento, IEditorCloseEvent } from 'vs/workbench/common/editor'; +import { INotebookEditor, NotebookLayoutInfo, CellState, NOTEBOOK_EDITOR_FOCUSED, CellFocusMode, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditorInput, NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; +import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; +import { CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; +import { IOutput, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview'; +import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IEditor, ICompositeCodeEditor } from 'vs/editor/common/editorCommon'; +import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { Emitter, Event } from 'vs/base/common/event'; +import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; +import { NotebookFindWidget } from 'vs/workbench/contrib/notebook/browser/contrib/notebookFindWidget'; +import { NotebookViewModel, INotebookEditorViewState, IModelDecorationsChangeAccessor } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; +import { Range } from 'vs/editor/common/core/range'; +import { CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; + +const $ = DOM.$; +const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; + +export class NotebookEditorOptions extends EditorOptions { + + readonly cellOptions?: IResourceEditorInput; + + constructor(options: Partial) { + super(); + this.overwrite(options); + this.cellOptions = options.cellOptions; + } + + with(options: Partial): NotebookEditorOptions { + return new NotebookEditorOptions({ ...this, ...options }); + } +} + +export class NotebookCodeEditors implements ICompositeCodeEditor { + + private readonly _disposables = new DisposableStore(); + private readonly _onDidChangeActiveEditor = new Emitter(); + readonly onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; + + constructor( + private _list: NotebookCellList, + private _renderedEditors: Map + ) { + _list.onDidChangeFocus(_e => this._onDidChangeActiveEditor.fire(this), undefined, this._disposables); + } + + dispose(): void { + this._onDidChangeActiveEditor.dispose(); + this._disposables.dispose(); + } + + get activeCodeEditor(): IEditor | undefined { + const [focused] = this._list.getFocusedElements(); + return focused instanceof CellViewModel + ? this._renderedEditors.get(focused) + : undefined; + } +} + +export class NotebookEditor extends BaseEditor implements INotebookEditor { + static readonly ID: string = 'workbench.editor.notebook'; + private rootElement!: HTMLElement; + private body!: HTMLElement; + private webview: BackLayerWebView | null = null; + private list: NotebookCellList | undefined; + private control: ICompositeCodeEditor | undefined; + private renderedEditors: Map = new Map(); + private notebookViewModel: NotebookViewModel | undefined; + private localStore: DisposableStore = this._register(new DisposableStore()); + private editorMemento: IEditorMemento; + private readonly groupListener = this._register(new MutableDisposable()); + private fontInfo: BareFontInfo | undefined; + private dimension: DOM.Dimension | null = null; + private editorFocus: IContextKey | null = null; + private outputRenderer: OutputRenderer; + private findWidget: NotebookFindWidget; + + constructor( + @ITelemetryService telemetryService: ITelemetryService, + @IThemeService themeService: IThemeService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IStorageService storageService: IStorageService, + @IWebviewService private webviewService: IWebviewService, + @INotebookService private notebookService: INotebookService, + @IEditorGroupsService editorGroupService: IEditorGroupsService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IEnvironmentService private readonly environmentSerice: IEnvironmentService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { + super(NotebookEditor.ID, telemetryService, themeService, storageService); + + this.editorMemento = this.getEditorMemento(editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); + this.outputRenderer = new OutputRenderer(this, this.instantiationService); + this.findWidget = this.instantiationService.createInstance(NotebookFindWidget, this); + this.findWidget.updateTheme(this.themeService.getColorTheme()); + } + + get viewModel() { + return this.notebookViewModel; + } + + get minimumWidth(): number { return 375; } + get maximumWidth(): number { return Number.POSITIVE_INFINITY; } + + // these setters need to exist because this extends from BaseEditor + set minimumWidth(value: number) { /*noop*/ } + set maximumWidth(value: number) { /*noop*/ } + + + //#region Editor Core + + + public get isNotebookEditor() { + return true; + } + + protected createEditor(parent: HTMLElement): void { + this.rootElement = DOM.append(parent, $('.notebook-editor')); + this.createBody(this.rootElement); + this.generateFontInfo(); + this.editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.contextKeyService); + this._register(this.onDidFocus(() => { + this.editorFocus?.set(true); + })); + + this._register(this.onDidBlur(() => { + this.editorFocus?.set(false); + })); + } + + private generateFontInfo(): void { + const editorOptions = this.configurationService.getValue('editor'); + this.fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()); + } + + private createBody(parent: HTMLElement): void { + this.body = document.createElement('div'); + DOM.addClass(this.body, 'cell-list-container'); + this.createCellList(); + DOM.append(parent, this.body); + DOM.append(parent, this.findWidget.getDomNode()); + } + + private createCellList(): void { + DOM.addClass(this.body, 'cell-list-container'); + + const renders = [ + this.instantiationService.createInstance(CodeCellRenderer, this, this.renderedEditors), + this.instantiationService.createInstance(MarkdownCellRenderer, this), + ]; + + this.list = this.instantiationService.createInstance( + NotebookCellList, + 'NotebookCellList', + this.body, + this.instantiationService.createInstance(NotebookCellListDelegate), + renders, + this.contextKeyService, + { + setRowLineHeight: false, + setRowHeight: false, + supportDynamicHeights: true, + horizontalScrolling: false, + keyboardSupport: false, + mouseSupport: true, + multipleSelectionSupport: false, + enableKeyboardNavigation: true, + overrideStyles: { + listBackground: editorBackground, + listActiveSelectionBackground: editorBackground, + listActiveSelectionForeground: foreground, + listFocusAndSelectionBackground: editorBackground, + listFocusAndSelectionForeground: foreground, + listFocusBackground: editorBackground, + listFocusForeground: foreground, + listHoverForeground: foreground, + listHoverBackground: editorBackground, + listHoverOutline: focusBorder, + listFocusOutline: focusBorder, + listInactiveSelectionBackground: editorBackground, + listInactiveSelectionForeground: foreground, + listInactiveFocusBackground: editorBackground, + listInactiveFocusOutline: editorBackground, + } + }, + ); + + this.control = new NotebookCodeEditors(this.list, this.renderedEditors); + this.webview = new BackLayerWebView(this.webviewService, this.notebookService, this, this.environmentSerice); + this.list.rowsContainer.appendChild(this.webview.element); + this._register(this.list); + } + + getControl() { + return this.control; + } + + onHide() { + this.editorFocus?.set(false); + if (this.webview) { + this.localStore.clear(); + this.list?.rowsContainer.removeChild(this.webview?.element); + this.webview?.dispose(); + this.webview = null; + } + + this.list?.splice(0, this.list?.length); + + if (this.notebookViewModel && !this.notebookViewModel.isDirty()) { + this.notebookService.destoryNotebookDocument(this.notebookViewModel.viewType!, this.notebookViewModel!.notebookDocument); + this.notebookViewModel.dispose(); + this.notebookViewModel = undefined; + } + + super.onHide(); + } + + setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + super.setEditorVisible(visible, group); + this.groupListener.value = ((group as IEditorGroupView).onWillCloseEditor(e => this.onWillCloseEditorInGroup(e))); + } + + private onWillCloseEditorInGroup(e: IEditorCloseEvent): void { + const editor = e.editor; + if (!(editor instanceof NotebookEditorInput)) { + return; // only handle files + } + + if (editor === this.input) { + this.saveTextEditorViewState(editor); + } + } + + focus() { + super.focus(); + this.editorFocus?.set(true); + } + + async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + if (this.input instanceof NotebookEditorInput) { + this.saveTextEditorViewState(this.input); + } + + await super.setInput(input, options, token); + const model = await input.resolve(); + + if (this.notebookViewModel === undefined || !this.notebookViewModel.equal(model) || this.webview === null) { + this.detachModel(); + await this.attachModel(input, model); + } + + // reveal cell if editor options tell to do so + if (options instanceof NotebookEditorOptions && options.cellOptions) { + const cellOptions = options.cellOptions; + const cell = this.notebookViewModel!.viewCells.find(cell => cell.uri.toString() === cellOptions.resource.toString()); + if (cell) { + this.revealInCenterIfOutsideViewport(cell); + const editor = this.renderedEditors.get(cell)!; + if (editor) { + if (cellOptions.options?.selection) { + const { selection } = cellOptions.options; + editor.setSelection({ + ...selection, + endLineNumber: selection.endLineNumber || selection.startLineNumber, + endColumn: selection.endColumn || selection.startColumn + }); + } + if (!cellOptions.options?.preserveFocus) { + editor.focus(); + } + } + } + } + } + + clearInput(): void { + if (this.input && this.input instanceof NotebookEditorInput && !this.input.isDisposed()) { + this.saveTextEditorViewState(this.input); + } + + super.clearInput(); + } + + private detachModel() { + this.localStore.clear(); + this.notebookViewModel?.dispose(); + this.notebookViewModel = undefined; + this.webview?.clearInsets(); + this.webview?.clearPreloadsCache(); + this.findWidget.clear(); + } + + private async attachModel(input: NotebookEditorInput, model: NotebookEditorModel) { + if (!this.webview) { + this.webview = new BackLayerWebView(this.webviewService, this.notebookService, this, this.environmentSerice); + this.list?.rowsContainer.insertAdjacentElement('afterbegin', this.webview!.element); + } + + this.notebookViewModel = this.instantiationService.createInstance(NotebookViewModel, input.viewType!, model); + const viewState = this.loadTextEditorViewState(input); + this.notebookViewModel.restoreEditorViewState(viewState); + + this.localStore.add(this.notebookViewModel.onDidChangeViewCells((e) => { + if (e.synchronous) { + e.splices.reverse().forEach((diff) => { + this.list?.splice(diff[0], diff[1], diff[2]); + }); + } else { + DOM.scheduleAtNextAnimationFrame(() => { + e.splices.reverse().forEach((diff) => { + this.list?.splice(diff[0], diff[1], diff[2]); + }); + }); + } + })); + + this.webview?.updateRendererPreloads(this.notebookViewModel.renderers); + + this.localStore.add(this.list!.onWillScroll(e => { + this.webview!.updateViewScrollTop(-e.scrollTop, []); + })); + + this.localStore.add(this.list!.onDidChangeContentHeight(() => { + const scrollTop = this.list?.scrollTop || 0; + const scrollHeight = this.list?.scrollHeight || 0; + this.webview!.element.style.height = `${scrollHeight}px`; + let updateItems: { cell: CellViewModel, output: IOutput, cellTop: number }[] = []; + + if (this.webview?.insetMapping) { + this.webview?.insetMapping.forEach((value, key) => { + let cell = value.cell; + let index = this.notebookViewModel!.getViewCellIndex(cell); + let cellTop = this.list?.getAbsoluteTop(index) || 0; + if (this.webview!.shouldUpdateInset(cell, key, cellTop)) { + updateItems.push({ + cell: cell, + output: key, + cellTop: cellTop + }); + } + }); + + if (updateItems.length) { + this.webview?.updateViewScrollTop(-scrollTop, updateItems); + } + } + })); + + this.localStore.add(this.list!.onDidChangeFocus((e) => { + if (e.elements.length > 0) { + this.notebookService.updateNotebookActiveCell(input.viewType!, input.resource!, e.elements[0].handle); + } + })); + + this.list?.splice(0, this.list?.length || 0); + this.list?.splice(0, 0, this.notebookViewModel!.viewCells as CellViewModel[]); + this.list?.layout(); + } + + private saveTextEditorViewState(input: NotebookEditorInput): void { + if (this.group && this.notebookViewModel) { + const state = this.notebookViewModel.saveEditorViewState(); + this.editorMemento.saveEditorState(this.group, input.resource, state); + } + } + + private loadTextEditorViewState(input: NotebookEditorInput): INotebookEditorViewState | undefined { + if (this.group) { + return this.editorMemento.loadEditorState(this.group, input.resource); + } + + return; + } + + layout(dimension: DOM.Dimension): void { + this.dimension = new DOM.Dimension(dimension.width, dimension.height); + DOM.toggleClass(this.rootElement, 'mid-width', dimension.width < 1000 && dimension.width >= 600); + DOM.toggleClass(this.rootElement, 'narrow-width', dimension.width < 600); + DOM.size(this.body, dimension.width, dimension.height); + this.list?.layout(dimension.height, dimension.width); + } + + protected saveState(): void { + if (this.input instanceof NotebookEditorInput) { + this.saveTextEditorViewState(this.input); + } + + super.saveState(); + } + + //#endregion + + //#region Editor Features + + selectElement(cell: ICellViewModel) { + const index = this.notebookViewModel?.getViewCellIndex(cell); + + if (index !== undefined) { + this.list?.setSelection([index]); + this.list?.setFocus([index]); + } + } + + revealInView(cell: ICellViewModel) { + const index = this.notebookViewModel?.getViewCellIndex(cell); + + if (index !== undefined) { + this.list?.revealInView(index); + } + } + + revealInCenterIfOutsideViewport(cell: ICellViewModel) { + const index = this.notebookViewModel?.getViewCellIndex(cell); + + if (index !== undefined) { + this.list?.revealInCenterIfOutsideViewport(index); + } + } + + revealInCenter(cell: ICellViewModel) { + const index = this.notebookViewModel?.getViewCellIndex(cell); + + if (index !== undefined) { + this.list?.revealInCenter(index); + } + } + + revealLineInView(cell: ICellViewModel, line: number): void { + const index = this.notebookViewModel?.getViewCellIndex(cell); + + if (index !== undefined) { + this.list?.revealLineInView(index, line); + } + } + + revealLineInCenter(cell: ICellViewModel, line: number) { + const index = this.notebookViewModel?.getViewCellIndex(cell); + + if (index !== undefined) { + this.list?.revealLineInCenter(index, line); + } + } + + revealLineInCenterIfOutsideViewport(cell: ICellViewModel, line: number) { + const index = this.notebookViewModel?.getViewCellIndex(cell); + + if (index !== undefined) { + this.list?.revealLineInCenterIfOutsideViewport(index, line); + } + } + + revealRangeInView(cell: ICellViewModel, range: Range): void { + const index = this.notebookViewModel?.getViewCellIndex(cell); + + if (index !== undefined) { + this.list?.revealRangeInView(index, range); + } + } + + revealRangeInCenter(cell: ICellViewModel, range: Range): void { + const index = this.notebookViewModel?.getViewCellIndex(cell); + + if (index !== undefined) { + this.list?.revealRangeInCenter(index, range); + } + } + + revealRangeInCenterIfOutsideViewport(cell: ICellViewModel, range: Range): void { + const index = this.notebookViewModel?.getViewCellIndex(cell); + + if (index !== undefined) { + this.list?.revealRangeInCenterIfOutsideViewport(index, range); + } + } + + setCellSelection(cell: ICellViewModel, range: Range): void { + const index = this.notebookViewModel?.getViewCellIndex(cell); + + if (index !== undefined) { + this.list?.setCellSelection(index, range); + } + } + + changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any { + return this.notebookViewModel?.changeDecorations(callback); + } + + //#endregion + + //#region Find Delegate + + public showFind() { + this.findWidget.reveal(); + } + + public hideFind() { + this.findWidget.hide(); + this.focus(); + } + + //#endregion + + //#region Cell operations + layoutNotebookCell(cell: ICellViewModel, height: number) { + let relayout = (cell: ICellViewModel, height: number) => { + let index = this.notebookViewModel!.getViewCellIndex(cell); + if (index >= 0) { + this.list?.updateElementHeight(index, height); + } + }; + + DOM.scheduleAtNextAnimationFrame(() => { + relayout(cell, height); + }); + } + + async insertNotebookCell(cell: ICellViewModel, type: CellKind, direction: 'above' | 'below', initialText: string = ''): Promise { + const newLanguages = this.notebookViewModel!.languages; + const language = newLanguages && newLanguages.length ? newLanguages[0] : 'markdown'; + const index = this.notebookViewModel!.getViewCellIndex(cell); + const insertIndex = direction === 'above' ? index : index + 1; + const newModeCell = await this.notebookService.createNotebookCell(this.notebookViewModel!.viewType, this.notebookViewModel!.uri, insertIndex, language, type); + newModeCell!.source = initialText.split(/\r?\n/g); + const newCell = this.notebookViewModel!.insertCell(insertIndex, newModeCell!, true); + this.list?.setFocus([insertIndex]); + + if (type === CellKind.Markdown) { + newCell.state = CellState.Editing; + } + + DOM.scheduleAtNextAnimationFrame(() => { + this.list?.revealInCenterIfOutsideViewport(insertIndex); + }); + } + + async deleteNotebookCell(cell: ICellViewModel): Promise { + (cell as CellViewModel).save(); + const index = this.notebookViewModel!.getViewCellIndex(cell); + await this.notebookService.deleteNotebookCell(this.notebookViewModel!.viewType, this.notebookViewModel!.uri, index); + this.notebookViewModel!.deleteCell(index, true); + } + + moveCellDown(cell: ICellViewModel): void { + const index = this.notebookViewModel!.getViewCellIndex(cell); + const newIdx = index + 1; + this.moveCellToIndex(cell, index, newIdx); + } + + moveCellUp(cell: ICellViewModel): void { + const index = this.notebookViewModel!.getViewCellIndex(cell); + const newIdx = index - 1; + this.moveCellToIndex(cell, index, newIdx); + } + + private moveCellToIndex(cell: ICellViewModel, index: number, newIdx: number): void { + if (!this.notebookViewModel!.moveCellToIdx(index, newIdx, true)) { + return; + } + + DOM.scheduleAtNextAnimationFrame(() => { + this.list?.revealInCenterIfOutsideViewport(index + 1); + }); + } + + editNotebookCell(cell: CellViewModel): void { + cell.state = CellState.Editing; + + this.renderedEditors.get(cell)?.focus(); + } + + saveNotebookCell(cell: ICellViewModel): void { + cell.state = CellState.Preview; + } + + getActiveCell() { + let elements = this.list?.getFocusedElements(); + + if (elements && elements.length) { + return elements[0]; + } + + return undefined; + } + + focusNotebookCell(cell: ICellViewModel, focusEditor: boolean) { + const index = this.notebookViewModel!.getViewCellIndex(cell); + + if (focusEditor) { + this.list?.setFocus([index]); + this.list?.setSelection([index]); + this.list?.focusView(); + + cell.state = CellState.Editing; + cell.focusMode = CellFocusMode.Editor; + this.revealInCenterIfOutsideViewport(cell); + } else { + let itemDOM = this.list?.domElementAtIndex(index); + if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { + (document.activeElement as HTMLElement).blur(); + } + + cell.state = CellState.Preview; + cell.focusMode = CellFocusMode.Editor; + + this.list?.setFocus([index]); + this.list?.setSelection([index]); + this.revealInCenterIfOutsideViewport(cell); + this.list?.focusView(); + } + } + + //#endregion + + //#region MISC + + getLayoutInfo(): NotebookLayoutInfo { + if (!this.list) { + throw new Error('Editor is not initalized successfully'); + } + + return { + width: this.dimension!.width, + height: this.dimension!.height, + fontInfo: this.fontInfo! + }; + } + getFontInfo(): BareFontInfo | undefined { + return this.fontInfo; + } + + triggerScroll(event: IMouseWheelEvent) { + this.list?.triggerScrollFromMouseWheelEvent(event); + } + + createInset(cell: CellViewModel, output: IOutput, shadowContent: string, offset: number) { + if (!this.webview) { + return; + } + + let preloads = this.notebookViewModel!.renderers; + + if (!this.webview!.insetMapping.has(output)) { + let index = this.notebookViewModel!.getViewCellIndex(cell); + let cellTop = this.list?.getAbsoluteTop(index) || 0; + + this.webview!.createInset(cell, output, cellTop, offset, shadowContent, preloads); + } else { + let index = this.notebookViewModel!.getViewCellIndex(cell); + let cellTop = this.list?.getAbsoluteTop(index) || 0; + let scrollTop = this.list?.scrollTop || 0; + + this.webview!.updateViewScrollTop(-scrollTop, [{ cell: cell, output: output, cellTop: cellTop }]); + } + } + + removeInset(output: IOutput) { + if (!this.webview) { + return; + } + + this.webview!.removeInset(output); + } + + getOutputRenderer(): OutputRenderer { + return this.outputRenderer; + } + + //#endregion +} + +const embeddedEditorBackground = 'walkThrough.embeddedEditorBackground'; + +registerThemingParticipant((theme, collector) => { + const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: '#f4f4f4', hc: null }); + if (color) { + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .monaco-editor-background, + .monaco-workbench .part.editor > .content .notebook-editor .cell .margin-view-overlays { background: ${color}; }`); + } + const link = theme.getColor(textLinkForeground); + if (link) { + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell a { color: ${link}; }`); + } + const activeLink = theme.getColor(textLinkActiveForeground); + if (activeLink) { + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell a:hover, + .monaco-workbench .part.editor > .content .notebook-editor .cell a:active { color: ${activeLink}; }`); + } + const shortcut = theme.getColor(textPreformatForeground); + if (shortcut) { + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor code, + .monaco-workbench .part.editor > .content .notebook-editor .shortcut { color: ${shortcut}; }`); + } + const border = theme.getColor(contrastBorder); + if (border) { + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-editor { border-color: ${border}; }`); + } + const quoteBackground = theme.getColor(textBlockQuoteBackground); + if (quoteBackground) { + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor blockquote { background: ${quoteBackground}; }`); + } + const quoteBorder = theme.getColor(textBlockQuoteBorder); + if (quoteBorder) { + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor blockquote { border-color: ${quoteBorder}; }`); + } + + const inactiveListItem = theme.getColor('list.inactiveSelectionBackground'); + + if (inactiveListItem) { + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output { background-color: ${inactiveListItem}; }`); + } + + // Cell Margin + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > div.cell { padding: 8px ${CELL_MARGIN}px 8px ${CELL_MARGIN}px; }`); + collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output { margin: 8px ${CELL_MARGIN}px; }`); +}); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts new file mode 100644 index 0000000000000..3f46e71b50335 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EditorInput, EditorModel, IEditorInput, GroupIdentifier, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; +import { Emitter, Event } from 'vs/base/common/event'; +import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { ICell, NotebookCellsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { URI } from 'vs/base/common/uri'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; + +export class NotebookEditorModel extends EditorModel { + private _dirty = false; + + protected readonly _onDidChangeDirty = this._register(new Emitter()); + readonly onDidChangeDirty = this._onDidChangeDirty.event; + + private readonly _onDidChangeCells = new Emitter(); + get onDidChangeCells(): Event { return this._onDidChangeCells.event; } + + + get notebook() { + return this._notebook; + } + + constructor( + private _notebook: NotebookTextModel + ) { + super(); + + if (_notebook && _notebook.onDidChangeCells) { + this._register(_notebook.onDidChangeContent(() => { + this._dirty = true; + this._onDidChangeDirty.fire(); + })); + this._register(_notebook.onDidChangeCells((e) => { + this._onDidChangeCells.fire(e); + })); + } + } + + isDirty() { + return this._dirty; + } + + getNotebook(): NotebookTextModel { + return this._notebook; + } + + insertCell(cell: ICell, index: number) { + let notebook = this.getNotebook(); + + if (notebook) { + let mainCell = new NotebookCellTextModel(URI.revive(cell.uri), cell.handle, cell.source, cell.language, cell.cellKind, cell.outputs); + this.notebook.insertNewCell(index, mainCell); + this._dirty = true; + this._onDidChangeDirty.fire(); + + } + } + + deleteCell(index: number) { + let notebook = this.getNotebook(); + + if (notebook) { + this.notebook.removeCell(index); + } + } + + async save(): Promise { + if (this._notebook) { + this._dirty = false; + this._onDidChangeDirty.fire(); + // todo, flush all states + return true; + } + + return false; + } +} + +export class NotebookEditorInput extends EditorInput { + static readonly ID: string = 'workbench.input.notebook'; + private promise: Promise | null = null; + private textModel: NotebookEditorModel | null = null; + + constructor( + public resource: URI, + public name: string, + public readonly viewType: string | undefined, + @INotebookService private readonly notebookService: INotebookService + ) { + super(); + } + + getTypeId(): string { + return NotebookEditorInput.ID; + } + + getName(): string { + return this.name; + } + + isDirty() { + return this.textModel?.isDirty() || false; + } + + async save(group: GroupIdentifier, options?: ISaveOptions): Promise { + if (this.textModel) { + await this.notebookService.save(this.textModel.notebook.viewType, this.textModel.notebook.uri); + await this.textModel.save(); + return this; + } + + return undefined; + } + + async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + if (this.textModel) { + // TODO@rebornix we need hashing + await this.textModel.save(); + } + } + + async resolve(): Promise { + if (!this.promise) { + await this.notebookService.canResolve(this.viewType!); + + this.promise = this.notebookService.resolveNotebook(this.viewType!, this.resource).then(notebook => { + this.textModel = new NotebookEditorModel(notebook!); + this.textModel.onDidChangeDirty(() => this._onDidChangeDirty.fire()); + return this.textModel; + }); + } + + return this.promise; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts b/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts new file mode 100644 index 0000000000000..f6ddc3cb0def8 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { BrandedService, IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; +import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; + +export type IOutputTransformCtor = IConstructorSignature1; + +export interface IOutputTransformDescription { + id: string; + kind: CellOutputKind; + ctor: IOutputTransformCtor; +} + +export namespace NotebookRegistry { + export function getOutputTransformContributions(): IOutputTransformDescription[] { + return NotebookRegistryImpl.INSTANCE.getNotebookOutputTransform(); + } +} + +export function registerOutputTransform(id: string, kind: CellOutputKind, ctor: { new(editor: INotebookEditor, ...services: Services): IOutputTransformContribution }): void { + NotebookRegistryImpl.INSTANCE.registerOutputTransform(id, kind, ctor); +} + +class NotebookRegistryImpl { + + static readonly INSTANCE = new NotebookRegistryImpl(); + + private readonly outputTransforms: IOutputTransformDescription[]; + + constructor() { + this.outputTransforms = []; + } + + registerOutputTransform(id: string, kind: CellOutputKind, ctor: { new(editor: INotebookEditor, ...services: Services): IOutputTransformContribution }): void { + this.outputTransforms.push({ id: id, kind: kind, ctor: ctor as IOutputTransformCtor }); + } + + getNotebookOutputTransform(): IOutputTransformDescription[] { + return this.outputTransforms.slice(0); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookService.ts b/src/vs/workbench/contrib/notebook/browser/notebookService.ts new file mode 100644 index 0000000000000..2746196d2c658 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookService.ts @@ -0,0 +1,337 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { URI } from 'vs/base/common/uri'; +import { notebookProviderExtensionPoint, notebookRendererExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; +import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; +import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; +import { Emitter, Event } from 'vs/base/common/event'; +import { INotebookTextModel, ICell, INotebookMimeTypeSelector, INotebookRendererInfo, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; +import { Iterable } from 'vs/base/common/iterator'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; + +function MODEL_ID(resource: URI): string { + return resource.toString(); +} + +export const INotebookService = createDecorator('notebookService'); + +export interface IMainNotebookController { + resolveNotebook(viewType: string, uri: URI): Promise; + executeNotebook(viewType: string, uri: URI): Promise; + updateNotebookActiveCell(uri: URI, cellHandle: number): void; + createRawCell(uri: URI, index: number, language: string, type: CellKind): Promise; + deleteCell(uri: URI, index: number): Promise + executeNotebookActiveCell(uri: URI): void; + destoryNotebookDocument(notebook: INotebookTextModel): Promise; + save(uri: URI): Promise; +} + +export interface INotebookService { + _serviceBrand: undefined; + canResolve(viewType: string): Promise; + onDidChangeActiveEditor: Event<{ viewType: string, uri: URI }>; + registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController): void; + unregisterNotebookProvider(viewType: string): void; + registerNotebookRenderer(handle: number, extensionData: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: URI[]): void; + unregisterNotebookRenderer(handle: number): void; + getRendererInfo(handle: number): INotebookRendererInfo | undefined; + resolveNotebook(viewType: string, uri: URI): Promise; + executeNotebook(viewType: string, uri: URI): Promise; + executeNotebookActiveCell(viewType: string, uri: URI): Promise; + getContributedNotebookProviders(resource: URI): readonly NotebookProviderInfo[]; + getNotebookProviderResourceRoots(): URI[]; + updateNotebookActiveCell(viewType: string, resource: URI, cellHandle: number): void; + createNotebookCell(viewType: string, resource: URI, index: number, language: string, type: CellKind): Promise; + deleteNotebookCell(viewType: string, resource: URI, index: number): Promise; + destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void; + updateActiveNotebookDocument(viewType: string, resource: URI): void; + save(viewType: string, resource: URI): Promise; +} + +export class NotebookProviderInfoStore { + private readonly contributedEditors = new Map(); + + clear() { + this.contributedEditors.clear(); + } + + get(viewType: string): NotebookProviderInfo | undefined { + return this.contributedEditors.get(viewType); + } + + add(info: NotebookProviderInfo): void { + if (this.contributedEditors.has(info.id)) { + console.log(`Custom editor with id '${info.id}' already registered`); + return; + } + this.contributedEditors.set(info.id, info); + } + + getContributedNotebook(resource: URI): readonly NotebookProviderInfo[] { + return [...Iterable.filter(this.contributedEditors.values(), customEditor => customEditor.matches(resource))]; + } +} + +export class NotebookOutputRendererInfoStore { + private readonly contributedRenderers = new Map(); + + clear() { + this.contributedRenderers.clear(); + } + + get(viewType: string): NotebookOutputRendererInfo | undefined { + return this.contributedRenderers.get(viewType); + } + + add(info: NotebookOutputRendererInfo): void { + if (this.contributedRenderers.has(info.id)) { + console.log(`Custom notebook output renderer with id '${info.id}' already registered`); + return; + } + this.contributedRenderers.set(info.id, info); + } + + getContributedRenderer(mimeType: string): readonly NotebookOutputRendererInfo[] { + return Array.from(this.contributedRenderers.values()).filter(customEditor => + customEditor.matches(mimeType)); + } +} + +class ModelData implements IDisposable { + private readonly _modelEventListeners = new DisposableStore(); + + constructor( + public model: NotebookTextModel, + onWillDispose: (model: INotebookTextModel) => void + ) { + this._modelEventListeners.add(model.onWillDispose(() => onWillDispose(model))); + } + + dispose(): void { + this._modelEventListeners.dispose(); + } +} + + +export class NotebookService extends Disposable implements INotebookService { + _serviceBrand: undefined; + private readonly _notebookProviders = new Map(); + private readonly _notebookRenderers = new Map(); + notebookProviderInfoStore: NotebookProviderInfoStore = new NotebookProviderInfoStore(); + notebookRenderersInfoStore: NotebookOutputRendererInfoStore = new NotebookOutputRendererInfoStore(); + private readonly _models: { [modelId: string]: ModelData; }; + private _onDidChangeActiveEditor = new Emitter<{ viewType: string, uri: URI }>(); + onDidChangeActiveEditor: Event<{ viewType: string, uri: URI }> = this._onDidChangeActiveEditor.event; + private _resolvePool = new Map void>(); + + constructor( + @IExtensionService private readonly extensionService: IExtensionService + ) { + super(); + + this._models = {}; + notebookProviderExtensionPoint.setHandler((extensions) => { + this.notebookProviderInfoStore.clear(); + + for (const extension of extensions) { + for (const notebookContribution of extension.value) { + this.notebookProviderInfoStore.add(new NotebookProviderInfo({ + id: notebookContribution.viewType, + displayName: notebookContribution.displayName, + selector: notebookContribution.selector || [], + })); + } + } + // console.log(this._notebookProviderInfoStore); + }); + + notebookRendererExtensionPoint.setHandler((renderers) => { + this.notebookRenderersInfoStore.clear(); + + for (const extension of renderers) { + for (const notebookContribution of extension.value) { + this.notebookRenderersInfoStore.add(new NotebookOutputRendererInfo({ + id: notebookContribution.viewType, + displayName: notebookContribution.displayName, + mimeTypes: notebookContribution.mimeTypes || [] + })); + } + } + + // console.log(this.notebookRenderersInfoStore); + }); + } + + async canResolve(viewType: string): Promise { + if (this._notebookProviders.has(viewType)) { + return; + } + + this.extensionService.activateByEvent(`onNotebookEditor:${viewType}`); + + let resolve: () => void; + const promise = new Promise(r => { resolve = r; }); + this._resolvePool.set(viewType, resolve!); + return promise; + } + + registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController) { + this._notebookProviders.set(viewType, { extensionData, controller }); + + let resolve = this._resolvePool.get(viewType); + if (resolve) { + resolve(); + this._resolvePool.delete(viewType); + } + } + + unregisterNotebookProvider(viewType: string): void { + this._notebookProviders.delete(viewType); + } + + registerNotebookRenderer(handle: number, extensionData: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: URI[]) { + this._notebookRenderers.set(handle, { extensionData, type, selectors, preloads }); + } + + unregisterNotebookRenderer(handle: number) { + this._notebookRenderers.delete(handle); + } + + getRendererInfo(handle: number): INotebookRendererInfo | undefined { + const renderer = this._notebookRenderers.get(handle); + + if (renderer) { + return { + id: renderer.extensionData.id, + extensionLocation: URI.revive(renderer.extensionData.location), + preloads: renderer.preloads + }; + } + + return; + } + + async resolveNotebook(viewType: string, uri: URI): Promise { + const provider = this._notebookProviders.get(viewType); + if (!provider) { + return undefined; + } + + const notebookModel = await provider.controller.resolveNotebook(viewType, uri); + if (!notebookModel) { + return undefined; + } + + // new notebook model created + const modelId = MODEL_ID(uri); + const modelData = new ModelData( + notebookModel, + (model) => this._onWillDispose(model), + ); + this._models[modelId] = modelData; + return modelData.model; + } + + updateNotebookActiveCell(viewType: string, resource: URI, cellHandle: number): void { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + provider.controller.updateNotebookActiveCell(resource, cellHandle); + } + } + + async createNotebookCell(viewType: string, resource: URI, index: number, language: string, type: CellKind): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + return provider.controller.createRawCell(resource, index, language, type); + } + + return; + } + + async deleteNotebookCell(viewType: string, resource: URI, index: number): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + return provider.controller.deleteCell(resource, index); + } + + return false; + } + + async executeNotebook(viewType: string, uri: URI): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + return provider.controller.executeNotebook(viewType, uri); + } + + return; + } + + async executeNotebookActiveCell(viewType: string, uri: URI): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + await provider.controller.executeNotebookActiveCell(uri); + } + } + + getContributedNotebookProviders(resource: URI): readonly NotebookProviderInfo[] { + return this.notebookProviderInfoStore.getContributedNotebook(resource); + } + + getContributedNotebookOutputRenderers(mimeType: string): readonly NotebookOutputRendererInfo[] { + return this.notebookRenderersInfoStore.getContributedRenderer(mimeType); + } + + getNotebookProviderResourceRoots(): URI[] { + let ret: URI[] = []; + this._notebookProviders.forEach(val => { + ret.push(URI.revive(val.extensionData.location)); + }); + + return ret; + } + + destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + provider.controller.destoryNotebookDocument(notebook); + } + } + + updateActiveNotebookDocument(viewType: string, resource: URI): void { + this._onDidChangeActiveEditor.fire({ viewType, uri: resource }); + } + + async save(viewType: string, resource: URI): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + return provider.controller.save(resource); + } + + return false; + } + + private _onWillDispose(model: INotebookTextModel): void { + let modelId = MODEL_ID(model.uri); + let modelData = this._models[modelId]; + + delete this._models[modelId]; + modelData?.dispose(); + + // this._onModelRemoved.fire(model); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts new file mode 100644 index 0000000000000..a1ac5d121d445 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -0,0 +1,362 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { IListRenderer, IListVirtualDelegate, ListError } from 'vs/base/browser/ui/list/list'; +import { Event } from 'vs/base/common/event'; +import { ScrollEvent } from 'vs/base/common/scrollable'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +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 { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; +import { isMacintosh } from 'vs/base/common/platform'; +import { NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { Range } from 'vs/editor/common/core/range'; +import { CellRevealType, CellRevealPosition, CursorAtBoundary } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; + +export class NotebookCellList extends WorkbenchList implements IDisposable { + get onWillScroll(): Event { return this.view.onWillScroll; } + + get rowsContainer(): HTMLElement { + return this.view.containerDomNode; + } + private _previousSelectedElements: CellViewModel[] = []; + private _localDisposableStore = new DisposableStore(); + + constructor( + private listUser: string, + container: HTMLElement, + delegate: IListVirtualDelegate, + renderers: IListRenderer[], + contextKeyService: IContextKeyService, + options: IWorkbenchListOptions, + @IListService listService: IListService, + @IThemeService themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService + + ) { + super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); + + this._previousSelectedElements = this.getSelectedElements(); + this._localDisposableStore.add(this.onDidChangeSelection((e) => { + this._previousSelectedElements.forEach(element => { + if (e.elements.indexOf(element) < 0) { + element.onDeselect(); + } + }); + this._previousSelectedElements = e.elements; + })); + + const notebookEditorCursorAtBoundaryContext = NOTEBOOK_EDITOR_CURSOR_BOUNDARY.bindTo(contextKeyService); + notebookEditorCursorAtBoundaryContext.set('none'); + + let cursorSelectionListener: IDisposable | null = null; + let textEditorAttachListener: IDisposable | null = null; + + const recomputeContext = (element: CellViewModel) => { + switch (element.cursorAtBoundary()) { + case CursorAtBoundary.Both: + notebookEditorCursorAtBoundaryContext.set('both'); + break; + case CursorAtBoundary.Top: + notebookEditorCursorAtBoundaryContext.set('top'); + break; + case CursorAtBoundary.Bottom: + notebookEditorCursorAtBoundaryContext.set('bottom'); + break; + default: + notebookEditorCursorAtBoundaryContext.set('none'); + break; + } + return; + }; + + // Cursor Boundary context + this._localDisposableStore.add(this.onDidChangeSelection((e) => { + if (e.elements.length) { + cursorSelectionListener?.dispose(); + textEditorAttachListener?.dispose(); + // we only validate the first focused element + const focusedElement = e.elements[0]; + + cursorSelectionListener = focusedElement.onDidChangeCursorSelection(() => { + recomputeContext(focusedElement); + }); + + textEditorAttachListener = focusedElement.onDidChangeEditorAttachState(() => { + if (focusedElement.editorAttached) { + recomputeContext(focusedElement); + } + }); + + recomputeContext(focusedElement); + return; + } + + // reset context + notebookEditorCursorAtBoundaryContext.set('none'); + })); + + } + + domElementAtIndex(index: number): HTMLElement | null { + return this.view.domElement(index); + } + + focusView() { + this.view.domNode.focus(); + } + + getAbsoluteTop(index: number): number { + if (index < 0 || index >= this.length) { + throw new ListError(this.listUser, `Invalid index ${index}`); + } + + return this.view.elementTop(index); + } + + triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) { + this.view.triggerScrollFromMouseWheelEvent(browserEvent); + } + + updateElementHeight(index: number, size: number): void { + const focused = this.getSelection(); + this.view.updateElementHeight(index, size, focused.length ? focused[0] : null); + // this.view.updateElementHeight(index, size, null); + } + + // override + domFocus() { + if (document.activeElement && this.view.domNode.contains(document.activeElement)) { + // for example, when focus goes into monaco editor, if we refocus the list view, the editor will lose focus. + return; + } + + if (!isMacintosh && document.activeElement && isContextMenuFocused()) { + return; + } + + super.domFocus(); + } + + private _revealRange(index: number, range: Range, revealType: CellRevealType, newlyCreated: boolean, alignToBottom: boolean) { + const element = this.view.element(index); + const scrollTop = this.view.getScrollTop(); + const wrapperBottom = scrollTop + this.view.renderHeight; + const startLineNumber = range.startLineNumber; + const lineOffset = element.getLineScrollTopOffset(startLineNumber); + const elementTop = this.view.elementTop(index); + const lineTop = elementTop + lineOffset; + + // TODO@rebornix 30 ---> line height * 1.5 + if (lineTop < scrollTop) { + this.view.setScrollTop(lineTop - 30); + } else if (lineTop > wrapperBottom) { + this.view.setScrollTop(scrollTop + lineTop - wrapperBottom + 30); + } else if (newlyCreated) { + // newly scrolled into view + if (alignToBottom) { + // align to the bottom + this.view.setScrollTop(scrollTop + lineTop - wrapperBottom + 30); + } else { + // align to to top + this.view.setScrollTop(lineTop - 30); + } + } + + if (revealType === CellRevealType.Range) { + element.revealRangeInCenter(range); + } + } + + // TODO@rebornix TEST & Fix potential bugs + // List items have real dynamic heights, which means after we set `scrollTop` based on the `elementTop(index)`, the element at `index` might still be removed from the view once all relayouting tasks are done. + // For example, we scroll item 10 into the view upwards, in the first round, items 7, 8, 9, 10 are all in the viewport. Then item 7 and 8 resize themselves to be larger and finally item 10 is removed from the view. + // To ensure that item 10 is always there, we need to scroll item 10 to the top edge of the viewport. + private _revealRangeInternal(index: number, range: Range, revealType: CellRevealType) { + const scrollTop = this.view.getScrollTop(); + const wrapperBottom = scrollTop + this.view.renderHeight; + const elementTop = this.view.elementTop(index); + const element = this.view.element(index); + + if (element.editorAttached) { + this._revealRange(index, range, revealType, false, false); + } else { + const elementHeight = this.view.elementHeight(index); + let upwards = false; + + if (elementTop + elementHeight < scrollTop) { + // scroll downwards + this.view.setScrollTop(elementTop); + upwards = false; + } else if (elementTop > wrapperBottom) { + // scroll upwards + this.view.setScrollTop(elementTop - this.view.renderHeight / 2); + upwards = true; + } + + const editorAttachedPromise = new Promise((resolve, reject) => { + element.onDidChangeEditorAttachState(state => state ? resolve() : reject()); + }); + + editorAttachedPromise.then(() => { + this._revealRange(index, range, revealType, true, upwards); + }); + } + } + + revealLineInView(index: number, line: number) { + this._revealRangeInternal(index, new Range(line, 1, line, 1), CellRevealType.Line); + } + + revealRangeInView(index: number, range: Range): void { + this._revealRangeInternal(index, range, CellRevealType.Range); + } + + private _revealRangeInCenterInternal(index: number, range: Range, revealType: CellRevealType) { + const reveal = (index: number, range: Range, revealType: CellRevealType) => { + const element = this.view.element(index); + let lineOffset = element.getLineScrollTopOffset(range.startLineNumber); + let lineOffsetInView = this.view.elementTop(index) + lineOffset; + this.view.setScrollTop(lineOffsetInView - this.view.renderHeight / 2); + + if (revealType === CellRevealType.Range) { + element.revealRangeInCenter(range); + } + }; + + const elementTop = this.view.elementTop(index); + const viewItemOffset = elementTop; + this.view.setScrollTop(viewItemOffset - this.view.renderHeight / 2); + const element = this.view.element(index); + + if (!element.editorAttached) { + getEditorAttachedPromise(element).then(() => reveal(index, range, revealType)); + } else { + reveal(index, range, revealType); + } + } + + revealLineInCenter(index: number, line: number) { + this._revealRangeInCenterInternal(index, new Range(line, 1, line, 1), CellRevealType.Line); + } + + revealRangeInCenter(index: number, range: Range): void { + this._revealRangeInCenterInternal(index, range, CellRevealType.Range); + } + + private _revealRangeInCenterIfOutsideViewportInternal(index: number, range: Range, revealType: CellRevealType) { + const reveal = (index: number, range: Range, revealType: CellRevealType) => { + const element = this.view.element(index); + let lineOffset = element.getLineScrollTopOffset(range.startLineNumber); + let lineOffsetInView = this.view.elementTop(index) + lineOffset; + this.view.setScrollTop(lineOffsetInView - this.view.renderHeight / 2); + + if (revealType === CellRevealType.Range) { + setTimeout(() => { + element.revealRangeInCenter(range); + }, 240); + } + }; + + const scrollTop = this.view.getScrollTop(); + const wrapperBottom = scrollTop + this.view.renderHeight; + const elementTop = this.view.elementTop(index); + const viewItemOffset = elementTop; + const element = this.view.element(index); + + if (viewItemOffset < scrollTop || viewItemOffset > wrapperBottom) { + // let it render + this.view.setScrollTop(viewItemOffset - this.view.renderHeight / 2); + + // after rendering, it might be pushed down due to markdown cell dynamic height + const elementTop = this.view.elementTop(index); + this.view.setScrollTop(elementTop - this.view.renderHeight / 2); + + // reveal editor + if (!element.editorAttached) { + getEditorAttachedPromise(element).then(() => reveal(index, range, revealType)); + } else { + // for example markdown + } + } else { + if (element.editorAttached) { + element.revealRangeInCenter(range); + } else { + // for example, markdown cell in preview mode + getEditorAttachedPromise(element).then(() => reveal(index, range, revealType)); + } + } + } + + revealLineInCenterIfOutsideViewport(index: number, line: number) { + this._revealRangeInCenterIfOutsideViewportInternal(index, new Range(line, 1, line, 1), CellRevealType.Line); + } + + revealRangeInCenterIfOutsideViewport(index: number, range: Range): void { + this._revealRangeInCenterIfOutsideViewportInternal(index, range, CellRevealType.Range); + } + + private _revealInternal(index: number, ignoreIfInsideViewport: boolean, revealPosition: CellRevealPosition) { + const scrollTop = this.view.getScrollTop(); + const wrapperBottom = scrollTop + this.view.renderHeight; + const elementTop = this.view.elementTop(index); + + if (ignoreIfInsideViewport && elementTop >= scrollTop && elementTop < wrapperBottom) { + // inside the viewport + return; + } + + // first render + const viewItemOffset = revealPosition === CellRevealPosition.Top ? elementTop : (elementTop - this.view.renderHeight / 2); + this.view.setScrollTop(viewItemOffset); + + // second scroll as markdown cell is dynamic + const newElementTop = this.view.elementTop(index); + const newViewItemOffset = revealPosition === CellRevealPosition.Top ? newElementTop : (newElementTop - this.view.renderHeight / 2); + this.view.setScrollTop(newViewItemOffset); + } + + revealInView(index: number) { + this._revealInternal(index, true, CellRevealPosition.Top); + } + + revealInCenter(index: number) { + this._revealInternal(index, false, CellRevealPosition.Center); + } + + revealInCenterIfOutsideViewport(index: number) { + this._revealInternal(index, true, CellRevealPosition.Center); + } + + setCellSelection(index: number, range: Range) { + const element = this.view.element(index); + if (element.editorAttached) { + element.setSelection(range); + } else { + getEditorAttachedPromise(element).then(() => { element.setSelection(range); }); + } + } + + dispose() { + this._localDisposableStore.dispose(); + super.dispose(); + } +} + +function getEditorAttachedPromise(element: CellViewModel) { + return new Promise((resolve, reject) => { + Event.once(element.onDidChangeEditorAttachState)(state => state ? resolve() : reject()); + }); +} + +function isContextMenuFocused() { + return !!DOM.findParentWithClass(document.activeElement, 'context-view'); +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts new file mode 100644 index 0000000000000..b6d6134632f57 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IOutput, IRenderOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; + +export class OutputRenderer { + protected readonly _contributions: { [key: string]: IOutputTransformContribution; }; + protected readonly _mimeTypeMapping: { [key: number]: IOutputTransformContribution; }; + + constructor( + notebookEditor: INotebookEditor, + private readonly instantiationService: IInstantiationService + ) { + this._contributions = {}; + this._mimeTypeMapping = {}; + + let contributions = NotebookRegistry.getOutputTransformContributions(); + + for (const desc of contributions) { + try { + const contribution = this.instantiationService.createInstance(desc.ctor, notebookEditor); + this._contributions[desc.id] = contribution; + this._mimeTypeMapping[desc.kind] = contribution; + } catch (err) { + onUnexpectedError(err); + } + } + } + + renderNoop(output: IOutput, container: HTMLElement): IRenderOutput { + const contentNode = document.createElement('p'); + + contentNode.innerText = `No renderer could be found for output. It has the following output type: ${output.outputKind}`; + container.appendChild(contentNode); + return { + hasDynamicHeight: false + }; + } + + render(output: IOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput { + let transform = this._mimeTypeMapping[output.outputKind]; + + if (transform) { + return transform.render(output, container, preferredMimeType); + } else { + return this.renderNoop(output, container); + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts new file mode 100644 index 0000000000000..d1a7627a60b68 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts @@ -0,0 +1,386 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IRenderOutput, CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { registerOutputTransform } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; +import * as DOM from 'vs/base/browser/dom'; +import { RGBA, Color } from 'vs/base/common/color'; +import { ansiColorIdentifiers } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; + +class ErrorTransform implements IOutputTransformContribution { + constructor( + public editor: INotebookEditor, + @IThemeService private readonly themeService: IThemeService + ) { + } + + render(output: any, container: HTMLElement): IRenderOutput { + const traceback = document.createElement('pre'); + DOM.addClasses(traceback, 'traceback'); + if (output.traceback) { + for (let j = 0; j < output.traceback.length; j++) { + traceback.appendChild(handleANSIOutput(output.traceback[j], this.themeService)); + } + } + container.appendChild(traceback); + return { + hasDynamicHeight: false + }; + } + + dispose(): void { + } +} + +registerOutputTransform('notebook.output.error', CellOutputKind.Error, ErrorTransform); + +/** + * @param text The content to stylize. + * @returns An {@link HTMLSpanElement} that contains the potentially stylized text. + */ +export function handleANSIOutput(text: string, themeService: IThemeService): HTMLSpanElement { + + const root: HTMLSpanElement = document.createElement('span'); + const textLength: number = text.length; + + let styleNames: string[] = []; + let customFgColor: RGBA | undefined; + let customBgColor: RGBA | undefined; + let currentPos: number = 0; + let buffer: string = ''; + + while (currentPos < textLength) { + + let sequenceFound: boolean = false; + + // Potentially an ANSI escape sequence. + // See http://ascii-table.com/ansi-escape-sequences.php & https://en.wikipedia.org/wiki/ANSI_escape_code + if (text.charCodeAt(currentPos) === 27 && text.charAt(currentPos + 1) === '[') { + + const startPos: number = currentPos; + currentPos += 2; // Ignore 'Esc[' as it's in every sequence. + + let ansiSequence: string = ''; + + while (currentPos < textLength) { + const char: string = text.charAt(currentPos); + ansiSequence += char; + + currentPos++; + + // Look for a known sequence terminating character. + if (char.match(/^[ABCDHIJKfhmpsu]$/)) { + sequenceFound = true; + break; + } + + } + + if (sequenceFound) { + + // Flush buffer with previous styles. + appendStylizedStringToContainer(root, buffer, styleNames, customFgColor, customBgColor); + + buffer = ''; + + /* + * Certain ranges that are matched here do not contain real graphics rendition sequences. For + * the sake of having a simpler expression, they have been included anyway. + */ + if (ansiSequence.match(/^(?:[34][0-8]|9[0-7]|10[0-7]|[013]|4|[34]9)(?:;[349][0-7]|10[0-7]|[013]|[245]|[34]9)?(?:;[012]?[0-9]?[0-9])*;?m$/)) { + + const styleCodes: number[] = ansiSequence.slice(0, -1) // Remove final 'm' character. + .split(';') // Separate style codes. + .filter(elem => elem !== '') // Filter empty elems as '34;m' -> ['34', '']. + .map(elem => parseInt(elem, 10)); // Convert to numbers. + + if (styleCodes[0] === 38 || styleCodes[0] === 48) { + // Advanced color code - can't be combined with formatting codes like simple colors can + // Ignores invalid colors and additional info beyond what is necessary + const colorType = (styleCodes[0] === 38) ? 'foreground' : 'background'; + + if (styleCodes[1] === 5) { + set8BitColor(styleCodes, colorType); + } else if (styleCodes[1] === 2) { + set24BitColor(styleCodes, colorType); + } + } else { + setBasicFormatters(styleCodes); + } + + } else { + // Unsupported sequence so simply hide it. + } + + } else { + currentPos = startPos; + } + } + + if (sequenceFound === false) { + buffer += text.charAt(currentPos); + currentPos++; + } + } + + // Flush remaining text buffer if not empty. + if (buffer) { + appendStylizedStringToContainer(root, buffer, styleNames, customFgColor, customBgColor); + } + + return root; + + /** + * Change the foreground or background color by clearing the current color + * and adding the new one. + * @param colorType If `'foreground'`, will change the foreground color, if + * `'background'`, will change the background color. + * @param color Color to change to. If `undefined` or not provided, + * will clear current color without adding a new one. + */ + function changeColor(colorType: 'foreground' | 'background', color?: RGBA | undefined): void { + if (colorType === 'foreground') { + customFgColor = color; + } else if (colorType === 'background') { + customBgColor = color; + } + styleNames = styleNames.filter(style => style !== `code-${colorType}-colored`); + if (color !== undefined) { + styleNames.push(`code-${colorType}-colored`); + } + } + + /** + * Calculate and set basic ANSI formatting. Supports bold, italic, underline, + * normal foreground and background colors, and bright foreground and + * background colors. Not to be used for codes containing advanced colors. + * Will ignore invalid codes. + * @param styleCodes Array of ANSI basic styling numbers, which will be + * applied in order. New colors and backgrounds clear old ones; new formatting + * does not. + * @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code } + */ + function setBasicFormatters(styleCodes: number[]): void { + for (let code of styleCodes) { + switch (code) { + case 0: { + styleNames = []; + customFgColor = undefined; + customBgColor = undefined; + break; + } + case 1: { + styleNames.push('code-bold'); + break; + } + case 3: { + styleNames.push('code-italic'); + break; + } + case 4: { + styleNames.push('code-underline'); + break; + } + case 39: { + changeColor('foreground', undefined); + break; + } + case 49: { + changeColor('background', undefined); + break; + } + default: { + setBasicColor(code); + break; + } + } + } + } + + /** + * Calculate and set styling for complicated 24-bit ANSI color codes. + * @param styleCodes Full list of integer codes that make up the full ANSI + * sequence, including the two defining codes and the three RGB codes. + * @param colorType If `'foreground'`, will set foreground color, if + * `'background'`, will set background color. + * @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit } + */ + function set24BitColor(styleCodes: number[], colorType: 'foreground' | 'background'): void { + if (styleCodes.length >= 5 && + styleCodes[2] >= 0 && styleCodes[2] <= 255 && + styleCodes[3] >= 0 && styleCodes[3] <= 255 && + styleCodes[4] >= 0 && styleCodes[4] <= 255) { + const customColor = new RGBA(styleCodes[2], styleCodes[3], styleCodes[4]); + changeColor(colorType, customColor); + } + } + + /** + * Calculate and set styling for advanced 8-bit ANSI color codes. + * @param styleCodes Full list of integer codes that make up the ANSI + * sequence, including the two defining codes and the one color code. + * @param colorType If `'foreground'`, will set foreground color, if + * `'background'`, will set background color. + * @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit } + */ + function set8BitColor(styleCodes: number[], colorType: 'foreground' | 'background'): void { + let colorNumber = styleCodes[2]; + const color = calcANSI8bitColor(colorNumber); + + if (color) { + changeColor(colorType, color); + } else if (colorNumber >= 0 && colorNumber <= 15) { + // Need to map to one of the four basic color ranges (30-37, 90-97, 40-47, 100-107) + colorNumber += 30; + if (colorNumber >= 38) { + // Bright colors + colorNumber += 52; + } + if (colorType === 'background') { + colorNumber += 10; + } + setBasicColor(colorNumber); + } + } + + /** + * Calculate and set styling for basic bright and dark ANSI color codes. Uses + * theme colors if available. Automatically distinguishes between foreground + * and background colors; does not support color-clearing codes 39 and 49. + * @param styleCode Integer color code on one of the following ranges: + * [30-37, 90-97, 40-47, 100-107]. If not on one of these ranges, will do + * nothing. + */ + function setBasicColor(styleCode: number): void { + const theme = themeService.getColorTheme(); + let colorType: 'foreground' | 'background' | undefined; + let colorIndex: number | undefined; + + if (styleCode >= 30 && styleCode <= 37) { + colorIndex = styleCode - 30; + colorType = 'foreground'; + } else if (styleCode >= 90 && styleCode <= 97) { + colorIndex = (styleCode - 90) + 8; // High-intensity (bright) + colorType = 'foreground'; + } else if (styleCode >= 40 && styleCode <= 47) { + colorIndex = styleCode - 40; + colorType = 'background'; + } else if (styleCode >= 100 && styleCode <= 107) { + colorIndex = (styleCode - 100) + 8; // High-intensity (bright) + colorType = 'background'; + } + + if (colorIndex !== undefined && colorType) { + const colorName = ansiColorIdentifiers[colorIndex]; + const color = theme.getColor(colorName); + if (color) { + changeColor(colorType, color.rgba); + } + } + } +} + +/** + * @param root The {@link HTMLElement} to append the content to. + * @param stringContent The text content to be appended. + * @param cssClasses The list of CSS styles to apply to the text content. + * @param linkDetector The {@link LinkDetector} responsible for generating links from {@param stringContent}. + * @param customTextColor If provided, will apply custom color with inline style. + * @param customBackgroundColor If provided, will apply custom color with inline style. + */ +export function appendStylizedStringToContainer( + root: HTMLElement, + stringContent: string, + cssClasses: string[], + customTextColor?: RGBA, + customBackgroundColor?: RGBA +): void { + if (!root || !stringContent) { + return; + } + + const container = linkify(stringContent, true); + container.className = cssClasses.join(' '); + if (customTextColor) { + container.style.color = + Color.Format.CSS.formatRGB(new Color(customTextColor)); + } + if (customBackgroundColor) { + container.style.backgroundColor = + Color.Format.CSS.formatRGB(new Color(customBackgroundColor)); + } + + root.appendChild(container); +} + +function linkify(text: string, splitLines?: boolean): HTMLElement { + if (splitLines) { + const lines = text.split('\n'); + for (let i = 0; i < lines.length - 1; i++) { + lines[i] = lines[i] + '\n'; + } + if (!lines[lines.length - 1]) { + // Remove the last element ('') that split added. + lines.pop(); + } + const elements = lines.map(line => linkify(line)); + if (elements.length === 1) { + // Do not wrap single line with extra span. + return elements[0]; + } + const container = document.createElement('span'); + elements.forEach(e => container.appendChild(e)); + return container; + } + + const container = document.createElement('span'); + container.appendChild(document.createTextNode(text)); + return container; +} + + + +/** + * Calculate the color from the color set defined in the ANSI 8-bit standard. + * Standard and high intensity colors are not defined in the standard as specific + * colors, so these and invalid colors return `undefined`. + * @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit } for info. + * @param colorNumber The number (ranging from 16 to 255) referring to the color + * desired. + */ +export function calcANSI8bitColor(colorNumber: number): RGBA | undefined { + if (colorNumber % 1 !== 0) { + // Should be integer + // {{SQL CARBON EDIT}} @todo anthonydresser 4/12/19 this is necessary because we don't use strict null checks + return undefined; + } if (colorNumber >= 16 && colorNumber <= 231) { + // Converts to one of 216 RGB colors + colorNumber -= 16; + + let blue: number = colorNumber % 6; + colorNumber = (colorNumber - blue) / 6; + let green: number = colorNumber % 6; + colorNumber = (colorNumber - green) / 6; + let red: number = colorNumber; + + // red, green, blue now range on [0, 5], need to map to [0,255] + const convFactor: number = 255 / 5; + blue = Math.round(blue * convFactor); + green = Math.round(green * convFactor); + red = Math.round(red * convFactor); + + return new RGBA(red, green, blue); + } else if (colorNumber >= 232 && colorNumber <= 255) { + // Converts to a grayscale value + colorNumber -= 232; + const colorLevel: number = Math.round(colorNumber / 23 * 255); + return new RGBA(colorLevel, colorLevel, colorLevel); + } else { + // {{SQL CARBON EDIT}} @todo anthonydresser 4/12/19 this is necessary because we don't use strict null checks + return undefined; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts new file mode 100644 index 0000000000000..d1a14293eb67b --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts @@ -0,0 +1,248 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IRenderOutput, CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { registerOutputTransform } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; +import * as DOM from 'vs/base/browser/dom'; +import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { isArray } from 'vs/base/common/types'; +import * as marked from 'vs/base/common/marked/marked'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { URI } from 'vs/base/common/uri'; + +class RichRenderer implements IOutputTransformContribution { + private _mdRenderer: marked.Renderer = new marked.Renderer({ gfm: true });; + private _richMimeTypeRenderers = new Map IRenderOutput>(); + + constructor( + public notebookEditor: INotebookEditor, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IModelService private readonly modelService: IModelService, + @IModeService private readonly modeService: IModeService + ) { + this._richMimeTypeRenderers.set('application/json', this.renderJSON.bind(this)); + this._richMimeTypeRenderers.set('application/javascript', this.renderJavaScript.bind(this)); + this._richMimeTypeRenderers.set('text/html', this.renderHTML.bind(this)); + this._richMimeTypeRenderers.set('image/svg+xml', this.renderSVG.bind(this)); + this._richMimeTypeRenderers.set('text/markdown', this.renderMarkdown.bind(this)); + this._richMimeTypeRenderers.set('image/png', this.renderPNG.bind(this)); + this._richMimeTypeRenderers.set('image/jpeg', this.renderJavaScript.bind(this)); + this._richMimeTypeRenderers.set('text/plain', this.renderPlainText.bind(this)); + this._richMimeTypeRenderers.set('text/x-javascript', this.renderCode.bind(this)); + } + + render(output: any, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput { + if (!output.data) { + const contentNode = document.createElement('p'); + contentNode.innerText = `No data could be found for output.`; + container.appendChild(contentNode); + + return { + hasDynamicHeight: false + }; + } + + if (!preferredMimeType || !this._richMimeTypeRenderers.has(preferredMimeType)) { + const contentNode = document.createElement('p'); + let mimeTypes = []; + for (const property in output.data) { + mimeTypes.push(property); + } + + let mimeTypesMessage = mimeTypes.join(', '); + + contentNode.innerText = `No renderer could be found for output. It has the following MIME types: ${mimeTypesMessage}`; + container.appendChild(contentNode); + + return { + hasDynamicHeight: false + }; + } + + let renderer = this._richMimeTypeRenderers.get(preferredMimeType); + return renderer!(output, container); + } + + renderJSON(output: any, container: HTMLElement) { + let data = output.data['application/json']; + let str = JSON.stringify(data, null, '\t'); + + const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { + ...getOutputSimpleEditorOptions(), + dimension: { + width: 0, + height: 0 + } + }, { + isSimpleWidget: true + }); + + let mode = this.modeService.create('json'); + let resource = URI.parse(`notebook-output-${Date.now()}.json`); + const textModel = this.modelService.createModel(str, mode, resource, false); + editor.setModel(textModel); + + let width = this.notebookEditor.getLayoutInfo().width; + let fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; + let height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18); + + editor.layout({ + height, + width + }); + + container.style.height = `${height + 16}px`; + + return { + hasDynamicHeight: true + }; + } + + renderCode(output: any, container: HTMLElement) { + let data = output.data['text/x-javascript']; + let str = isArray(data) ? data.join('') : data; + + const editor = this.instantiationService.createInstance(CodeEditorWidget, container, { + ...getOutputSimpleEditorOptions(), + dimension: { + width: 0, + height: 0 + } + }, { + isSimpleWidget: true + }); + + let mode = this.modeService.create('javascript'); + let resource = URI.parse(`notebook-output-${Date.now()}.js`); + const textModel = this.modelService.createModel(str, mode, resource, false); + editor.setModel(textModel); + + let width = this.notebookEditor.getLayoutInfo().width; + let fontInfo = this.notebookEditor.getLayoutInfo().fontInfo; + let height = Math.min(textModel.getLineCount(), 16) * (fontInfo.lineHeight || 18); + + editor.layout({ + height, + width + }); + + container.style.height = `${height + 16}px`; + + return { + hasDynamicHeight: true + }; + } + + renderJavaScript(output: any, container: HTMLElement) { + let data = output.data['application/javascript']; + let str = isArray(data) ? data.join('') : data; + let scriptVal = ``; + return { + shadowContent: scriptVal, + hasDynamicHeight: false + }; + } + + renderHTML(output: any, container: HTMLElement) { + let data = output.data['text/html']; + let str = isArray(data) ? data.join('') : data; + return { + shadowContent: str, + hasDynamicHeight: false + }; + + } + + renderSVG(output: any, container: HTMLElement) { + let data = output.data['image/svg+xml']; + let str = isArray(data) ? data.join('') : data; + return { + shadowContent: str, + hasDynamicHeight: false + }; + } + + renderMarkdown(output: any, container: HTMLElement) { + let data = output.data['text/markdown']; + const str = isArray(data) ? data.join('') : data; + const mdOutput = document.createElement('div'); + mdOutput.innerHTML = marked(str, { renderer: this._mdRenderer }); + container.appendChild(mdOutput); + + return { + hasDynamicHeight: true + }; + } + + renderPNG(output: any, container: HTMLElement) { + const image = document.createElement('img'); + image.src = `data:image/png;base64,${output.data['image/png']}`; + const display = document.createElement('div'); + DOM.addClasses(display, 'display'); + display.appendChild(image); + container.appendChild(display); + return { + hasDynamicHeight: true + }; + + } + + renderJPEG(output: any, container: HTMLElement) { + const image = document.createElement('img'); + image.src = `data:image/jpeg;base64,${output.data['image/jpeg']}`; + const display = document.createElement('div'); + DOM.addClasses(display, 'display'); + display.appendChild(image); + container.appendChild(display); + return { + hasDynamicHeight: true + }; + } + + renderPlainText(output: any, container: HTMLElement) { + let data = output.data['text/plain']; + let str = isArray(data) ? data.join('') : data; + const contentNode = document.createElement('p'); + contentNode.innerText = str; + container.appendChild(contentNode); + + return { + hasDynamicHeight: false + }; + } + + dispose(): void { + } +} + +registerOutputTransform('notebook.output.rich', CellOutputKind.Rich, RichRenderer); + + +export function getOutputSimpleEditorOptions(): IEditorOptions { + return { + readOnly: true, + wordWrap: 'on', + overviewRulerLanes: 0, + glyphMargin: false, + selectOnLineNumbers: false, + hideCursorInOverviewRuler: true, + selectionHighlight: false, + lineDecorationsWidth: 0, + overviewRulerBorder: false, + scrollBeyondLastLine: false, + renderLineHighlight: 'none', + minimap: { + enabled: false + }, + lineNumbers: 'off', + scrollbar: { + alwaysConsumeMouseWheel: false + } + }; +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts new file mode 100644 index 0000000000000..32ed484dd5045 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IRenderOutput, CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { registerOutputTransform } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; +import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; + +class StreamRenderer implements IOutputTransformContribution { + constructor( + editor: INotebookEditor + ) { + } + + render(output: any, container: HTMLElement): IRenderOutput { + const contentNode = document.createElement('p'); + contentNode.innerText = output.text; + container.appendChild(contentNode); + return { + hasDynamicHeight: false + }; + + } + + dispose(): void { + } +} + +registerOutputTransform('notebook.output.stream', CellOutputKind.Text, StreamRenderer); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts new file mode 100644 index 0000000000000..d8e56c10eacd2 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -0,0 +1,417 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { Disposable } from 'vs/base/common/lifecycle'; +import * as path from 'vs/base/common/path'; +import { URI } from 'vs/base/common/uri'; +import * as UUID from 'vs/base/common/uuid'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; +import { CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/constants'; + +export interface IDimentionMessage { + type: 'dimension'; + id: string; + data: DOM.Dimension; +} + + +export interface IScrollAckMessage { + type: 'scroll-ack'; + data: { top: number }; + version: number; +} + +export interface IClearMessage { + type: 'clear'; +} + +export interface ICreationRequestMessage { + type: 'html'; + content: string; + id: string; + outputId: string; + top: number; +} + +export interface IContentWidgetTopRequest { + id: string; + top: number; +} + +export interface IViewScrollTopRequestMessage { + type: 'view-scroll'; + top?: number; + widgets: IContentWidgetTopRequest[]; + version: number; +} + +export interface IScrollRequestMessage { + type: 'scroll'; + id: string; + top: number; + widgetTop?: number; + version: number; +} + +export interface IUpdatePreloadResourceMessage { + type: 'preload'; + resources: string[]; +} + +type IMessage = IDimentionMessage | IScrollAckMessage; + +let version = 0; +export class BackLayerWebView extends Disposable { + element: HTMLElement; + webview: WebviewElement; + insetMapping: Map = new Map(); + reversedInsetMapping: Map = new Map(); + preloadsCache: Map = new Map(); + localResourceRootsCache: URI[] | undefined = undefined; + rendererRootsCache: URI[] = []; + + constructor(public webviewService: IWebviewService, public notebookService: INotebookService, public notebookEditor: INotebookEditor, public environmentSerice: IEnvironmentService) { + super(); + this.element = document.createElement('div'); + + this.element.style.width = `calc(100% - ${CELL_MARGIN * 2}px)`; + this.element.style.height = '1400px'; + this.element.style.position = 'absolute'; + this.element.style.margin = '0px 0 0px 24px'; + + const loader = URI.file(path.join(environmentSerice.appRoot, '/out/vs/loader.js')).with({ scheme: WebviewResourceScheme }); + + let content = /* html */` + + + + + + + + +
+
+ + +`; + + this.webview = this._createInset(webviewService, content); + this.webview.mountTo(this.element); + + this._register(this.webview.onDidWheel(e => { + this.notebookEditor.triggerScroll(e); + })); + + this._register(this.webview.onMessage((data: IMessage) => { + if (data.type === 'dimension') { + let output = this.reversedInsetMapping.get(data.id); + + if (!output) { + return; + } + + let cell = this.insetMapping.get(output)!.cell; + let height = data.data.height; + let outputHeight = height === 0 ? 0 : height + 16; + + if (cell) { + let outputIndex = cell.outputs.indexOf(output); + cell.updateOutputHeight(outputIndex, outputHeight); + this.notebookEditor.layoutNotebookCell(cell, cell.getCellTotalHeight()); + } + } else if (data.type === 'scroll-ack') { + // const date = new Date(); + // const top = data.data.top; + // console.log('ack top ', top, ' version: ', data.version, ' - ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); + } + })); + } + + private _createInset(webviewService: IWebviewService, content: string) { + this.localResourceRootsCache = [...this.notebookService.getNotebookProviderResourceRoots(), URI.file(this.environmentSerice.appRoot)]; + const webview = webviewService.createWebviewElement('' + UUID.generateUuid(), { + enableFindWidget: false, + }, { + allowScripts: true, + localResourceRoots: this.localResourceRootsCache + }); + webview.html = content; + return webview; + } + + shouldUpdateInset(cell: CellViewModel, output: IOutput, cellTop: number) { + let outputCache = this.insetMapping.get(output)!; + let outputIndex = cell.outputs.indexOf(output); + + let outputOffsetInOutputContainer = cell.getOutputOffset(outputIndex); + let outputOffset = cellTop + cell.editorHeight + 16 /* editor padding */ + 8 + outputOffsetInOutputContainer; + + if (outputOffset === outputCache.cacheOffset) { + return false; + } + + return true; + } + + updateViewScrollTop(top: number, items: { cell: CellViewModel, output: IOutput, cellTop: number }[]) { + let widgets: IContentWidgetTopRequest[] = items.map(item => { + let outputCache = this.insetMapping.get(item.output)!; + let id = outputCache.outputId; + let outputIndex = item.cell.outputs.indexOf(item.output); + + let outputOffsetInOutputContainer = item.cell.getOutputOffset(outputIndex); + let outputOffset = item.cellTop + item.cell.editorHeight + 16 /* editor padding */ + 8 + outputOffsetInOutputContainer; + outputCache.cacheOffset = outputOffset; + + // console.log('trigger output offset change', outputOffset); + + return { + id: id, + top: outputOffset + }; + }); + + let message: IViewScrollTopRequestMessage = { + top, + type: 'view-scroll', + version: version++, + widgets: widgets + }; + + this.webview.sendMessage(message); + } + + createInset(cell: CellViewModel, output: IOutput, cellTop: number, offset: number, shadowContent: string, preloads: Set) { + this.updateRendererPreloads(preloads); + let initialTop = cellTop + offset; + let outputId = UUID.generateUuid(); + + let message: ICreationRequestMessage = { + type: 'html', + content: shadowContent, + id: cell.id, + outputId: outputId, + top: initialTop + }; + + this.webview.sendMessage(message); + this.insetMapping.set(output, { outputId: outputId, cell: cell, cacheOffset: initialTop }); + this.reversedInsetMapping.set(outputId, output); + } + + removeInset(output: IOutput) { + let outputCache = this.insetMapping.get(output); + if (!outputCache) { + return; + } + + let id = outputCache.outputId; + + this.webview.sendMessage({ + type: 'clearOutput', + id: id + }); + this.insetMapping.delete(output); + this.reversedInsetMapping.delete(id); + } + + clearInsets() { + this.webview.sendMessage({ + type: 'clear' + }); + + this.insetMapping = new Map(); + this.reversedInsetMapping = new Map(); + } + + updateRendererPreloads(preloads: Set) { + let resources: string[] = []; + let extensionLocations: URI[] = []; + preloads.forEach(preload => { + let rendererInfo = this.notebookService.getRendererInfo(preload); + + if (rendererInfo) { + let preloadResources = rendererInfo.preloads.map(preloadResource => preloadResource.with({ scheme: WebviewResourceScheme })); + extensionLocations.push(rendererInfo.extensionLocation); + preloadResources.forEach(e => { + if (!this.preloadsCache.has(e.toString())) { + resources.push(e.toString()); + this.preloadsCache.set(e.toString(), true); + } + }); + } + }); + + this.rendererRootsCache = extensionLocations; + const mixedResourceRoots = [...(this.localResourceRootsCache || []), ...this.rendererRootsCache]; + + this.webview.contentOptions = { + allowScripts: true, + enableCommandUris: true, + localResourceRoots: mixedResourceRoots + }; + + let message: IUpdatePreloadResourceMessage = { + type: 'preload', + resources: resources + }; + + this.webview.sendMessage(message); + } + + clearPreloadsCache() { + this.preloadsCache.clear(); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts new file mode 100644 index 0000000000000..c51a5845dd602 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -0,0 +1,376 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getZoomLevel } from 'vs/base/browser/browser'; +import * as DOM from 'vs/base/browser/dom'; +import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IAction } from 'vs/base/common/actions'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { deepClone } from 'vs/base/common/objects'; +import 'vs/css!vs/workbench/contrib/notebook/browser/notebook'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { InsertCodeCellAboveAction, INotebookCellActionContext, InsertCodeCellBelowAction, InsertMarkdownCellAboveAction, InsertMarkdownCellBelowAction, EditCellAction, SaveCellAction, DeleteCellAction, MoveCellUpAction, MoveCellDownAction } from 'vs/workbench/contrib/notebook/browser/contrib/notebookActions'; +import { CellRenderTemplate, INotebookEditor, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/codeCell'; +import { StatefullMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellViewModel } from '../../viewModel/notebookCellViewModel'; +import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; + +export class NotebookCellListDelegate implements IListVirtualDelegate { + private _lineHeight: number; + private _toolbarHeight = EDITOR_TOOLBAR_HEIGHT; + + constructor( + @IConfigurationService private readonly configurationService: IConfigurationService + ) { + const editorOptions = this.configurationService.getValue('editor'); + this._lineHeight = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()).lineHeight; + } + + getHeight(element: CellViewModel): number { + return element.getHeight(this._lineHeight) + this._toolbarHeight; + } + + hasDynamicHeight(element: CellViewModel): boolean { + return element.hasDynamicHeight(); + } + + getTemplateId(element: CellViewModel): string { + if (element.cellKind === CellKind.Markdown) { + return MarkdownCellRenderer.TEMPLATE_ID; + } else { + return CodeCellRenderer.TEMPLATE_ID; + } + } +} + +abstract class AbstractCellRenderer { + protected editorOptions: IEditorOptions; + + constructor( + protected readonly instantiationService: IInstantiationService, + protected readonly notebookEditor: INotebookEditor, + protected readonly contextMenuService: IContextMenuService, + private readonly configurationService: IConfigurationService, + private readonly keybindingService: IKeybindingService, + private readonly notificationService: INotificationService, + language: string, + ) { + const editorOptions = deepClone(this.configurationService.getValue('editor', { overrideIdentifier: language })); + this.editorOptions = { + ...editorOptions, + padding: { + top: EDITOR_TOP_PADDING, + bottom: EDITOR_BOTTOM_PADDING + }, + scrollBeyondLastLine: false, + scrollbar: { + verticalScrollbarSize: 14, + horizontal: 'auto', + useShadows: true, + verticalHasArrows: false, + horizontalHasArrows: false, + alwaysConsumeMouseWheel: false + }, + overviewRulerLanes: 3, + fixedOverflowWidgets: false, + lineNumbersMinChars: 1, + minimap: { enabled: false }, + }; + } + + protected createToolbar(container: HTMLElement): ToolBar { + const toolbar = new ToolBar(container, this.contextMenuService, { + actionViewItemProvider: action => { + if (action instanceof MenuItemAction) { + const item = new ContextAwareMenuEntryActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + return item; + } + + return undefined; + } + }); + + return toolbar; + } + + showContextMenu(listIndex: number | undefined, element: CellViewModel, x: number, y: number) { + const actions: IAction[] = [ + this.instantiationService.createInstance(InsertCodeCellAboveAction), + this.instantiationService.createInstance(InsertCodeCellBelowAction), + this.instantiationService.createInstance(InsertMarkdownCellAboveAction), + this.instantiationService.createInstance(InsertMarkdownCellBelowAction), + ]; + actions.push(...this.getAdditionalContextMenuActions()); + actions.push(...[ + this.instantiationService.createInstance(DeleteCellAction) + ]); + + this.contextMenuService.showContextMenu({ + getAnchor: () => { + return { + x, + y + }; + }, + getActions: () => actions, + getActionsContext: () => { + cell: element, + notebookEditor: this.notebookEditor + }, + autoSelectFirstItem: false + }); + } + + abstract getAdditionalContextMenuActions(): IAction[]; +} + +export class MarkdownCellRenderer extends AbstractCellRenderer implements IListRenderer { + static readonly TEMPLATE_ID = 'markdown_cell'; + private disposables: Map = new Map(); + + constructor( + notehookEditor: INotebookEditor, + @IInstantiationService instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService, + @IContextMenuService contextMenuService: IContextMenuService, + @IKeybindingService keybindingService: IKeybindingService, + @INotificationService notificationService: INotificationService, + ) { + super(instantiationService, notehookEditor, contextMenuService, configurationService, keybindingService, notificationService, 'markdown'); + } + + get templateId() { + return MarkdownCellRenderer.TEMPLATE_ID; + } + + renderTemplate(container: HTMLElement): CellRenderTemplate { + const codeInnerContent = document.createElement('div'); + DOM.addClasses(codeInnerContent, 'cell', 'code'); + codeInnerContent.style.display = 'none'; + + const disposables = new DisposableStore(); + const toolbar = this.createToolbar(container); + toolbar.setActions([ + this.instantiationService.createInstance(MoveCellUpAction), + this.instantiationService.createInstance(MoveCellDownAction), + this.instantiationService.createInstance(InsertCodeCellBelowAction), + this.instantiationService.createInstance(EditCellAction), + this.instantiationService.createInstance(SaveCellAction), + this.instantiationService.createInstance(DeleteCellAction) + ])(); + disposables.add(toolbar); + + container.appendChild(codeInnerContent); + + const innerContent = document.createElement('div'); + DOM.addClasses(innerContent, 'cell', 'markdown'); + container.appendChild(innerContent); + + const action = document.createElement('div'); + DOM.addClasses(action, 'menu', 'codicon-settings-gear', 'codicon'); + container.appendChild(action); + + return { + container: container, + cellContainer: innerContent, + menuContainer: action, + editingContainer: codeInnerContent, + disposables, + toolbar + }; + } + + renderElement(element: CellViewModel, index: number, templateData: CellRenderTemplate, height: number | undefined): void { + templateData.editingContainer!.style.display = 'none'; + templateData.cellContainer.innerHTML = ''; + let renderedHTML = element.getHTML(); + if (renderedHTML) { + templateData.cellContainer.appendChild(renderedHTML); + } + + if (height) { + this.disposables.get(element)?.clear(); + if (!this.disposables.has(element)) { + this.disposables.set(element, new DisposableStore()); + } + let elementDisposable = this.disposables.get(element); + + elementDisposable!.add(DOM.addStandardDisposableListener(templateData.menuContainer!, 'mousedown', e => { + const { top, height } = DOM.getDomNodePagePosition(templateData.menuContainer!); + e.preventDefault(); + + const listIndexAttr = templateData.menuContainer?.parentElement?.getAttribute('data-index'); + const listIndex = listIndexAttr ? Number(listIndexAttr) : undefined; + this.showContextMenu(listIndex, element, e.posx, top + height); + })); + + elementDisposable!.add(DOM.addStandardDisposableListener(templateData.menuContainer!, DOM.EventType.MOUSE_LEAVE, e => { + templateData.menuContainer?.classList.remove('mouseover'); + })); + + elementDisposable!.add(DOM.addStandardDisposableListener(templateData.menuContainer!, DOM.EventType.MOUSE_ENTER, e => { + templateData.menuContainer?.classList.add('mouseover'); + })); + + elementDisposable!.add(new StatefullMarkdownCell(this.notebookEditor, element, templateData, this.editorOptions, this.instantiationService)); + } + + templateData.toolbar!.context = { + cell: element, + notebookEditor: this.notebookEditor + }; + } + + getAdditionalContextMenuActions(): IAction[] { + return [ + this.instantiationService.createInstance(EditCellAction), + this.instantiationService.createInstance(SaveCellAction), + ]; + } + + disposeTemplate(templateData: CellRenderTemplate): void { + // throw nerendererw Error('Method not implemented.'); + + } + + disposeElement(element: ICellViewModel, index: number, templateData: CellRenderTemplate, height: number | undefined): void { + if (height) { + this.disposables.get(element)?.clear(); + } + } +} + +export class CodeCellRenderer extends AbstractCellRenderer implements IListRenderer { + static readonly TEMPLATE_ID = 'code_cell'; + private disposables: Map = new Map(); + + constructor( + protected notebookEditor: INotebookEditor, + private renderedEditors: Map, + @IContextMenuService contextMenuService: IContextMenuService, + @IConfigurationService configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, + @IKeybindingService keybindingService: IKeybindingService, + @INotificationService notificationService: INotificationService, + ) { + super(instantiationService, notebookEditor, contextMenuService, configurationService, keybindingService, notificationService, 'python'); + } + + get templateId() { + return CodeCellRenderer.TEMPLATE_ID; + } + + renderTemplate(container: HTMLElement): CellRenderTemplate { + const disposables = new DisposableStore(); + const toolbarContainer = document.createElement('div'); + container.appendChild(toolbarContainer); + DOM.addClasses(toolbarContainer, 'menu', 'codicon-settings-gear', 'codicon'); + const toolbar = this.createToolbar(container); + toolbar.setActions([ + this.instantiationService.createInstance(MoveCellUpAction), + this.instantiationService.createInstance(MoveCellDownAction), + this.instantiationService.createInstance(InsertCodeCellBelowAction), + this.instantiationService.createInstance(DeleteCellAction) + ])(); + disposables.add(toolbar); + + const cellContainer = document.createElement('div'); + DOM.addClasses(cellContainer, 'cell', 'code'); + container.appendChild(cellContainer); + const editor = this.instantiationService.createInstance(CodeEditorWidget, cellContainer, { + ...this.editorOptions, + dimension: { + width: 0, + height: 0 + } + }, {}); + const menuContainer = document.createElement('div'); + DOM.addClasses(menuContainer, 'menu', 'codicon-settings-gear', 'codicon'); + container.appendChild(menuContainer); + + const outputContainer = document.createElement('div'); + DOM.addClasses(outputContainer, 'output'); + container.appendChild(outputContainer); + + return { + container, + cellContainer, + menuContainer, + toolbar, + outputContainer, + editor, + disposables + }; + } + + renderElement(element: CellViewModel, index: number, templateData: CellRenderTemplate, height: number | undefined): void { + if (height === undefined) { + return; + } + + if (templateData.outputContainer) { + templateData.outputContainer!.innerHTML = ''; + } + + this.disposables.get(element)?.clear(); + if (!this.disposables.has(element)) { + this.disposables.set(element, new DisposableStore()); + } + + const elementDisposable = this.disposables.get(element); + + elementDisposable?.add(DOM.addStandardDisposableListener(templateData.menuContainer!, 'mousedown', e => { + let { top, height } = DOM.getDomNodePagePosition(templateData.menuContainer!); + e.preventDefault(); + + const listIndexAttr = templateData.menuContainer?.parentElement?.getAttribute('data-index'); + const listIndex = listIndexAttr ? Number(listIndexAttr) : undefined; + + this.showContextMenu(listIndex, element, e.posx, top + height); + })); + + elementDisposable!.add(DOM.addStandardDisposableListener(templateData.menuContainer!, DOM.EventType.MOUSE_LEAVE, e => { + templateData.menuContainer?.classList.remove('mouseover'); + })); + + elementDisposable!.add(DOM.addStandardDisposableListener(templateData.menuContainer!, DOM.EventType.MOUSE_ENTER, e => { + templateData.menuContainer?.classList.add('mouseover'); + })); + + elementDisposable?.add(this.instantiationService.createInstance(CodeCell, this.notebookEditor, element, templateData)); + this.renderedEditors.set(element, templateData.editor); + + templateData.toolbar!.context = { + cell: element, + notebookEditor: this.notebookEditor + }; + } + + getAdditionalContextMenuActions(): IAction[] { + return []; + } + + disposeTemplate(templateData: CellRenderTemplate): void { + templateData.disposables.clear(); + } + + disposeElement(element: ICellViewModel, index: number, templateData: CellRenderTemplate, height: number | undefined): void { + this.disposables.get(element)?.clear(); + this.renderedEditors.delete(element); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts new file mode 100644 index 0000000000000..e485594155105 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -0,0 +1,389 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import * as DOM from 'vs/base/browser/dom'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver'; +import { IOutput, ITransformedDisplayOutputDto, IRenderOutput, CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellRenderTemplate, INotebookEditor, CellFocusMode } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { raceCancellation } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; +import { CELL_MARGIN, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; + +interface IMimeTypeRenderer extends IQuickPickItem { + index: number; +} + +export class CodeCell extends Disposable { + private outputResizeListeners = new Map(); + private outputElements = new Map(); + constructor( + private notebookEditor: INotebookEditor, + private viewCell: CellViewModel, + private templateData: CellRenderTemplate, + @INotebookService private notebookService: INotebookService, + @IQuickInputService private readonly quickInputService: IQuickInputService + ) { + super(); + + let width: number; + const listDimension = notebookEditor.getLayoutInfo(); + width = listDimension.width - CELL_MARGIN * 2; + // if (listDimension) { + // } else { + // width = templateData.container.clientWidth - 24 /** for scrollbar and margin right */; + // } + + const lineNum = viewCell.lineCount; + const lineHeight = notebookEditor.getLayoutInfo().fontInfo.lineHeight; + const totalHeight = lineNum * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; + templateData.editor?.layout( + { + width: width, + height: totalHeight + } + ); + viewCell.editorHeight = totalHeight; + + const cts = new CancellationTokenSource(); + this._register({ dispose() { cts.dispose(true); } }); + raceCancellation(viewCell.resolveTextModel(), cts.token).then(model => { + if (model && templateData.editor) { + templateData.editor.setModel(model); + viewCell.attachTextEditor(templateData.editor); + if (notebookEditor.getActiveCell() === viewCell && viewCell.focusMode === CellFocusMode.Editor) { + templateData.editor?.focus(); + } + + let realContentHeight = templateData.editor?.getContentHeight(); + let width: number; + const listDimension = notebookEditor.getLayoutInfo(); + width = listDimension.width - CELL_MARGIN * 2; + // if (listDimension) { + // } else { + // width = templateData.container.clientWidth - 24 /** for scrollbar and margin right */; + // } + + if (realContentHeight !== undefined && realContentHeight !== totalHeight) { + templateData.editor?.layout( + { + width: width, + height: realContentHeight + } + ); + + viewCell.editorHeight = realContentHeight; + } + + if (this.notebookEditor.getActiveCell() === this.viewCell && viewCell.focusMode === CellFocusMode.Editor) { + templateData.editor?.focus(); + } + } + }); + + this._register(viewCell.onDidChangeFocusMode(() => { + if (viewCell.focusMode === CellFocusMode.Editor) { + templateData.editor?.focus(); + } + })); + + let cellWidthResizeObserver = getResizesObserver(templateData.cellContainer, { + width: width, + height: totalHeight + }, () => { + let newWidth = cellWidthResizeObserver.getWidth(); + let realContentHeight = templateData.editor!.getContentHeight(); + templateData.editor?.layout( + { + width: newWidth, + height: realContentHeight + } + ); + + viewCell.editorHeight = realContentHeight; + }); + + cellWidthResizeObserver.startObserving(); + this._register(cellWidthResizeObserver); + + this._register(templateData.editor!.onDidContentSizeChange((e) => { + if (e.contentHeightChanged) { + if (this.viewCell.editorHeight !== e.contentHeight) { + let viewLayout = templateData.editor!.getLayoutInfo(); + + templateData.editor?.layout( + { + width: viewLayout.width, + height: e.contentHeight + } + ); + + this.viewCell.editorHeight = e.contentHeight; + + notebookEditor.layoutNotebookCell(this.viewCell, viewCell.getCellTotalHeight()); + } + + } + })); + + this._register(templateData.editor!.onDidChangeCursorSelection(() => { + const primarySelection = templateData.editor!.getSelection(); + + if (primarySelection) { + this.notebookEditor.revealLineInView(viewCell, primarySelection!.positionLineNumber); + } + })); + + this._register(viewCell.onDidChangeOutputs((splices) => { + if (!splices.length) { + return; + } + + if (this.viewCell.outputs.length) { + this.templateData.outputContainer!.style.display = 'block'; + } else { + this.templateData.outputContainer!.style.display = 'none'; + } + + let reversedSplices = splices.reverse(); + + reversedSplices.forEach(splice => { + viewCell.spliceOutputHeights(splice[0], splice[1], splice[2].map(_ => 0)); + }); + + let removedKeys: IOutput[] = []; + + this.outputElements.forEach((value, key) => { + if (viewCell.outputs.indexOf(key) < 0) { + // already removed + removedKeys.push(key); + // remove element from DOM + this.templateData?.outputContainer?.removeChild(value); + this.notebookEditor.removeInset(key); + } + }); + + removedKeys.forEach(key => { + // remove element cache + this.outputElements.delete(key); + // remove elment resize listener if there is one + this.outputResizeListeners.delete(key); + }); + + let prevElement: HTMLElement | undefined = undefined; + + this.viewCell.outputs.reverse().forEach(output => { + if (this.outputElements.has(output)) { + // already exist + prevElement = this.outputElements.get(output); + return; + } + + // newly added element + let currIndex = this.viewCell.outputs.indexOf(output); + this.renderOutput(output, currIndex, prevElement); + prevElement = this.outputElements.get(output); + }); + + let editorHeight = templateData.editor!.getContentHeight(); + viewCell.editorHeight = editorHeight; + notebookEditor.layoutNotebookCell(viewCell, viewCell.getCellTotalHeight()); + })); + + if (viewCell.outputs.length > 0) { + this.templateData.outputContainer!.style.display = 'block'; + // there are outputs, we need to calcualte their sizes and trigger relayout + // @todo, if there is no resizable output, we should not check their height individually, which hurts the performance + for (let index = 0; index < this.viewCell.outputs.length; index++) { + const currOutput = this.viewCell.outputs[index]; + + // always add to the end + this.renderOutput(currOutput, index, undefined); + } + + viewCell.editorHeight = totalHeight; + this.notebookEditor.layoutNotebookCell(viewCell, viewCell.getCellTotalHeight()); + } else { + // noop + this.templateData.outputContainer!.style.display = 'none'; + } + } + + renderOutput(currOutput: IOutput, index: number, beforeElement?: HTMLElement) { + if (!this.outputResizeListeners.has(currOutput)) { + this.outputResizeListeners.set(currOutput, new DisposableStore()); + } + + let outputItemDiv = document.createElement('div'); + let result: IRenderOutput | undefined = undefined; + + if (currOutput.outputKind === CellOutputKind.Rich) { + let transformedDisplayOutput = currOutput as ITransformedDisplayOutputDto; + + if (transformedDisplayOutput.orderedMimeTypes.length > 1) { + outputItemDiv.style.position = 'relative'; + const mimeTypePicker = DOM.$('.multi-mimetype-output'); + DOM.addClasses(mimeTypePicker, 'codicon', 'codicon-list-selection'); + outputItemDiv.appendChild(mimeTypePicker); + this.outputResizeListeners.get(currOutput)!.add(DOM.addStandardDisposableListener(mimeTypePicker, 'mousedown', async e => { + e.preventDefault(); + e.stopPropagation(); + await this.pickActiveMimeTypeRenderer(transformedDisplayOutput); + })); + } + let pickedMimeTypeRenderer = currOutput.orderedMimeTypes[currOutput.pickedMimeTypeIndex]; + + if (pickedMimeTypeRenderer.isResolved) { + // html + result = this.notebookEditor.getOutputRenderer().render({ outputKind: CellOutputKind.Rich, data: { 'text/html': pickedMimeTypeRenderer.output! } } as any, outputItemDiv, 'text/html'); + } else { + result = this.notebookEditor.getOutputRenderer().render(currOutput, outputItemDiv, pickedMimeTypeRenderer.mimeType); + } + } else { + // for text and error, there is no mimetype + result = this.notebookEditor.getOutputRenderer().render(currOutput, outputItemDiv, undefined); + } + + if (!result) { + this.viewCell.updateOutputHeight(index, 0); + return; + } + + this.outputElements.set(currOutput, outputItemDiv); + + if (beforeElement) { + this.templateData.outputContainer?.insertBefore(outputItemDiv, beforeElement); + } else { + this.templateData.outputContainer?.appendChild(outputItemDiv); + } + + if (result.shadowContent) { + this.viewCell.selfSizeMonitoring = true; + let editorHeight = this.viewCell.editorHeight; + this.notebookEditor.createInset(this.viewCell, currOutput, result.shadowContent, editorHeight + 8 + this.viewCell.getOutputOffset(index)); + } else { + DOM.addClass(outputItemDiv, 'foreground'); + } + + let hasDynamicHeight = result.hasDynamicHeight; + + if (hasDynamicHeight) { + let clientHeight = outputItemDiv.clientHeight; + let listDimension = this.notebookEditor.getLayoutInfo(); + let dimension = listDimension ? { + width: listDimension.width - CELL_MARGIN * 2, + height: clientHeight + } : undefined; + const elementSizeObserver = getResizesObserver(outputItemDiv, dimension, () => { + if (this.templateData.outputContainer && document.body.contains(this.templateData.outputContainer!)) { + let height = elementSizeObserver.getHeight() + 8 * 2; // include padding + + if (clientHeight === height) { + // console.log(this.viewCell.outputs); + return; + } + + const currIndex = this.viewCell.outputs.indexOf(currOutput); + if (currIndex < 0) { + return; + } + + this.viewCell.updateOutputHeight(currIndex, height); + this.notebookEditor.layoutNotebookCell(this.viewCell, this.viewCell.getCellTotalHeight()); + } + }); + elementSizeObserver.startObserving(); + this.outputResizeListeners.get(currOutput)!.add(elementSizeObserver); + this.viewCell.updateOutputHeight(index, clientHeight); + } else { + if (result.shadowContent) { + // webview + // noop + // let cachedHeight = this.viewCell.getOutputHeight(currOutput); + } else { + // static output + + // @TODO, if we stop checking output height, we need to evaluate it later when checking the height of output container + let clientHeight = outputItemDiv.clientHeight; + this.viewCell.updateOutputHeight(index, clientHeight); + } + } + } + + generateRendererInfo(renderId: number | undefined): string { + if (renderId === undefined || renderId === -1) { + return 'builtin'; + } + + let renderInfo = this.notebookService.getRendererInfo(renderId); + + if (renderInfo) { + return renderInfo.id.value; + } + + return 'builtin'; + } + + async pickActiveMimeTypeRenderer(output: ITransformedDisplayOutputDto) { + let currIndex = output.pickedMimeTypeIndex; + const items = output.orderedMimeTypes.map((mimeType, index): IMimeTypeRenderer => ({ + label: mimeType.mimeType, + id: mimeType.mimeType, + index: index, + picked: index === currIndex, + description: this.generateRendererInfo(mimeType.rendererId) + (index === currIndex + ? nls.localize('curruentActiveMimeType', " (Currently Active)") + : ''), + })); + + const picker = this.quickInputService.createQuickPick(); + picker.items = items; + picker.activeItems = items.filter(item => !!item.picked); + picker.placeholder = nls.localize('promptChooseMimeType.placeHolder', "Select output mimetype to render for current output"); + + const pick = await new Promise(resolve => { + picker.onDidAccept(() => { + resolve(picker.selectedItems.length === 1 ? (picker.selectedItems[0] as IMimeTypeRenderer).index : undefined); + picker.dispose(); + }); + picker.show(); + }); + + if (pick === undefined) { + return; + } + + if (pick !== currIndex) { + // user chooses another mimetype + let index = this.viewCell.outputs.indexOf(output); + let nextElement = index + 1 < this.viewCell.outputs.length ? this.outputElements.get(this.viewCell.outputs[index + 1]) : undefined; + this.outputResizeListeners.get(output)?.clear(); + let element = this.outputElements.get(output); + if (element) { + this.templateData?.outputContainer?.removeChild(element); + this.notebookEditor.removeInset(output); + } + + output.pickedMimeTypeIndex = pick; + + this.renderOutput(output, index, nextElement); + this.notebookEditor.layoutNotebookCell(this.viewCell, this.viewCell.getCellTotalHeight()); + } + } + + dispose() { + this.viewCell.detachTextEditor(); + this.outputResizeListeners.forEach((value) => { + value.dispose(); + }); + + super.dispose(); + } +} + diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts new file mode 100644 index 0000000000000..7cc24ffc14258 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -0,0 +1,218 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver'; +import { INotebookEditor, CellRenderTemplate, CellFocusMode, CellState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { raceCancellation } from 'vs/base/common/async'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; +import { CELL_MARGIN, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; + +export class StatefullMarkdownCell extends Disposable { + private editor: CodeEditorWidget | null = null; + private cellContainer: HTMLElement; + private editingContainer?: HTMLElement; + + private localDisposables: DisposableStore; + + constructor( + notebookEditor: INotebookEditor, + public viewCell: CellViewModel, + templateData: CellRenderTemplate, + editorOptions: IEditorOptions, + instantiationService: IInstantiationService + ) { + super(); + + this.cellContainer = templateData.cellContainer; + this.editingContainer = templateData.editingContainer; + this.localDisposables = new DisposableStore(); + this._register(this.localDisposables); + + const viewUpdate = () => { + if (viewCell.state === CellState.Editing) { + // switch to editing mode + let width: number; + const listDimension = notebookEditor.getLayoutInfo(); + width = listDimension.width - CELL_MARGIN * 2; + // if (listDimension) { + // } else { + // width = this.cellContainer.clientWidth - 24 /** for scrollbar and margin right */; + // } + + const lineNum = viewCell.lineCount; + const lineHeight = notebookEditor.getLayoutInfo().fontInfo.lineHeight; + const totalHeight = Math.max(lineNum, 1) * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; + + if (this.editor) { + // not first time, we don't need to create editor or bind listeners + this.editingContainer!.style.display = 'block'; + viewCell.attachTextEditor(this.editor!); + if (notebookEditor.getActiveCell() === viewCell) { + this.editor!.focus(); + } + } else { + this.editingContainer!.style.display = 'block'; + this.editingContainer!.innerHTML = ''; + this.editor = instantiationService.createInstance(CodeEditorWidget, this.editingContainer!, { + ...editorOptions, + dimension: { + width: width, + height: totalHeight + } + }, {}); + + + const cts = new CancellationTokenSource(); + this._register({ dispose() { cts.dispose(true); } }); + raceCancellation(viewCell.resolveTextModel(), cts.token).then(model => { + if (!model) { + return; + } + + this.editor!.setModel(model); + if (notebookEditor.getActiveCell() === viewCell) { + this.editor!.focus(); + } + + const realContentHeight = this.editor!.getContentHeight(); + if (realContentHeight !== totalHeight) { + this.editor!.layout( + { + width: width, + height: realContentHeight + } + ); + } + + viewCell.attachTextEditor(this.editor!); + + this.localDisposables.add(model.onDidChangeContent(() => { + viewCell.setText(model.getLinesContent()); + let clientHeight = this.cellContainer.clientHeight; + this.cellContainer.innerHTML = ''; + let renderedHTML = viewCell.getHTML(); + if (renderedHTML) { + this.cellContainer.appendChild(renderedHTML); + clientHeight = this.cellContainer.clientHeight; + } + + notebookEditor.layoutNotebookCell(viewCell, this.editor!.getContentHeight() + 32 + clientHeight); + })); + + if (viewCell.state === CellState.Editing) { + this.editor!.focus(); + } + }); + + this.localDisposables.add(this.editor.onDidContentSizeChange(e => { + let viewLayout = this.editor!.getLayoutInfo(); + + if (e.contentHeightChanged) { + this.editor!.layout( + { + width: viewLayout.width, + height: e.contentHeight + } + ); + const clientHeight = this.cellContainer.clientHeight; + notebookEditor.layoutNotebookCell(viewCell, e.contentHeight + 32 + clientHeight); + } + })); + + let cellWidthResizeObserver = getResizesObserver(templateData.editingContainer!, { + width: width, + height: totalHeight + }, () => { + let newWidth = cellWidthResizeObserver.getWidth(); + let realContentHeight = this.editor!.getContentHeight(); + let layoutInfo = this.editor!.getLayoutInfo(); + + // the dimension generated by the resize observer are float numbers, let's round it a bit to avoid relayout. + if (newWidth < layoutInfo.width - 0.3 || layoutInfo.width + 0.3 < newWidth) { + this.editor!.layout( + { + width: newWidth, + height: realContentHeight + } + ); + } + }); + + cellWidthResizeObserver.startObserving(); + this.localDisposables.add(cellWidthResizeObserver); + + let markdownRenderer = viewCell.getMarkdownRenderer(); + this.cellContainer.innerHTML = ''; + let renderedHTML = viewCell.getHTML(); + if (renderedHTML) { + this.cellContainer.appendChild(renderedHTML); + this.localDisposables.add(markdownRenderer.onDidUpdateRender(() => { + const clientHeight = this.cellContainer.clientHeight; + notebookEditor.layoutNotebookCell(viewCell, clientHeight); + })); + } + } + + const clientHeight = this.cellContainer.clientHeight; + notebookEditor.layoutNotebookCell(viewCell, totalHeight + 32 + clientHeight); + this.editor.focus(); + } else { + this.viewCell.detachTextEditor(); + if (this.editor) { + // switch from editing mode + this.editingContainer!.style.display = 'none'; + const clientHeight = templateData.container.clientHeight; + notebookEditor.layoutNotebookCell(viewCell, clientHeight); + } else { + // first time, readonly mode + this.editingContainer!.style.display = 'none'; + + this.cellContainer.innerHTML = ''; + let markdownRenderer = viewCell.getMarkdownRenderer(); + let renderedHTML = viewCell.getHTML(); + if (renderedHTML) { + this.cellContainer.appendChild(renderedHTML); + } + + this.localDisposables.add(markdownRenderer.onDidUpdateRender(() => { + const clientHeight = templateData.container.clientHeight; + notebookEditor.layoutNotebookCell(viewCell, clientHeight); + })); + + this.localDisposables.add(viewCell.onDidChangeContent(() => { + this.cellContainer.innerHTML = ''; + let renderedHTML = viewCell.getHTML(); + if (renderedHTML) { + this.cellContainer.appendChild(renderedHTML); + } + })); + } + } + }; + + this._register(viewCell.onDidChangeCellState(() => { + this.localDisposables.clear(); + viewUpdate(); + })); + + this._register(viewCell.onDidChangeFocusMode(() => { + if (viewCell.focusMode === CellFocusMode.Editor) { + this.editor?.focus(); + } + })); + + viewUpdate(); + } + + dispose() { + this.viewCell.detachTextEditor(); + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer.ts new file mode 100644 index 0000000000000..4806000fadf40 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { renderMarkdown, MarkdownRenderOptions } from 'vs/base/browser/markdownRenderer'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { TokenizationRegistry } from 'vs/editor/common/modes'; + +export interface IMarkdownRenderResult extends IDisposable { + element: HTMLElement; +} + +export class MarkdownRenderer extends Disposable { + + private _onDidUpdateRender = this._register(new Emitter()); + readonly onDidUpdateRender: Event = this._onDidUpdateRender.event; + + constructor( + @IModeService private readonly _modeService: IModeService, + @IOpenerService private readonly _openerService: IOpenerService + ) { + super(); + } + + private getOptions(disposeables: DisposableStore): MarkdownRenderOptions { + return { + codeBlockRenderer: (languageAlias, value) => { + // In markdown, + // it is possible that we stumble upon language aliases (e.g.js instead of javascript) + // it is possible no alias is given in which case we fall back to the current editor lang + let modeId: string | null = null; + modeId = this._modeService.getModeIdForLanguageName(languageAlias || ''); + + this._modeService.triggerMode(modeId || ''); + return Promise.resolve(true).then(_ => { + const promise = TokenizationRegistry.getPromise(modeId || ''); + if (promise) { + return promise.then(support => tokenizeToString(value, support)); + } + return tokenizeToString(value, undefined); + }).then(code => { + return `${code}`; + }); + }, + codeBlockRenderCallback: () => this._onDidUpdateRender.fire(), + actionHandler: { + callback: (content) => { + this._openerService.open(content, { fromUserGesture: true }).catch(onUnexpectedError); + }, + disposeables + } + }; + } + + render(markdown: IMarkdownString | undefined): IMarkdownRenderResult { + const disposeables = new DisposableStore(); + + let element: HTMLElement; + if (!markdown) { + element = document.createElement('span'); + } else { + element = renderMarkdown(markdown, this.getOptions(disposeables), { gfm: true }); + } + + return { + element, + dispose: () => disposeables.dispose() + }; + } +} + diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver.ts new file mode 100644 index 0000000000000..6a5b6ba604102 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IDimension } from 'vs/editor/common/editorCommon'; +import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver'; + +declare const ResizeObserver: any; + +export interface IResizeObserver { + startObserving: () => void; + stopObserving: () => void; + getWidth(): number; + getHeight(): number; + dispose(): void; +} + +export class BrowserResizeObserver extends Disposable implements IResizeObserver { + private readonly referenceDomElement: HTMLElement | null; + + private readonly observer: any; + private width: number; + private height: number; + + constructor(referenceDomElement: HTMLElement | null, dimension: IDimension | undefined, changeCallback: () => void) { + super(); + + this.referenceDomElement = referenceDomElement; + this.width = -1; + this.height = -1; + + this.observer = new ResizeObserver((entries: any) => { + for (let entry of entries) { + if (entry.target === referenceDomElement && entry.contentRect) { + if (this.width !== entry.contentRect.width || this.height !== entry.contentRect.height) { + this.width = entry.contentRect.width; + this.height = entry.contentRect.height; + DOM.scheduleAtNextAnimationFrame(() => { + changeCallback(); + }); + } + } + } + }); + } + + getWidth(): number { + return this.width; + } + + getHeight(): number { + return this.height; + } + + startObserving(): void { + this.observer.observe(this.referenceDomElement!); + } + + stopObserving(): void { + this.observer.unobserve(this.referenceDomElement!); + } + + dispose(): void { + this.observer.disconnect(); + super.dispose(); + } +} + +export function getResizesObserver(referenceDomElement: HTMLElement | null, dimension: IDimension | undefined, changeCallback: () => void): IResizeObserver { + if (ResizeObserver) { + return new BrowserResizeObserver(referenceDomElement, dimension, changeCallback); + } else { + return new ElementSizeObserver(referenceDomElement, dimension, changeCallback); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts new file mode 100644 index 0000000000000..839c2b003065b --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/cellEdit.ts @@ -0,0 +1,114 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ICell } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IResourceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; +import { URI } from 'vs/base/common/uri'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; + + +/** + * It should not modify Undo/Redo stack + */ +export interface ICellEditingDelegate { + insertCell?(index: number, viewCell: CellViewModel): void; + deleteCell?(index: number, cell: ICell): void; + moveCell?(fromIndex: number, toIndex: number): void; +} + +export class InsertCellEdit implements IResourceUndoRedoElement { + type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; + label: string = 'Insert Cell'; + constructor( + public resource: URI, + private insertIndex: number, + private cell: CellViewModel, + private editingDelegate: ICellEditingDelegate + ) { + } + + undo(): void | Promise { + if (!this.editingDelegate.deleteCell) { + throw new Error('Notebook Delete Cell not implemented for Undo/Redo'); + } + + this.editingDelegate.deleteCell(this.insertIndex, this.cell.cell); + } + redo(): void | Promise { + if (!this.editingDelegate.insertCell) { + throw new Error('Notebook Insert Cell not implemented for Undo/Redo'); + } + + this.editingDelegate.insertCell(this.insertIndex, this.cell); + } +} + +export class DeleteCellEdit implements IResourceUndoRedoElement { + type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; + label: string = 'Delete Cell'; + + private _rawCell: ICell; + constructor( + public resource: URI, + private insertIndex: number, + cell: CellViewModel, + private editingDelegate: ICellEditingDelegate, + private instantiationService: IInstantiationService, + private notebookViewModel: NotebookViewModel + ) { + this._rawCell = cell.cell; + + // save inmem text to `ICell` + this._rawCell.source = [cell.getText()]; + } + + undo(): void | Promise { + if (!this.editingDelegate.insertCell) { + throw new Error('Notebook Insert Cell not implemented for Undo/Redo'); + } + + const cell = this.instantiationService.createInstance(CellViewModel, this.notebookViewModel.viewType, this.notebookViewModel.handle, this._rawCell); + this.editingDelegate.insertCell(this.insertIndex, cell); + } + + redo(): void | Promise { + if (!this.editingDelegate.deleteCell) { + throw new Error('Notebook Delete Cell not implemented for Undo/Redo'); + } + + this.editingDelegate.deleteCell(this.insertIndex, this._rawCell); + } +} + +export class MoveCellEdit implements IResourceUndoRedoElement { + type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; + label: string = 'Delete Cell'; + + constructor( + public resource: URI, + private fromIndex: number, + private toIndex: number, + private editingDelegate: ICellEditingDelegate + ) { + } + + undo(): void | Promise { + if (!this.editingDelegate.moveCell) { + throw new Error('Notebook Move Cell not implemented for Undo/Redo'); + } + + this.editingDelegate.moveCell(this.toIndex, this.fromIndex); + } + + redo(): void | Promise { + if (!this.editingDelegate.moveCell) { + throw new Error('Notebook Move Cell not implemented for Undo/Redo'); + } + + this.editingDelegate.moveCell(this.fromIndex, this.toIndex); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts new file mode 100644 index 0000000000000..b2c2483403702 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel.ts @@ -0,0 +1,511 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import * as UUID from 'vs/base/common/uuid'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { Range } from 'vs/editor/common/core/range'; +import * as editorCommon from 'vs/editor/common/editorCommon'; +import * as model from 'vs/editor/common/model'; +import { SearchParams } from 'vs/editor/common/model/textModelSearch'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer'; +import { CellKind, ICell, IOutput, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellFindMatch, CellState, CursorAtBoundary, CellFocusMode, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; + +export class CellViewModel extends Disposable implements ICellViewModel { + + private _mdRenderer: MarkdownRenderer | null = null; + private _html: HTMLElement | null = null; + protected readonly _onDidDispose = new Emitter(); + readonly onDidDispose = this._onDidDispose.event; + protected readonly _onDidChangeCellState = new Emitter(); + readonly onDidChangeCellState = this._onDidChangeCellState.event; + protected readonly _onDidChangeFocusMode = new Emitter(); + readonly onDidChangeFocusMode = this._onDidChangeFocusMode.event; + protected readonly _onDidChangeOutputs = new Emitter(); + readonly onDidChangeOutputs = this._onDidChangeOutputs.event; + private _outputCollection: number[] = []; + protected _outputsTop: PrefixSumComputer | null = null; + + get handle() { + return this.cell.handle; + } + + get uri() { + return this.cell.uri; + } + + get cellKind() { + return this.cell.cellKind; + } + get lineCount() { + return this.cell.source.length; + } + get outputs() { + return this.cell.outputs; + } + + private _state: CellState = CellState.Preview; + + get state(): CellState { + return this._state; + } + + set state(newState: CellState) { + if (newState === this._state) { + return; + } + + this._state = newState; + this._onDidChangeCellState.fire(); + } + + private _focusMode: CellFocusMode = CellFocusMode.Container; + + get focusMode() { + return this._focusMode; + } + + set focusMode(newMode: CellFocusMode) { + this._focusMode = newMode; + this._onDidChangeFocusMode.fire(); + } + + private _selfSizeMonitoring: boolean = false; + + set selfSizeMonitoring(newVal: boolean) { + this._selfSizeMonitoring = newVal; + } + + get selfSizeMonitoring() { + return this._selfSizeMonitoring; + } + + private _editorHeight = 0; + set editorHeight(height: number) { + this._editorHeight = height; + } + + get editorHeight(): number { + return this._editorHeight; + } + + protected readonly _onDidChangeEditorAttachState = new Emitter(); + readonly onDidChangeEditorAttachState = this._onDidChangeEditorAttachState.event; + + get editorAttached(): boolean { + return !!this._textEditor; + } + + private _textModel?: model.ITextModel; + private _textEditor?: ICodeEditor; + private _buffer: model.ITextBuffer | null; + private _editorViewStates: editorCommon.ICodeEditorViewState | null; + private _lastDecorationId: number = 0; + private _resolvedDecorations = new Map(); + private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); + public readonly onDidChangeContent: Event = this._onDidChangeContent.event; + private readonly _onDidChangeCursorSelection: Emitter = this._register(new Emitter()); + public readonly onDidChangeCursorSelection: Event = this._onDidChangeCursorSelection.event; + + private _cursorChangeListener: IDisposable | null = null; + + readonly id: string = UUID.generateUuid(); + + constructor( + readonly viewType: string, + readonly notebookHandle: number, + readonly cell: ICell, + @IInstantiationService private readonly _instaService: IInstantiationService, + @ITextModelService private readonly _modelService: ITextModelService, + ) { + super(); + if (this.cell.onDidChangeOutputs) { + this._register(this.cell.onDidChangeOutputs((splices) => { + this._outputCollection = new Array(this.cell.outputs.length); + this._outputsTop = null; + this._onDidChangeOutputs.fire(splices); + })); + } + + this._outputCollection = new Array(this.cell.outputs.length); + this._buffer = null; + this._editorViewStates = null; + } + + restoreEditorViewState(editorViewStates: editorCommon.ICodeEditorViewState | null) { + this._editorViewStates = editorViewStates; + } + + saveEditorViewState() { + if (this._textEditor) { + this._editorViewStates = this.saveViewState(); + } + + return this._editorViewStates; + } + + + //#region Search + private readonly _hasFindResult = this._register(new Emitter()); + public readonly hasFindResult: Event = this._hasFindResult.event; + + startFind(value: string): CellFindMatch | null { + let cellMatches: model.FindMatch[] = []; + + if (this.assertTextModelAttached()) { + cellMatches = this._textModel!.findMatches(value, false, false, false, null, false); + } else { + if (!this._buffer) { + this._buffer = this.cell.resolveTextBufferFactory().create(model.DefaultEndOfLine.LF); + } + + const lineCount = this._buffer.getLineCount(); + const fullRange = new Range(1, 1, lineCount, this._buffer.getLineLength(lineCount) + 1); + const searchParams = new SearchParams(value, false, false, null); + const searchData = searchParams.parseSearchRequest(); + + if (!searchData) { + return null; + } + + cellMatches = this._buffer.findMatchesLineByLine(fullRange, searchData, false, 1000); + } + + return { + cell: this, + matches: cellMatches + }; + } + + assertTextModelAttached(): boolean { + if (this._textModel && this._textEditor && this._textEditor.getModel() === this._textModel) { + return true; + } + + return false; + } + + private saveViewState(): editorCommon.ICodeEditorViewState | null { + if (!this._textEditor) { + return null; + } + + return this._textEditor.saveViewState(); + } + + + private restoreViewState(state: editorCommon.ICodeEditorViewState | null): void { + if (state) { + this._textEditor?.restoreViewState(state); + } + } + + //#endregion + + hasDynamicHeight() { + if (this.selfSizeMonitoring) { + // if there is an output rendered in the webview, it should always be false + return false; + } + + if (this.cellKind === CellKind.Code) { + if (this.outputs && this.outputs.length > 0) { + // if it contains output, it will be marked as dynamic height + // thus when it's being rendered, the list view will `probeHeight` + // inside which, we will check domNode's height directly instead of doing another `renderElement` with height undefined. + return true; + } + else { + return false; + } + } + + return true; + } + + getHeight(lineHeight: number) { + if (this.cellKind === CellKind.Markdown) { + return 100; + } + else { + return this.lineCount * lineHeight + 16 + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; + } + } + setText(strs: string[]) { + this.cell.source = strs; + this._html = null; + } + + save() { + if (this._textModel && !this._textModel.isDisposed() && this.state === CellState.Editing) { + let cnt = this._textModel.getLineCount(); + this.cell.source = this._textModel.getLinesContent().map((str, index) => str + (index !== cnt - 1 ? '\n' : '')); + } + } + getText(): string { + if (this._textModel) { + return this._textModel.getValue(); + } + + return this.cell.source.join('\n'); + } + + getHTML(): HTMLElement | null { + if (this.cellKind === CellKind.Markdown) { + if (this._html) { + return this._html; + } + let renderer = this.getMarkdownRenderer(); + this._html = renderer.render({ value: this.getText(), isTrusted: true }).element; + return this._html; + } + return null; + } + + async resolveTextModel(): Promise { + if (!this._textModel) { + const ref = await this._modelService.createModelReference(this.cell.uri); + this._textModel = ref.object.textEditorModel; + this._buffer = this._textModel.getTextBuffer(); + this._register(ref); + this._register(this._textModel.onDidChangeContent(() => { + this.cell.contentChange(); + this._html = null; + this._onDidChangeContent.fire(); + })); + } + return this._textModel; + } + + attachTextEditor(editor: ICodeEditor) { + if (!editor.hasModel()) { + throw new Error('Invalid editor: model is missing'); + } + + if (this._textEditor === editor) { + if (this._cursorChangeListener === null) { + this._cursorChangeListener = this._textEditor.onDidChangeCursorSelection(() => this._onDidChangeCursorSelection.fire()); + this._onDidChangeCursorSelection.fire(); + } + return; + } + + this._textEditor = editor; + + if (this._editorViewStates) { + this.restoreViewState(this._editorViewStates); + } + + this._resolvedDecorations.forEach((value, key) => { + if (key.startsWith('_lazy_')) { + // lazy ones + + const ret = this._textEditor!.deltaDecorations([], [value.options]); + this._resolvedDecorations.get(key)!.id = ret[0]; + } else { + const ret = this._textEditor!.deltaDecorations([], [value.options]); + this._resolvedDecorations.get(key)!.id = ret[0]; + } + }); + + this._cursorChangeListener = this._textEditor.onDidChangeCursorSelection(() => this._onDidChangeCursorSelection.fire()); + this._onDidChangeCursorSelection.fire(); + this._onDidChangeEditorAttachState.fire(true); + } + + detachTextEditor() { + this._editorViewStates = this.saveViewState(); + + // decorations need to be cleared first as editors can be resued. + this._resolvedDecorations.forEach(value => { + let resolvedid = value.id; + + if (resolvedid) { + this._textEditor?.deltaDecorations([resolvedid], []); + } + }); + this._textEditor = undefined; + this._cursorChangeListener?.dispose(); + this._cursorChangeListener = null; + this._onDidChangeEditorAttachState.fire(false); + } + + revealRangeInCenter(range: Range) { + this._textEditor?.revealRangeInCenter(range, editorCommon.ScrollType.Immediate); + } + + setSelection(range: Range) { + this._textEditor?.setSelection(range); + } + + getLineScrollTopOffset(line: number): number { + if (!this._textEditor) { + return 0; + } + + return this._textEditor.getTopForLineNumber(line) + EDITOR_TOP_PADDING + EDITOR_TOOLBAR_HEIGHT; + } + + addDecoration(decoration: model.IModelDeltaDecoration): string { + if (!this._textEditor) { + const id = ++this._lastDecorationId; + const decorationId = `_lazy_${this.id};${id}`; + + this._resolvedDecorations.set(decorationId, { options: decoration }); + return decorationId; + } + + const result = this._textEditor.deltaDecorations([], [decoration]); + this._resolvedDecorations.set(result[0], { id: result[0], options: decoration }); + + return result[0]; + } + + removeDecoration(decorationId: string) { + const realDecorationId = this._resolvedDecorations.get(decorationId); + + if (this._textEditor && realDecorationId && realDecorationId.id !== undefined) { + this._textEditor.deltaDecorations([realDecorationId.id!], []); + } + + // lastly, remove all the cache + this._resolvedDecorations.delete(decorationId); + } + + deltaDecorations(oldDecorations: string[], newDecorations: model.IModelDeltaDecoration[]): string[] { + oldDecorations.forEach(id => { + this.removeDecoration(id); + }); + + const ret = newDecorations.map(option => { + return this.addDecoration(option); + }); + + return ret; + } + + onDeselect() { + this.state = CellState.Preview; + } + + cursorAtBoundary(): CursorAtBoundary { + if (!this._textEditor) { + return CursorAtBoundary.None; + } + + // only validate primary cursor + const selection = this._textEditor.getSelection(); + + // only validate empty cursor + if (!selection || !selection.isEmpty()) { + return CursorAtBoundary.None; + } + + // we don't allow attaching text editor without a model + const lineCnt = this._textEditor.getModel()!.getLineCount(); + + if (selection.startLineNumber === lineCnt) { + // bottom + + if (selection.startLineNumber === 1) { + return CursorAtBoundary.Both; + } else { + return CursorAtBoundary.Bottom; + } + } + + if (selection.startLineNumber === 1) { + return CursorAtBoundary.Top; + } + + return CursorAtBoundary.None; + } + + getMarkdownRenderer() { + if (!this._mdRenderer) { + this._mdRenderer = this._instaService.createInstance(MarkdownRenderer); + } + return this._mdRenderer; + } + + updateOutputHeight(index: number, height: number) { + if (index >= this._outputCollection.length) { + throw new Error('Output index out of range!'); + } + + this._outputCollection[index] = height; + this._ensureOutputsTop(); + this._outputsTop!.changeValue(index, height); + } + + getOutputOffset(index: number): number { + if (index >= this._outputCollection.length) { + throw new Error('Output index out of range!'); + } + + this._ensureOutputsTop(); + + return this._outputsTop!.getAccumulatedValue(index - 1); + } + + getOutputHeight(output: IOutput): number | undefined { + let index = this.cell.outputs.indexOf(output); + + if (index < 0) { + return undefined; + } + + if (index < this._outputCollection.length) { + return this._outputCollection[index]; + } + + return undefined; + } + + private getOutputTotalHeight(): number { + this._ensureOutputsTop(); + + return this._outputsTop!.getTotalValue(); + } + + spliceOutputHeights(start: number, deleteCnt: number, heights: number[]) { + this._ensureOutputsTop(); + + this._outputsTop!.removeValues(start, deleteCnt); + if (heights.length) { + const values = new Uint32Array(heights.length); + for (let i = 0; i < heights.length; i++) { + values[i] = heights[i]; + } + + this._outputsTop!.insertValues(start, values); + } + } + + getCellTotalHeight(): number { + if (this.outputs.length) { + return EDITOR_TOOLBAR_HEIGHT + this.editorHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING + 16 + this.getOutputTotalHeight(); + } else { + return EDITOR_TOOLBAR_HEIGHT + this.editorHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING + this.getOutputTotalHeight(); + } + } + + protected _ensureOutputsTop(): void { + if (!this._outputsTop) { + const values = new Uint32Array(this._outputCollection.length); + for (let i = 0; i < this._outputCollection.length; i++) { + values[i] = this._outputCollection[i]; + } + + this._outputsTop = new PrefixSumComputer(values); + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts new file mode 100644 index 0000000000000..209a7116bd88c --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -0,0 +1,380 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import * as editorCommon from 'vs/editor/common/editorCommon'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; +import { ICell } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IModelDeltaDecoration } from 'vs/editor/common/model'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { CellFindMatch, CellState, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { Range } from 'vs/editor/common/core/range'; +import { WorkspaceTextEdit } from 'vs/editor/common/modes'; +import { URI } from 'vs/base/common/uri'; +import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { InsertCellEdit, DeleteCellEdit, MoveCellEdit } from 'vs/workbench/contrib/notebook/browser/viewModel/cellEdit'; + +export interface INotebookEditorViewState { + editingCells: { [key: number]: boolean }; + editorViewStates: { [key: number]: editorCommon.ICodeEditorViewState | null }; +} + +export interface ICellModelDecorations { + ownerId: number; + decorations: string[]; +} + +export interface ICellModelDeltaDecorations { + ownerId: number; + decorations: IModelDeltaDecoration[]; +} + +export interface IModelDecorationsChangeAccessor { + deltaDecorations(oldDecorations: ICellModelDecorations[], newDecorations: ICellModelDeltaDecorations[]): ICellModelDecorations[]; +} + +const invalidFunc = () => { throw new Error(`Invalid change accessor`); }; + + +export type NotebookViewCellsSplice = [ + number /* start */, + number /* delete count */, + CellViewModel[] +]; + +export interface INotebookViewCellsUpdateEvent { + synchronous: boolean; + splices: NotebookViewCellsSplice[]; +} + +export class NotebookViewModel extends Disposable { + private _localStore: DisposableStore = this._register(new DisposableStore()); + private _viewCells: CellViewModel[] = []; + + get viewCells(): ICellViewModel[] { + return this._viewCells; + } + + get notebookDocument() { + return this._model.notebook; + } + + get renderers() { + return this._model.notebook!.renderers; + } + + get handle() { + return this._model.notebook.handle; + } + + get languages() { + return this._model.notebook.languages; + } + + get uri() { + return this._model.notebook.uri; + } + + private readonly _onDidChangeViewCells = new Emitter(); + get onDidChangeViewCells(): Event { return this._onDidChangeViewCells.event; } + + private _lastNotebookEditResource: URI[] = []; + + get lastNotebookEditResource(): URI | null { + if (this._lastNotebookEditResource.length) { + return this._lastNotebookEditResource[this._lastNotebookEditResource.length - 1]; + } + return null; + } + + constructor( + public viewType: string, + private _model: NotebookEditorModel, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IBulkEditService private readonly bulkEditService: IBulkEditService, + @IUndoRedoService private readonly undoService: IUndoRedoService + ) { + super(); + + this._register(this._model.onDidChangeCells(e => { + this._onDidChangeViewCells.fire({ + synchronous: true, + splices: e.map(splice => { + return [splice[0], splice[1], splice[2].map(cell => this.instantiationService.createInstance(CellViewModel, this.viewType, this.handle, cell))]; + }) + }); + })); + + this._viewCells = this._model!.notebook!.cells.map(cell => { + const viewCell = this.instantiationService.createInstance(CellViewModel, this.viewType, this._model!.notebook!.handle, cell); + this._localStore.add(viewCell); + return viewCell; + }); + } + + isDirty() { + return this._model.isDirty(); + } + + hide() { + this._viewCells.forEach(cell => { + if (cell.getText() !== '') { + cell.state = CellState.Preview; + } + }); + } + + getViewCellIndex(cell: ICellViewModel) { + return this._viewCells.indexOf(cell as CellViewModel); + } + + private _insertCellDelegate(insertIndex: number, insertCell: CellViewModel) { + this._viewCells!.splice(insertIndex, 0, insertCell); + this._model.insertCell(insertCell.cell, insertIndex); + this._localStore.add(insertCell); + this._onDidChangeViewCells.fire({ synchronous: true, splices: [[insertIndex, 0, [insertCell]]] }); + } + + private _deleteCellDelegate(deleteIndex: number, cell: ICell) { + this._viewCells.splice(deleteIndex, 1); + this._model.deleteCell(deleteIndex); + this._onDidChangeViewCells.fire({ synchronous: true, splices: [[deleteIndex, 1, []]] }); + } + + insertCell(index: number, cell: ICell, synchronous: boolean): CellViewModel { + const newCell = this.instantiationService.createInstance(CellViewModel, this.viewType, this.handle, cell); + this._viewCells!.splice(index, 0, newCell); + this._model.insertCell(newCell.cell, index); + this._localStore.add(newCell); + this.undoService.pushElement(new InsertCellEdit(this.uri, index, newCell, { + insertCell: this._insertCellDelegate.bind(this), + deleteCell: this._deleteCellDelegate.bind(this) + })); + + this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 0, [newCell]]] }); + return newCell; + } + + deleteCell(index: number, synchronous: boolean) { + let viewCell = this._viewCells[index]; + this._viewCells.splice(index, 1); + this._model.deleteCell(index); + + this.undoService.pushElement(new DeleteCellEdit(this.uri, index, viewCell, { + insertCell: this._insertCellDelegate.bind(this), + deleteCell: this._deleteCellDelegate.bind(this) + }, this.instantiationService, this)); + + this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 1, []]] }); + viewCell.dispose(); + } + + moveCellToIdx(index: number, newIdx: number, synchronous: boolean, pushedToUndoStack: boolean = true): boolean { + const viewCell = this.viewCells[index] as CellViewModel; + if (!viewCell) { + return false; + } + + this.viewCells.splice(index, 1); + this._model.deleteCell(index); + + this.viewCells!.splice(newIdx, 0, viewCell); + this._model.insertCell(viewCell.cell, newIdx); + + if (pushedToUndoStack) { + this.undoService.pushElement(new MoveCellEdit(this.uri, index, newIdx, { + moveCell: (fromIndex: number, toIndex: number) => { + this.moveCellToIdx(fromIndex, toIndex, true, false); + } + })); + } + + this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[index, 1, []]] }); + this._onDidChangeViewCells.fire({ synchronous: synchronous, splices: [[newIdx, 0, [viewCell]]] }); + + return true; + } + + saveEditorViewState(): INotebookEditorViewState { + const state: { [key: number]: boolean } = {}; + this._viewCells.filter(cell => cell.state === CellState.Editing).forEach(cell => state[cell.cell.handle] = true); + const editorViewStates: { [key: number]: editorCommon.ICodeEditorViewState } = {}; + this._viewCells.map(cell => ({ handle: cell.cell.handle, state: cell.saveEditorViewState() })).forEach(viewState => { + if (viewState.state) { + editorViewStates[viewState.handle] = viewState.state; + } + }); + + return { + editingCells: state, + editorViewStates: editorViewStates + }; + } + + restoreEditorViewState(viewState: INotebookEditorViewState | undefined): void { + if (!viewState) { + return; + } + + this._viewCells.forEach(cell => { + const isEditing = viewState.editingCells && viewState.editingCells[cell.handle]; + const editorViewState = viewState.editorViewStates && viewState.editorViewStates[cell.handle]; + + cell.state = isEditing ? CellState.Editing : CellState.Preview; + cell.restoreEditorViewState(editorViewState); + }); + } + + /** + * Editor decorations across cells. For example, find decorations for multiple code cells + * The reason that we can't completely delegate this to CodeEditorWidget is most of the time, the editors for cells are not created yet but we already have decorations for them. + */ + changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T | null { + const changeAccessor: IModelDecorationsChangeAccessor = { + deltaDecorations: (oldDecorations: ICellModelDecorations[], newDecorations: ICellModelDeltaDecorations[]): ICellModelDecorations[] => { + return this.deltaDecorationsImpl(oldDecorations, newDecorations); + } + }; + + let result: T | null = null; + try { + result = callback(changeAccessor); + } catch (e) { + onUnexpectedError(e); + } + + changeAccessor.deltaDecorations = invalidFunc; + + return result; + } + + deltaDecorationsImpl(oldDecorations: ICellModelDecorations[], newDecorations: ICellModelDeltaDecorations[]): ICellModelDecorations[] { + + const mapping = new Map(); + oldDecorations.forEach(oldDecoration => { + const ownerId = oldDecoration.ownerId; + + if (!mapping.has(ownerId)) { + const cell = this._viewCells.find(cell => cell.handle === ownerId); + if (cell) { + mapping.set(ownerId, { cell: cell, oldDecorations: [], newDecorations: [] }); + } + } + + const data = mapping.get(ownerId)!; + if (data) { + data.oldDecorations = oldDecoration.decorations; + } + }); + + newDecorations.forEach(newDecoration => { + const ownerId = newDecoration.ownerId; + + if (!mapping.has(ownerId)) { + const cell = this._viewCells.find(cell => cell.handle === ownerId); + + if (cell) { + mapping.set(ownerId, { cell: cell, oldDecorations: [], newDecorations: [] }); + } + } + + const data = mapping.get(ownerId)!; + if (data) { + data.newDecorations = newDecoration.decorations; + } + }); + + const ret: ICellModelDecorations[] = []; + mapping.forEach((value, ownerId) => { + const cellRet = value.cell.deltaDecorations(value.oldDecorations, value.newDecorations); + ret.push({ + ownerId: ownerId, + decorations: cellRet + }); + }); + + return ret; + } + + + /** + * Search in notebook text model + * @param value + */ + find(value: string): CellFindMatch[] { + const matches: CellFindMatch[] = []; + this._viewCells.forEach(cell => { + const cellMatches = cell.startFind(value); + if (cellMatches) { + matches.push(cellMatches); + } + }); + + return matches; + } + + replaceOne(cell: ICellViewModel, range: Range, text: string): Promise { + const viewCell = cell as CellViewModel; + this._lastNotebookEditResource.push(viewCell.uri); + return viewCell.resolveTextModel().then(() => { + this.bulkEditService.apply({ edits: [{ edit: { range: range, text: text }, resource: cell.uri }] }, { quotableLabel: 'Notebook Replace' }); + }); + } + + async replaceAll(matches: CellFindMatch[], text: string): Promise { + if (!matches.length) { + return; + } + + let textEdits: WorkspaceTextEdit[] = []; + this._lastNotebookEditResource.push(matches[0].cell.uri); + + matches.forEach(match => { + match.matches.forEach(singleMatch => { + textEdits.push({ + edit: { range: singleMatch.range, text: text }, + resource: match.cell.uri + }); + }); + }); + + return Promise.all(matches.map(match => { + return match.cell.resolveTextModel(); + })).then(async () => { + this.bulkEditService.apply({ edits: textEdits }, { quotableLabel: 'Notebook Replace All' }); + return; + }); + } + + canUndo(): boolean { + return this.undoService.canUndo(this.uri); + } + + undo() { + this.undoService.undo(this.uri); + } + + redo() { + this.undoService.redo(this.uri); + } + + equal(model: NotebookEditorModel) { + return this._model === model; + } + + dispose() { + this._localStore.clear(); + this._viewCells.forEach(cell => { + cell.save(); + cell.dispose(); + }); + + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts new file mode 100644 index 0000000000000..e11f76aadb711 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -0,0 +1,69 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { ICell, IOutput, NotebookCellOutputsSplice, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { PieceTreeTextBufferFactory, PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; +import { URI } from 'vs/base/common/uri'; + +export class NotebookCellTextModel implements ICell { + private _onDidChangeOutputs = new Emitter(); + onDidChangeOutputs: Event = this._onDidChangeOutputs.event; + + private _onDidChangeContent = new Emitter(); + onDidChangeContent: Event = this._onDidChangeContent.event; + + private _outputs: IOutput[]; + + get outputs(): IOutput[] { + return this._outputs; + } + + get source() { + return this._source; + } + + set source(newValue: string[]) { + this._source = newValue; + this._buffer = null; + } + + private _buffer: PieceTreeTextBufferFactory | null = null; + + constructor( + readonly uri: URI, + public handle: number, + private _source: string[], + public language: string, + public cellKind: CellKind, + outputs: IOutput[] + ) { + this._outputs = outputs; + } + + contentChange() { + this._onDidChangeContent.fire(); + + } + + spliceNotebookCellOutputs(splices: NotebookCellOutputsSplice[]): void { + splices.reverse().forEach(splice => { + this.outputs.splice(splice[0], splice[1], ...splice[2]); + }); + + this._onDidChangeOutputs.fire(splices); + } + + resolveTextBufferFactory(): PieceTreeTextBufferFactory { + if (this._buffer) { + return this._buffer; + } + + let builder = new PieceTreeTextBufferBuilder(); + builder.acceptChunk(this.source.join('\n')); + this._buffer = builder.finish(true); + return this._buffer; + } +} diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts new file mode 100644 index 0000000000000..c976c610eebd2 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +export class NotebookTextModel extends Disposable implements INotebookTextModel { + private readonly _onWillDispose: Emitter = this._register(new Emitter()); + readonly onWillDispose: Event = this._onWillDispose.event; + private readonly _onDidChangeCells = new Emitter(); + get onDidChangeCells(): Event { return this._onDidChangeCells.event; } + private _onDidChangeContent = new Emitter(); + onDidChangeContent: Event = this._onDidChangeContent.event; + private _mapping: Map = new Map(); + private _cellListeners: Map = new Map(); + cells: NotebookCellTextModel[]; + activeCell: NotebookCellTextModel | undefined; + languages: string[] = []; + renderers = new Set(); + + constructor( + public handle: number, + public viewType: string, + public uri: URI + ) { + super(); + this.cells = []; + } + + updateLanguages(languages: string[]) { + this.languages = languages; + } + + updateRenderers(renderers: number[]) { + renderers.forEach(render => { + this.renderers.add(render); + }); + } + + updateActiveCell(handle: number) { + this.activeCell = this._mapping.get(handle); + } + + insertNewCell(index: number, cell: NotebookCellTextModel): void { + this._mapping.set(cell.handle, cell); + this.cells.splice(index, 0, cell); + let dirtyStateListener = cell.onDidChangeContent(() => { + this._onDidChangeContent.fire(); + }); + + this._cellListeners.set(cell.handle, dirtyStateListener); + this._onDidChangeContent.fire(); + return; + } + + removeCell(index: number) { + let cell = this.cells[index]; + this._cellListeners.get(cell.handle)?.dispose(); + this._cellListeners.delete(cell.handle); + this.cells.splice(index, 1); + this._onDidChangeContent.fire(); + } + + + // TODO@rebornix should this trigger content change event? + $spliceNotebookCells(splices: NotebookCellsSplice[]): void { + splices.reverse().forEach(splice => { + let cellDtos = splice[2]; + let newCells = cellDtos.map(cell => { + let mainCell = new NotebookCellTextModel(URI.revive(cell.uri), cell.handle, cell.source, cell.language, cell.cellKind, cell.outputs || []); + this._mapping.set(cell.handle, mainCell); + let dirtyStateListener = mainCell.onDidChangeContent(() => { + this._onDidChangeContent.fire(); + }); + this._cellListeners.set(cell.handle, dirtyStateListener); + return mainCell; + }); + + this.cells.splice(splice[0], splice[1], ...newCells); + }); + + this._onDidChangeCells.fire(splices); + } + + // TODO@rebornix should this trigger content change event? + $spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]): void { + let cell = this._mapping.get(cellHandle); + cell?.spliceNotebookCellOutputs(splices); + } + + dispose() { + this._onWillDispose.fire(); + this._cellListeners.forEach(val => val.dispose()); + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts new file mode 100644 index 0000000000000..73afe15d585ac --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -0,0 +1,328 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import * as glob from 'vs/base/common/glob'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { isWindows } from 'vs/base/common/platform'; +import { ISplice } from 'vs/base/common/sequence'; +import { URI } from 'vs/base/common/uri'; +import * as editorCommon from 'vs/editor/common/editorCommon'; +import { PieceTreeTextBufferFactory } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; + +export enum CellKind { + Markdown = 1, + Code = 2 +} + +export enum CellOutputKind { + Text = 1, + Error = 2, + Rich = 3 +} + +export const NOTEBOOK_DISPLAY_ORDER = [ + 'application/json', + 'application/javascript', + 'text/html', + 'image/svg+xml', + 'text/markdown', + 'image/png', + 'image/jpeg', + 'text/plain' +]; + +export interface INotebookDisplayOrder { + defaultOrder: string[]; + userOrder?: string[]; +} + +export interface INotebookMimeTypeSelector { + type: string; + subTypes?: string[]; +} + +export interface INotebookRendererInfo { + id: ExtensionIdentifier; + extensionLocation: URI, + preloads: URI[] +} + +export interface INotebookSelectors { + readonly filenamePattern?: string; +} + +export interface IStreamOutput { + outputKind: CellOutputKind.Text; + text: string; +} + +export interface IErrorOutput { + outputKind: CellOutputKind.Error; + /** + * Exception Name + */ + ename?: string; + /** + * Exception Value + */ + evalue?: string; + /** + * Exception call stacks + */ + traceback?: string[]; +} + +export interface IDisplayOutput { + outputKind: CellOutputKind.Rich; + /** + * { mime_type: value } + */ + data: { [key: string]: any; } +} + +export enum MimeTypeRendererResolver { + Core, + Active, + Lazy +} + +export interface IOrderedMimeType { + mimeType: string; + isResolved: boolean; + rendererId?: number; + output?: string; +} + +export interface ITransformedDisplayOutputDto { + outputKind: CellOutputKind.Rich; + data: { [key: string]: any; } + + orderedMimeTypes: IOrderedMimeType[]; + pickedMimeTypeIndex: number; +} + +export interface IGenericOutput { + outputKind: CellOutputKind; + pickedMimeType?: string; + pickedRenderer?: number; + transformedOutput?: { [key: string]: IDisplayOutput }; +} + +export type IOutput = ITransformedDisplayOutputDto | IStreamOutput | IErrorOutput; + +export interface ICell { + readonly uri: URI; + handle: number; + source: string[]; + language: string; + cellKind: CellKind; + outputs: IOutput[]; + onDidChangeOutputs?: Event; + resolveTextBufferFactory(): PieceTreeTextBufferFactory; + // TODO@rebornix it should be later on replaced by moving textmodel resolution into CellTextModel + contentChange(): void; +} + +export interface LanguageInfo { + file_extension: string; +} + +export interface IMetadata { + language_info: LanguageInfo; +} + +export interface INotebookTextModel { + handle: number; + viewType: string; + // metadata: IMetadata; + readonly uri: URI; + languages: string[]; + cells: ICell[]; + renderers: Set; + onDidChangeCells?: Event; + onDidChangeContent: Event; + onWillDispose(listener: () => void): IDisposable; +} + +export interface IRenderOutput { + shadowContent?: string; + hasDynamicHeight: boolean; +} + +export type NotebookCellsSplice = [ + number /* start */, + number /* delete count */, + ICell[] +]; + +export type NotebookCellOutputsSplice = [ + number /* start */, + number /* delete count */, + IOutput[] +]; + +export namespace CellUri { + + export const scheme = 'vscode-notebook'; + + export function generate(notebook: URI, handle: number): URI { + return notebook.with({ + query: JSON.stringify({ cell: handle, notebook: notebook.toString() }), + scheme, + }); + } + + export function parse(cell: URI): { notebook: URI, handle: number } | undefined { + if (cell.scheme !== scheme) { + return undefined; + } + try { + const data = <{ cell: number, notebook: string }>JSON.parse(cell.query); + return { + handle: data.cell, + notebook: URI.parse(data.notebook) + }; + } catch { + return undefined; + } + } +} + +export function mimeTypeSupportedByCore(mimeType: string) { + if ([ + 'application/json', + 'application/javascript', + 'text/html', + 'image/svg+xml', + 'text/markdown', + 'image/png', + 'image/jpeg', + 'text/plain', + 'text/x-javascript' + ].indexOf(mimeType) > -1) { + return true; + } + + return false; +} + +// if (isWindows) { +// value = value.replace(/\//g, '\\'); +// } + +function matchGlobUniversal(pattern: string, path: string) { + if (isWindows) { + pattern = pattern.replace(/\//g, '\\'); + path = path.replace(/\//g, '\\'); + } + + return glob.match(pattern, path); +} + + +function getMimeTypeOrder(mimeType: string, userDisplayOrder: string[], documentDisplayOrder: string[], defaultOrder: string[]) { + let order = 0; + for (let i = 0; i < userDisplayOrder.length; i++) { + if (matchGlobUniversal(userDisplayOrder[i], mimeType)) { + return order; + } + order++; + } + + for (let i = 0; i < documentDisplayOrder.length; i++) { + if (matchGlobUniversal(documentDisplayOrder[i], mimeType)) { + return order; + } + + order++; + } + + for (let i = 0; i < defaultOrder.length; i++) { + if (matchGlobUniversal(defaultOrder[i], mimeType)) { + return order; + } + + order++; + } + + return order; +} + +export function sortMimeTypes(mimeTypes: string[], userDisplayOrder: string[], documentDisplayOrder: string[], defaultOrder: string[]) { + const sorted = mimeTypes.sort((a, b) => { + return getMimeTypeOrder(a, userDisplayOrder, documentDisplayOrder, defaultOrder) - getMimeTypeOrder(b, userDisplayOrder, documentDisplayOrder, defaultOrder); + }); + + return sorted; +} + +interface IMutableSplice extends ISplice { + deleteCount: number; +} + +export function diff(before: T[], after: T[], contains: (a: T) => boolean): ISplice[] { + const result: IMutableSplice[] = []; + + function pushSplice(start: number, deleteCount: number, toInsert: T[]): void { + if (deleteCount === 0 && toInsert.length === 0) { + return; + } + + const latest = result[result.length - 1]; + + if (latest && latest.start + latest.deleteCount === start) { + latest.deleteCount += deleteCount; + latest.toInsert.push(...toInsert); + } else { + result.push({ start, deleteCount, toInsert }); + } + } + + let beforeIdx = 0; + let afterIdx = 0; + + while (true) { + if (beforeIdx === before.length) { + pushSplice(beforeIdx, 0, after.slice(afterIdx)); + break; + } + + if (afterIdx === after.length) { + pushSplice(beforeIdx, before.length - beforeIdx, []); + break; + } + + const beforeElement = before[beforeIdx]; + const afterElement = after[afterIdx]; + + if (beforeElement === afterElement) { + // equal + beforeIdx += 1; + afterIdx += 1; + continue; + } + + if (contains(afterElement)) { + // `afterElement` exists before, which means some elements before `afterElement` are deleted + pushSplice(beforeIdx, 1, []); + beforeIdx += 1; + } else { + // `afterElement` added + pushSplice(beforeIdx, 0, [afterElement]); + afterIdx += 1; + } + } + + return result; +} + +export interface ICellEditorViewState { + selections: editorCommon.ICursorState[]; +} + +export const NOTEBOOK_EDITOR_CURSOR_BOUNDARY = new RawContextKey<'none' | 'top' | 'bottom' | 'both'>('notebookEditorCursorAtBoundary', 'none'); diff --git a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts new file mode 100644 index 0000000000000..796de2f51da9d --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as glob from 'vs/base/common/glob'; + +export class NotebookOutputRendererInfo { + + readonly id: string; + readonly displayName: string; + readonly mimeTypes: readonly string[]; + readonly mimeTypeGlobs: glob.ParsedPattern[]; + + constructor(descriptor: { + readonly id: string; + readonly displayName: string; + readonly mimeTypes: readonly string[]; + }) { + this.id = descriptor.id; + this.displayName = descriptor.displayName; + this.mimeTypes = descriptor.mimeTypes; + this.mimeTypeGlobs = this.mimeTypes.map(pattern => glob.parse(pattern)); + } + + matches(mimeType: string) { + let matched = this.mimeTypeGlobs.find(pattern => pattern(mimeType)); + return matched; + } +} diff --git a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts new file mode 100644 index 0000000000000..f2d5a49b540a4 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as glob from 'vs/base/common/glob'; +import { URI } from 'vs/base/common/uri'; +import { basename } from 'vs/base/common/resources'; + +export interface NotebookSelector { + readonly filenamePattern?: string; + readonly excludeFileNamePattern?: string; +} + +export class NotebookProviderInfo { + + readonly id: string; + readonly displayName: string; + readonly selector: readonly NotebookSelector[]; + + constructor(descriptor: { + readonly id: string; + readonly displayName: string; + readonly selector: readonly NotebookSelector[]; + }) { + this.id = descriptor.id; + this.displayName = descriptor.displayName; + this.selector = descriptor.selector; + } + + matches(resource: URI): boolean { + return this.selector.some(selector => NotebookProviderInfo.selectorMatches(selector, resource)); + } + + static selectorMatches(selector: NotebookSelector, resource: URI): boolean { + if (selector.filenamePattern) { + if (glob.match(selector.filenamePattern.toLowerCase(), basename(resource).toLowerCase())) { + if (selector.excludeFileNamePattern) { + if (glob.match(selector.excludeFileNamePattern.toLowerCase(), basename(resource).toLowerCase())) { + // should exclude + + return false; + } + } + return true; + } + } + return false; + } +} diff --git a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts new file mode 100644 index 0000000000000..11f71061e3710 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts @@ -0,0 +1,341 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { NOTEBOOK_DISPLAY_ORDER, sortMimeTypes, CellKind, diff, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { TestCell } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; +import { URI } from 'vs/base/common/uri'; + +suite('NotebookCommon', () => { + test('sortMimeTypes default orders', function () { + const defaultDisplayOrder = NOTEBOOK_DISPLAY_ORDER; + + assert.deepEqual(sortMimeTypes( + [ + 'application/json', + 'application/javascript', + 'text/html', + 'image/svg+xml', + 'text/markdown', + 'image/png', + 'image/jpeg', + 'text/plain' + ], [], [], defaultDisplayOrder), + [ + 'application/json', + 'application/javascript', + 'text/html', + 'image/svg+xml', + 'text/markdown', + 'image/png', + 'image/jpeg', + 'text/plain' + ] + ); + + assert.deepEqual(sortMimeTypes( + [ + 'application/json', + 'text/markdown', + 'application/javascript', + 'text/html', + 'text/plain', + 'image/png', + 'image/jpeg', + 'image/svg+xml' + ], [], [], defaultDisplayOrder), + [ + 'application/json', + 'application/javascript', + 'text/html', + 'image/svg+xml', + 'text/markdown', + 'image/png', + 'image/jpeg', + 'text/plain' + ] + ); + + assert.deepEqual(sortMimeTypes( + [ + 'text/markdown', + 'application/json', + 'text/plain', + 'image/jpeg', + 'application/javascript', + 'text/html', + 'image/png', + 'image/svg+xml' + ], [], [], defaultDisplayOrder), + [ + 'application/json', + 'application/javascript', + 'text/html', + 'image/svg+xml', + 'text/markdown', + 'image/png', + 'image/jpeg', + 'text/plain' + ] + ); + }); + + test('sortMimeTypes document orders', function () { + const defaultDisplayOrder = NOTEBOOK_DISPLAY_ORDER; + assert.deepEqual(sortMimeTypes( + [ + 'application/json', + 'application/javascript', + 'text/html', + 'image/svg+xml', + 'text/markdown', + 'image/png', + 'image/jpeg', + 'text/plain' + ], [], + [ + 'text/markdown', + 'text/html', + 'application/json' + ], defaultDisplayOrder), + [ + 'text/markdown', + 'text/html', + 'application/json', + 'application/javascript', + 'image/svg+xml', + 'image/png', + 'image/jpeg', + 'text/plain' + ] + ); + + assert.deepEqual(sortMimeTypes( + [ + 'text/markdown', + 'application/json', + 'text/plain', + 'application/javascript', + 'text/html', + 'image/svg+xml', + 'image/jpeg', + 'image/png' + ], [], + [ + 'text/html', + 'text/markdown', + 'application/json' + ], defaultDisplayOrder), + [ + 'text/html', + 'text/markdown', + 'application/json', + 'application/javascript', + 'image/svg+xml', + 'image/png', + 'image/jpeg', + 'text/plain' + ] + ); + }); + + test('sortMimeTypes user orders', function () { + const defaultDisplayOrder = NOTEBOOK_DISPLAY_ORDER; + assert.deepEqual(sortMimeTypes( + [ + 'application/json', + 'application/javascript', + 'text/html', + 'image/svg+xml', + 'text/markdown', + 'image/png', + 'image/jpeg', + 'text/plain' + ], + [ + 'image/png', + 'text/plain', + ], + [ + 'text/markdown', + 'text/html', + 'application/json' + ], defaultDisplayOrder), + [ + 'image/png', + 'text/plain', + 'text/markdown', + 'text/html', + 'application/json', + 'application/javascript', + 'image/svg+xml', + 'image/jpeg', + ] + ); + + assert.deepEqual(sortMimeTypes( + [ + 'text/markdown', + 'application/json', + 'text/plain', + 'application/javascript', + 'text/html', + 'image/svg+xml', + 'image/jpeg', + 'image/png' + ], + [ + 'application/json', + 'text/html', + ], + [ + 'text/html', + 'text/markdown', + 'application/json' + ], defaultDisplayOrder), + [ + 'application/json', + 'text/html', + 'text/markdown', + 'application/javascript', + 'image/svg+xml', + 'image/png', + 'image/jpeg', + 'text/plain' + ] + ); + }); + + test('sortMimeTypes glob', function () { + const defaultDisplayOrder = NOTEBOOK_DISPLAY_ORDER; + + // unknown mime types come last + assert.deepEqual(sortMimeTypes( + [ + 'application/json', + 'application/vnd-vega.json', + 'application/vnd-plot.json', + 'application/javascript', + 'text/html' + ], [], + [ + 'text/markdown', + 'text/html', + 'application/json' + ], defaultDisplayOrder), + [ + 'text/html', + 'application/json', + 'application/javascript', + 'application/vnd-vega.json', + 'application/vnd-plot.json' + ], + 'unknown mimetypes keep the ordering' + ); + + assert.deepEqual(sortMimeTypes( + [ + 'application/json', + 'application/javascript', + 'text/html', + 'application/vnd-plot.json', + 'application/vnd-vega.json' + ], [], + [ + 'application/vnd-vega*', + 'text/markdown', + 'text/html', + 'application/json' + ], defaultDisplayOrder), + [ + 'application/vnd-vega.json', + 'text/html', + 'application/json', + 'application/javascript', + 'application/vnd-plot.json' + ], + 'glob *' + ); + }); + + test('diff cells', function () { + const cells: TestCell[] = []; + + for (let i = 0; i < 5; i++) { + cells.push( + new TestCell('notebook', i, [`var a = ${i};`], 'javascript', CellKind.Code, []) + ); + } + + assert.deepEqual(diff(cells, [], (cell) => { + return cells.indexOf(cell) > -1; + }), [ + { + start: 0, + deleteCount: 5, + toInsert: [] + } + ] + ); + + assert.deepEqual(diff([], cells, (cell) => { + return false; + }), [ + { + start: 0, + deleteCount: 0, + toInsert: cells + } + ] + ); + + const cellA = new TestCell('notebook', 6, ['var a = 6;'], 'javascript', CellKind.Code, []); + const cellB = new TestCell('notebook', 7, ['var a = 7;'], 'javascript', CellKind.Code, []); + + const modifiedCells = [ + cells[0], + cells[1], + cellA, + cells[3], + cellB, + cells[4] + ]; + + const splices = diff(cells, modifiedCells, (cell) => { + return cells.indexOf(cell) > -1; + }); + + assert.deepEqual(splices, + [ + { + start: 2, + deleteCount: 1, + toInsert: [cellA] + }, + { + start: 4, + deleteCount: 0, + toInsert: [cellB] + } + ] + ); + }); +}); + + +suite('CellUri', function () { + + test('parse, generate', function () { + + const nb = URI.parse('foo:///bar/følder/file.nb'); + const id = 17; + + const data = CellUri.generate(nb, id); + const actual = CellUri.parse(data); + assert.ok(Boolean(actual)); + assert.equal(actual?.handle, id); + assert.equal(actual?.notebook.toString(), nb.toString()); + }); +}); diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts new file mode 100644 index 0000000000000..9cc8e077e6558 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { withTestNotebook, TestCell } 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'; + +suite('NotebookViewModel', () => { + const instantiationService = new TestInstantiationService(); + const blukEditService = instantiationService.get(IBulkEditService); + const undoRedoService = instantiationService.stub(IUndoRedoService, () => { }); + instantiationService.spy(IUndoRedoService, 'pushElement'); + + test('ctor', function () { + const notebook = new NotebookTextModel(0, 'notebook', URI.parse('test')); + const model = new NotebookEditorModel(notebook); + const viewModel = new NotebookViewModel('notebook', model, instantiationService, blukEditService, undoRedoService); + assert.equal(viewModel.viewType, 'notebook'); + }); + + test('insert/delete', function () { + withTestNotebook( + instantiationService, + blukEditService, + undoRedoService, + [ + [['var a = 1;'], 'javascript', CellKind.Code, []], + [['var b = 2;'], 'javascript', CellKind.Code, []] + ], + (editor, viewModel) => { + const cell = viewModel.insertCell(1, new TestCell(viewModel.viewType, 0, ['var c = 3;'], 'javascript', CellKind.Code, []), true); + assert.equal(viewModel.viewCells.length, 3); + assert.equal(viewModel.notebookDocument.cells.length, 3); + assert.equal(viewModel.getViewCellIndex(cell), 1); + + viewModel.deleteCell(1, true); + assert.equal(viewModel.viewCells.length, 2); + assert.equal(viewModel.notebookDocument.cells.length, 2); + assert.equal(viewModel.getViewCellIndex(cell), -1); + } + ); + }); + + test('index', function () { + withTestNotebook( + instantiationService, + blukEditService, + undoRedoService, + [ + [['var a = 1;'], 'javascript', CellKind.Code, []], + [['var b = 2;'], 'javascript', CellKind.Code, []] + ], + (editor, viewModel) => { + const firstViewCell = viewModel.viewCells[0]; + const lastViewCell = viewModel.viewCells[viewModel.viewCells.length - 1]; + + const insertIndex = viewModel.getViewCellIndex(firstViewCell) + 1; + const cell = viewModel.insertCell(insertIndex, new TestCell(viewModel.viewType, 3, ['var c = 3;'], 'javascript', CellKind.Code, []), true); + + const addedCellIndex = viewModel.getViewCellIndex(cell); + viewModel.deleteCell(addedCellIndex, true); + + const secondInsertIndex = viewModel.getViewCellIndex(lastViewCell) + 1; + const cell2 = viewModel.insertCell(secondInsertIndex, new TestCell(viewModel.viewType, 4, ['var d = 4;'], 'javascript', CellKind.Code, []), true); + + assert.equal(viewModel.viewCells.length, 3); + assert.equal(viewModel.notebookDocument.cells.length, 3); + assert.equal(viewModel.getViewCellIndex(cell2), 2); + } + ); + }); +}); diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts new file mode 100644 index 0000000000000..889e61c1f8de5 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -0,0 +1,195 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { PieceTreeTextBufferFactory } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; +import { CellKind, ICell, IOutput, NotebookCellOutputsSplice, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookViewModel, IModelDecorationsChangeAccessor } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookCellViewModel'; +import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { INotebookEditor, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; +import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { Range } from 'vs/editor/common/core/range'; +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 { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; + +export class TestCell implements ICell { + uri: URI; + private _onDidChangeOutputs = new Emitter(); + onDidChangeOutputs: Event = this._onDidChangeOutputs.event; + private _isDirty: boolean = false; + private _outputs: IOutput[]; + get outputs(): IOutput[] { + return this._outputs; + } + + get isDirty() { + return this._isDirty; + } + + set isDirty(newState: boolean) { + this._isDirty = newState; + + } + + constructor( + public viewType: string, + public handle: number, + public source: string[], + public language: string, + public cellKind: CellKind, + outputs: IOutput[] + ) { + this._outputs = outputs; + this.uri = CellUri.generate(URI.parse('test:///fake/notebook'), handle); + } + contentChange(): void { + // throw new Error('Method not implemented.'); + } + + resolveTextBufferFactory(): PieceTreeTextBufferFactory { + throw new Error('Method not implemented.'); + } +} + +export class TestNotebookEditor implements INotebookEditor { + + get viewModel() { + return undefined; + } + + constructor( + ) { } + + setCellSelection(cell: CellViewModel, selection: Range): void { + throw new Error('Method not implemented.'); + } + + selectElement(cell: CellViewModel): void { + throw new Error('Method not implemented.'); + } + + moveCellDown(cell: CellViewModel): void { + throw new Error('Method not implemented.'); + } + + moveCellUp(cell: CellViewModel): void { + throw new Error('Method not implemented.'); + } + + setSelection(cell: CellViewModel, selection: Range): void { + throw new Error('Method not implemented.'); + } + revealRangeInView(cell: CellViewModel, range: Range): void { + throw new Error('Method not implemented.'); + } + revealRangeInCenter(cell: CellViewModel, range: Range): void { + throw new Error('Method not implemented.'); + } + revealRangeInCenterIfOutsideViewport(cell: CellViewModel, range: Range): void { + throw new Error('Method not implemented.'); + } + + revealLineInView(cell: CellViewModel, line: number): void { + throw new Error('Method not implemented.'); + } + getLayoutInfo(): NotebookLayoutInfo { + throw new Error('Method not implemented.'); + } + revealLineInCenterIfOutsideViewport(cell: CellViewModel, line: number): void { + throw new Error('Method not implemented.'); + } + revealLineInCenter(cell: CellViewModel, line: number): void { + throw new Error('Method not implemented.'); + } + focus(): void { + throw new Error('Method not implemented.'); + } + showFind(): void { + throw new Error('Method not implemented.'); + } + hideFind(): void { + throw new Error('Method not implemented.'); + } + revealInView(cell: CellViewModel): void { + throw new Error('Method not implemented.'); + } + revealInCenter(cell: CellViewModel): void { + throw new Error('Method not implemented.'); + } + revealInCenterIfOutsideViewport(cell: CellViewModel): void { + throw new Error('Method not implemented.'); + } + async insertNotebookCell(cell: CellViewModel, type: CellKind, direction: 'above' | 'below'): Promise { + // throw new Error('Method not implemented.'); + } + deleteNotebookCell(cell: CellViewModel): void { + // throw new Error('Method not implemented.'); + } + editNotebookCell(cell: CellViewModel): void { + // throw new Error('Method not implemented.'); + } + saveNotebookCell(cell: CellViewModel): void { + // throw new Error('Method not implemented.'); + } + focusNotebookCell(cell: CellViewModel, focusEditor: boolean): void { + // throw new Error('Method not implemented.'); + } + getActiveCell(): CellViewModel | undefined { + // throw new Error('Method not implemented.'); + return; + } + layoutNotebookCell(cell: CellViewModel, height: number): void { + // throw new Error('Method not implemented.'); + } + createInset(cell: CellViewModel, output: IOutput, shadowContent: string, offset: number): void { + // throw new Error('Method not implemented.'); + } + removeInset(output: IOutput): void { + // throw new Error('Method not implemented.'); + } + triggerScroll(event: IMouseWheelEvent): void { + // throw new Error('Method not implemented.'); + } + getFontInfo(): BareFontInfo | undefined { + return BareFontInfo.createFromRawSettings({ + fontFamily: 'Monaco', + }, 1, true); + } + getOutputRenderer(): OutputRenderer { + throw new Error('Method not implemented.'); + } + + changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any { + throw new Error('Method not implemented.'); + } +} + +export function createTestCellViewModel(instantiationService: IInstantiationService, viewType: string, notebookHandle: number, cellhandle: number, source: string[], language: string, cellKind: CellKind, outputs: IOutput[]) { + const mockCell = new TestCell(viewType, cellhandle, source, language, cellKind, outputs); + return instantiationService.createInstance(CellViewModel, viewType, notebookHandle, mockCell); +} + +export function withTestNotebook(instantiationService: IInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string[], string, CellKind, IOutput[]][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel) => void) { + const viewType = 'notebook'; + const editor = new TestNotebookEditor(); + const notebook = new NotebookTextModel(0, viewType, URI.parse('test')); + notebook.cells = cells.map((cell, index) => { + return new NotebookCellTextModel(notebook.uri, index, cell[0], cell[1], cell[2], cell[3]); + }); + const model = new NotebookEditorModel(notebook); + const viewModel = new NotebookViewModel(viewType, model, instantiationService, blukEditService, undoRedoService); + + callback(editor, viewModel); + + viewModel.dispose(); + return; +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 154e738b70c8c..51d2257b8138e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -295,7 +295,7 @@ configurationRegistry.registerConfiguration({ default: true }, 'terminal.integrated.allowMnemonics': { - markdownDescription: nls.localize('terminal.integrated.allowMnemonics', "Whether to allow menubar mnemonics (eg. alt+f) to trigger the open the menubar. Note that this will cause all alt keystrokes will skip the shell when true."), + markdownDescription: nls.localize('terminal.integrated.allowMnemonics', "Whether to allow menubar mnemonics (eg. alt+f) to trigger the open the menubar. Note that this will cause all alt keystrokes will skip the shell when true. This does nothing on macOS."), type: 'boolean', default: false }, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 6b4d0453bc1ce..ca99dedcaae2b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -621,7 +621,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } // Skip processing by xterm.js of keyboard events that match menu bar mnemonics - if (this._configHelper.config.allowMnemonics && event.altKey) { + if (this._configHelper.config.allowMnemonics && !platform.isMacintosh && event.altKey) { return false; } diff --git a/src/vs/workbench/services/editor/browser/codeEditorService.ts b/src/vs/workbench/services/editor/browser/codeEditorService.ts index 4c933e05f9a96..2b86f427df443 100644 --- a/src/vs/workbench/services/editor/browser/codeEditorService.ts +++ b/src/vs/workbench/services/editor/browser/codeEditorService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, isCodeEditor, isDiffEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser'; import { CodeEditorServiceImpl } from 'vs/editor/browser/services/codeEditorServiceImpl'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; @@ -32,10 +32,15 @@ export class CodeEditorService extends CodeEditorServiceImpl { return activeTextEditorControl.getModifiedEditor(); } + const activeControl = this.editorService.activeEditorPane?.getControl(); + if (isCompositeEditor(activeControl) && isCodeEditor(activeControl.activeCodeEditor)) { + return activeControl.activeCodeEditor; + } + return null; } - openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise { + async openCodeEditor(input: IResourceEditorInput, source: ICodeEditor | null, sideBySide?: boolean): Promise { // Special case: If the active editor is a diff editor and the request to open originates and // targets the modified side of it, we just apply the request there to prevent opening the modified @@ -55,7 +60,7 @@ export class CodeEditorService extends CodeEditorServiceImpl { const textOptions = TextEditorOptions.create(input.options); textOptions.apply(targetEditor, ScrollType.Smooth); - return Promise.resolve(targetEditor); + return targetEditor; } // Open using our normal editor service diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 4b03ac1b38d2c..c4820d7695b6c 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -21,7 +21,7 @@ import { IResourceEditorInputType, SIDE_GROUP, IResourceEditorReplacement, IOpen import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable, IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { coalesce, distinct } from 'vs/base/common/arrays'; -import { isCodeEditor, isDiffEditor, ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { isCodeEditor, isDiffEditor, ICodeEditor, IDiffEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorGroupView, IEditorOpeningEvent, EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { ILabelService } from 'vs/platform/label/common/label'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -400,6 +400,9 @@ export class EditorService extends Disposable implements EditorServiceImpl { if (isCodeEditor(activeControl) || isDiffEditor(activeControl)) { return activeControl; } + if (isCompositeEditor(activeControl) && isCodeEditor(activeControl.activeCodeEditor)) { + return activeControl.activeCodeEditor; + } } return undefined; diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index ed8b9499f696e..72d2116f82ccd 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -251,7 +251,7 @@ export class ProgressService extends Disposable implements IProgressService { return toDisposable(() => promiseResolve()); }; - const createNotification = (message: string, increment?: number): INotificationHandle => { + const createNotification = (message: string, silent: boolean, increment?: number): INotificationHandle => { const notificationDisposables = new DisposableStore(); const primaryActions = options.primaryActions ? Array.from(options.primaryActions) : []; @@ -294,7 +294,8 @@ export class ProgressService extends Disposable implements IProgressService { message, source: options.source, actions: { primary: primaryActions, secondary: secondaryActions }, - progress: typeof increment === 'number' && increment >= 0 ? { total: 100, worked: increment } : { infinite: true } + progress: typeof increment === 'number' && increment >= 0 ? { total: 100, worked: increment } : { infinite: true }, + silent }); // Switch to window based progress once the notification @@ -302,8 +303,7 @@ export class ProgressService extends Disposable implements IProgressService { // Remove that window based progress once the notification // shows again. let windowProgressDisposable: IDisposable | undefined = undefined; - notificationDisposables.add(notification.onDidChangeVisibility(visible => { - + const onVisibilityChange = (visible: boolean) => { // Clear any previous running window progress dispose(windowProgressDisposable); @@ -311,7 +311,11 @@ export class ProgressService extends Disposable implements IProgressService { if (!visible && !progressStateModel.done) { windowProgressDisposable = createWindowProgress(); } - })); + }; + notificationDisposables.add(notification.onDidChangeVisibility(onVisibilityChange)); + if (silent) { + onVisibilityChange(false); + } // Clear upon dispose Event.once(notification.onDidClose)(() => notificationDisposables.dispose()); @@ -346,10 +350,10 @@ export class ProgressService extends Disposable implements IProgressService { // create notification now or after a delay if (typeof options.delay === 'number' && options.delay > 0) { if (typeof notificationTimeout !== 'number') { - notificationTimeout = setTimeout(() => notificationHandle = createNotification(titleAndMessage!, step?.increment), options.delay); + notificationTimeout = setTimeout(() => notificationHandle = createNotification(titleAndMessage!, !!options.silent, step?.increment), options.delay); } } else { - notificationHandle = createNotification(titleAndMessage, step?.increment); + notificationHandle = createNotification(titleAndMessage, !!options.silent, step?.increment); } } diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index ec284cc0e58a1..19bb3a7db451a 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -142,6 +142,9 @@ import 'vs/workbench/contrib/preferences/browser/preferences.contribution'; import 'vs/workbench/contrib/preferences/browser/keybindingsEditorContribution'; import 'vs/workbench/contrib/preferences/browser/preferencesSearch'; +// Notebook +import 'vs/workbench/contrib/notebook/browser/notebook.contribution'; + // Logs import 'vs/workbench/contrib/logs/common/logs.contribution';