// ***************************************************************************** // Copyright (C) 2023 TypeFox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import * as React from '@theia/core/shared/react'; import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer'; import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string'; import { NotebookModel } from '../view-model/notebook-model'; import { CellRenderer, observeCellHeight } from './notebook-cell-list-view'; import { NotebookCellModel } from '../view-model/notebook-cell-model'; import { CellEditor } from './notebook-cell-editor'; import { inject, injectable } from '@theia/core/shared/inversify'; import { MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor'; import { CommandRegistry, nls } from '@theia/core'; import { NotebookContextManager } from '../service/notebook-context-manager'; import { NotebookOptionsService } from '../service/notebook-options'; import { NotebookCodeCellStatus } from './notebook-code-cell-view'; import { NotebookEditorFindMatch, NotebookEditorFindMatchOptions } from './notebook-find-widget'; import * as mark from 'advanced-mark.js'; import { NotebookCellEditorService } from '../service/notebook-cell-editor-service'; import { NotebookCellStatusBarService } from '../service/notebook-cell-status-bar-service'; import { LabelParser } from '@theia/core/lib/browser/label-parser'; import { NotebookViewModel } from '../view-model/notebook-view-model'; @injectable() export class NotebookMarkdownCellRenderer implements CellRenderer { @inject(MarkdownRenderer) private readonly markdownRenderer: MarkdownRenderer; @inject(MonacoEditorServices) protected readonly monacoServices: MonacoEditorServices; @inject(NotebookContextManager) protected readonly notebookContextManager: NotebookContextManager; @inject(CommandRegistry) protected readonly commandRegistry: CommandRegistry; @inject(NotebookOptionsService) protected readonly notebookOptionsService: NotebookOptionsService; @inject(NotebookCellEditorService) protected readonly notebookCellEditorService: NotebookCellEditorService; @inject(NotebookCellStatusBarService) protected readonly notebookCellStatusBarService: NotebookCellStatusBarService; @inject(LabelParser) protected readonly labelParser: LabelParser; @inject(NotebookViewModel) protected readonly notebookViewModel: NotebookViewModel; render(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode { return ; } renderSidebar(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode { return
; } renderDragImage(cell: NotebookCellModel): HTMLElement { const dragImage = document.createElement('div'); dragImage.style.width = this.notebookContextManager.context?.clientWidth + 'px'; const markdownString = new MarkdownStringImpl(cell.source, { supportHtml: true, isTrusted: true }); const markdownElement = this.markdownRenderer.render(markdownString).element; dragImage.appendChild(markdownElement); return dragImage; } } interface MarkdownCellProps { markdownRenderer: MarkdownRenderer; monacoServices: MonacoEditorServices; commandRegistry: CommandRegistry; cell: NotebookCellModel; notebookModel: NotebookModel; notebookViewModel: NotebookViewModel; notebookContextManager: NotebookContextManager; notebookOptionsService: NotebookOptionsService; notebookCellEditorService: NotebookCellEditorService; notebookCellStatusBarService: NotebookCellStatusBarService; labelParser: LabelParser; } function MarkdownCell({ markdownRenderer, monacoServices, cell, notebookModel, notebookViewModel, notebookContextManager, notebookOptionsService, commandRegistry, notebookCellEditorService, notebookCellStatusBarService, labelParser }: MarkdownCellProps): React.JSX.Element { const [editMode, setEditMode] = React.useState(notebookViewModel.cellViewModels.get(cell.handle)?.editing || false); const [, forceUpdate] = React.useReducer(x => x + 1, 0); let empty = false; React.useEffect(() => { if (!editMode) { const listener = cell.onDidChangeContent(type => { if (type === 'content') { forceUpdate(); } }); return () => listener.dispose(); } }, [editMode, cell]); React.useEffect(() => { const cellViewModel = notebookViewModel.cellViewModels.get(cell.handle); const listener = cellViewModel?.onDidRequestCellEditChange(cellEdit => setEditMode(cellEdit)); return () => listener?.dispose(); }, [editMode, notebookViewModel, cell]); React.useEffect(() => { if (!editMode) { const instance = new mark(markdownContent); cell.onMarkdownFind = options => { instance.unmark(); if (empty) { return []; } return searchInMarkdown(instance, options); }; return () => { cell.onMarkdownFind = undefined; instance.unmark(); }; } }, [editMode, cell.source]); let markdownContent: HTMLElement[] = React.useMemo(() => { const markdownString = new MarkdownStringImpl(cell.source, { supportHtml: true, isTrusted: true }); const rendered = markdownRenderer.render(markdownString).element; const children: HTMLElement[] = []; rendered.childNodes.forEach(child => { if (child instanceof HTMLElement) { children.push(child); } }); return children; }, [cell.source]); if (markdownContent.length === 0) { const italic = document.createElement('i'); italic.className = 'theia-notebook-empty-markdown'; italic.innerText = nls.localizeByDefault('Empty markdown cell, double-click or press enter to edit.'); italic.style.pointerEvents = 'none'; markdownContent = [italic]; empty = true; } return editMode ? (
observeCellHeight(ref, cell)}> notebookViewModel.cellViewModels.get(cell.handle)?.requestFocusEditor()} />
) : (
notebookViewModel.cellViewModels.get(cell.handle)?.requestEdit()} ref={node => { node?.replaceChildren(...markdownContent); observeCellHeight(node, cell); }} />); } function searchInMarkdown(instance: mark, options: NotebookEditorFindMatchOptions): NotebookEditorFindMatch[] { const matches: NotebookEditorFindMatch[] = []; const markOptions: mark.MarkOptions & mark.RegExpOptions = { className: 'theia-find-match', diacritics: false, caseSensitive: options.matchCase, acrossElements: true, separateWordSearch: false, each: node => { matches.push(new MarkdownEditorFindMatch(node)); } }; if (options.regex || options.wholeWord) { let search = options.search; if (options.wholeWord) { if (!options.regex) { search = escapeRegExp(search); } search = '\\b' + search + '\\b'; } instance.markRegExp(new RegExp(search, options.matchCase ? '' : 'i'), markOptions); } else { instance.mark(options.search, markOptions); } return matches; } function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } class MarkdownEditorFindMatch implements NotebookEditorFindMatch { constructor(readonly node: Node) { } private _selected = false; get selected(): boolean { return this._selected; } set selected(selected: boolean) { this._selected = selected; const className = 'theia-find-match-selected'; if (this.node instanceof HTMLElement) { if (selected) { this.node.classList.add(className); } else { this.node.classList.remove(className); } } } show(): void { if (this.node instanceof HTMLElement) { this.node.scrollIntoView({ behavior: 'instant', block: 'center' }); } } }