From 52ab029070d4f37562bfef577d4af9dc75d550de Mon Sep 17 00:00:00 2001 From: Jonah Iden Date: Fri, 5 Apr 2024 11:48:56 +0200 Subject: [PATCH] Notebook Outline-View and Breadcrumbs. (#13562) * notbook outline and breadcrumbs + jupyter show Table of contents menu item working Signed-off-by: Jonah Iden * some basic review comment changes Signed-off-by: Jonah Iden * parse markdown cell content as plain text Signed-off-by: Jonah Iden * fixed lint Signed-off-by: Jonah Iden * import type instead of class Co-authored-by: Mark Sujew --------- Signed-off-by: Jonah Iden Co-authored-by: Mark Sujew --- packages/notebook/package.json | 4 +- .../notebook-label-provider-contribution.ts | 63 ++++++++++ .../notebook-outline-contribution.ts | 112 ++++++++++++++++++ .../src/browser/notebook-frontend-module.ts | 9 +- .../src/browser/view-model/notebook-model.ts | 1 + packages/notebook/tsconfig.json | 3 + .../outline-breadcrumbs-contribution.tsx | 4 + .../src/browser/outline-view-service.ts | 9 ++ packages/plugin-ext-vscode/package.json | 1 + .../plugin-vscode-commands-contribution.ts | 8 ++ packages/plugin-ext-vscode/tsconfig.json | 3 + 11 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 packages/notebook/src/browser/contributions/notebook-label-provider-contribution.ts create mode 100644 packages/notebook/src/browser/contributions/notebook-outline-contribution.ts diff --git a/packages/notebook/package.json b/packages/notebook/package.json index fd1e14a2bf514..f96708192efc9 100644 --- a/packages/notebook/package.json +++ b/packages/notebook/package.json @@ -7,6 +7,7 @@ "@theia/editor": "1.48.0", "@theia/filesystem": "1.48.0", "@theia/monaco": "1.48.0", + "@theia/outline-view": "1.48.0", "@theia/monaco-editor-core": "1.83.101", "react-perfect-scrollbar": "^1.5.8", "tslib": "^2.6.2" @@ -45,7 +46,8 @@ }, "devDependencies": { "@theia/ext-scripts": "1.48.0", - "@types/vscode-notebook-renderer": "^1.72.0" + "@types/vscode-notebook-renderer": "^1.72.0", + "@types/markdown-it": "^12.2.3" }, "nyc": { "extends": "../../configs/nyc.json" diff --git a/packages/notebook/src/browser/contributions/notebook-label-provider-contribution.ts b/packages/notebook/src/browser/contributions/notebook-label-provider-contribution.ts new file mode 100644 index 0000000000000..ec75fcf2cc5c5 --- /dev/null +++ b/packages/notebook/src/browser/contributions/notebook-label-provider-contribution.ts @@ -0,0 +1,63 @@ +// ***************************************************************************** +// Copyright (C) 2024 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 { codicon, LabelProvider, LabelProviderContribution } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { CellKind } from '../../common'; +import { NotebookService } from '../service/notebook-service'; +import { NotebookCellOutlineNode } from './notebook-outline-contribution'; +import type Token = require('markdown-it/lib/token'); +import markdownit = require('@theia/core/shared/markdown-it'); + +@injectable() +export class NotebookLabelProviderContribution implements LabelProviderContribution { + + @inject(NotebookService) + protected readonly notebookService: NotebookService; + + @inject(LabelProvider) + protected readonly labelProvider: LabelProvider; + + protected markdownIt = markdownit(); + + canHandle(element: object): number { + if (NotebookCellOutlineNode.is(element)) { + return 200; + } + return 0; + } + + getIcon(element: NotebookCellOutlineNode): string { + return element.notebookCell.cellKind === CellKind.Markup ? codicon('markdown') : codicon('code'); + } + + getName(element: NotebookCellOutlineNode): string { + return element.notebookCell.cellKind === CellKind.Code ? + element.notebookCell.text.split('\n')[0] : + this.extractPlaintext(this.markdownIt.parse(element.notebookCell.text.split('\n')[0], {})); + } + + getLongName(element: NotebookCellOutlineNode): string { + return element.notebookCell.cellKind === CellKind.Code ? + element.notebookCell.text.split('\n')[0] : + this.extractPlaintext(this.markdownIt.parse(element.notebookCell.text.split('\n')[0], {})); + } + + extractPlaintext(parsedMarkdown: Token[]): string { + return parsedMarkdown.map(token => token.children ? this.extractPlaintext(token.children) : token.content).join(''); + } + +} diff --git a/packages/notebook/src/browser/contributions/notebook-outline-contribution.ts b/packages/notebook/src/browser/contributions/notebook-outline-contribution.ts new file mode 100644 index 0000000000000..e1423dbc457a7 --- /dev/null +++ b/packages/notebook/src/browser/contributions/notebook-outline-contribution.ts @@ -0,0 +1,112 @@ +// ***************************************************************************** +// Copyright (C) 2024 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 { codicon, FrontendApplicationContribution, LabelProvider, TreeNode } from '@theia/core/lib/browser'; +import { NotebookEditorWidgetService } from '../service/notebook-editor-widget-service'; +import { OutlineViewService } from '@theia/outline-view/lib/browser/outline-view-service'; +import { NotebookModel } from '../view-model/notebook-model'; +import { OutlineSymbolInformationNode } from '@theia/outline-view/lib/browser/outline-view-widget'; +import { NotebookEditorWidget } from '../notebook-editor-widget'; +import { NotebookCellModel } from '../view-model/notebook-cell-model'; +import { DisposableCollection, URI } from '@theia/core'; +import { CellKind, CellUri } from '../../common'; +import { NotebookService } from '../service/notebook-service'; +export interface NotebookCellOutlineNode extends OutlineSymbolInformationNode { + notebookCell: NotebookCellModel; + uri: URI; +} + +export namespace NotebookCellOutlineNode { + export function is(element: object): element is NotebookCellOutlineNode { + return TreeNode.is(element) && OutlineSymbolInformationNode.is(element) && 'notebookCell' in element; + } +} + +@injectable() +export class NotebookOutlineContribution implements FrontendApplicationContribution { + + @inject(NotebookEditorWidgetService) + protected readonly notebookEditorWidgetService: NotebookEditorWidgetService; + + @inject(OutlineViewService) + protected readonly outlineViewService: OutlineViewService; + + @inject(LabelProvider) + protected readonly labelProvider: LabelProvider; + + @inject(NotebookService) + protected readonly notebookService: NotebookService; + + protected currentEditor?: NotebookEditorWidget; + + protected editorListeners: DisposableCollection = new DisposableCollection(); + protected editorModelListeners: DisposableCollection = new DisposableCollection(); + + onStart(): void { + this.notebookEditorWidgetService.onDidChangeFocusedEditor(editor => this.updateOutline(editor)); + + this.outlineViewService.onDidSelect(node => this.selectCell(node)); + this.outlineViewService.onDidTapNode(node => this.selectCell(node)); + } + + protected async updateOutline(editor: NotebookEditorWidget | undefined): Promise { + if (editor && !editor.isDisposed) { + await editor.ready; + this.currentEditor = editor; + this.editorListeners.dispose(); + this.editorListeners.push(editor.onDidChangeVisibility(() => { + if (this.currentEditor === editor && !editor.isVisible) { + this.outlineViewService.publish([]); + } + })); + if (editor.model) { + this.editorModelListeners.dispose(); + this.editorModelListeners.push(editor.model.onDidChangeSelectedCell(() => { + if (editor === this.currentEditor) { + this.updateOutline(editor); + } + })); + const roots = editor && editor.model && await this.createRoots(editor.model); + this.outlineViewService.publish(roots || []); + } + } + } + + protected async createRoots(model: NotebookModel): Promise { + return model.cells.map(cell => ({ + id: cell.uri.toString(), + iconClass: cell.cellKind === CellKind.Markup ? codicon('markdown') : codicon('code'), + parent: undefined, + children: [], + selected: model.selectedCell === cell, + expanded: false, + notebookCell: cell, + uri: model.uri, + } as NotebookCellOutlineNode)); + } + + selectCell(node: object): void { + if (NotebookCellOutlineNode.is(node)) { + const parsed = CellUri.parse(node.notebookCell.uri); + const model = parsed && this.notebookService.getNotebookEditorModel(parsed.notebook); + if (model) { + model.setSelectedCell(node.notebookCell); + } + } + } + +} diff --git a/packages/notebook/src/browser/notebook-frontend-module.ts b/packages/notebook/src/browser/notebook-frontend-module.ts index 962b4cf4811bc..6255d227283f2 100644 --- a/packages/notebook/src/browser/notebook-frontend-module.ts +++ b/packages/notebook/src/browser/notebook-frontend-module.ts @@ -16,7 +16,7 @@ import '../../src/browser/style/index.css'; import { ContainerModule } from '@theia/core/shared/inversify'; -import { KeybindingContribution, OpenHandler, WidgetFactory } from '@theia/core/lib/browser'; +import { FrontendApplicationContribution, KeybindingContribution, LabelProviderContribution, OpenHandler, WidgetFactory } from '@theia/core/lib/browser'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { NotebookOpenHandler } from './notebook-open-handler'; import { CommandContribution, MenuContribution, ResourceResolver, } from '@theia/core'; @@ -40,6 +40,8 @@ import { NotebookEditorWidgetService } from './service/notebook-editor-widget-se import { NotebookRendererMessagingService } from './service/notebook-renderer-messaging-service'; import { NotebookColorContribution } from './contributions/notebook-color-contribution'; import { NotebookMonacoTextModelService } from './service/notebook-monaco-text-model-service'; +import { NotebookOutlineContribution } from './contributions/notebook-outline-contribution'; +import { NotebookLabelProviderContribution } from './contributions/notebook-label-provider-contribution'; import { NotebookOutputActionContribution } from './contributions/notebook-output-action-contribution'; export default new ContainerModule((bind, unbind, isBound, rebind) => { @@ -93,4 +95,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { ); bind(NotebookMonacoTextModelService).toSelf().inSingletonScope(); + + bind(NotebookOutlineContribution).toSelf().inSingletonScope(); + bind(FrontendApplicationContribution).toService(NotebookOutlineContribution); + bind(NotebookLabelProviderContribution).toSelf().inSingletonScope(); + bind(LabelProviderContribution).toService(NotebookLabelProviderContribution); }); diff --git a/packages/notebook/src/browser/view-model/notebook-model.ts b/packages/notebook/src/browser/view-model/notebook-model.ts index e25c866446240..f6762feaa4f53 100644 --- a/packages/notebook/src/browser/view-model/notebook-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-model.ts @@ -148,6 +148,7 @@ export class NotebookModel implements Saveable, Disposable { this.onDidSaveNotebookEmitter.dispose(); this.onDidAddOrRemoveCellEmitter.dispose(); this.onDidChangeContentEmitter.dispose(); + this.onDidChangeSelectedCellEmitter.dispose(); this.cells.forEach(cell => cell.dispose()); } diff --git a/packages/notebook/tsconfig.json b/packages/notebook/tsconfig.json index 8f53c0fe2dd53..6c992299de5cc 100644 --- a/packages/notebook/tsconfig.json +++ b/packages/notebook/tsconfig.json @@ -20,6 +20,9 @@ }, { "path": "../monaco" + }, + { + "path": "../outline-view" } ] } diff --git a/packages/outline-view/src/browser/outline-breadcrumbs-contribution.tsx b/packages/outline-view/src/browser/outline-breadcrumbs-contribution.tsx index 285d76cc928c3..530e194a49e08 100644 --- a/packages/outline-view/src/browser/outline-breadcrumbs-contribution.tsx +++ b/packages/outline-view/src/browser/outline-breadcrumbs-contribution.tsx @@ -30,10 +30,14 @@ export interface BreadcrumbPopupOutlineViewFactory { export class BreadcrumbPopupOutlineView extends OutlineViewWidget { @inject(OpenerService) protected readonly openerService: OpenerService; + @inject(OutlineViewService) + protected readonly outlineViewService: OutlineViewService; + protected override tapNode(node?: TreeNode): void { if (UriSelection.is(node) && OutlineSymbolInformationNode.hasRange(node)) { open(this.openerService, node.uri, { selection: node.range }); } else { + this.outlineViewService.didTapNode(node as OutlineSymbolInformationNode); super.tapNode(node); } } diff --git a/packages/outline-view/src/browser/outline-view-service.ts b/packages/outline-view/src/browser/outline-view-service.ts index 278813371f661..dd2f58465a0f7 100644 --- a/packages/outline-view/src/browser/outline-view-service.ts +++ b/packages/outline-view/src/browser/outline-view-service.ts @@ -30,6 +30,7 @@ export class OutlineViewService implements WidgetFactory { protected readonly onDidChangeOpenStateEmitter = new Emitter(); protected readonly onDidSelectEmitter = new Emitter(); protected readonly onDidOpenEmitter = new Emitter(); + protected readonly onDidTapNodeEmitter = new Emitter(); constructor(@inject(OutlineViewWidgetFactory) protected factory: OutlineViewWidgetFactory) { } @@ -49,10 +50,18 @@ export class OutlineViewService implements WidgetFactory { return this.onDidChangeOpenStateEmitter.event; } + get onDidTapNode(): Event { + return this.onDidTapNodeEmitter.event; + } + get open(): boolean { return this.widget !== undefined && this.widget.isVisible; } + didTapNode(node: OutlineSymbolInformationNode): void { + this.onDidTapNodeEmitter.fire(node); + } + /** * Publish the collection of outline view symbols. * - Publishing includes setting the `OutlineViewWidget` tree with symbol information. diff --git a/packages/plugin-ext-vscode/package.json b/packages/plugin-ext-vscode/package.json index b7fc61a1e8df8..a753581864f20 100644 --- a/packages/plugin-ext-vscode/package.json +++ b/packages/plugin-ext-vscode/package.json @@ -16,6 +16,7 @@ "@theia/typehierarchy": "1.48.0", "@theia/userstorage": "1.48.0", "@theia/workspace": "1.48.0", + "@theia/outline-view": "1.48.0", "decompress": "^4.2.1", "filenamify": "^4.1.0", "tslib": "^2.6.2" diff --git a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts index d7bce9e0a203a..092f71e93d16a 100755 --- a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts +++ b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts @@ -79,6 +79,7 @@ import { WindowService } from '@theia/core/lib/browser/window/window-service'; import * as monaco from '@theia/monaco-editor-core'; import { VSCodeExtensionUri } from '../common/plugin-vscode-uri'; import { CodeEditorWidgetUtil } from '@theia/plugin-ext/lib/main/browser/menus/vscode-theia-menu-mappings'; +import { OutlineViewContribution } from '@theia/outline-view/lib/browser/outline-view-contribution'; export namespace VscodeCommands { export const OPEN: Command = { @@ -180,6 +181,8 @@ export class PluginVscodeCommandsContribution implements CommandContribution { protected readonly windowService: WindowService; @inject(MessageService) protected readonly messageService: MessageService; + @inject(OutlineViewContribution) + protected outlineViewContribution: OutlineViewContribution; private async openWith(commandId: string, resource: URI, columnOrOptions?: ViewColumn | TextDocumentShowOptions, openerId?: string): Promise { if (!resource) { @@ -912,6 +915,11 @@ export class PluginVscodeCommandsContribution implements CommandContribution { }; } }); + + // required by Jupyter for the show table of contents action + commands.registerCommand({ id: 'outline.focus' }, { + execute: () => this.outlineViewContribution.openView({ activate: true }) + }); } private async resolveLanguageId(resource: URI): Promise { diff --git a/packages/plugin-ext-vscode/tsconfig.json b/packages/plugin-ext-vscode/tsconfig.json index deb8724d9036a..c1cb4d0e2f0d2 100644 --- a/packages/plugin-ext-vscode/tsconfig.json +++ b/packages/plugin-ext-vscode/tsconfig.json @@ -32,6 +32,9 @@ { "path": "../navigator" }, + { + "path": "../outline-view" + }, { "path": "../plugin" },