// *****************************************************************************
// 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 { inject, injectable } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import { MonacoEditorServices } from '@theia/monaco/lib/browser/monaco-editor';
import { NotebookRendererRegistry } from '../notebook-renderer-registry';
import { NotebookCellModel } from '../view-model/notebook-cell-model';
import { NotebookModel } from '../view-model/notebook-model';
import { CellEditor } from './notebook-cell-editor';
import { CellRenderer, observeCellHeight } from './notebook-cell-list-view';
import { NotebookCellToolbarFactory } from './notebook-cell-toolbar-factory';
import { NotebookCellActionContribution, NotebookCellCommands } from '../contributions/notebook-cell-actions-contribution';
import { CellExecution, NotebookExecutionStateService } from '../service/notebook-execution-state-service';
import { codicon } from '@theia/core/lib/browser';
import { NotebookCellExecutionState } from '../../common';
import { CancellationToken, CommandRegistry, DisposableCollection, nls } from '@theia/core';
import { NotebookContextManager } from '../service/notebook-context-manager';
import { NotebookViewportService } from './notebook-viewport-service';
import { EditorPreferences } from '@theia/editor/lib/common/editor-preferences';
import { NotebookOptionsService } from '../service/notebook-options';
import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
import { MarkdownStringImpl as MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string';
import { NotebookCellEditorService } from '../service/notebook-cell-editor-service';
import { CellOutputWebview } from '../renderers/cell-output-webview';
import { NotebookCellStatusBarItem, NotebookCellStatusBarItemList, 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 NotebookCodeCellRenderer implements CellRenderer {
@inject(MonacoEditorServices)
protected readonly monacoServices: MonacoEditorServices;
@inject(NotebookRendererRegistry)
protected readonly notebookRendererRegistry: NotebookRendererRegistry;
@inject(NotebookCellToolbarFactory)
protected readonly notebookCellToolbarFactory: NotebookCellToolbarFactory;
@inject(NotebookExecutionStateService)
protected readonly executionStateService: NotebookExecutionStateService;
@inject(NotebookContextManager)
protected readonly notebookContextManager: NotebookContextManager;
@inject(NotebookViewportService)
protected readonly notebookViewportService: NotebookViewportService;
@inject(EditorPreferences)
protected readonly editorPreferences: EditorPreferences;
@inject(NotebookCellEditorService)
protected readonly notebookCellEditorService: NotebookCellEditorService;
@inject(CommandRegistry)
protected readonly commandRegistry: CommandRegistry;
@inject(NotebookOptionsService)
protected readonly notebookOptionsService: NotebookOptionsService;
@inject(MarkdownRenderer)
protected readonly markdownRenderer: MarkdownRenderer;
@inject(CellOutputWebview)
protected readonly outputWebview: CellOutputWebview;
@inject(NotebookCellStatusBarService)
protected readonly notebookCellStatusBarService: NotebookCellStatusBarService;
@inject(LabelParser)
protected readonly labelParser: LabelParser;
@inject(NotebookViewModel)
protected readonly notebookViewModel: NotebookViewModel;
render(notebookModel: NotebookModel, cell: NotebookCellModel, handle: number): React.ReactNode {
return
observeCellHeight(ref, cell)}>
this.notebookViewModel.cellViewModels.get(cell.handle)?.requestFocusEditor()} />
;
}
renderSidebar(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode {
return
this.notebookCellToolbarFactory.renderSidebar(NotebookCellActionContribution.OUTPUT_SIDEBAR_MENU, cell, {
contextMenuArgs: () => [notebookModel, cell, cell.outputs[0]]
})
} />
;
}
renderDragImage(cell: NotebookCellModel): HTMLElement {
const dragImage = document.createElement('div');
dragImage.className = 'theia-notebook-drag-image';
dragImage.style.width = this.notebookContextManager.context?.clientWidth + 'px';
dragImage.style.height = '100px';
dragImage.style.display = 'flex';
const fakeRunButton = document.createElement('span');
fakeRunButton.className = `${codicon('play')} theia-notebook-cell-status-item`;
dragImage.appendChild(fakeRunButton);
const fakeEditor = document.createElement('div');
dragImage.appendChild(fakeEditor);
const lines = cell.source.split('\n').slice(0, 5).join('\n');
const codeSequence = this.getMarkdownCodeSequence(lines);
const firstLine = new MarkdownString(`${codeSequence}${cell.language}\n${lines}\n${codeSequence}`, { supportHtml: true, isTrusted: false });
fakeEditor.appendChild(this.markdownRenderer.render(firstLine).element);
fakeEditor.classList.add('theia-notebook-cell-editor-container');
fakeEditor.style.padding = '10px';
return dragImage;
}
protected getMarkdownCodeSequence(input: string): string {
// We need a minimum of 3 backticks to start a code block.
let longest = 2;
let current = 0;
for (let i = 0; i < input.length; i++) {
const char = input.charAt(i);
if (char === '`') {
current++;
if (current > longest) {
longest = current;
}
} else {
current = 0;
}
}
return Array(longest + 1).fill('`').join('');
}
}
export interface NotebookCodeCellSidebarProps {
cell: NotebookCellModel;
notebook: NotebookModel;
notebookCellToolbarFactory: NotebookCellToolbarFactory
}
export class NotebookCodeCellSidebar extends React.Component {
protected toDispose = new DisposableCollection();
constructor(props: NotebookCodeCellSidebarProps) {
super(props);
this.toDispose.push(props.cell.onDidCellHeightChange(() => this.forceUpdate()));
}
override componentWillUnmount(): void {
this.toDispose.dispose();
}
override render(): React.ReactNode {
return
{this.props.notebookCellToolbarFactory.renderSidebar(NotebookCellActionContribution.CODE_CELL_SIDEBAR_MENU, this.props.cell, {
contextMenuArgs: () => [this.props.cell], commandArgs: () => [this.props.notebook, this.props.cell]
})
}
;
}
}
export interface NotebookCodeCellStatusProps {
notebook: NotebookModel;
cell: NotebookCellModel;
commandRegistry: CommandRegistry;
cellStatusBarService: NotebookCellStatusBarService;
executionStateService?: NotebookExecutionStateService;
labelParser: LabelParser;
onClick: () => void;
}
export interface NotebookCodeCellStatusState {
currentExecution?: CellExecution;
executionTime: number;
}
export class NotebookCodeCellStatus extends React.Component {
protected toDispose = new DisposableCollection();
protected statusBarItems: NotebookCellStatusBarItemList[] = [];
constructor(props: NotebookCodeCellStatusProps) {
super(props);
this.state = {
executionTime: 0
};
let currentInterval: NodeJS.Timeout | undefined;
if (props.executionStateService) {
this.toDispose.push(props.executionStateService.onDidChangeExecution(event => {
if (event.affectsCell(this.props.cell.uri)) {
this.setState({ currentExecution: event.changed, executionTime: 0 });
clearInterval(currentInterval);
if (event.changed?.state === NotebookCellExecutionState.Executing) {
const startTime = Date.now();
// The resolution of the time display is only a single digit after the decimal point.
// Therefore, we only need to update the display every 100ms.
currentInterval = setInterval(() => {
this.setState({
executionTime: Date.now() - startTime
});
}, 100);
}
}
}));
}
this.toDispose.push(props.cell.onDidChangeLanguage(() => {
this.forceUpdate();
}));
this.updateStatusBarItems();
this.props.cellStatusBarService.onDidChangeItems(() => this.updateStatusBarItems());
this.props.notebook.onContentChanged(() => this.updateStatusBarItems());
}
async updateStatusBarItems(): Promise {
this.statusBarItems = await this.props.cellStatusBarService.getStatusBarItemsForCell(
this.props.notebook.uri,
this.props.notebook.cells.indexOf(this.props.cell),
this.props.notebook.viewType,
CancellationToken.None);
this.forceUpdate();
}
override componentWillUnmount(): void {
this.toDispose.dispose();
}
override render(): React.ReactNode {
return this.props.onClick()}>
{this.props.executionStateService && this.renderExecutionState()}
{this.statusBarItems?.length && this.renderStatusBarItems()}
{
this.props.commandRegistry.executeCommand(NotebookCellCommands.CHANGE_CELL_LANGUAGE.id, this.props.notebook, this.props.cell);
}}>{this.props.cell.languageName}
;
}
protected renderExecutionState(): React.ReactNode {
const state = this.state.currentExecution?.state;
const { lastRunSuccess } = this.props.cell.internalMetadata;
let iconClasses: string | undefined = undefined;
let color: string | undefined = undefined;
if (!state && lastRunSuccess) {
iconClasses = codicon('check');
color = 'green';
} else if (!state && lastRunSuccess === false) {
iconClasses = codicon('error');
color = 'red';
} else if (state === NotebookCellExecutionState.Pending || state === NotebookCellExecutionState.Unconfirmed) {
iconClasses = codicon('clock');
} else if (state === NotebookCellExecutionState.Executing) {
iconClasses = `${codicon('sync')} theia-animation-spin`;
}
return <>
{iconClasses &&
<>
{this.renderTime(this.getExecutionTime())}
>}
>;
}
protected getExecutionTime(): number {
const { runStartTime, runEndTime } = this.props.cell.internalMetadata;
const { executionTime } = this.state;
if (runStartTime !== undefined && runEndTime !== undefined) {
return runEndTime - runStartTime;
}
return executionTime;
}
protected renderTime(ms: number): string {
return `${(ms / 1000).toLocaleString(undefined, { maximumFractionDigits: 1, minimumFractionDigits: 1 })}s`;
}
protected renderStatusBarItems(): React.ReactNode {
return <>
{
this.statusBarItems.flatMap((itemList, listIndex) =>
itemList.items.map((item, index) => this.renderStatusBarItem(item, `${listIndex}-${index}`)
)
)
}
>;
}
protected renderStatusBarItem(item: NotebookCellStatusBarItem, key: string): React.ReactNode {
const content = this.props.labelParser.parse(item.text).map(part => {
if (typeof part === 'string') {
return part;
} else {
return ;
}
});
return {
if (item.command) {
if (typeof item.command === 'string') {
this.props.commandRegistry.executeCommand(item.command);
} else {
this.props.commandRegistry.executeCommand(item.command.id, ...(item.command.arguments ?? []));
}
}
}}>
{content}
;
}
}
interface NotebookCellOutputProps {
cell: NotebookCellModel;
notebook: NotebookModel;
outputWebview: CellOutputWebview;
renderSidebar: () => React.ReactNode;
}
export class NotebookCodeCellOutputs extends React.Component {
protected toDispose = new DisposableCollection();
protected outputHeight: number = 0;
override async componentDidMount(): Promise {
const { cell } = this.props;
this.toDispose.push(cell.onDidChangeOutputs(() => this.forceUpdate()));
this.toDispose.push(this.props.cell.onDidChangeOutputVisibility(() => this.forceUpdate()));
this.toDispose.push(this.props.outputWebview.onDidRenderOutput(event => {
if (event.cellHandle === this.props.cell.handle) {
this.outputHeight = event.outputHeight;
this.forceUpdate();
}
}));
}
override componentWillUnmount(): void {
this.toDispose.dispose();
}
override render(): React.ReactNode {
if (!this.props.cell.outputs?.length) {
return <>>;
}
if (this.props.cell.outputVisible) {
return
{this.props.renderSidebar()}
;
}
return {nls.localizeByDefault('Outputs are collapsed')}
;
}
}
interface NotebookCellExecutionOrderProps {
cell: NotebookCellModel;
}
function CodeCellExecutionOrder({ cell }: NotebookCellExecutionOrderProps): React.JSX.Element {
const [executionOrder, setExecutionOrder] = React.useState(cell.internalMetadata.executionOrder ?? ' ');
React.useEffect(() => {
const listener = cell.onDidChangeInternalMetadata(e => {
setExecutionOrder(cell.internalMetadata.executionOrder ?? ' ');
});
return () => listener.dispose();
}, []);
return {`[${executionOrder}]`};
}