From 81d1654731d3467dd5ff7ebbbd4ae9eb18ecee63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Collonval?= Date: Fri, 8 Sep 2023 11:22:16 +0200 Subject: [PATCH] Backport PR #50: Add factory token for jupytext customization --- .gitignore | 1 + packages/application/src/plugins/rise.ts | 7 +- packages/lab/package.json | 1 + packages/lab/src/index.ts | 73 +++++++------- packages/lab/src/preview.ts | 123 +++++++++++++++++++---- packages/lab/src/tokens.ts | 45 +++++++++ 6 files changed, 192 insertions(+), 58 deletions(-) create mode 100644 packages/lab/src/tokens.ts diff --git a/.gitignore b/.gitignore index 41d5cbd..f052292 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.bundle.* lib/ node_modules/ +.yarn/ .eslintcache .stylelintcache *.egg-info/ diff --git a/packages/application/src/plugins/rise.ts b/packages/application/src/plugins/rise.ts index b4ee748..00072e5 100644 --- a/packages/application/src/plugins/rise.ts +++ b/packages/application/src/plugins/rise.ts @@ -88,7 +88,12 @@ export const plugin: JupyterFrontEndPlugin = { app.restored ]).then(async ([settings]) => { const notebookPath = PageConfig.getOption('notebookPath'); - const notebookPanel = documentManager.open(notebookPath) as NotebookPanel; + const notebookPanel = (documentManager.open(notebookPath, 'Notebook') ?? + // If the file cannot be opened with the Notebook factory, try jupytext + documentManager.open( + notebookPath, + 'Jupytext Notebook' + )) as NotebookPanel; Rise.registerCommands(app.commands, notebookPanel, trans); if (palette) { diff --git a/packages/lab/package.json b/packages/lab/package.json index 86fe211..c6c09b3 100644 --- a/packages/lab/package.json +++ b/packages/lab/package.json @@ -55,6 +55,7 @@ "@lumino/coreutils": "^1.11.0", "@lumino/disposable": "^1.7.0", "@lumino/messaging": "^1.10.1", + "@lumino/signaling": "^1.11.1", "@lumino/widgets": "^1.19.0" }, "devDependencies": { diff --git a/packages/lab/src/index.ts b/packages/lab/src/index.ts index e992b2a..5afd2a7 100644 --- a/packages/lab/src/index.ts +++ b/packages/lab/src/index.ts @@ -11,8 +11,6 @@ import { WidgetTracker } from '@jupyterlab/apputils'; -import { PageConfig, URLExt } from '@jupyterlab/coreutils'; - import { DocumentRegistry } from '@jupyterlab/docregistry'; import { @@ -24,7 +22,7 @@ import { import { ISettingRegistry } from '@jupyterlab/settingregistry'; -import { ITranslator } from '@jupyterlab/translation'; +import { ITranslator, nullTranslator } from '@jupyterlab/translation'; import { toArray } from '@lumino/algorithm'; @@ -32,13 +30,11 @@ import { ReadonlyPartialJSONObject } from '@lumino/coreutils'; import { fullScreenIcon, RISEIcon } from './icons'; -import { - RisePreview, - IRisePreviewTracker, - RisePreviewFactory -} from './preview'; +import { RisePreview } from './preview'; -export { IRisePreviewTracker } from './preview'; +import { IRisePreviewFactory, IRisePreviewTracker } from './tokens'; + +export { IRisePreviewFactory, IRisePreviewTracker } from './tokens'; /** * Command IDs namespace for JupyterLab RISE extension @@ -59,27 +55,46 @@ namespace CommandIDs { export const riseSetSlideType = 'RISE:set-slide-type'; } +const factory: JupyterFrontEndPlugin = { + id: 'jupyterlab-rise:factory', + provides: IRisePreviewFactory, + optional: [ITranslator], + activate: ( + app: JupyterFrontEnd, + translator: ITranslator | null + ): IRisePreviewFactory => { + const { commands, docRegistry } = app; + return new RisePreview.FactoryToken({ + commands, + docRegistry, + translator: translator ?? undefined + }); + } +}; + /** * Open the notebook with RISE. */ const plugin: JupyterFrontEndPlugin = { id: 'jupyterlab-rise:plugin', autoStart: true, - requires: [ITranslator], + requires: [IRisePreviewFactory], optional: [ INotebookTracker, ICommandPalette, ILayoutRestorer, - ISettingRegistry + ISettingRegistry, + ITranslator ], provides: IRisePreviewTracker, activate: ( app: JupyterFrontEnd, - translator: ITranslator, + factory: IRisePreviewFactory, notebookTracker: INotebookTracker | null, palette: ICommandPalette | null, restorer: ILayoutRestorer | null, - settingRegistry: ISettingRegistry | null + settingRegistry: ISettingRegistry | null, + translator: ITranslator | null ): IRisePreviewTracker => { console.log('JupyterLab extension jupyterlab-rise is activated!'); @@ -92,8 +107,8 @@ const plugin: JupyterFrontEndPlugin = { return tracker; } - const { commands, docRegistry, shell } = app; - const trans = translator.load('rise'); + const { commands, shell } = app; + const trans = (translator ?? nullTranslator).load('rise'); let settings: ISettingRegistry.ISettings | null = null; if (settingRegistry) { @@ -102,27 +117,19 @@ const plugin: JupyterFrontEndPlugin = { }); } - const factory = new RisePreviewFactory(getRiseUrl, commands, { - name: 'rise', - fileTypes: ['notebook'], - modelName: 'notebook' - }); - if (restorer) { restorer.restore(tracker, { // Need to modify to handle auto full screen command: 'docmanager:open', args: panel => ({ path: panel.context.path, - factory: factory.name + factory: RisePreview.FACTORY_NAME }), name: panel => panel.context.path, when: app.serviceManager.ready }); } - docRegistry.addWidgetFactory(factory); - function getCurrent(args: ReadonlyPartialJSONObject): NotebookPanel | null { const widget = notebookTracker?.currentWidget ?? null; const activate = args['activate'] !== false; @@ -141,15 +148,6 @@ const plugin: JupyterFrontEndPlugin = { ); } - function getRiseUrl(path: string, activeCellIndex?: number): string { - const baseUrl = PageConfig.getBaseUrl(); - let url = `${baseUrl}rise/${path}`; - if (typeof activeCellIndex === 'number') { - url += URLExt.objectToQueryString({ activeCellIndex }); - } - return url; - } - factory.widgetCreated.connect((sender, widget) => { // Notify the widget tracker if restore data needs to update. widget.context.pathChanged.connect(() => { @@ -175,7 +173,10 @@ const plugin: JupyterFrontEndPlugin = { } await current.context.save(); window.open( - getRiseUrl(current.context.path, current.content.activeCellIndex) + RisePreview.getRiseUrl( + current.context.path, + current.content.activeCellIndex + ) ); }, isEnabled @@ -197,7 +198,7 @@ const plugin: JupyterFrontEndPlugin = { 'docmanager:open', { path: context.path, - factory: 'rise', + factory: RisePreview.FACTORY_NAME, options: { mode: 'split-right' } @@ -378,4 +379,4 @@ const plugin: JupyterFrontEndPlugin = { } }; -export default plugin; +export default [factory, plugin]; diff --git a/packages/lab/src/preview.ts b/packages/lab/src/preview.ts index a48c7ac..db5bcfe 100644 --- a/packages/lab/src/preview.ts +++ b/packages/lab/src/preview.ts @@ -1,9 +1,6 @@ -import { - IFrame, - ToolbarButton, - IWidgetTracker, - Toolbar -} from '@jupyterlab/apputils'; +import { IFrame, ToolbarButton, Toolbar } from '@jupyterlab/apputils'; + +import { PageConfig, URLExt } from '@jupyterlab/coreutils'; import { ABCWidgetFactory, @@ -19,28 +16,18 @@ import { refreshIcon } from '@jupyterlab/ui-components'; import { CommandRegistry } from '@lumino/commands'; -import { PromiseDelegate, Token } from '@lumino/coreutils'; +import { PromiseDelegate } from '@lumino/coreutils'; + +import { DisposableSet, IDisposable } from '@lumino/disposable'; import { Message } from '@lumino/messaging'; -import { Signal } from '@lumino/signaling'; +import { ISignal, Signal } from '@lumino/signaling'; import { Widget } from '@lumino/widgets'; import { fullScreenIcon, RISEIcon } from './icons'; - -/** - * A class that tracks Rise Preview widgets. - */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface IRisePreviewTracker extends IWidgetTracker {} - -/** - * The Rise Preview tracker token. - */ -export const IRisePreviewTracker = new Token( - 'jupyterlab-rise:IRisePreviewTracker' -); +import { IRisePreviewFactory } from './tokens'; /** * A DocumentWidget that shows a Rise preview in an IFrame. @@ -230,6 +217,8 @@ export class RisePreview extends DocumentWidget { * A namespace for RisePreview statics. */ export namespace RisePreview { + export const FACTORY_NAME = 'rise'; + /** * Instantiation options for `RisePreview`. */ @@ -249,8 +238,100 @@ export namespace RisePreview { */ renderOnSave?: boolean; } + + /** + * Generate the URL required to open a file as RISE slideshow. + * + * @param path File path + * @param activeCellIndex Active cell index + * @returns URL to open + */ + export function getRiseUrl(path: string, activeCellIndex?: number): string { + const baseUrl = PageConfig.getBaseUrl(); + let url = `${baseUrl}rise/${path}`; + if (typeof activeCellIndex === 'number') { + url += URLExt.objectToQueryString({ activeCellIndex }); + } + return url; + } + + /** + * RISE Preview document factory token implementation. + */ + export class FactoryToken implements IRisePreviewFactory { + constructor({ + commands, + docRegistry, + fileTypes, + translator + }: { + commands: CommandRegistry; + docRegistry: DocumentRegistry; + fileTypes?: string[]; + translator?: ITranslator; + }) { + this._commands = commands; + this._docRegistry = docRegistry; + this._fileTypes = fileTypes ?? ['notebook']; + + this._updateFactory(); + } + + /** + * Add a new file type to the RISE preview factory. + * + * #### Notes + * Useful to add file types for jupytext. + * + * @param ft File type + */ + addFileType(ft: string): void { + if (!this._fileTypes.includes(ft)) { + this._fileTypes.push(ft); + this._updateFactory(); + } + } + + /** + * Signal emitted when a RISE preview is created. + */ + get widgetCreated(): ISignal { + return this._widgetCreated; + } + + private _updateFactory(): void { + if (this._disposeFactory) { + this._disposeFactory.dispose(); + this._disposeFactory = null; + } + + const factory = new RisePreviewFactory(getRiseUrl, this._commands, { + name: FACTORY_NAME, + fileTypes: this._fileTypes, + modelName: 'notebook' + }); + + factory.widgetCreated.connect((_, args) => { + this._widgetCreated.emit(args); + }, this); + + this._disposeFactory = DisposableSet.from([ + this._docRegistry.addWidgetFactory(factory), + factory + ]); + } + + private _commands: CommandRegistry; + private _disposeFactory: IDisposable | null = null; + private _docRegistry: DocumentRegistry; + private _fileTypes: string[]; + private _widgetCreated = new Signal(this); + } } +/** + * RISE Preview widget factory + */ export class RisePreviewFactory extends ABCWidgetFactory< RisePreview, INotebookModel diff --git a/packages/lab/src/tokens.ts b/packages/lab/src/tokens.ts new file mode 100644 index 0000000..08b58ac --- /dev/null +++ b/packages/lab/src/tokens.ts @@ -0,0 +1,45 @@ +import { IWidgetTracker } from '@jupyterlab/apputils'; +import { Token } from '@lumino/coreutils'; +import { ISignal } from '@lumino/signaling'; +import type { RisePreview } from './preview'; + +/** + * A class that tracks Rise Preview widgets. + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface IRisePreviewTracker extends IWidgetTracker {} + +/** + * The Rise Preview tracker token. + */ +export const IRisePreviewTracker = new Token( + 'jupyterlab-rise:IRisePreviewTracker', + 'Adds a tracker for RISE slides preview widgets.' +); + +/** + * RISE Preview document factory interface + */ +export interface IRisePreviewFactory { + /** + * Signal emitted when a RISE preview is created. + */ + readonly widgetCreated: ISignal; + /** + * Add a new file type to the RISE preview factory. + * + * #### Notes + * Useful to add file types for jupytext. + * + * @param ft File type + */ + addFileType(ft: string): void; +} + +/** + * RISE Preview factory token. + */ +export const IRisePreviewFactory = new Token( + 'jupyterlab-rise:IRisePreviewFactory', + 'Customize the RISE slides preview factory.' +);