From 3b452ca300b058aafe9dc6083f7f9299f1845e0c Mon Sep 17 00:00:00 2001 From: Marcos Alves Date: Wed, 26 Jun 2024 13:12:59 -0300 Subject: [PATCH 1/2] feat: partially adding support to markdown cells (#247) --- packages/react/src/components/cell/Cell.tsx | 55 ++++++---- .../react/src/components/cell/CellAdapter.ts | 102 +++++++++++++----- 2 files changed, 112 insertions(+), 45 deletions(-) diff --git a/packages/react/src/components/cell/Cell.tsx b/packages/react/src/components/cell/Cell.tsx index 82a583b7..5ea93d6e 100644 --- a/packages/react/src/components/cell/Cell.tsx +++ b/packages/react/src/components/cell/Cell.tsx @@ -15,7 +15,12 @@ import useCellStore from './CellState'; export type ICellProps = { /** - * Code cell source. + * Cell type + */ + type: 'code' | 'markdown' | 'raw'; + + /** + * Cell source */ source?: string; /** @@ -25,14 +30,35 @@ export type ICellProps = { }; export const Cell = (props: ICellProps) => { - const { source = '', autoStart } = props; + const { type='code', source = '', autoStart } = props; const { serverSettings, defaultKernel } = useJupyter(); const cellStore = useCellStore(); const [adapter, setAdapter] = useState(); + + const handleCodeCellState = (adapter: CellAdapter) => { + (adapter.codeCell as CodeCell).outputArea.outputLengthChanged?.connect( + (outputArea, outputsCount) => { + cellStore.setOutputsCount(outputsCount); + } + ); + adapter.sessionContext.initialize().then(() => { + if (autoStart) { + const execute = CodeCell.execute( + (adapter.codeCell as CodeCell), + adapter.sessionContext + ); + execute.then((msg: void | KernelMessage.IExecuteReplyMsg) => { + cellStore.setKernelAvailable(true); + }); + } + }); + } + useEffect(() => { if (defaultKernel && serverSettings) { defaultKernel.ready.then(() => { const adapter = new CellAdapter({ + type, source, serverSettings, kernel: defaultKernel, @@ -44,22 +70,10 @@ export const Cell = (props: ICellProps) => { cellStore.setSource(cellModel.sharedModel.getSource()); } ); - adapter.codeCell.outputArea.outputLengthChanged.connect( - (outputArea, outputsCount) => { - cellStore.setOutputsCount(outputsCount); - } - ); - adapter.sessionContext.initialize().then(() => { - if (autoStart) { - const execute = CodeCell.execute( - adapter.codeCell, - adapter.sessionContext - ); - execute.then((msg: void | KernelMessage.IExecuteReplyMsg) => { - cellStore.setKernelAvailable(true); - }); - } - }); + + if (type === 'code') { + handleCodeCellState(adapter); + } setAdapter(adapter); }); } @@ -85,6 +99,11 @@ export const Cell = (props: ICellProps) => { height: 'auto !important', position: 'relative', }, + '& .jp-MarkdownCell': { + height: 'auto !important', + minHeight: '65px', + position: 'relative', + }, '& .jp-Cell-outputArea': { paddingBottom: '30px', }, diff --git a/packages/react/src/components/cell/CellAdapter.ts b/packages/react/src/components/cell/CellAdapter.ts index 341dcc4d..f402ec52 100755 --- a/packages/react/src/components/cell/CellAdapter.ts +++ b/packages/react/src/components/cell/CellAdapter.ts @@ -13,7 +13,7 @@ import { Toolbar, ToolbarButton, } from '@jupyterlab/apputils'; -import { CodeCellModel, CodeCell, Cell } from '@jupyterlab/cells'; +import { CodeCellModel, CodeCell, Cell, MarkdownCell, RawCell, MarkdownCellModel } from '@jupyterlab/cells'; import { ybinding, CodeMirrorMimeTypeService, @@ -22,6 +22,7 @@ import { EditorExtensionRegistry, EditorThemeRegistry, } from '@jupyterlab/codemirror'; +import { MathJaxTypesetter } from '@jupyterlab/mathjax-extension'; import { Completer, CompleterModel, @@ -41,7 +42,7 @@ import { KernelSpecManager, } from '@jupyterlab/services'; import { runIcon } from '@jupyterlab/ui-components'; -import { createStandaloneCell, YCodeCell, IYText } from '@jupyter/ydoc'; +import { createStandaloneCell, YCodeCell, IYText, YMarkdownCell } from '@jupyter/ydoc'; import { WIDGET_MIMETYPE, WidgetRenderer, @@ -49,21 +50,25 @@ import { import { requireLoader as loader } from '../../jupyter/ipywidgets/libembed-amd'; import ClassicWidgetManager from '../../jupyter/ipywidgets/classic/manager'; import Kernel from '../../jupyter/kernel/Kernel'; +import getMarked from '../notebook/marked/marked'; import CellCommands from './CellCommands'; export class CellAdapter { - private _codeCell: CodeCell; + private _codeCell: CodeCell | MarkdownCell | RawCell; private _kernel: Kernel; private _panel: BoxPanel; private _sessionContext: SessionContext; + private _type: 'code' | 'markdown' | 'raw' constructor(options: CellAdapter.ICellAdapterOptions) { - const { source, serverSettings, kernel } = options; + const { type, source, serverSettings, kernel } = options; this._kernel = kernel; - this.setupCell(source, serverSettings, kernel); + this._type = type; + this.setupCell(type, source, serverSettings, kernel); } private setupCell( + type = 'code', source: string, serverSettings: ServerConnection.ISettings, kernel: Kernel @@ -184,7 +189,11 @@ export class CellAdapter { }, useCapture ); - const rendermime = new RenderMimeRegistry({ initialFactories }); + const rendermime = new RenderMimeRegistry({ + initialFactories, + latexTypesetter: new MathJaxTypesetter(), + markdownParser: getMarked(languages), + }); const iPyWidgetsClassicManager = new ClassicWidgetManager({ loader }); rendermime.addFactory( { @@ -200,21 +209,44 @@ export class CellAdapter { extensions: editorExtensions(), languages, }); - this._codeCell = new CodeCell({ - rendermime, - model: new CodeCellModel({ - sharedModel: createStandaloneCell({ - cell_type: 'code', - source: source, - metadata: {}, - }) as YCodeCell, - }), - contentFactory: new Cell.ContentFactory({ - editorFactory: factoryService.newInlineEditor.bind(factoryService), - }), - }); + + if (type === 'code') { + this._codeCell = new CodeCell({ + rendermime, + model: new CodeCellModel({ + sharedModel: createStandaloneCell({ + cell_type: 'code', + source: source, + metadata: {}, + }) as YCodeCell, + }), + contentFactory: new Cell.ContentFactory({ + editorFactory: factoryService.newInlineEditor.bind(factoryService), + }), + }); + } else if (type === 'markdown') { + this._codeCell = new MarkdownCell({ + rendermime, + model: new MarkdownCellModel({ + sharedModel: createStandaloneCell({ + cell_type: 'markdown', + source: source, + metadata: {}, + }) as YMarkdownCell, + }), + contentFactory: new Cell.ContentFactory({ + editorFactory: factoryService.newInlineEditor.bind(factoryService), + }), + }); + } + this._codeCell.addClass('dla-Jupyter-Cell'); this._codeCell.initializeState(); + + if (this._type === 'markdown') { + (this._codeCell as MarkdownCell).rendered = false; + } + this._sessionContext.kernelChanged.connect( (_, arg: Session.ISessionConnection.IKernelChangedArgs) => { const kernelConnection = arg.newValue; @@ -235,9 +267,11 @@ export class CellAdapter { ); this._sessionContext.kernelChanged.connect(() => { void this._sessionContext.session?.kernel?.info.then(info => { - const lang = info.language_info; - const mimeType = mimeService.getMimeTypeByLanguage(lang); - this._codeCell.model.mimeType = mimeType; + if (this._type === 'code') { + const lang = info.language_info; + const mimeType = mimeService.getMimeTypeByLanguage(lang); + this._codeCell.model.mimeType = mimeType; + } }); }); const editor = this._codeCell.editor; @@ -268,7 +302,10 @@ export class CellAdapter { }); }); handler.editor = editor; - CellCommands(commands, this._codeCell!, this._sessionContext, handler); + + if (this._type === 'code') { + CellCommands(commands, (this._codeCell as CodeCell)!, this._sessionContext, handler); + } completer.hide(); completer.addClass('jp-Completer-Cell'); Widget.attach(completer, document.body); @@ -277,7 +314,11 @@ export class CellAdapter { const runButton = new ToolbarButton({ icon: runIcon, onClick: () => { - CodeCell.execute(this._codeCell, this._sessionContext); + if (this._type === 'code') { + CodeCell.execute(this._codeCell as CodeCell, this._sessionContext); + } else if (this._type === 'markdown') { + (this.codeCell as MarkdownCell).rendered = true; + } }, tooltip: 'Run', }); @@ -296,7 +337,9 @@ export class CellAdapter { Toolbar.createKernelStatusItem(this._sessionContext) ); - this._codeCell.outputsScrolled = false; + if (this._type === 'code') { + (this._codeCell as CodeCell).outputsScrolled = false; + } this._codeCell.activate(); this._panel = new BoxPanel(); @@ -316,7 +359,7 @@ export class CellAdapter { return this._panel; } - get codeCell(): CodeCell { + get codeCell(): CodeCell | MarkdownCell | RawCell { return this._codeCell; } @@ -329,12 +372,17 @@ export class CellAdapter { } execute = () => { - CodeCell.execute(this._codeCell, this._sessionContext); + if (this._type === 'code') { + CodeCell.execute((this._codeCell as CodeCell), this._sessionContext); + } else if (this._type === 'markdown') { + (this._codeCell as MarkdownCell).rendered = true; + } }; } export namespace CellAdapter { export type ICellAdapterOptions = { + type: 'code' | 'markdown' | 'raw'; source: string; serverSettings: ServerConnection.ISettings; kernel: Kernel; From e1702bc663cf451414ae4e0fa6d03982058cc127 Mon Sep 17 00:00:00 2001 From: Marcos Alves Date: Fri, 28 Jun 2024 11:49:06 -0300 Subject: [PATCH 2/2] feat: adding more functionalities to markdown Cell (#247) --- packages/react/src/components/cell/Cell.tsx | 67 ++++++++++--- .../react/src/components/cell/CellAdapter.ts | 99 +++++++++---------- .../react/src/components/cell/CellCommands.ts | 12 ++- 3 files changed, 109 insertions(+), 69 deletions(-) diff --git a/packages/react/src/components/cell/Cell.tsx b/packages/react/src/components/cell/Cell.tsx index 5ea93d6e..10026a83 100644 --- a/packages/react/src/components/cell/Cell.tsx +++ b/packages/react/src/components/cell/Cell.tsx @@ -5,7 +5,7 @@ */ import { useState, useEffect } from 'react'; -import { CodeCell } from '@jupyterlab/cells'; +import { CodeCell, MarkdownCell } from '@jupyterlab/cells'; import { KernelMessage } from '@jupyterlab/services'; import { Box } from '@primer/react'; import CellAdapter from './CellAdapter'; @@ -27,30 +27,52 @@ export type ICellProps = { * Whether to execute directly the code cell or not. */ autoStart?: boolean; + /** + * Whether to show the toolbar for cell or not + */ + showToolbar?: boolean; }; export const Cell = (props: ICellProps) => { - const { type='code', source = '', autoStart } = props; + const { type='code', source = '', autoStart, showToolbar=true } = props; const { serverSettings, defaultKernel } = useJupyter(); const cellStore = useCellStore(); const [adapter, setAdapter] = useState(); - const handleCodeCellState = (adapter: CellAdapter) => { - (adapter.codeCell as CodeCell).outputArea.outputLengthChanged?.connect( - (outputArea, outputsCount) => { - cellStore.setOutputsCount(outputsCount); + const handleCellInitEvents = (adapter: CellAdapter) => { + adapter.cell.model.contentChanged.connect( + (cellModel, changedArgs) => { + cellStore.setSource(cellModel.sharedModel.getSource()); } ); + + if (adapter.cell instanceof CodeCell) { + adapter.cell.outputArea.outputLengthChanged?.connect( + (outputArea, outputsCount) => { + cellStore.setOutputsCount(outputsCount); + } + ); + } + adapter.sessionContext.initialize().then(() => { - if (autoStart) { + if (!autoStart) { + return + } + + // Perform auto-start for code or markdown cells + if (adapter.cell instanceof CodeCell) { const execute = CodeCell.execute( - (adapter.codeCell as CodeCell), + adapter.cell, adapter.sessionContext ); execute.then((msg: void | KernelMessage.IExecuteReplyMsg) => { cellStore.setKernelAvailable(true); }); } + + if (adapter.cell instanceof MarkdownCell) { + adapter.cell.rendered = true; + } }); } @@ -62,19 +84,32 @@ export const Cell = (props: ICellProps) => { source, serverSettings, kernel: defaultKernel, + boxOptions: {showToolbar} }); cellStore.setAdapter(adapter); cellStore.setSource(source); - adapter.codeCell.model.contentChanged.connect( - (cellModel, changedArgs) => { - cellStore.setSource(cellModel.sharedModel.getSource()); + handleCellInitEvents(adapter); + setAdapter(adapter); + + const handleDblClick = (event: Event) => { + let target = event.target as HTMLElement; + /** + * Find the DOM searching by the markdown output class (since child elements can be clicked also) + * If a rendered markdown was found, then back cell to editor mode + */ + while (target && !target.classList.contains('jp-MarkdownOutput')) { + target = target.parentElement as HTMLElement; } - ); + if (target && target.classList.contains('jp-MarkdownOutput')) { + (adapter.cell as MarkdownCell).rendered = false; + } + }; - if (type === 'code') { - handleCodeCellState(adapter); - } - setAdapter(adapter); + // Adds the event for double click and the removal on component's destroy + document.addEventListener('dblclick', handleDblClick); + return () => { + document.removeEventListener('dblclick', handleDblClick); + }; }); } }, [source, defaultKernel, serverSettings]); diff --git a/packages/react/src/components/cell/CellAdapter.ts b/packages/react/src/components/cell/CellAdapter.ts index f402ec52..baefc65f 100755 --- a/packages/react/src/components/cell/CellAdapter.ts +++ b/packages/react/src/components/cell/CellAdapter.ts @@ -53,25 +53,29 @@ import Kernel from '../../jupyter/kernel/Kernel'; import getMarked from '../notebook/marked/marked'; import CellCommands from './CellCommands'; +interface BoxOptions { + showToolbar?: boolean; +} export class CellAdapter { - private _codeCell: CodeCell | MarkdownCell | RawCell; + private _cell: CodeCell | MarkdownCell | RawCell; private _kernel: Kernel; private _panel: BoxPanel; private _sessionContext: SessionContext; private _type: 'code' | 'markdown' | 'raw' constructor(options: CellAdapter.ICellAdapterOptions) { - const { type, source, serverSettings, kernel } = options; + const { type, source, serverSettings, kernel, boxOptions } = options; this._kernel = kernel; this._type = type; - this.setupCell(type, source, serverSettings, kernel); + this.setupCell(type, source, serverSettings, kernel, boxOptions); } private setupCell( type = 'code', source: string, serverSettings: ServerConnection.ISettings, - kernel: Kernel + kernel: Kernel, + boxOptions?: BoxOptions ) { const kernelManager = kernel.kernelManager ?? @@ -210,41 +214,31 @@ export class CellAdapter { languages, }); + const cellModel = createStandaloneCell({ + cell_type: type, + source: source, + metadata: {}, + }); + const contentFactory = new Cell.ContentFactory({ + editorFactory: factoryService.newInlineEditor.bind(factoryService), + }); if (type === 'code') { - this._codeCell = new CodeCell({ + this._cell = new CodeCell({ rendermime, - model: new CodeCellModel({ - sharedModel: createStandaloneCell({ - cell_type: 'code', - source: source, - metadata: {}, - }) as YCodeCell, - }), - contentFactory: new Cell.ContentFactory({ - editorFactory: factoryService.newInlineEditor.bind(factoryService), - }), + model: new CodeCellModel({sharedModel: cellModel as YCodeCell}), + contentFactory: contentFactory, }); } else if (type === 'markdown') { - this._codeCell = new MarkdownCell({ + this._cell = new MarkdownCell({ rendermime, - model: new MarkdownCellModel({ - sharedModel: createStandaloneCell({ - cell_type: 'markdown', - source: source, - metadata: {}, - }) as YMarkdownCell, - }), - contentFactory: new Cell.ContentFactory({ - editorFactory: factoryService.newInlineEditor.bind(factoryService), - }), + model: new MarkdownCellModel({sharedModel: cellModel as YMarkdownCell}), + contentFactory: contentFactory, }); } - - this._codeCell.addClass('dla-Jupyter-Cell'); - this._codeCell.initializeState(); - + this._cell.addClass('dla-Jupyter-Cell'); + this._cell.initializeState(); if (this._type === 'markdown') { - (this._codeCell as MarkdownCell).rendered = false; + (this._cell as MarkdownCell).rendered = false; } this._sessionContext.kernelChanged.connect( @@ -270,18 +264,18 @@ export class CellAdapter { if (this._type === 'code') { const lang = info.language_info; const mimeType = mimeService.getMimeTypeByLanguage(lang); - this._codeCell.model.mimeType = mimeType; + this._cell.model.mimeType = mimeType; } }); }); - const editor = this._codeCell.editor; + const editor = this._cell.editor; const model = new CompleterModel(); const completer = new Completer({ editor, model }); const timeout = 1000; const provider = new KernelCompleterProvider(); const reconciliator = new ProviderReconciliator({ context: { - widget: this._codeCell, + widget: this._cell, editor, session: this._sessionContext.session, }, @@ -293,7 +287,7 @@ export class CellAdapter { const provider = new KernelCompleterProvider(); handler.reconciliator = new ProviderReconciliator({ context: { - widget: this._codeCell, + widget: this._cell, editor, session: this._sessionContext.session, }, @@ -303,9 +297,7 @@ export class CellAdapter { }); handler.editor = editor; - if (this._type === 'code') { - CellCommands(commands, (this._codeCell as CodeCell)!, this._sessionContext, handler); - } + CellCommands(commands, this._cell!, this._sessionContext, handler); completer.hide(); completer.addClass('jp-Completer-Cell'); Widget.attach(completer, document.body); @@ -315,9 +307,9 @@ export class CellAdapter { icon: runIcon, onClick: () => { if (this._type === 'code') { - CodeCell.execute(this._codeCell as CodeCell, this._sessionContext); + CodeCell.execute(this._cell as CodeCell, this._sessionContext); } else if (this._type === 'markdown') { - (this.codeCell as MarkdownCell).rendered = true; + (this._cell as MarkdownCell).rendered = true; } }, tooltip: 'Run', @@ -338,17 +330,23 @@ export class CellAdapter { ); if (this._type === 'code') { - (this._codeCell as CodeCell).outputsScrolled = false; + (this._cell as CodeCell).outputsScrolled = false; } - this._codeCell.activate(); + this._cell.activate(); this._panel = new BoxPanel(); this._panel.direction = 'top-to-bottom'; this._panel.spacing = 0; - this._panel.addWidget(toolbar); - this._panel.addWidget(this._codeCell); - BoxPanel.setStretch(toolbar, 0); - BoxPanel.setStretch(this._codeCell, 1); + + if (boxOptions?.showToolbar !== false) { + this._panel.addWidget(toolbar); + } + this._panel.addWidget(this._cell); + + if (boxOptions?.showToolbar !== false) { + BoxPanel.setStretch(toolbar, 0); + } + BoxPanel.setStretch(this._cell, 1); window.addEventListener('resize', () => { this._panel.update(); }); @@ -359,8 +357,8 @@ export class CellAdapter { return this._panel; } - get codeCell(): CodeCell | MarkdownCell | RawCell { - return this._codeCell; + get cell(): CodeCell | MarkdownCell | RawCell { + return this._cell; } get sessionContext(): SessionContext { @@ -373,9 +371,9 @@ export class CellAdapter { execute = () => { if (this._type === 'code') { - CodeCell.execute((this._codeCell as CodeCell), this._sessionContext); + CodeCell.execute((this._cell as CodeCell), this._sessionContext); } else if (this._type === 'markdown') { - (this._codeCell as MarkdownCell).rendered = true; + (this._cell as MarkdownCell).rendered = true; } }; } @@ -386,6 +384,7 @@ export namespace CellAdapter { source: string; serverSettings: ServerConnection.ISettings; kernel: Kernel; + boxOptions?: BoxOptions; }; } diff --git a/packages/react/src/components/cell/CellCommands.ts b/packages/react/src/components/cell/CellCommands.ts index bc93f421..b08f8b2e 100644 --- a/packages/react/src/components/cell/CellCommands.ts +++ b/packages/react/src/components/cell/CellCommands.ts @@ -6,7 +6,7 @@ import { CommandRegistry } from '@lumino/commands'; import { CompletionHandler } from '@jupyterlab/completer'; -import { CodeCell } from '@jupyterlab/cells'; +import { CodeCell, MarkdownCell, RawCell } from '@jupyterlab/cells'; import { SessionContext } from '@jupyterlab/apputils'; const cmdIds = { @@ -16,7 +16,7 @@ const cmdIds = { export const CellCommands = ( commandRegistry: CommandRegistry, - codeCell: CodeCell, + cell: CodeCell | MarkdownCell | RawCell, sessionContext: SessionContext, completerHandler: CompletionHandler ): void => { @@ -29,7 +29,13 @@ export const CellCommands = ( execute: () => completerHandler.completer.selectActive(), }); commandRegistry.addCommand('run:cell', { - execute: () => CodeCell.execute(codeCell, sessionContext), + execute: () => { + if (cell instanceof CodeCell) { + CodeCell.execute(cell, sessionContext) + } else if (cell instanceof MarkdownCell) { + (cell as MarkdownCell).rendered = true; + } + }, }); commandRegistry.addKeyBinding({ selector: '.jp-InputArea-editor.jp-mod-completer-enabled',