Skip to content

Commit

Permalink
Notebook Outline-View and Breadcrumbs. (#13562)
Browse files Browse the repository at this point in the history
* notbook outline and breadcrumbs
+ jupyter show Table of contents menu item working

Signed-off-by: Jonah Iden <[email protected]>

* some basic review comment changes

Signed-off-by: Jonah Iden <[email protected]>

* parse markdown cell content as plain text

Signed-off-by: Jonah Iden <[email protected]>

* fixed lint

Signed-off-by: Jonah Iden <[email protected]>

* import type instead of class

Co-authored-by: Mark Sujew <[email protected]>

---------

Signed-off-by: Jonah Iden <[email protected]>
Co-authored-by: Mark Sujew <[email protected]>
  • Loading branch information
jonah-iden and msujew authored Apr 5, 2024
1 parent 09051c4 commit 52ab029
Show file tree
Hide file tree
Showing 11 changed files with 215 additions and 2 deletions.
4 changes: 3 additions & 1 deletion packages/notebook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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('');
}

}
Original file line number Diff line number Diff line change
@@ -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<void> {
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<OutlineSymbolInformationNode[] | undefined> {
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);
}
}
}

}
9 changes: 8 additions & 1 deletion packages/notebook/src/browser/notebook-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) => {
Expand Down Expand Up @@ -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);
});
1 change: 1 addition & 0 deletions packages/notebook/src/browser/view-model/notebook-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

Expand Down
3 changes: 3 additions & 0 deletions packages/notebook/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
},
{
"path": "../monaco"
},
{
"path": "../outline-view"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
9 changes: 9 additions & 0 deletions packages/outline-view/src/browser/outline-view-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export class OutlineViewService implements WidgetFactory {
protected readonly onDidChangeOpenStateEmitter = new Emitter<boolean>();
protected readonly onDidSelectEmitter = new Emitter<OutlineSymbolInformationNode>();
protected readonly onDidOpenEmitter = new Emitter<OutlineSymbolInformationNode>();
protected readonly onDidTapNodeEmitter = new Emitter<OutlineSymbolInformationNode>();

constructor(@inject(OutlineViewWidgetFactory) protected factory: OutlineViewWidgetFactory) { }

Expand All @@ -49,10 +50,18 @@ export class OutlineViewService implements WidgetFactory {
return this.onDidChangeOpenStateEmitter.event;
}

get onDidTapNode(): Event<OutlineSymbolInformationNode> {
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.
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-ext-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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<boolean> {
if (!resource) {
Expand Down Expand Up @@ -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<string> {
Expand Down
3 changes: 3 additions & 0 deletions packages/plugin-ext-vscode/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
{
"path": "../navigator"
},
{
"path": "../outline-view"
},
{
"path": "../plugin"
},
Expand Down

0 comments on commit 52ab029

Please sign in to comment.