From 27858a0faf78e502acf3c8f620f86a671a55e473 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 16 Jul 2021 15:44:04 -0700 Subject: [PATCH] Remove the built-in markdown rendering for notebooks (#128806) This switches our notebooks to always use contributed markdown rendering instead of our built-in markdown renderer We'd held off on switching over due to accessibility. I've tried to address this using the `aria-describedby` attributed to link the markdown container with a copy of rendered (and sanitized) html from the webview. --- src/vs/base/browser/dom.ts | 2 +- .../notebook/browser/notebook.contribution.ts | 7 +- .../notebook/browser/notebookBrowser.ts | 1 - .../notebook/browser/notebookEditorWidget.ts | 39 +---- .../view/renderers/backLayerWebView.ts | 9 +- .../browser/view/renderers/cellRenderer.ts | 41 +---- .../browser/view/renderers/markdownCell.ts | 150 ++++-------------- .../browser/view/renderers/webviewMessages.ts | 7 + .../browser/view/renderers/webviewPreloads.ts | 21 +++ .../browser/viewModel/markupCellViewModel.ts | 46 +----- .../contrib/notebook/common/notebookCommon.ts | 1 - 11 files changed, 83 insertions(+), 241 deletions(-) diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 041fbc8a0ad..171c9bfe17d 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1392,7 +1392,7 @@ const _ttpSafeInnerHtml = window.trustedTypes?.createPolicy('safeInnerHtml', { export function safeInnerHtml(node: HTMLElement, value: string): void { const options = _extInsaneOptions({ - allowedTags: ['a', 'button', 'blockquote', 'code', 'div', 'h1', 'h2', 'h3', 'input', 'label', 'li', 'p', 'pre', 'select', 'small', 'span', 'strong', 'textarea', 'ul', 'ol'], + allowedTags: ['a', 'button', 'blockquote', 'code', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'input', 'label', 'li', 'p', 'pre', 'select', 'small', 'span', 'strong', 'textarea', 'ul', 'ol'], allowedAttributes: { 'a': ['href', 'x-dispatch'], 'button': ['data-href', 'x-dispatch'], diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 701c6628239..d4a9e7f0e68 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -30,7 +30,7 @@ import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEd import { isCompositeNotebookEditorInput, NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl'; -import { CellKind, CellToolbarLocation, CellToolbarVisibility, CellUri, DisplayOrderKey, UndoRedoPerCell, ExperimentalUseMarkdownRenderer, IResolvedNotebookEditorModel, NotebookDocumentBackupData, NotebookTextDiffEditorPreview, NotebookWorkingCopyTypeIdentifier, ShowCellStatusBar, CompactView, FocusIndicator, InsertToolbarLocation, GlobalToolbar, ConsolidatedOutputButton, ShowFoldingControls, DragAndDropEnabled, NotebookCellEditorOptionsCustomizations, ConsolidatedRunButton } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellToolbarLocation, CellToolbarVisibility, CellUri, DisplayOrderKey, UndoRedoPerCell, IResolvedNotebookEditorModel, NotebookDocumentBackupData, NotebookTextDiffEditorPreview, NotebookWorkingCopyTypeIdentifier, ShowCellStatusBar, CompactView, FocusIndicator, InsertToolbarLocation, GlobalToolbar, ConsolidatedOutputButton, ShowFoldingControls, DragAndDropEnabled, NotebookCellEditorOptionsCustomizations, ConsolidatedRunButton } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; @@ -651,11 +651,6 @@ configurationRegistry.registerConfiguration({ type: 'boolean', default: true }, - [ExperimentalUseMarkdownRenderer]: { - description: nls.localize('notebook.experimental.useMarkdownRenderer.description', "Enable/disable using the new extensible markdown renderer."), - type: 'boolean', - default: true - }, [CellToolbarVisibility]: { markdownDescription: nls.localize('notebook.cellToolbarVisibility.description', "Whether the cell toolbar should appear on hover or click."), type: 'string', diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 9879682c4fe..d27b34e0205 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -749,7 +749,6 @@ export interface MarkdownCellRenderTemplate extends BaseCellRenderTemplate { foldingIndicator: HTMLElement; focusIndicatorBottom: HTMLElement; currentEditor?: ICodeEditor; - readonly useRenderer: boolean; } export interface CodeCellRenderTemplate extends BaseCellRenderTemplate { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 56f7546bc41..3b0779ede30 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -33,7 +33,6 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; -import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { contrastBorder, diffInserted, diffRemoved, editorBackground, errorForeground, focusBorder, foreground, listInactiveSelectionBackground, registerColor, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground, transparent } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; @@ -55,11 +54,10 @@ import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbenc import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel'; import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, ExperimentalUseMarkdownRenderer, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; import { editorGutterModifiedBackground } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { mark } from 'vs/workbench/contrib/notebook/common/notebookPerformance'; import { readFontInfo } from 'vs/editor/browser/config/configuration'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; @@ -248,8 +246,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private _isDisposed: boolean = false; - private useRenderer = false; - get isDisposed() { return this._isDisposed; } @@ -322,8 +318,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor constructor( readonly creationOptions: INotebookEditorCreationOptions, @IInstantiationService instantiationService: IInstantiationService, - @IStorageService storageService: IStorageService, - @IAccessibilityService accessibilityService: IAccessibilityService, @INotebookRendererMessagingService private readonly notebookRendererMessaging: INotebookRendererMessagingService, @INotebookEditorService private readonly notebookEditorService: INotebookEditorService, @INotebookKernelService private readonly notebookKernelService: INotebookKernelService, @@ -340,7 +334,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.isEmbedded = creationOptions.isEmbedded ?? false; this._readOnly = creationOptions.isReadOnly ?? false; - this.useRenderer = !!this.configurationService.getValue(ExperimentalUseMarkdownRenderer) && !accessibilityService.isScreenReaderOptimized(); this._notebookOptions = new NotebookOptions(this.configurationService); this._register(this._notebookOptions); this._viewContext = new ViewContext(this._notebookOptions, new NotebookEventDispatcher()); @@ -805,7 +798,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const getScopedContextKeyService = (container: HTMLElement) => this._list.contextKeyService.createScoped(container); const renderers = [ this.instantiationService.createInstance(CodeCellRenderer, this, this._renderedEditors, this._dndController, getScopedContextKeyService), - this.instantiationService.createInstance(MarkupCellRenderer, this, this._dndController, this._renderedEditors, getScopedContextKeyService, { useRenderer: this.useRenderer }), + this.instantiationService.createInstance(MarkupCellRenderer, this, this._dndController, this._renderedEditors, getScopedContextKeyService), ]; renderers.forEach(renderer => { @@ -1290,11 +1283,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor })); // init rendering - if (this.useRenderer) { - await this._warmupWithMarkdownRenderer(this.viewModel, viewState); - } else { - this._list.attachViewModel(this.viewModel); - } + await this._warmupWithMarkdownRenderer(this.viewModel, viewState); mark(textModel.uri, 'customMarkdownLoaded'); @@ -2286,11 +2275,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } async createMarkupPreview(cell: MarkupCellViewModel) { - if (!this.useRenderer) { - // TODO: handle case where custom renderer is disabled? - return; - } - if (!this._webview) { return; } @@ -2315,11 +2299,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } async unhideMarkupPreviews(cells: readonly MarkupCellViewModel[]) { - if (!this.useRenderer) { - // TODO: handle case where custom renderer is disabled? - return; - } - if (!this._webview) { return; } @@ -2332,11 +2311,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } async hideMarkupPreviews(cells: readonly MarkupCellViewModel[]) { - if (!this.useRenderer) { - // TODO: handle case where custom renderer is disabled? - return; - } - if (!this._webview || !cells.length) { return; } @@ -2349,11 +2323,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } async deleteMarkupPreviews(cells: readonly MarkupCellViewModel[]) { - if (!this.useRenderer) { - // TODO: handle case where custom renderer is disabled? - return; - } - if (!this._webview) { return; } @@ -2366,7 +2335,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } private async updateSelectedMarkdownPreviews(): Promise { - if (!this.useRenderer || !this._webview) { + if (!this._webview) { return; } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index db56aedcce4..e16fdf953fe 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -580,7 +580,14 @@ export class BackLayerWebView extends Disposable { this.notebookEditor.didEndDragMarkupCell(data.cellId); break; } - + case 'renderedMarkup': + { + const cell = this.notebookEditor.getCellById(data.cellId); + if (cell instanceof MarkupCellViewModel) { + cell.renderedHtml = data.html; + } + break; + } case 'telemetryFoundRenderedMarkdownMath': { this.telemetryService.publicLog2<{}, {}>('notebook/markdown/renderedLatex', {}); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index d58bbecb980..7e77c4d863d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -40,7 +40,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { syncing } from 'vs/platform/theme/common/iconRegistry'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { DeleteCellAction, INotebookActionContext, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import { BaseCellRenderTemplate, CellEditState, CodeCellLayoutInfo, CodeCellRenderTemplate, EXPAND_CELL_INPUT_COMMAND_ID, ICellViewModel, INotebookEditor, isCodeCellRenderTemplate, MarkdownCellRenderTemplate, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BaseCellRenderTemplate, CodeCellLayoutInfo, CodeCellRenderTemplate, EXPAND_CELL_INPUT_COMMAND_ID, ICellViewModel, INotebookEditor, isCodeCellRenderTemplate, MarkdownCellRenderTemplate, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { errorStateIcon, successStateIcon, unfoldIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellActionView'; import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; @@ -302,14 +302,11 @@ abstract class AbstractCellRenderer { export class MarkupCellRenderer extends AbstractCellRenderer implements IListRenderer { static readonly TEMPLATE_ID = 'markdown_cell'; - private readonly useRenderer: boolean; - constructor( notebookEditor: INotebookEditor, dndController: CellDragAndDropController, private renderedEditors: Map, contextKeyServiceProvider: (container: HTMLElement) => IContextKeyService, - options: { useRenderer: boolean; }, @IConfigurationService private configurationService: IConfigurationService, @IInstantiationService instantiationService: IInstantiationService, @IContextMenuService contextMenuService: IContextMenuService, @@ -318,7 +315,6 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen @INotificationService notificationService: INotificationService, ) { super(instantiationService, notebookEditor, contextMenuService, menuService, configurationService, keybindingService, notificationService, contextKeyServiceProvider, 'markdown', dndController); - this.useRenderer = options.useRenderer; } get templateId() { @@ -361,7 +357,6 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen const titleMenu = disposables.add(this.menuService.createMenu(this.notebookEditor.creationOptions.menuIds.cellTitleToolbar, contextKeyService)); const templateData: MarkdownCellRenderTemplate = { - useRenderer: this.useRenderer, rootContainer, collapsedPart, expandButton, @@ -386,43 +381,11 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen toJSON: () => { return {}; } }; - if (!this.useRenderer) { - this.dndController?.registerDragHandle(templateData, rootContainer, container, () => this.getDragImage(templateData)); - } - this.commonRenderTemplate(templateData); return templateData; } - private getDragImage(templateData: MarkdownCellRenderTemplate): HTMLElement { - if (templateData.currentRenderedCell?.getEditState() === CellEditState.Editing) { - return this.getEditDragImage(templateData); - } else { - return this.getMarkdownDragImage(templateData); - } - } - - private getMarkdownDragImage(templateData: MarkdownCellRenderTemplate): HTMLElement { - const dragImageContainer = DOM.$('.cell-drag-image.monaco-list-row.focused.markdown-cell-row'); - DOM.reset(dragImageContainer, templateData.container.cloneNode(true)); - - // Remove all rendered content nodes after the - const markdownContent = dragImageContainer.querySelector('.cell.markdown'); - const contentNodes = markdownContent?.children[0].children; - if (contentNodes) { - for (let i = contentNodes.length - 1; i >= 1; i--) { - contentNodes.item(i)!.remove(); - } - } - - return dragImageContainer; - } - - private getEditDragImage(templateData: MarkdownCellRenderTemplate): HTMLElement { - return new CodeCellDragImageRenderer().getDragImage(templateData, templateData.currentEditor!, 'markdown'); - } - renderElement(element: MarkupCellViewModel, index: number, templateData: MarkdownCellRenderTemplate, height: number | undefined): void { if (!this.notebookEditor.hasModel()) { throw new Error('The notebook editor is not attached with view model yet.'); @@ -511,7 +474,7 @@ export class MarkupCellRenderer extends AbstractCellRenderer implements IListRen this.setBetweenCellToolbarContext(templateData, element, toolbarContext); const scopedInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, templateData.contextKeyService])); - const markdownCell = scopedInstaService.createInstance(StatefulMarkdownCell, this.notebookEditor, element, templateData, cellEditorOptions.getValue(element.internalMetadata), this.renderedEditors, { useRenderer: templateData.useRenderer }); + const markdownCell = scopedInstaService.createInstance(StatefulMarkdownCell, this.notebookEditor, element, templateData, cellEditorOptions.getValue(element.internalMetadata), this.renderedEditors,); elementDisposables.add(markdownCell); elementDisposables.add(cellEditorOptions.onDidChange(newValue => markdownCell.updateEditorOptions(cellEditorOptions.getValue(element.internalMetadata)))); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts index 2c60b3ba3c2..ae8bb06f5e6 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -6,7 +6,7 @@ import * as DOM from 'vs/base/browser/dom'; import { disposableTimeout, raceCancellation } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -17,100 +17,22 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; import { collapsedIcon, expandedIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; -interface IMarkdownRenderStrategy extends IDisposable { - update(): void; -} - -class WebviewMarkdownRenderer extends Disposable implements IMarkdownRenderStrategy { - constructor( - readonly notebookEditor: IActiveNotebookEditor, - readonly viewCell: MarkupCellViewModel - ) { - super(); - } - - update(): void { - this.notebookEditor.createMarkupPreview(this.viewCell); - } -} - -class BuiltinMarkdownRenderer extends Disposable implements IMarkdownRenderStrategy { - private readonly localDisposables = this._register(new DisposableStore()); - - constructor( - private readonly notebookEditor: IActiveNotebookEditor, - private readonly viewCell: MarkupCellViewModel, - private readonly container: HTMLElement, - private readonly markdownContainer: HTMLElement, - private readonly editorAccessor: () => CodeEditorWidget | null - ) { - super(); - - this._register(getResizesObserver(this.markdownContainer, undefined, () => { - if (viewCell.getEditState() === CellEditState.Preview) { - this.viewCell.renderedMarkdownHeight = container.clientHeight; - } - })).startObserving(); - } - - update(): void { - - const markdownRenderer = this.viewCell.getMarkdownRenderer(); - const renderedHTML = this.viewCell.getHTML(); - if (renderedHTML) { - this.markdownContainer.appendChild(renderedHTML); - } - - if (this.editorAccessor()) { - // switch from editing mode - this.viewCell.renderedMarkdownHeight = this.container.clientHeight; - this.relayoutCell(); - } else { - this.localDisposables.clear(); - this.localDisposables.add(markdownRenderer.onDidRenderAsync(() => { - if (this.viewCell.getEditState() === CellEditState.Preview) { - this.viewCell.renderedMarkdownHeight = this.container.clientHeight; - } - this.relayoutCell(); - })); - - this.localDisposables.add(this.viewCell.textBuffer.onDidChangeContent(() => { - this.markdownContainer.innerText = ''; - this.viewCell.clearHTML(); - const renderedHTML = this.viewCell.getHTML(); - if (renderedHTML) { - this.markdownContainer.appendChild(renderedHTML); - } - })); - - this.viewCell.renderedMarkdownHeight = this.container.clientHeight; - this.relayoutCell(); - } - } - - relayoutCell() { - this.notebookEditor.layoutNotebookCell(this.viewCell, this.viewCell.layoutInfo.totalHeight); - } -} export class StatefulMarkdownCell extends Disposable { private editor: CodeEditorWidget | null = null; - private markdownContainer: HTMLElement; + private markdownAccessibilityContainer: HTMLElement; private editorPart: HTMLElement; private readonly localDisposables = this._register(new DisposableStore()); private readonly focusSwitchDisposable = this._register(new MutableDisposable()); private readonly editorDisposables = this._register(new DisposableStore()); private foldingState: CellFoldingState; - private useRenderer: boolean = false; - private renderStrategy: IMarkdownRenderStrategy; constructor( private readonly notebookEditor: IActiveNotebookEditor, @@ -118,25 +40,28 @@ export class StatefulMarkdownCell extends Disposable { private readonly templateData: MarkdownCellRenderTemplate, private editorOptions: IEditorOptions, private readonly renderedEditors: Map, - options: { useRenderer: boolean; }, @IContextKeyService private readonly contextKeyService: IContextKeyService, @INotebookCellStatusBarService readonly notebookCellStatusBarService: INotebookCellStatusBarService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); - this.markdownContainer = templateData.cellContainer; + // Create an element that is only used to announce markup cell content to screen readers + const id = `aria-markup-cell-${this.viewCell.id}`; + this.markdownAccessibilityContainer = templateData.cellContainer; + this.markdownAccessibilityContainer.id = id; + // Hide the element from non-screen readers + this.markdownAccessibilityContainer.style.height = '1px'; + this.markdownAccessibilityContainer.style.position = 'absolute'; + this.markdownAccessibilityContainer.style.top = '10000px'; + this.markdownAccessibilityContainer.ariaHidden = 'false'; + + this.templateData.rootContainer.setAttribute('aria-describedby', id); + this.editorPart = templateData.editorPart; - this.useRenderer = options.useRenderer; - if (this.useRenderer) { - this.templateData.container.classList.toggle('webview-backed-markdown-cell', true); - this.renderStrategy = new WebviewMarkdownRenderer(this.notebookEditor, this.viewCell); - } else { - this.renderStrategy = new BuiltinMarkdownRenderer(this.notebookEditor, this.viewCell, this.templateData.container, this.markdownContainer, () => this.editor); - } + this.templateData.container.classList.toggle('webview-backed-markdown-cell', true); - this._register(this.renderStrategy); this._register(toDisposable(() => renderedEditors.delete(this.viewCell))); this._register(viewCell.onDidChangeState((e) => { @@ -199,11 +124,9 @@ export class StatefulMarkdownCell extends Disposable { } })); - if (this.useRenderer) { - // the markdown preview's height might already be updated after the renderer calls `element.getHeight()` - if (this.viewCell.layoutInfo.totalHeight > 0) { - this.relayoutCell(); - } + // the markdown preview's height might already be updated after the renderer calls `element.getHeight()` + if (this.viewCell.layoutInfo.totalHeight > 0) { + this.relayoutCell(); } // apply decorations @@ -211,32 +134,20 @@ export class StatefulMarkdownCell extends Disposable { this._register(viewCell.onCellDecorationsChanged((e) => { e.added.forEach(options => { if (options.className) { - if (this.useRenderer) { - this.notebookEditor.deltaCellOutputContainerClassNames(this.viewCell.id, [options.className], []); - } else { - templateData.rootContainer.classList.add(options.className); - } + this.notebookEditor.deltaCellOutputContainerClassNames(this.viewCell.id, [options.className], []); } }); e.removed.forEach(options => { if (options.className) { - if (this.useRenderer) { - this.notebookEditor.deltaCellOutputContainerClassNames(this.viewCell.id, [], [options.className]); - } else { - templateData.rootContainer.classList.remove(options.className); - } + this.notebookEditor.deltaCellOutputContainerClassNames(this.viewCell.id, [], [options.className]); } }); })); viewCell.getCellDecorations().forEach(options => { if (options.className) { - if (this.useRenderer) { - this.notebookEditor.deltaCellOutputContainerClassNames(this.viewCell.id, [options.className], []); - } else { - templateData.rootContainer.classList.add(options.className); - } + this.notebookEditor.deltaCellOutputContainerClassNames(this.viewCell.id, [options.className], []); } }); @@ -267,7 +178,8 @@ export class StatefulMarkdownCell extends Disposable { private viewUpdateCollapsed(): void { DOM.show(this.templateData.collapsedPart); DOM.hide(this.editorPart); - DOM.hide(this.markdownContainer); + this.markdownAccessibilityContainer.ariaHidden = 'true'; + this.templateData.container.classList.toggle('collapsed', true); this.viewCell.renderedMarkdownHeight = 0; this.viewCell.layoutChange({}); @@ -278,12 +190,10 @@ export class StatefulMarkdownCell extends Disposable { let editorHeight: number; DOM.show(this.editorPart); - DOM.hide(this.markdownContainer); + this.markdownAccessibilityContainer.ariaHidden = 'true'; DOM.hide(this.templateData.collapsedPart); - if (this.useRenderer) { - this.notebookEditor.hideMarkupPreviews([this.viewCell]); - } + this.notebookEditor.hideMarkupPreviews([this.viewCell]); this.templateData.container.classList.toggle('collapsed', false); this.templateData.container.classList.toggle('markdown-cell-edit-mode', true); @@ -371,16 +281,18 @@ export class StatefulMarkdownCell extends Disposable { this.viewCell.detachTextEditor(); DOM.hide(this.editorPart); DOM.hide(this.templateData.collapsedPart); - DOM.show(this.markdownContainer); + this.markdownAccessibilityContainer.ariaHidden = 'false'; this.templateData.container.classList.toggle('collapsed', false); this.templateData.container.classList.toggle('markdown-cell-edit-mode', false); this.renderedEditors.delete(this.viewCell); - this.markdownContainer.innerText = ''; - this.viewCell.clearHTML(); + this.markdownAccessibilityContainer.innerText = ''; + if (this.viewCell.renderedHtml) { + DOM.safeInnerHtml(this.markdownAccessibilityContainer, this.viewCell.renderedHtml); + } - this.renderStrategy.update(); + this.notebookEditor.createMarkupPreview(this.viewCell); } private focusEditorIfNeeded() { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index aa6921f27c3..ba9f9488d70 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -129,6 +129,12 @@ export interface IInitializedMarkupMessage extends BaseToWebviewMessage { readonly type: 'initializedMarkup'; } +export interface IRenderedMarkupMessage extends BaseToWebviewMessage { + readonly type: 'renderedMarkup'; + readonly cellId: string; + readonly html: string; +} + export interface ITelemetryFoundRenderedMarkdownMath extends BaseToWebviewMessage { readonly type: 'telemetryFoundRenderedMarkdownMath'; } @@ -342,6 +348,7 @@ export type FromWebviewMessage = WebviewIntialized | ICellDropMessage | ICellDragEndMessage | IInitializedMarkupMessage | + IRenderedMarkupMessage | ITelemetryFoundRenderedMarkdownMath | ITelemetryFoundUnrenderedMarkdownMath; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index bc9632cd2a3..a3b61d41688 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -1130,6 +1130,27 @@ async function webviewPreloads(style: PreloadStyles, options: PreloadOptions, re } } + const root = (this.element.shadowRoot ?? this.element); + const html = []; + for (const child of root.children) { + switch (child.tagName) { + case 'LINK': + case 'SCRIPT': + case 'STYLE': + // not worth sending over since it will be stripped before rendering + break; + + default: + html.push(child.outerHTML); + break; + } + } + + postNotebookMessage('renderedMarkup', { + cellId: this.id, + html: html.join(''), + }); + dimensionUpdater.updateHeight(this.id, this.element.offsetHeight, { isOutput: false }); diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts index abb5165cb17..febf317c636 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel.ts @@ -6,11 +6,9 @@ import { Emitter, Event } from 'vs/base/common/event'; import * as UUID from 'vs/base/common/uuid'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; import { CellEditState, CellFindMatch, ICellOutputViewModel, ICellViewModel, MarkdownCellLayoutChangeEvent, MarkdownCellLayoutInfo, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { MarkdownRenderer } from 'vs/editor/browser/core/markdownRenderer'; import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; import { NotebookCellStateChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; @@ -18,15 +16,21 @@ import { CellKind, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ViewContext } from 'vs/workbench/contrib/notebook/browser/viewModel/viewContext'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { dirname } from 'vs/base/common/resources'; export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewModel { readonly cellKind = CellKind.Markup; - private _html: HTMLElement | null = null; private _layoutInfo: MarkdownCellLayoutInfo; + private _renderedHtml?: string; + + public get renderedHtml(): string | undefined { return this._renderedHtml; } + public set renderedHtml(value: string | undefined) { + this._renderedHtml = value; + this._onDidChangeState.fire({ contentChanged: true }); + } + get layoutInfo() { return this._layoutInfo; } @@ -101,8 +105,6 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM private readonly _onDidHideInput = new Emitter(); readonly onDidHideInput = this._onDidHideInput.event; - private readonly _mdRenderer: MarkdownRenderer; - constructor( viewType: string, model: NotebookCellTextModel, @@ -115,8 +117,6 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM ) { super(viewType, model, UUID.generateUuid(), viewContext, configurationService, textModelService); - this._mdRenderer = this._register(instantiationService.createInstance(MarkdownRenderer, { baseUrl: dirname(model.uri) })); - const { bottomToolbarGap } = this.viewContext.notebookOptions.computeBottomToolbarDimensions(this.viewType); this._layoutInfo = { @@ -246,43 +246,13 @@ export class MarkupCellViewModel extends BaseCellViewModel implements ICellViewM } } - clearHTML() { - this._html = null; - } - - getHTML(): HTMLElement | null { - if (this.cellKind === CellKind.Markup) { - if (this._html) { - return this._html; - } - const renderer = this.getMarkdownRenderer(); - const text = this.getText(); - - if (text.length === 0) { - const el = document.createElement('p'); - el.className = 'emptyMarkdownPlaceholder'; - el.innerText = nls.localize('notebook.emptyMarkdownPlaceholder', "Empty markdown cell, double click or press enter to edit."); - this._html = el; - } else { - this._html = renderer.render({ value: this.getText(), isTrusted: true }, undefined, { gfm: true }).element; - } - - return this._html; - } - return null; - } - protected onDidChangeTextModelContent(): void { - this._html = null; this._onDidChangeState.fire({ contentChanged: true }); } onDeselect() { } - getMarkdownRenderer() { - return this._mdRenderer; - } private readonly _hasFindResult = this._register(new Emitter()); public readonly hasFindResult: Event = this._hasFindResult.event; diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 0552b7ea24b..f724b57cfaf 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -838,7 +838,6 @@ export const CellToolbarVisibility = 'notebook.cellToolbarVisibility'; export type ShowCellStatusBarType = 'hidden' | 'visible' | 'visibleAfterExecute'; export const ShowCellStatusBar = 'notebook.showCellStatusBar'; export const NotebookTextDiffEditorPreview = 'notebook.diff.enablePreview'; -export const ExperimentalUseMarkdownRenderer = 'notebook.experimental.useMarkdownRenderer'; export const ExperimentalInsertToolbarAlignment = 'notebook.experimental.insertToolbarAlignment'; export const CompactView = 'notebook.compactView'; export const FocusIndicator = 'notebook.cellFocusIndicator';