diff --git a/docs/dependency-graph.svg b/docs/dependency-graph.svg index e7f696c5f10a..dbc200f9e79c 100644 --- a/docs/dependency-graph.svg +++ b/docs/dependency-graph.svg @@ -4,332 +4,358 @@ - - + + G - + application - - -application + + +application docregistry - - -docregistry + + +docregistry application->docregistry - - + + rendermime - - -rendermime + + +rendermime docregistry->rendermime - - + + apputils - - -apputils + + +apputils services - - -services + + +services apputils->services - - + + coreutils - - -coreutils + + +coreutils -services->coreutils - - - - -rendermime->apputils - - +services->coreutils + + rendermime-interfaces - - -rendermime-interfaces + + +rendermime-interfaces -rendermime->rendermime-interfaces - - +rendermime->rendermime-interfaces + + codemirror - - -codemirror + + +codemirror -rendermime->codemirror - - +rendermime->codemirror + + + + +codemirror->apputils + + codeeditor - - -codeeditor + + +codeeditor codemirror->codeeditor - - + + cells - - -cells + + +cells outputarea - - -outputarea + + +outputarea -cells->outputarea - - +cells->outputarea + + outputarea->rendermime - - + + -codeeditor->coreutils - - +codeeditor->coreutils + + completer - - -completer + + +completer completer->apputils - - + + completer->codeeditor - - + + console - - -console + + +console console->cells - - + + csvviewer - - -csvviewer + + +csvviewer csvviewer->docregistry - - + + docmanager - - -docmanager + + +docmanager docmanager->docregistry - - + + filebrowser - - -filebrowser + + +filebrowser filebrowser->docmanager - - + + fileeditor - - -fileeditor + + +fileeditor fileeditor->docregistry - - + + imageviewer - - -imageviewer + + +imageviewer imageviewer->docregistry - - + + inspector - - -inspector + + +inspector inspector->rendermime - - + + launcher - - -launcher + + +launcher launcher->apputils - - + + + + +mainmenu + + +mainmenu + + + + +mainmenu->apputils + + -notebook - - -notebook +notebook + + +notebook -notebook->docregistry - - +notebook->docregistry + + -notebook->cells - - +notebook->cells + + -running - - -running +running + + +running -running->apputils - - +running->apputils + + + + +settingeditor + + +settingeditor + + + + +settingeditor->inspector + + -terminal - - -terminal +terminal + + +terminal -terminal->apputils - - +terminal->apputils + + -tooltip - - -tooltip +tooltip + + +tooltip -tooltip->rendermime - - +tooltip->rendermime + + diff --git a/docs/scripts/graph-dependencies.js b/docs/scripts/graph-dependencies.js index feaa261b7549..d863856e0c39 100644 --- a/docs/scripts/graph-dependencies.js +++ b/docs/scripts/graph-dependencies.js @@ -2,6 +2,7 @@ var childProcess = require('child_process'); var fs = require('fs-extra'); var glob = require('glob'); var path = require('path'); +var url = require('url'); var basePath = path.resolve('..'); var baseUrl = 'https://github.com/jupyterlab/jupyterlab/tree/master/packages'; @@ -40,7 +41,7 @@ packages.forEach(function(packagePath) { } // Construct a URL to the package on GitHub. - var Url = path.join(baseUrl, path.basename(packagePath)); + var Url = url.resolve(baseUrl, 'packages/'+path.basename(packagePath)); // Remove the '@jupyterlab' part of the name. var name = '"'+data.name.split('/')[1] +'"'; diff --git a/jupyterlab/package.json b/jupyterlab/package.json index 3059968ef8bc..f0535df90c4a 100644 --- a/jupyterlab/package.json +++ b/jupyterlab/package.json @@ -41,6 +41,8 @@ "@jupyterlab/json-extension": "^0.12.0", "@jupyterlab/launcher": "^0.12.0", "@jupyterlab/launcher-extension": "^0.12.0", + "@jupyterlab/mainmenu": "^0.1.0", + "@jupyterlab/mainmenu-extension": "^0.1.0", "@jupyterlab/markdownviewer-extension": "^0.12.0", "@jupyterlab/notebook": "^0.12.0", "@jupyterlab/notebook-extension": "^0.12.0", @@ -98,6 +100,7 @@ "@jupyterlab/imageviewer-extension": "", "@jupyterlab/inspector-extension": "", "@jupyterlab/launcher-extension": "", + "@jupyterlab/mainmenu-extension": "", "@jupyterlab/markdownviewer-extension": "", "@jupyterlab/notebook-extension": "", "@jupyterlab/running-extension": "", @@ -169,6 +172,8 @@ "@jupyterlab/json-extension": "../packages/json-extension", "@jupyterlab/launcher": "../packages/launcher", "@jupyterlab/launcher-extension": "../packages/launcher-extension", + "@jupyterlab/mainmenu": "../packages/mainmenu", + "@jupyterlab/mainmenu-extension": "../packages/mainmenu-extension", "@jupyterlab/markdownviewer-extension": "../packages/markdownviewer-extension", "@jupyterlab/notebook": "../packages/notebook", "@jupyterlab/notebook-extension": "../packages/notebook-extension", diff --git a/packages/application-extension/src/index.ts b/packages/application-extension/src/index.ts index 5486a449a2cb..d6192b7abac3 100644 --- a/packages/application-extension/src/index.ts +++ b/packages/application-extension/src/index.ts @@ -47,6 +47,12 @@ const main: JupyterLabPlugin = { activate: (app: JupyterLab, palette: ICommandPalette) => { addCommands(app, palette); + // If the currently active widget changes, + // trigger a refresh of the commands. + app.shell.currentChanged.connect(() => { + app.commands.notifyCommandChanged(CommandIDs.closeAll); + }); + let builder = app.serviceManager.builder; let doBuild = () => { diff --git a/packages/apputils-extension/src/index.ts b/packages/apputils-extension/src/index.ts index 90d73af60f64..0a5c1be2ba36 100644 --- a/packages/apputils-extension/src/index.ts +++ b/packages/apputils-extension/src/index.ts @@ -8,7 +8,7 @@ import { } from '@jupyterlab/application'; import { - ICommandPalette, IMainMenu, MainMenu, IThemeManager, ThemeManager, + ICommandPalette, IThemeManager, ThemeManager, ISplashScreen } from '@jupyterlab/apputils'; @@ -28,10 +28,6 @@ import { DisposableDelegate, IDisposable } from '@phosphor/disposable'; -import { - Widget -} from '@phosphor/widgets'; - import { activatePalette } from './palette'; @@ -102,29 +98,6 @@ class SettingsConnector extends DataConnector } -/** - * A service providing an interface to the main menu. - */ -const menu: JupyterLabPlugin = { - id: '@jupyterlab/apputils-extension:menu', - provides: IMainMenu, - activate: (app: JupyterLab): IMainMenu => { - let menu = new MainMenu(); - menu.id = 'jp-MainMenu'; - - let logo = new Widget(); - logo.addClass('jp-MainAreaPortraitIcon'); - logo.addClass('jp-JupyterIcon'); - logo.id = 'jp-MainLogo'; - - app.shell.addToTopArea(logo); - app.shell.addToTopArea(menu); - - return menu; - } -}; - - /** * The default commmand palette extension. */ @@ -231,7 +204,7 @@ const state: JupyterLabPlugin = { * Export the plugins as default. */ const plugins: JupyterLabPlugin[] = [ - menu, palette, settings, state, splash, themes + palette, settings, state, splash, themes ]; export default plugins; diff --git a/packages/apputils/src/index.ts b/packages/apputils/src/index.ts index 0594f6c748d8..e0909f8221b7 100644 --- a/packages/apputils/src/index.ts +++ b/packages/apputils/src/index.ts @@ -12,7 +12,6 @@ export * from './domutils'; export * from './hoverbox'; export * from './iframe'; export * from './instancetracker'; -export * from './mainmenu'; export * from './sanitizer'; export * from './spinner'; export * from './splash'; diff --git a/packages/codemirror-extension/package.json b/packages/codemirror-extension/package.json index 46ff60b3c729..8fa05ae648d9 100644 --- a/packages/codemirror-extension/package.json +++ b/packages/codemirror-extension/package.json @@ -36,6 +36,7 @@ "@jupyterlab/codemirror": "^0.12.0", "@jupyterlab/coreutils": "^0.12.0", "@jupyterlab/fileeditor": "^0.12.0", + "@jupyterlab/mainmenu": "^0.1.0", "@phosphor/coreutils": "^1.3.0", "@phosphor/widgets": "^1.5.0" }, diff --git a/packages/codemirror-extension/src/index.ts b/packages/codemirror-extension/src/index.ts index 3a9e1cb5f652..7ae5a21e2e51 100644 --- a/packages/codemirror-extension/src/index.ts +++ b/packages/codemirror-extension/src/index.ts @@ -14,9 +14,13 @@ import { } from '@jupyterlab/application'; import { - ICommandPalette, IMainMenu + ICommandPalette } from '@jupyterlab/apputils'; +import { + IMainMenu, IEditMenu +} from '@jupyterlab/mainmenu'; + import { IEditorServices } from '@jupyterlab/codeeditor'; @@ -30,7 +34,7 @@ import { } from '@jupyterlab/coreutils'; import { - IEditorTracker + IEditorTracker, FileEditor } from '@jupyterlab/fileeditor'; @@ -146,18 +150,12 @@ function activateEditorCommands(app: JupyterLab, tracker: IEditorTracker, mainMe } }); - // Update the command registry when the codemirror state changes. - tracker.currentChanged.connect(() => { - if (tracker.size <= 1) { - commands.notifyCommandChanged(CommandIDs.changeKeyMap); - } - }); - /** * A test for whether the tracker has an active widget. */ - function hasWidget(): boolean { - return tracker.currentWidget !== null; + function isEnabled(): boolean { + return tracker.currentWidget !== null && + tracker.currentWidget === app.shell.currentWidget; } /** @@ -187,7 +185,7 @@ function activateEditorCommands(app: JupyterLab, tracker: IEditorTracker, mainMe console.error(`Failed to set ${id}:${key} - ${reason.message}`); }); }, - isEnabled: hasWidget, + isEnabled, isToggled: args => args['theme'] === theme }); @@ -205,12 +203,12 @@ function activateEditorCommands(app: JupyterLab, tracker: IEditorTracker, mainMe console.error(`Failed to set ${id}:${key} - ${reason.message}`); }); }, - isEnabled: hasWidget, + isEnabled, isToggled: args => args['keyMap'] === keyMap }); commands.addCommand(CommandIDs.find, { - label: 'Find', + label: 'Find...', execute: () => { let widget = tracker.currentWidget; if (!widget) { @@ -219,11 +217,11 @@ function activateEditorCommands(app: JupyterLab, tracker: IEditorTracker, mainMe let editor = widget.editor as CodeMirrorEditor; editor.execCommand('find'); }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.findAndReplace, { - label: 'Find & Replace', + label: 'Find & Replace...', execute: () => { let widget = tracker.currentWidget; if (!widget) { @@ -232,7 +230,7 @@ function activateEditorCommands(app: JupyterLab, tracker: IEditorTracker, mainMe let editor = widget.editor as CodeMirrorEditor; editor.execCommand('replace'); }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.changeMode, { @@ -247,7 +245,7 @@ function activateEditorCommands(app: JupyterLab, tracker: IEditorTracker, mainMe } } }, - isEnabled: hasWidget, + isEnabled, isToggled: args => { let widget = tracker.currentWidget; if (!widget) { @@ -306,15 +304,10 @@ function activateEditorCommands(app: JupyterLab, tracker: IEditorTracker, mainMe palette.addItem({ command, args, category: 'Editor' }); } - menu.addItem({ type: 'submenu', submenu: modeMenu }); + menu.addItem({ command: 'fileeditor:toggle-autoclosing-brackets' }); menu.addItem({ type: 'submenu', submenu: tabMenu }); - menu.addItem({ command: CommandIDs.find }); - menu.addItem({ command: CommandIDs.findAndReplace }); menu.addItem({ type: 'separator' }); - menu.addItem({ command: 'fileeditor:toggle-line-numbers' }); - menu.addItem({ command: 'fileeditor:toggle-line-wrap' }); - menu.addItem({ command: 'fileeditor:toggle-match-brackets' }); - menu.addItem({ command: 'fileeditor:toggle-autoclosing-brackets' }); + menu.addItem({ type: 'submenu', submenu: modeMenu }); menu.addItem({ type: 'submenu', submenu: keyMapMenu }); menu.addItem({ type: 'submenu', submenu: themeMenu }); @@ -323,13 +316,16 @@ function activateEditorCommands(app: JupyterLab, tracker: IEditorTracker, mainMe mainMenu.addMenu(createMenu(), { rank: 30 }); - [ - 'editor:line-numbers', - 'editor:line-wrap', - 'editor:match-brackets', - 'editor-autoclosing-brackets', - 'editor:create-console', - 'editor:run-code' - ].forEach(command => palette.addItem({ command, category: 'Editor' })); - + // Add find-replace capabilities to the edit menu. + mainMenu.editMenu.findReplacers.set('Editor', { + tracker, + find: (widget: FileEditor) => { + let editor = widget.editor as CodeMirrorEditor; + editor.execCommand('find'); + }, + findAndReplace: (widget: FileEditor) => { + let editor = widget.editor as CodeMirrorEditor; + editor.execCommand('replace'); + } + } as IEditMenu.IFindReplacer) } diff --git a/packages/console-extension/package.json b/packages/console-extension/package.json index b92585328628..4d9c387508c6 100644 --- a/packages/console-extension/package.json +++ b/packages/console-extension/package.json @@ -34,10 +34,11 @@ "@jupyterlab/codeeditor": "^0.12.0", "@jupyterlab/console": "^0.12.0", "@jupyterlab/coreutils": "^0.12.0", + "@jupyterlab/filebrowser": "^0.12.1", "@jupyterlab/launcher": "^0.12.0", + "@jupyterlab/mainmenu": "^0.1.0", "@phosphor/algorithm": "^1.1.2", - "@phosphor/coreutils": "^1.3.0", - "@phosphor/widgets": "^1.5.0" + "@phosphor/coreutils": "^1.3.0" }, "devDependencies": { "rimraf": "~2.6.2", diff --git a/packages/console-extension/src/index.ts b/packages/console-extension/src/index.ts index f757c1e8cd68..17534e88ee17 100644 --- a/packages/console-extension/src/index.ts +++ b/packages/console-extension/src/index.ts @@ -6,7 +6,7 @@ import { } from '@jupyterlab/application'; import { - Dialog, ICommandPalette, IMainMenu, InstanceTracker, showDialog + Dialog, ICommandPalette, InstanceTracker, showDialog } from '@jupyterlab/apputils'; import { @@ -21,10 +21,18 @@ import { PageConfig } from '@jupyterlab/coreutils'; +import { + IFileBrowserFactory +} from '@jupyterlab/filebrowser'; + import { ILauncher } from '@jupyterlab/launcher'; +import { + IEditMenu, IFileMenu, IKernelMenu, IMainMenu, IRunMenu +} from '@jupyterlab/mainmenu'; + import { find } from '@phosphor/algorithm'; @@ -33,10 +41,6 @@ import { ReadonlyJSONObject } from '@phosphor/coreutils'; -import { - Menu -} from '@phosphor/widgets'; - /** * The command IDs used by the console plugin. @@ -49,7 +53,7 @@ namespace CommandIDs { const clear = 'console:clear'; export - const run = 'console:run'; + const runUnforced = 'console:run-unforced'; export const runForced = 'console:run-forced'; @@ -88,7 +92,8 @@ const tracker: JupyterLabPlugin = { ICommandPalette, ConsolePanel.IContentFactory, IEditorServices, - ILayoutRestorer + ILayoutRestorer, + IFileBrowserFactory ], optional: [ILauncher], activate: activateConsole, @@ -122,11 +127,10 @@ export default plugins; /** * Activate the console extension. */ -function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette, contentFactory: ConsolePanel.IContentFactory, editorServices: IEditorServices, restorer: ILayoutRestorer, launcher: ILauncher | null): IConsoleTracker { +function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette, contentFactory: ConsolePanel.IContentFactory, editorServices: IEditorServices, restorer: ILayoutRestorer, browserFactory: IFileBrowserFactory, launcher: ILauncher | null): IConsoleTracker { const manager = app.serviceManager; const { commands, shell } = app; const category = 'Console'; - const menu = new Menu({ commands }); // Create an instance tracker for all console panels. const tracker = new InstanceTracker({ namespace: 'console' }); @@ -142,13 +146,6 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand when: manager.ready }); - // Update the command registry when the console state changes. - tracker.currentChanged.connect(() => { - if (tracker.size <= 1) { - commands.notifyCommandChanged(CommandIDs.interrupt); - } - }); - // The launcher callback. let callback = (cwd: string, name: string) => { return createConsole({ basePath: cwd, kernelPreference: { name } }); @@ -183,9 +180,6 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand }); } - // Set the main menu title. - menu.title.label = category; - /** * Create a console for a given path. */ @@ -210,8 +204,9 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand /** * Whether there is an active console. */ - function hasWidget(): boolean { - return tracker.currentWidget !== null; + function isEnabled(): boolean { + return tracker.currentWidget !== null + && tracker.currentWidget === app.shell.currentWidget; } let command = CommandIDs.open; @@ -239,13 +234,12 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand command = CommandIDs.create; commands.addCommand(command, { - label: 'Start New Console', + label: 'Console', execute: (args: Partial) => { - let basePath = args.basePath || '.'; + let basePath = args.basePath || browserFactory.defaultBrowser.model.path; return createConsole({ basePath, ...args }); } }); - palette.addItem({ command, category }); // Get the current widget and activate unless the args specify otherwise. function getCurrent(args: ReadonlyJSONObject): ConsolePanel | null { @@ -267,13 +261,13 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand } current.console.clear(); }, - isEnabled: hasWidget + isEnabled }); palette.addItem({ command, category }); - command = CommandIDs.run; + command = CommandIDs.runUnforced; commands.addCommand(command, { - label: 'Run Cell', + label: 'Run Cell (unforced)', execute: args => { let current = getCurrent(args); if (!current) { @@ -281,7 +275,7 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand } return current.console.execute(); }, - isEnabled: hasWidget + isEnabled }); palette.addItem({ command, category }); @@ -295,7 +289,7 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand } current.console.execute(true); }, - isEnabled: hasWidget + isEnabled }); palette.addItem({ command, category }); @@ -309,7 +303,7 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand } current.console.insertLinebreak(); }, - isEnabled: hasWidget + isEnabled }); palette.addItem({ command, category }); @@ -326,7 +320,7 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand return kernel.interrupt(); } }, - isEnabled: hasWidget + isEnabled }); palette.addItem({ command, category }); @@ -340,7 +334,7 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand } return current.console.session.restart(); }, - isEnabled: hasWidget + isEnabled }); palette.addItem({ command, category }); @@ -366,7 +360,7 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand } }); }, - isEnabled: hasWidget + isEnabled }); command = CommandIDs.inject; @@ -384,7 +378,7 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand return false; }); }, - isEnabled: hasWidget + isEnabled }); command = CommandIDs.changeKernel; @@ -397,23 +391,65 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand } return current.console.session.selectKernel(); }, - isEnabled: hasWidget + isEnabled }); palette.addItem({ command, category }); - menu.addItem({ command: CommandIDs.run }); - menu.addItem({ command: CommandIDs.runForced }); - menu.addItem({ command: CommandIDs.linebreak }); - menu.addItem({ type: 'separator' }); - menu.addItem({ command: CommandIDs.clear }); - menu.addItem({ type: 'separator' }); - menu.addItem({ command: CommandIDs.interrupt }); - menu.addItem({ command: CommandIDs.restart }); - menu.addItem({ command: CommandIDs.changeKernel }); - menu.addItem({ type: 'separator' }); - menu.addItem({ command: CommandIDs.closeAndShutdown }); - - mainMenu.addMenu(menu, {rank: 50}); + // Add a console creator to the File menu + mainMenu.fileMenu.newMenu.addItem({ command: CommandIDs.create }); + + // Add a close and shutdown command to the file menu. + mainMenu.fileMenu.closeAndCleaners.set('Console', { + tracker, + action: 'Shutdown', + closeAndCleanup: (current: ConsolePanel) => { + return showDialog({ + title: 'Shutdown the console?', + body: `Are you sure you want to close "${current.title.label}"?`, + buttons: [Dialog.cancelButton(), Dialog.warnButton()] + }).then(result => { + if (result.button.accept) { + current.console.session.shutdown().then(() => { + current.dispose(); + }); + } else { + return void 0; + } + }); + } + } as IFileMenu.ICloseAndCleaner); + + // Add a kernel user to the Kernel menu + mainMenu.kernelMenu.kernelUsers.set('Console', { + tracker, + interruptKernel: current => { + let kernel = current.console.session.kernel; + if (kernel) { + return kernel.interrupt(); + } + return Promise.resolve(void 0); + }, + restartKernel: current => current.console.session.restart(), + changeKernel: current => current.console.session.selectKernel(), + shutdownKernel: current => current.console.session.shutdown() + } as IKernelMenu.IKernelUser); + + // Add a code runner to the Run menu. + mainMenu.runMenu.codeRunners.set('Console', { + tracker, + noun: 'Cell', + run: current => current.console.execute(true) + } as IRunMenu.ICodeRunner); + + // Add a group to the edit menu. + mainMenu.editMenu.addGroup([{ command: CommandIDs.linebreak }]); + + // Add a clearer to the edit menu + mainMenu.editMenu.clearers.set('Console', { + tracker, + noun: 'Console', + clear: (current: ConsolePanel) => { return current.console.clear() } + } as IEditMenu.IClearer); app.contextMenu.addItem({command: CommandIDs.clear, selector: '.jp-CodeConsole'}); app.contextMenu.addItem({command: CommandIDs.restart, selector: '.jp-CodeConsole'}); diff --git a/packages/console/src/widget.ts b/packages/console/src/widget.ts index 220d79f1e324..42a0a7d7a85b 100644 --- a/packages/console/src/widget.ts +++ b/packages/console/src/widget.ts @@ -51,6 +51,16 @@ import { } from './history'; +/** + * The data attribute added to a widget that has an active kernel. + */ +const KERNEL_USER = 'jpKernelUser'; + +/** + * The data attribute added to a widget can run code. + */ +const CODE_RUNNER = 'jpCodeRunner'; + /** * The class name added to console widgets. */ @@ -102,6 +112,9 @@ class CodeConsole extends Widget { constructor(options: CodeConsole.IOptions) { super(); this.addClass(CONSOLE_CLASS); + this.node.dataset[KERNEL_USER] = 'true'; + this.node.dataset[CODE_RUNNER] = 'true'; + this.node.tabIndex = -1; // Allow the widget to take focus. // Create the panels that hold the content and input. let layout = this.layout = new PanelLayout(); @@ -355,6 +368,10 @@ class CodeConsole extends Widget { case 'keydown': this._evtKeyDown(event as KeyboardEvent); break; + case 'onclick': + this.node.focus(); + event.preventDefault(); + break; default: break; } @@ -366,6 +383,7 @@ class CodeConsole extends Widget { protected onAfterAttach(msg: Message): void { let node = this.node; node.addEventListener('keydown', this, true); + node.addEventListener('click', this); // Create a prompt if necessary. if (!this.promptCell) { this.newPromptCell(); @@ -381,6 +399,7 @@ class CodeConsole extends Widget { protected onBeforeDetach(msg: Message): void { let node = this.node; node.removeEventListener('keydown', this, true); + node.removeEventListener('click', this); } /** diff --git a/packages/docmanager-extension/src/index.ts b/packages/docmanager-extension/src/index.ts index 14205594179f..554c7084a3e5 100644 --- a/packages/docmanager-extension/src/index.ts +++ b/packages/docmanager-extension/src/index.ts @@ -6,7 +6,7 @@ import { } from '@jupyterlab/application'; import { - showDialog, showErrorMessage, Spinner, Dialog, ICommandPalette, IMainMenu + showDialog, showErrorMessage, Spinner, Dialog, ICommandPalette } from '@jupyterlab/apputils'; import { @@ -75,8 +75,8 @@ namespace CommandIDs { const plugin: JupyterLabPlugin = { id: '@jupyterlab/docmanager-extension:plugin', provides: IDocumentManager, - requires: [ICommandPalette, IMainMenu], - activate: (app: JupyterLab, palette: ICommandPalette, mainMenu: IMainMenu): IDocumentManager => { + requires: [ICommandPalette], + activate: (app: JupyterLab, palette: ICommandPalette): IDocumentManager => { const manager = app.serviceManager; const contexts = new WeakSet(); const opener: DocumentManager.IWidgetOpener = { @@ -133,9 +133,28 @@ function addCommands(app: JupyterLab, docManager: IDocumentManager, palette: ICo const { currentWidget } = app.shell; return !!(currentWidget && docManager.contextForWidget(currentWidget)); }; + const fileName = () => { + const { currentWidget } = app.shell; + if (!currentWidget) { + return 'File'; + } + const context = docManager.contextForWidget(currentWidget); + if (!context) { + return 'File'; + } + // TODO: we should consider eliding the name + // if it is very long. + return `"${currentWidget.title.label}"`; + }; commands.addCommand(CommandIDs.close, { - label: 'Close', + label: () => { + const widget = app.shell.currentWidget; + return `Close ${widget && widget.title.label ? + `"${widget.title.label}"` : 'Tab'}`; + }, + isEnabled: () => !!app.shell.currentWidget && + !!app.shell.currentWidget.title.closable, execute: () => { if (app.shell.currentWidget) { app.shell.currentWidget.close(); @@ -196,7 +215,7 @@ function addCommands(app: JupyterLab, docManager: IDocumentManager, palette: ICo }); commands.addCommand(CommandIDs.restoreCheckpoint, { - label: 'Revert to Checkpoint', + label: () => `Revert ${fileName()} to Checkpoint`, caption: 'Revert contents to previous checkpoint', isEnabled, execute: () => { @@ -211,7 +230,7 @@ function addCommands(app: JupyterLab, docManager: IDocumentManager, palette: ICo }); commands.addCommand(CommandIDs.save, { - label: 'Save', + label: () => `Save ${fileName()}`, caption: 'Save and create checkpoint', isEnabled, execute: () => { @@ -230,7 +249,7 @@ function addCommands(app: JupyterLab, docManager: IDocumentManager, palette: ICo }); commands.addCommand(CommandIDs.saveAs, { - label: 'Save As...', + label: () => `Save ${fileName()} As…`, caption: 'Save with new path', isEnabled, execute: () => { @@ -242,39 +261,19 @@ function addCommands(app: JupyterLab, docManager: IDocumentManager, palette: ICo }); commands.addCommand(CommandIDs.rename, { - isVisible: () => { - const widget = app.shell.currentWidget; - if (!widget) { - return; - } - // Find the context for the widget. - let context = docManager.contextForWidget(widget); - return context !== null; - }, + label: () => `Rename ${fileName()}`, + isEnabled, execute: () => { - const widget = app.shell.currentWidget; - if (!widget) { - return; - } - // Find the context for the widget. - let context = docManager.contextForWidget(widget); - if (context) { - return renameDialog(docManager, context.path); + if (isEnabled()) { + let context = docManager.contextForWidget(app.shell.currentWidget); + return renameDialog(docManager, context!.path); } - }, - label: 'Rename' + } }); commands.addCommand(CommandIDs.clone, { - isVisible: () => { - const widget = app.shell.currentWidget; - if (!widget) { - return; - } - // Find the context for the widget. - let context = docManager.contextForWidget(widget); - return context !== null; - }, + label: () => `New View Into ${fileName()}`, + isEnabled, execute: () => { const widget = app.shell.currentWidget; if (!widget) { @@ -286,7 +285,6 @@ function addCommands(app: JupyterLab, docManager: IDocumentManager, palette: ICo opener.open(child); } }, - label: 'New View into File' }); app.contextMenu.addItem({ diff --git a/packages/docregistry/src/registry.ts b/packages/docregistry/src/registry.ts index 593c514b8c72..f458099ae5ed 100644 --- a/packages/docregistry/src/registry.ts +++ b/packages/docregistry/src/registry.ts @@ -99,7 +99,6 @@ class DocumentRegistry implements IDisposable { } this._fileTypes.length = 0; - this._creators.length = 0; Signal.clearData(this); } @@ -276,37 +275,6 @@ class DocumentRegistry implements IDisposable { }); } - /** - * Add a creator to the registry. - * - * @params creator - The file creator object to register. - * - * @returns A disposable which will unregister the creator. - */ - addCreator(creator: DocumentRegistry.IFileCreator): IDisposable { - let index = ArrayExt.findFirstIndex(this._creators, (value) => { - return value.name.localeCompare(creator.name) > 0; - }); - if (index !== -1) { - ArrayExt.insert(this._creators, index, creator); - } else { - this._creators.push(creator); - } - this._changed.emit({ - type: 'fileCreator', - name: creator.name, - change: 'added' - }); - return new DisposableDelegate(() => { - ArrayExt.removeFirstOf(this._creators, creator); - this._changed.emit({ - type: 'fileCreator', - name: creator.name, - change: 'removed' - }); - }); - } - /** * Get a list of the preferred widget factories. * @@ -438,15 +406,6 @@ class DocumentRegistry implements IDisposable { return new ArrayIterator(this._fileTypes); } - /** - * Create an iterator over the file creators that have been registered. - * - * @returns A new iterator of file creatores. - */ - creators(): IIterator { - return new ArrayIterator(this._creators); - } - /** * Get a widget factory by name. * @@ -479,16 +438,6 @@ class DocumentRegistry implements IDisposable { }); } - /** - * Get a creator by name. - */ - getCreator(name: string): DocumentRegistry.IFileCreator | undefined { - name = name.toLowerCase(); - return find(this._creators, creator => { - return creator.name.toLowerCase() === name; - }); - } - /** * Get a kernel preference. * @@ -586,7 +535,6 @@ class DocumentRegistry implements IDisposable { private _defaultWidgetFactories: { [key: string]: string } = Object.create(null); private _widgetFactoryExtensions: {[key: string]: string[] } = Object.create(null); private _fileTypes: DocumentRegistry.IFileType[] = []; - private _creators: DocumentRegistry.IFileCreator[] = []; private _extenders: { [key: string] : DocumentRegistry.WidgetExtension[] } = Object.create(null); private _changed = new Signal(this); private _isDisposed = false; @@ -1041,32 +989,6 @@ namespace DocumentRegistry { fileFormat: 'text' }; - /** - * An interface for a "Create New" item. - */ - export - interface IFileCreator { - /** - * The name of the file creator. - */ - readonly name: string; - - /** - * The filetype name associated with the creator. - */ - readonly fileType: string; - - /** - * The optional widget name. - */ - readonly widgetName?: string; - - /** - * The optional kernel name. - */ - readonly kernelName?: string; - } - /** * An arguments object for the `changed` signal. */ @@ -1075,7 +997,7 @@ namespace DocumentRegistry { /** * The type of the changed item. */ - readonly type: 'widgetFactory' | 'modelFactory' | 'widgetExtension' | 'fileCreator' | 'fileType'; + readonly type: 'widgetFactory' | 'modelFactory' | 'widgetExtension' | 'fileType'; /** * The name of the item or the widget factory being extended. diff --git a/packages/filebrowser-extension/src/index.ts b/packages/filebrowser-extension/src/index.ts index 31c86cca6fc4..9c7fa2b5e5c4 100644 --- a/packages/filebrowser-extension/src/index.ts +++ b/packages/filebrowser-extension/src/index.ts @@ -6,7 +6,7 @@ import { } from '@jupyterlab/application'; import { - IMainMenu, InstanceTracker, ToolbarButton + InstanceTracker, ToolbarButton } from '@jupyterlab/apputils'; import { @@ -107,16 +107,6 @@ const factory: JupyterLabPlugin = { requires: [IDocumentManager, IStateDB] }; -/** - * The default file browser menu extension. - */ -const menu: JupyterLabPlugin = { - activate: activateMenu, - id: '@jupyterlab/filebrowser-extension:menu', - requires: [IMainMenu], - autoStart: true -}; - /** * The file browser namespace token. */ @@ -125,7 +115,7 @@ const namespace = 'filebrowser'; /** * Export the plugins as default. */ -const plugins: JupyterLabPlugin[] = [factory, browser, menu]; +const plugins: JupyterLabPlugin[] = [factory, browser]; export default plugins; @@ -220,15 +210,6 @@ function activateBrowser(app: JupyterLab, factory: IFileBrowserFactory, restorer }); } -/** - * Activate the default file browser menu in the main menu. - */ -function activateMenu(app: JupyterLab, mainMenu: IMainMenu): void { - let menu = createMenu(app); - - mainMenu.addMenu(menu, { rank: 1 }); -} - /** * Add the main file browser commands to the application's command registry. @@ -392,31 +373,6 @@ function addCommands(app: JupyterLab, tracker: InstanceTracker, bro } -/** - * Create a top level menu for the file browser. - */ -function createMenu(app: JupyterLab): Menu { - const { commands } = app; - const menu = new Menu({ commands }); - - menu.title.label = 'File'; - [ - CommandIDs.createLauncher, - 'docmanager:save', - 'docmanager:save-as', - 'docmanager:rename', - 'docmanager:restore-checkpoint', - 'docmanager:clone', - 'docmanager:close', - 'docmanager:close-all-files' - ].forEach(command => { menu.addItem({ command }); }); - menu.addItem({ type: 'separator' }); - menu.addItem({ command: 'settingeditor:open' }); - - return menu; -} - - /** * Create a context menu for the file browser listing. * diff --git a/packages/fileeditor-extension/package.json b/packages/fileeditor-extension/package.json index fb1f9c8c38b4..e28664b15a4d 100644 --- a/packages/fileeditor-extension/package.json +++ b/packages/fileeditor-extension/package.json @@ -34,8 +34,10 @@ "@jupyterlab/apputils": "^0.12.4", "@jupyterlab/codeeditor": "^0.12.0", "@jupyterlab/coreutils": "^0.12.0", + "@jupyterlab/filebrowser": "^0.12.1", "@jupyterlab/fileeditor": "^0.12.0", - "@jupyterlab/launcher": "^0.12.0" + "@jupyterlab/launcher": "^0.12.0", + "@jupyterlab/mainmenu": "^0.1.0" }, "devDependencies": { "rimraf": "~2.6.2", diff --git a/packages/fileeditor-extension/src/index.ts b/packages/fileeditor-extension/src/index.ts index 38e54b4b88a1..c04e8d5b8b1d 100644 --- a/packages/fileeditor-extension/src/index.ts +++ b/packages/fileeditor-extension/src/index.ts @@ -17,6 +17,10 @@ import { ISettingRegistry, MarkdownCodeBlocks, PathExt } from '@jupyterlab/coreutils'; +import { + IFileBrowserFactory +} from '@jupyterlab/filebrowser'; + import { FileEditor, FileEditorFactory, IEditorTracker } from '@jupyterlab/fileeditor'; @@ -25,6 +29,10 @@ import { ILauncher } from '@jupyterlab/launcher'; +import { + IEditMenu, IMainMenu, IKernelMenu, IViewMenu +} from '@jupyterlab/mainmenu'; + /** * The class name for the text editor icon from the default theme. @@ -41,6 +49,9 @@ const FACTORY = 'Editor'; * The command IDs used by the fileeditor plugin. */ namespace CommandIDs { + export + const createNew = 'fileeditor:create-new'; + export const lineNumbers = 'fileeditor:toggle-line-numbers'; @@ -73,8 +84,8 @@ namespace CommandIDs { const plugin: JupyterLabPlugin = { activate, id: '@jupyterlab/fileeditor-extension:plugin', - requires: [ILayoutRestorer, IEditorServices, ISettingRegistry], - optional: [ILauncher], + requires: [IEditorServices, IFileBrowserFactory, ILayoutRestorer, ISettingRegistry], + optional: [ILauncher, IMainMenu], provides: IEditorTracker, autoStart: true }; @@ -89,7 +100,7 @@ export default plugin; /** * Activate the editor tracker plugin. */ -function activate(app: JupyterLab, restorer: ILayoutRestorer, editorServices: IEditorServices, settingRegistry: ISettingRegistry, launcher: ILauncher | null): IEditorTracker { +function activate(app: JupyterLab, editorServices: IEditorServices, browserFactory: IFileBrowserFactory, restorer: ILayoutRestorer, settingRegistry: ISettingRegistry, launcher: ILauncher | null, menu: IMainMenu | null): IEditorTracker { const id = plugin.id; const namespace = 'editor'; const factory = new FileEditorFactory({ @@ -98,7 +109,8 @@ function activate(app: JupyterLab, restorer: ILayoutRestorer, editorServices: IE }); const { commands, restored } = app; const tracker = new InstanceTracker({ namespace }); - const hasWidget = () => !!tracker.currentWidget; + const isEnabled = () => tracker.currentWidget !== null && + tracker.currentWidget === app.shell.currentWidget; let { lineNumbers, lineWrap, matchBrackets, autoClosingBrackets @@ -181,7 +193,7 @@ function activate(app: JupyterLab, restorer: ILayoutRestorer, editorServices: IE console.error(`Failed to set ${id}:${key} - ${reason.message}`); }); }, - isEnabled: hasWidget, + isEnabled, isToggled: () => lineNumbers, label: 'Line Numbers' }); @@ -196,7 +208,7 @@ function activate(app: JupyterLab, restorer: ILayoutRestorer, editorServices: IE console.error(`Failed to set ${id}:${key} - ${reason.message}`); }); }, - isEnabled: hasWidget, + isEnabled, isToggled: () => lineWrap, label: 'Word Wrap' }); @@ -214,7 +226,7 @@ function activate(app: JupyterLab, restorer: ILayoutRestorer, editorServices: IE editor.setOption('insertSpaces', insertSpaces); editor.setOption('tabSize', size); }, - isEnabled: hasWidget, + isEnabled, isToggled: args => { let widget = tracker.currentWidget; if (!widget) { @@ -239,7 +251,7 @@ function activate(app: JupyterLab, restorer: ILayoutRestorer, editorServices: IE return settingRegistry.set(id, 'matchBrackets', matchBrackets); }, label: 'Match Brackets', - isEnabled: hasWidget, + isEnabled, isToggled: () => matchBrackets }); @@ -253,7 +265,7 @@ function activate(app: JupyterLab, restorer: ILayoutRestorer, editorServices: IE .set(id, 'autoClosingBrackets', autoClosingBrackets); }, label: 'Auto-Closing Brackets', - isEnabled: hasWidget, + isEnabled, isToggled: () => autoClosingBrackets }); @@ -271,7 +283,7 @@ function activate(app: JupyterLab, restorer: ILayoutRestorer, editorServices: IE preferredLanguage: widget.context.model.defaultKernelLanguage }); }, - isEnabled: hasWidget, + isEnabled, label: 'Create Console for Editor' }); @@ -329,7 +341,7 @@ function activate(app: JupyterLab, restorer: ILayoutRestorer, editorServices: IE return Promise.resolve(void 0); } }, - isEnabled: hasWidget, + isEnabled, label: 'Run Code' }); @@ -349,6 +361,28 @@ function activate(app: JupyterLab, restorer: ILayoutRestorer, editorServices: IE label: 'Show Markdown Preview' }); + // Function to create a new untitled text file, given + // the current working directory. + const createNew = (cwd: string) => { + return commands.execute('docmanager:new-untitled', { + path: cwd, type: 'file' + }).then(model => { + return commands.execute('docmanager:open', { + path: model.path, factory: FACTORY + }); + }); + } + + // Add a command for creating a new text file. + commands.addCommand(CommandIDs.createNew, { + label: 'Text File', + caption: 'Create a new text file', + execute: () => { + let cwd = browserFactory.defaultBrowser.model.path; + return createNew(cwd); + } + }); + // Add a launcher item if the launcher is available. if (launcher) { launcher.add({ @@ -356,15 +390,58 @@ function activate(app: JupyterLab, restorer: ILayoutRestorer, editorServices: IE category: 'Other', rank: 1, iconClass: EDITOR_ICON_CLASS, - callback: cwd => { - return commands.execute('docmanager:new-untitled', { - path: cwd, type: 'file' - }).then(model => { - return commands.execute('docmanager:open', { - path: model.path, factory: FACTORY - }); - }); + callback: createNew + }); + } + + if (menu) { + // Add new text file creation to the file menu. + menu.fileMenu.newMenu.addItem({ command: CommandIDs.createNew }); + + // Add undo/redo hooks to the edit menu. + menu.editMenu.undoers.set('Editor', { + tracker, + undo: widget => { widget.editor.undo(); }, + redo: widget => { widget.editor.redo(); } + } as IEditMenu.IUndoer); + + // Add editor view options. + menu.viewMenu.editorViewers.set('Editor', { + tracker, + toggleLineNumbers: widget => { + const lineNumbers = !widget.editor.getOption('lineNumbers'); + widget.editor.setOption('lineNumbers', lineNumbers); + }, + toggleWordWrap: widget => { + const wordWrap = !widget.editor.getOption('lineWrap'); + widget.editor.setOption('lineWrap', wordWrap); + }, + toggleMatchBrackets: widget => { + const matchBrackets = !widget.editor.getOption('matchBrackets'); + widget.editor.setOption('matchBrackets', matchBrackets); + }, + lineNumbersToggled: widget => widget.editor.getOption('lineNumbers'), + wordWrapToggled: widget => widget.editor.getOption('lineWrap'), + matchBracketsToggled: widget => widget.editor.getOption('matchBrackets') + } as IViewMenu.IEditorViewer); + + // Add a console creator the the Kernel menu. + menu.kernelMenu.consoleCreators.set('Editor', { + tracker, + createConsole: current => { + const options = { + path: current.context.path, + preferredLanguage: current.context.model.defaultKernelLanguage + }; + return commands.execute('console:create', options); } + } as IKernelMenu.IConsoleCreator); + + // Add a code runner to the Run menu. + menu.runMenu.codeRunners.set('Editor', { + tracker, + noun: 'Code Snippet', + run: () => commands.execute(CommandIDs.runCode) }); } diff --git a/packages/fileeditor/src/widget.ts b/packages/fileeditor/src/widget.ts index 6878b597ef0b..ec1ce8b0affb 100644 --- a/packages/fileeditor/src/widget.ts +++ b/packages/fileeditor/src/widget.ts @@ -25,6 +25,16 @@ import { Message } from '@phosphor/messaging'; +/** + * The data attribute added to a widget that can run code. + */ +const CODE_RUNNER = 'jpCodeRunner'; + +/** + * The data attribute added to a widget that can undo. + */ +const UNDOER = 'jpUndoer'; + /** * The class name added to a dirty widget. */ @@ -54,6 +64,8 @@ class FileEditorCodeWrapper extends CodeEditorWrapper { const editor = this.editor; this.addClass(EDITOR_CLASS); + this.node.dataset[CODE_RUNNER] = 'true'; + this.node.dataset[UNDOER] = 'true'; editor.model.value.text = context.model.toString(); context.ready.then(() => { this._onContextReady(); }); diff --git a/packages/help-extension/package.json b/packages/help-extension/package.json index 3201fb968821..f9f9d3944e4a 100644 --- a/packages/help-extension/package.json +++ b/packages/help-extension/package.json @@ -33,6 +33,7 @@ "@jupyterlab/application": "^0.12.0", "@jupyterlab/apputils": "^0.12.4", "@jupyterlab/coreutils": "^0.12.0", + "@jupyterlab/mainmenu": "^0.1.0", "@phosphor/messaging": "^1.2.2", "@phosphor/virtualdom": "^1.1.2", "@phosphor/widgets": "^1.5.0" diff --git a/packages/help-extension/src/index.ts b/packages/help-extension/src/index.ts index 4b59ce641fa7..fa64389e8869 100644 --- a/packages/help-extension/src/index.ts +++ b/packages/help-extension/src/index.ts @@ -6,13 +6,17 @@ import { } from '@jupyterlab/application'; import { - Dialog, ICommandPalette, IFrame, IMainMenu, InstanceTracker, showDialog + Dialog, ICommandPalette, IFrame, InstanceTracker, showDialog } from '@jupyterlab/apputils'; import { PageConfig, URLExt } from '@jupyterlab/coreutils'; +import { + IMainMenu +} from '@jupyterlab/mainmenu'; + import { Message } from '@phosphor/messaging'; @@ -22,7 +26,7 @@ import { } from '@phosphor/virtualdom'; import { - Menu, PanelLayout, Widget + PanelLayout, Widget } from '@phosphor/widgets'; import '../style/index.css'; @@ -186,7 +190,6 @@ function activate(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette let counter = 0; const category = 'Help'; const namespace = 'help-doc'; - const menu = createMenu(); const { commands, shell, info} = app; const tracker = new InstanceTracker({ namespace }); @@ -210,26 +213,17 @@ function activate(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette return iframe; } - /** - * Create a menu for the help plugin. - */ - function createMenu(): Menu { - let { commands } = app; - let menu = new Menu({ commands }); - menu.title.label = category; - - menu.addItem({ command: CommandIDs.about }); - menu.addItem({ command: 'faq-jupyterlab:open' }); - menu.addItem({ command: CommandIDs.launchClassic }); - menu.addItem({ type: 'separator' }); - RESOURCES.forEach(args => { - menu.addItem({ args, command: CommandIDs.open }); - }); - menu.addItem({ type: 'separator' }); - menu.addItem({ command: 'apputils:clear-statedb' }); - - return menu; - } + // Populate the Help menu. + const helpMenu = mainMenu.helpMenu; + const labGroup = [ + CommandIDs.about, + 'faq-jupyterlab:open', + CommandIDs.launchClassic + ].map(command => { return { command }; }); + helpMenu.addGroup(labGroup, 0); + const resourcesGroup = + RESOURCES.map(args => { return { args, command: CommandIDs.open }; }); + helpMenu.addGroup(resourcesGroup, 10); commands.addCommand(CommandIDs.about, { label: `About ${info.name}`, @@ -303,5 +297,4 @@ function activate(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette palette.addItem({ command: 'apputils:clear-statedb', category }); palette.addItem({ command: CommandIDs.launchClassic, category }); - mainMenu.addMenu(menu); } diff --git a/packages/imageviewer-extension/package.json b/packages/imageviewer-extension/package.json index 3bfe36ed033f..c2942257134d 100644 --- a/packages/imageviewer-extension/package.json +++ b/packages/imageviewer-extension/package.json @@ -31,8 +31,7 @@ "dependencies": { "@jupyterlab/application": "^0.12.0", "@jupyterlab/apputils": "^0.12.4", - "@jupyterlab/imageviewer": "^0.12.0", - "@phosphor/commands": "^1.4.0" + "@jupyterlab/imageviewer": "^0.12.0" }, "devDependencies": { "rimraf": "~2.6.2", diff --git a/packages/imageviewer-extension/src/index.ts b/packages/imageviewer-extension/src/index.ts index 9bcaa06a4b76..3f8bb2da3131 100644 --- a/packages/imageviewer-extension/src/index.ts +++ b/packages/imageviewer-extension/src/index.ts @@ -13,11 +13,6 @@ import { ImageViewer, ImageViewerFactory, IImageTracker } from '@jupyterlab/imageviewer'; -import { - CommandRegistry -} from '@phosphor/commands'; - - /** * The command IDs used by the image widget plugin. */ @@ -99,7 +94,7 @@ function activate(app: JupyterLab, palette: ICommandPalette, restorer: ILayoutRe } }); - addCommands(tracker, app.commands); + addCommands(app, tracker); const category = 'Image Viewer'; @@ -114,38 +109,33 @@ function activate(app: JupyterLab, palette: ICommandPalette, restorer: ILayoutRe * Add the commands for the image widget. */ export -function addCommands(tracker: IImageTracker, commands: CommandRegistry) { +function addCommands(app: JupyterLab, tracker: IImageTracker) { + const { commands } = app; /** - * Whether there is an active notebook. + * Whether there is an active image viewer. */ - function hasWidget(): boolean { - return tracker.currentWidget !== null; + function isEnabled(): boolean { + return tracker.currentWidget !== null && + tracker.currentWidget === app.shell.currentWidget; } - // Update the command registry when the image viewer state changes. - tracker.currentChanged.connect(() => { - if (tracker.size <= 1) { - commands.notifyCommandChanged(CommandIDs.zoomIn); - } - }); - commands.addCommand('imageviewer:zoom-in', { execute: zoomIn, label: 'Zoom In', - isEnabled: hasWidget + isEnabled }); commands.addCommand('imageviewer:zoom-out', { execute: zoomOut, label: 'Zoom Out', - isEnabled: hasWidget + isEnabled }); commands.addCommand('imageviewer:reset-zoom', { execute: resetZoom, label: 'Reset Zoom', - isEnabled: hasWidget + isEnabled }); function zoomIn(): void { diff --git a/packages/mainmenu-extension/README.md b/packages/mainmenu-extension/README.md new file mode 100644 index 000000000000..0c290bcf278b --- /dev/null +++ b/packages/mainmenu-extension/README.md @@ -0,0 +1,3 @@ +# @jupyterlab/mainmenu-extension + +An extension for JupyterLab which provides an entry point for the [@jupyterlab/mainmenu](../mainmenu) package. diff --git a/packages/mainmenu-extension/package.json b/packages/mainmenu-extension/package.json new file mode 100644 index 000000000000..9c5d5cdfa49e --- /dev/null +++ b/packages/mainmenu-extension/package.json @@ -0,0 +1,42 @@ +{ + "name": "@jupyterlab/mainmenu-extension", + "version": "0.1.0", + "description": "JupyterLab - Main Menu Extension", + "homepage": "https://github.com/jupyterlab/jupyterlab", + "bugs": { + "url": "https://github.com/jupyterlab/jupyterlab/issues" + }, + "license": "BSD-3-Clause", + "author": "Project Jupyter", + "files": [ + "lib/*.d.ts", + "lib/*.js.map", + "lib/*.js" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "directories": { + "lib": "lib/" + }, + "repository": { + "type": "git", + "url": "https://github.com/jupyterlab/jupyterlab.git" + }, + "scripts": { + "build": "tsc", + "clean": "rimraf lib", + "watch": "tsc -w" + }, + "dependencies": { + "@jupyterlab/application": "^0.12.0", + "@jupyterlab/mainmenu": "^0.1.0", + "@phosphor/widgets": "^1.5.0" + }, + "devDependencies": { + "rimraf": "~2.6.2", + "typescript": "~2.6.2" + }, + "jupyterlab": { + "extension": true + } +} diff --git a/packages/mainmenu-extension/src/index.ts b/packages/mainmenu-extension/src/index.ts new file mode 100644 index 000000000000..59447d0fa66d --- /dev/null +++ b/packages/mainmenu-extension/src/index.ts @@ -0,0 +1,439 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { + JupyterLab, JupyterLabPlugin +} from '@jupyterlab/application'; + +import { + Widget +} from '@phosphor/widgets'; + +import { + IMainMenu, IMenuExtender, + EditMenu, FileMenu, KernelMenu, MainMenu, RunMenu, ViewMenu +} from '@jupyterlab/mainmenu'; + + +/** + * A namespace for command IDs of semantic extension points. + */ +export +namespace CommandIDs { + export + const undo = 'editmenu:undo'; + + export + const redo = 'editmenu:redo'; + + export + const clear = 'editmenu:clear'; + + export + const find = 'editmenu:find'; + + export + const findAndReplace = 'editmenu:find-and-replace'; + + export + const closeAndCleanup = 'filemenu:close-and-cleanup'; + + export + const interruptKernel = 'kernelmenu:interrupt'; + + export + const restartKernel = 'kernelmenu:restart'; + + export + const changeKernel = 'kernelmenu:change'; + + export + const shutdownKernel = 'kernelmenu:shutdown'; + + export + const createConsole = 'kernelmenu:create-console'; + + export + const wordWrap = 'viewmenu:word-wrap'; + + export + const lineNumbering = 'viewmenu:line-numbering'; + + export + const matchBrackets = 'viewmenu:match-brackets'; + + export + const run = 'runmenu:run'; + + export + const runAll = 'runmenu:run-all'; + + export + const runAbove = 'runmenu:run-above'; + + export + const runBelow = 'runmenu:run-below'; +} + +/** + * A service providing an interface to the main menu. + */ +const menuPlugin: JupyterLabPlugin = { + id: '@jupyterlab/mainmenu-extension:plugin', + provides: IMainMenu, + activate: (app: JupyterLab): IMainMenu => { + let menu = new MainMenu(app.commands); + menu.id = 'jp-MainMenu'; + + let logo = new Widget(); + logo.addClass('jp-MainAreaPortraitIcon'); + logo.addClass('jp-JupyterIcon'); + logo.id = 'jp-MainLogo'; + + // Create the application menus. + createEditMenu(app, menu.editMenu); + createFileMenu(app, menu.fileMenu); + createKernelMenu(app, menu.kernelMenu); + createRunMenu(app, menu.runMenu); + createViewMenu(app, menu.viewMenu); + + app.shell.addToTopArea(logo); + app.shell.addToTopArea(menu); + + return menu; + } +}; + +/** + * Create the basic `Edit` menu. + */ +function createEditMenu(app: JupyterLab, menu: EditMenu): void { + const commands = menu.menu.commands; + + // Add the undo/redo commands the the Edit menu. + commands.addCommand(CommandIDs.undo, { + label: 'Undo', + isEnabled: + Private.delegateEnabled(app, menu.undoers, 'undo'), + execute: + Private.delegateExecute(app, menu.undoers, 'undo') + }); + commands.addCommand(CommandIDs.redo, { + label: 'Redo', + isEnabled: + Private.delegateEnabled(app, menu.undoers, 'redo'), + execute: + Private.delegateExecute(app, menu.undoers, 'redo') + }); + menu.addGroup([ + { command: CommandIDs.undo }, + { command: CommandIDs.redo } + ], 0); + + // Add the clear command to the Edit menu. + commands.addCommand(CommandIDs.clear, { + label: () => { + const action = + Private.delegateLabel(app, menu.clearers, 'noun'); + return `Clear${action ? ` ${action}` : '…'}`; + }, + isEnabled: + Private.delegateEnabled(app, menu.clearers, 'clear'), + execute: + Private.delegateExecute(app, menu.clearers, 'clear') + }); + menu.addGroup([{ command: CommandIDs.clear }], 10); + + // Add the find/replace commands the the Edit menu. + commands.addCommand(CommandIDs.find, { + label: 'Find…', + isEnabled: + Private.delegateEnabled(app, menu.findReplacers, 'find'), + execute: + Private.delegateExecute(app, menu.findReplacers, 'find') + }); + commands.addCommand(CommandIDs.findAndReplace, { + label: 'Find and Replace…', + isEnabled: + Private.delegateEnabled(app, menu.findReplacers, 'findAndReplace'), + execute: + Private.delegateExecute(app, menu.findReplacers, 'findAndReplace') + }); + menu.addGroup([ + { command: CommandIDs.find }, + { command: CommandIDs.findAndReplace } + ], 200); +} + +/** + * Create the basic `File` menu. + */ +function createFileMenu(app: JupyterLab, menu: FileMenu): void { + const commands = menu.menu.commands; + + // Add a delegator command for closing and cleaning up an activity. + commands.addCommand(CommandIDs.closeAndCleanup, { + label: () => { + const widget = app.shell.currentWidget; + const name = widget ? widget.title.label : '...'; + const action = + Private.delegateLabel(app, menu.closeAndCleaners, 'action'); + return `Close and ${action ? ` ${action} "${name}"` : 'Shutdown…'}`; + }, + isEnabled: + Private.delegateEnabled(app, menu.closeAndCleaners, 'closeAndCleanup'), + execute: + Private.delegateExecute(app, menu.closeAndCleaners, 'closeAndCleanup') + }); + + // Add the commands to the File menu. + const fileOperationGroup = [ + 'docmanager:save', + 'docmanager:save-as', + 'docmanager:rename', + 'docmanager:restore-checkpoint', + 'docmanager:clone' + ].map(command => { return { command }; }); + + const closeGroup = [ + 'docmanager:close', + 'filemenu:close-and-cleanup', + 'docmanager:close-all-files' + ].map(command => { return { command }; }); + + menu.addGroup(fileOperationGroup, 1); + menu.addGroup(closeGroup, 2); + menu.addGroup([{ command: 'settingeditor:open' }], 1000); +} + +/** + * Create the basic `Kernel` menu. + */ +function createKernelMenu(app: JupyterLab, menu: KernelMenu): void { + const commands = menu.menu.commands; + + commands.addCommand(CommandIDs.interruptKernel, { + label: 'Interrupt Kernel', + isEnabled: Private.delegateEnabled(app, menu.kernelUsers, 'interruptKernel'), + execute: Private.delegateExecute(app, menu.kernelUsers, 'interruptKernel') + }); + + commands.addCommand(CommandIDs.restartKernel, { + label: 'Restart Kernel', + isEnabled: Private.delegateEnabled(app, menu.kernelUsers, 'restartKernel'), + execute: Private.delegateExecute(app, menu.kernelUsers, 'restartKernel') + }); + + commands.addCommand(CommandIDs.changeKernel, { + label: 'Change Kernel', + isEnabled: Private.delegateEnabled(app, menu.kernelUsers, 'changeKernel'), + execute: Private.delegateExecute(app, menu.kernelUsers, 'changeKernel') + }); + + commands.addCommand(CommandIDs.shutdownKernel, { + label: 'Shutdown Kernel', + isEnabled: Private.delegateEnabled(app, menu.kernelUsers, 'shutdownKernel'), + execute: Private.delegateExecute(app, menu.kernelUsers, 'shutdownKernel') + }); + + commands.addCommand(CommandIDs.createConsole, { + label: () => { + const name = Private.findExtenderName(app, menu.consoleCreators); + const label = `Create Console for${name ? ` ${name}` : '…'}`; + return label; + }, + isEnabled: Private.delegateEnabled(app, menu.consoleCreators, 'createConsole'), + execute: Private.delegateExecute(app, menu.consoleCreators, 'createConsole') + }); + + const kernelUserGroup = [ + CommandIDs.interruptKernel, + CommandIDs.restartKernel, + CommandIDs.changeKernel, + CommandIDs.shutdownKernel + ].map(command => { return { command }; }); + menu.addGroup(kernelUserGroup, 0); + + menu.addGroup([{ command: CommandIDs.createConsole }], 1); +} + +/** + * Create the basic `View` menu. + */ +function createViewMenu(app: JupyterLab, menu: ViewMenu): void { + const commands = menu.menu.commands; + + commands.addCommand(CommandIDs.lineNumbering, { + label: 'Line Numbers', + isEnabled: Private.delegateEnabled(app, menu.editorViewers, 'toggleLineNumbers'), + isToggled: Private.delegateToggled(app, menu.editorViewers, 'lineNumbersToggled'), + execute: Private.delegateExecute(app, menu.editorViewers, 'toggleLineNumbers') + }); + + commands.addCommand(CommandIDs.matchBrackets, { + label: 'Match Brackets', + isEnabled: Private.delegateEnabled(app, menu.editorViewers, 'toggleMatchBrackets'), + isToggled: Private.delegateToggled(app, menu.editorViewers, 'matchBracketsToggled'), + execute: Private.delegateExecute(app, menu.editorViewers, 'toggleMatchBrackets') + }); + + commands.addCommand(CommandIDs.wordWrap, { + label: 'Word Wrap', + isEnabled: Private.delegateEnabled(app, menu.editorViewers, 'toggleWordWrap'), + isToggled: Private.delegateToggled(app, menu.editorViewers, 'wordWrapToggled'), + execute: Private.delegateExecute(app, menu.editorViewers, 'toggleWordWrap') + }); + + const editorViewerGroup = [ + CommandIDs.lineNumbering, + CommandIDs.matchBrackets, + CommandIDs.wordWrap + ].map( command => { return { command }; }); + menu.addGroup(editorViewerGroup, 10); + + // Add commands for cycling the active tabs. + menu.addGroup([ + { command: 'application:activate-next-tab' }, + { command: 'application:activate-previous-tab' } + ], 0); + + // Add the command for toggling single-document mode. + menu.addGroup([ { command: 'application:toggle-mode' }], 1000); +} + +function createRunMenu(app: JupyterLab, menu: RunMenu): void { + const commands = menu.menu.commands; + + commands.addCommand(CommandIDs.run, { + label: () => { + const noun = Private.delegateLabel(app, menu.codeRunners, 'noun'); + return `Run${noun ? ` ${noun}(s)` : ''}`; + }, + isEnabled: Private.delegateEnabled(app, menu.codeRunners, 'run'), + execute: Private.delegateExecute(app, menu.codeRunners, 'run') + }); + + commands.addCommand(CommandIDs.runAll, { + label: () => { + const noun = Private.delegateLabel(app, menu.codeRunners, 'noun'); + return `Run All${noun ? ` ${noun}s` : ''}`; + }, + isEnabled: Private.delegateEnabled(app, menu.codeRunners, 'runAll'), + execute: Private.delegateExecute(app, menu.codeRunners, 'runAll') + }); + + commands.addCommand(CommandIDs.runAbove, { + label: () => { + const noun = Private.delegateLabel(app, menu.codeRunners, 'noun'); + return `Run${noun ? ` ${noun}(s)` : ''} Above`; + }, + isEnabled: Private.delegateEnabled(app, menu.codeRunners, 'runAbove'), + execute: Private.delegateExecute(app, menu.codeRunners, 'runAbove') + }); + + commands.addCommand(CommandIDs.runBelow, { + label: () => { + const noun = Private.delegateLabel(app, menu.codeRunners, 'noun'); + return `Run${noun ? ` ${noun}(s)` : ''} Below`; + }, + isEnabled: Private.delegateEnabled(app, menu.codeRunners, 'runBelow'), + execute: Private.delegateExecute(app, menu.codeRunners, 'runBelow') + }); + + const codeRunnerGroup = [ + CommandIDs.run, + CommandIDs.runAll, + CommandIDs.runAbove, + CommandIDs.runBelow, + ].map(command => { return { command }; }); + menu.addGroup(codeRunnerGroup, 0) +} +export default menuPlugin; + +/** + * A namespace for Private data. + */ +namespace Private { + /** + * Given a widget and a map containing IMenuExtenders, + * check the tracker and return the extender, if any, + * that holds the widget. + */ + function findExtender>(widget: Widget, map: Map): [string, E] { + let extender: E; + let name = ''; + map.forEach((value, key) => { + if (value.tracker.has(widget)) { + extender = value; + name = key; + } + }); + return [name, extender]; + } + + /** + * Given a map containing IMenuExtenders, + * return the key of the extender, or the empty string if none is found. + */ + export + function findExtenderName>(app: JupyterLab, map: Map): string { + const widget = app.shell.currentWidget; + const [name, ] = findExtender(widget, map); + return name; + } + + /** + * A utility function that delegates a portion of a label to an IMenuExtender. + */ + export + function delegateLabel>(app: JupyterLab, map: Map, label: keyof E): string { + let widget = app.shell.currentWidget; + const [, extender] = findExtender(widget, map); + if (!extender) { + return ''; + } + return extender[label]; + } + + /** + * A utility function that delegates command execution + * to an IMenuExtender. + */ + export + function delegateExecute>(app: JupyterLab, map: Map, executor: keyof E): () => Promise { + return () => { + let widget = app.shell.currentWidget; + const [, extender] = findExtender(widget, map); + if (!extender) { + return Promise.resolve(void 0); + } + return extender[executor](widget); + }; + } + + /** + * A utility function that delegates whether a command is enabled + * to an IMenuExtender. + */ + export + function delegateEnabled>(app: JupyterLab, map: Map, executor: keyof E): () => boolean { + return () => { + let widget = app.shell.currentWidget; + const [, extender] = findExtender(widget, map); + return !!extender && !!extender[executor]; + }; + } + + /** + * A utility function that delegates whether a command is toggled + * for an IMenuExtender. + */ + export + function delegateToggled>(app: JupyterLab, map: Map, toggled: keyof E): () => boolean { + return () => { + let widget = app.shell.currentWidget; + const [, extender] = findExtender(widget, map); + return !!extender && !!extender[toggled] && !!extender[toggled](widget); + }; + } +} diff --git a/packages/mainmenu-extension/tsconfig.json b/packages/mainmenu-extension/tsconfig.json new file mode 100644 index 000000000000..f0d24f39d29c --- /dev/null +++ b/packages/mainmenu-extension/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "declaration": true, + "noImplicitAny": true, + "noEmitOnError": true, + "noUnusedLocals": true, + "module": "commonjs", + "moduleResolution": "node", + "target": "ES5", + "outDir": "./lib", + "lib": ["ES5", "ES2015.Promise", "DOM", "ES2015.Collection"], + "types": [] + }, + "include": ["src/*"] +} diff --git a/packages/mainmenu/README.md b/packages/mainmenu/README.md new file mode 100644 index 000000000000..af6c1c5ca231 --- /dev/null +++ b/packages/mainmenu/README.md @@ -0,0 +1,3 @@ +# @jupyterlab/mainmenu + +A JupyterLab extension which provides the application menubar. diff --git a/packages/mainmenu/package.json b/packages/mainmenu/package.json new file mode 100644 index 000000000000..c3deef961711 --- /dev/null +++ b/packages/mainmenu/package.json @@ -0,0 +1,43 @@ +{ + "name": "@jupyterlab/mainmenu", + "version": "0.1.0", + "description": "JupyterLab - Main Menu", + "homepage": "https://github.com/jupyterlab/jupyterlab", + "bugs": { + "url": "https://github.com/jupyterlab/jupyterlab/issues" + }, + "license": "BSD-3-Clause", + "author": "Project Jupyter", + "files": [ + "lib/*.d.ts", + "lib/*.js.map", + "lib/*.js" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "directories": { + "lib": "lib/" + }, + "repository": { + "type": "git", + "url": "https://github.com/jupyterlab/jupyterlab.git" + }, + "scripts": { + "build": "tsc", + "clean": "rimraf lib", + "watch": "tsc -w" + }, + "dependencies": { + "@jupyterlab/apputils": "^0.12.4", + "@jupyterlab/services": "^0.51.0", + "@phosphor/algorithm": "^1.1.2", + "@phosphor/commands": "^1.4.0", + "@phosphor/coreutils": "^1.3.0", + "@phosphor/disposable": "^1.1.2", + "@phosphor/widgets": "^1.5.0" + }, + "devDependencies": { + "rimraf": "~2.6.2", + "typescript": "~2.6.2" + } +} diff --git a/packages/mainmenu/src/edit.ts b/packages/mainmenu/src/edit.ts new file mode 100644 index 000000000000..2dd6e365b631 --- /dev/null +++ b/packages/mainmenu/src/edit.ts @@ -0,0 +1,152 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { + Menu, Widget +} from '@phosphor/widgets'; + +import { + IJupyterLabMenu, IMenuExtender, JupyterLabMenu +} from './labmenu'; + + +/** + * An interface for an Edit menu. + */ +export +interface IEditMenu extends IJupyterLabMenu { + /** + * A map storing IUndoers for the Edit menu. + * + * ### Notes + * The key for the map may be used in menu labels. + */ + readonly undoers: Map>; + + /** + * A map storing IClearers for the Edit menu. + * + * ### Notes + * The key for the map may be used in menu labels. + */ + readonly clearers: Map>; + + /** + * A map storing IClearers for the Edit menu. + * + * ### Notes + * The key for the map may be used in menu labels. + */ + readonly findReplacers: Map>; +} + +/** + * An extensible Edit menu for the application. + */ +export +class EditMenu extends JupyterLabMenu implements IEditMenu { + /** + * Construct the edit menu. + */ + constructor(options: Menu.IOptions) { + super(options); + this.menu.title.label = 'Edit'; + + this.undoers = + new Map>(); + + this.clearers = + new Map>(); + + this.findReplacers = + new Map>(); + } + + /** + * A map storing IUndoers for the Edit menu. + * + * ### Notes + * The key for the map may be used in menu labels. + */ + readonly undoers: Map>; + + /** + * A map storing IClearers for the Edit menu. + * + * ### Notes + * The key for the map may be used in menu labels. + */ + readonly clearers: Map>; + + /** + * A map storing IClearers for the Edit menu. + * + * ### Notes + * The key for the map may be used in menu labels. + */ + readonly findReplacers: Map>; + + /** + * Dispose of the resources held by the edit menu. + */ + dispose(): void { + this.undoers.clear(); + this.clearers.clear(); + this.findReplacers.clear(); + super.dispose(); + } +} + +/** + * Namespace for IEditMenu + */ +export +namespace IEditMenu { + /** + * Interface for an activity that uses Undo/Redo. + */ + export + interface IUndoer extends IMenuExtender { + /** + * Execute an undo command for the activity. + */ + undo?: (widget: T) => void; + + /** + * Execute a redo command for the activity. + */ + redo?: (widget: T) => void; + } + + /** + * Interface for an activity that wants to register a 'Clear...' menu item + */ + export + interface IClearer extends IMenuExtender { + /** + * A label for the thing to be cleared. + */ + noun: string; + + /** + * A function to clear an activity. + */ + clear: (widget: T) => void; + } + + /** + * Interface for an activity that uses Find/Find+Replace. + */ + export + interface IFindReplacer extends IMenuExtender { + /** + * Execute a find command for the activity. + */ + find?: (widget: T) => void; + + /** + * Execute a find/replace command for the activity. + */ + findAndReplace?: (widget: T) => void; + } +} diff --git a/packages/mainmenu/src/file.ts b/packages/mainmenu/src/file.ts new file mode 100644 index 000000000000..a523dfa8ae46 --- /dev/null +++ b/packages/mainmenu/src/file.ts @@ -0,0 +1,91 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { + Menu, Widget +} from '@phosphor/widgets'; + +import { + IJupyterLabMenu, IMenuExtender, JupyterLabMenu +} from './labmenu'; + +/** + * An interface for a File menu. + */ +export +interface IFileMenu extends IJupyterLabMenu { + /** + * A submenu for creating new files/launching new activities. + */ + readonly newMenu: Menu; + + /** + * The close and cleanup extension point. + */ + readonly closeAndCleaners: Map>; +} + +/** + * An extensible FileMenu for the application. + */ +export +class FileMenu extends JupyterLabMenu implements IFileMenu { + constructor(options: Menu.IOptions) { + super(options); + + this.menu.title.label = 'File'; + + // Create the "New" submenu. + this.newMenu = new Menu(options); + this.newMenu.title.label = 'New'; + this.addGroup([{ + type: 'submenu', + submenu: this.newMenu + }], 0); + + this.closeAndCleaners = + new Map>(); + } + + /** + * The New submenu. + */ + readonly newMenu: Menu; + + /** + * The close and cleanup extension point. + */ + readonly closeAndCleaners: Map>; + + /** + * Dispose of the resources held by the file menu. + */ + dispose(): void { + this.newMenu.dispose(); + super.dispose(); + } +} + + +/** + * Namespace for IFileMenu + */ +export +namespace IFileMenu { + /** + * Interface for an activity that has some cleanup action associated + * with it in addition to merely closing its widget in the main area. + */ + export + interface ICloseAndCleaner extends IMenuExtender { + /** + * A label to use for the cleanup action. + */ + action: string; + + /** + * A function to perform the close and cleanup action. + */ + closeAndCleanup: (widget: T) => Promise; + } +} diff --git a/packages/mainmenu/src/help.ts b/packages/mainmenu/src/help.ts new file mode 100644 index 000000000000..3af3683a09be --- /dev/null +++ b/packages/mainmenu/src/help.ts @@ -0,0 +1,31 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { + Menu +} from '@phosphor/widgets'; + +import { + IJupyterLabMenu, JupyterLabMenu +} from './labmenu'; + +/** + * An interface for a Help menu. + */ +export +interface IHelpMenu extends IJupyterLabMenu { +} + +/** + * An extensible Help menu for the application. + */ +export +class HelpMenu extends JupyterLabMenu implements IHelpMenu { + /** + * Construct the help menu. + */ + constructor(options: Menu.IOptions) { + super(options); + this.menu.title.label = 'Help'; + } +} diff --git a/packages/mainmenu/src/index.ts b/packages/mainmenu/src/index.ts new file mode 100644 index 000000000000..ffdcbc7bad61 --- /dev/null +++ b/packages/mainmenu/src/index.ts @@ -0,0 +1,11 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +export * from './mainmenu'; +export * from './labmenu'; +export * from './edit'; +export * from './file'; +export * from './help'; +export * from './kernel'; +export * from './run'; +export * from './view'; diff --git a/packages/mainmenu/src/kernel.ts b/packages/mainmenu/src/kernel.ts new file mode 100644 index 000000000000..67d165940dd4 --- /dev/null +++ b/packages/mainmenu/src/kernel.ts @@ -0,0 +1,124 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { + Kernel +} from '@jupyterlab/services'; + +import { + Menu, Widget +} from '@phosphor/widgets'; + +import { + IJupyterLabMenu, JupyterLabMenu, IMenuExtender +} from './labmenu'; + +/** + * An interface for a Kernel menu. + */ +export +interface IKernelMenu extends IJupyterLabMenu { + /** + * A map storing IKernelUsers for the Kernel menu. + * + * ### Notes + * The key for the map may be used in menu labels. + */ + readonly kernelUsers: Map>; + + /** + * A map storing IConsoleCreators for the Kernel menu. + * + * ### Notes + * The key for the map may be used in menu labels. + */ + readonly consoleCreators: Map>; +} + +/** + * An extensible Kernel menu for the application. + */ +export +class KernelMenu extends JupyterLabMenu implements IKernelMenu { + /** + * Construct the kernel menu. + */ + constructor(options: Menu.IOptions) { + super(options); + this.menu.title.label = 'Kernel'; + + this.kernelUsers = + new Map>(); + this.consoleCreators = + new Map>(); + } + + /** + * A map storing IKernelUsers for the Kernel menu. + * + * ### Notes + * The key for the map may be used in menu labels. + */ + readonly kernelUsers: Map>; + + /** + * A map storing IConsoleCreators for the Kernel menu. + * + * ### Notes + * The key for the map may be used in menu labels. + */ + readonly consoleCreators: Map>; + + /** + * Dispose of the resources held by the kernel menu. + */ + dispose(): void { + this.kernelUsers.clear(); + this.consoleCreators.clear(); + super.dispose(); + } +} + +/** + * Namespace for IKernelMenu + */ +export +namespace IKernelMenu { + /** + * Interface for a Kernel user to register itself + * with the IKernelMenu's semantic extension points. + */ + export + interface IKernelUser extends IMenuExtender { + /** + * A function to interrupt the kernel. + */ + interruptKernel?: (widget: T) => Promise; + + /** + * A function to restart the kernel. + */ + restartKernel?: (widget: T) => Promise; + + /** + * A function to change the kernel. + */ + changeKernel?: (widget: T) => Promise; + + /** + * A function to shut down the kernel. + */ + shutdownKernel?: (widget: T) => Promise; + } + + /** + * Interface for a command to create a console for an activity. + */ + export + interface IConsoleCreator extends IMenuExtender { + /** + * The function to create the console. + */ + createConsole: (widget: T) => Promise; + } +} diff --git a/packages/mainmenu/src/labmenu.ts b/packages/mainmenu/src/labmenu.ts new file mode 100644 index 000000000000..f485cb2982ae --- /dev/null +++ b/packages/mainmenu/src/labmenu.ts @@ -0,0 +1,152 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { + IInstanceTracker +} from '@jupyterlab/apputils'; + +import { + ArrayExt +} from '@phosphor/algorithm'; + +import { + IDisposable +} from '@phosphor/disposable'; + +import { + Menu, Widget +} from '@phosphor/widgets'; + + +/** + * A common interface for extensible JupyterLab application menus. + * + * Plugins are still free to define their own menus in any way + * they like. However, JupyterLab defines a few top-level + * application menus that may be extended by plugins as well, + * such as "Edit" and "View" + */ +export +interface IJupyterLabMenu extends IDisposable { + /** + * Add a group of menu items specific to a particular + * plugin. + */ + addGroup(items: Menu.IItemOptions[], rank?: number): void; +} + +/** + * A base interface for a consumer of one of the menu + * semantic extension points. The IMenuExtender gives + * an instance tracker which is checked when the menu + * is deciding which IMenuExtender to delegate to upon + * selection of the menu item. + */ +export +interface IMenuExtender { + /** + * A widget tracker for identifying the appropriate extender. + */ + tracker: IInstanceTracker; +} + +/** + * An extensible menu for JupyterLab application menus. + */ +export +class JupyterLabMenu implements IJupyterLabMenu { + /** + * Construct a new menu. + */ + constructor(options: Menu.IOptions) { + this.menu = new Menu(options); + } + + /** + * Add a group of menu items specific to a particular + * plugin. + */ + addGroup(items: Menu.IItemOptions[], rank?: number): void { + const rankGroup = { items, rank: rank === undefined ? 100 : rank }; + + // Insert the plugin group into the list of groups. + const groupIndex = ArrayExt.upperBound(this._groups, rankGroup, Private.itemCmp); + + // Determine the index of the menu at which to insert the group. + let insertIndex = 0; + for (let i = 0; i < groupIndex; ++i) { + if (this._groups.length > 0) { + // Increase the insert index by two extra in order + // to include the leading and trailing separators. + insertIndex += this._groups[i].items.length + 2; + } + } + + // Insert a separator before the group. + // Phosphor takes care of superfluous leading, + // trailing, and duplicate separators. + this.menu.insertItem(insertIndex++, { type: 'separator' }); + // Insert the group. + for (let item of items) { + this.menu.insertItem(insertIndex++, item); + } + // Insert a separator after the group. + this.menu.insertItem(insertIndex++, { type: 'separator' }); + + ArrayExt.insert(this._groups, groupIndex, rankGroup); + } + + /** + * The underlying Phosphor menu. + */ + readonly menu: Menu; + + /** + * Whether the menu has been disposed. + */ + get isDisposed(): boolean { + return this._isDisposed; + } + + /** + * Dispose of the resources held by the menu. + */ + dispose(): void { + this._groups.length = 0; + this._isDisposed = true; + this.menu.dispose(); + } + + private _groups: Private.IRankGroup[] = []; + private _isDisposed = false; +} + + +/** + * A namespace for private data. + */ +namespace Private { + /** + * An object which holds a menu and its sort rank. + */ + export + interface IRankGroup { + /** + * A menu grouping. + */ + items: Menu.IItemOptions[]; + + /** + * The sort rank of the group. + */ + rank: number; + } + + /** + * A comparator function for menu rank items. + */ + export + function itemCmp(first: IRankGroup, second: IRankGroup): number { + return first.rank - second.rank; + } +} diff --git a/packages/apputils/src/mainmenu.ts b/packages/mainmenu/src/mainmenu.ts similarity index 50% rename from packages/apputils/src/mainmenu.ts rename to packages/mainmenu/src/mainmenu.ts index ee7115b3aac5..af7a6b2f41d1 100644 --- a/packages/apputils/src/mainmenu.ts +++ b/packages/mainmenu/src/mainmenu.ts @@ -5,6 +5,10 @@ import { ArrayExt } from '@phosphor/algorithm'; +import { + CommandRegistry +} from '@phosphor/commands'; + import { Token } from '@phosphor/coreutils'; @@ -13,6 +17,31 @@ import { Menu, MenuBar } from '@phosphor/widgets'; +import { + IFileMenu, FileMenu +} from './file'; + +import { + IEditMenu, EditMenu +} from './edit'; + +import { + IHelpMenu, HelpMenu +} from './help'; + +import { + IKernelMenu, KernelMenu +} from './kernel'; + +import { + IRunMenu, RunMenu +} from './run'; + +import { + IViewMenu, ViewMenu +} from './view'; + + /* tslint:disable */ /** @@ -32,6 +61,36 @@ interface IMainMenu { * Add a new menu to the main menu bar. */ addMenu(menu: Menu, options?: IMainMenu.IAddOptions): void; + + /** + * The application "File" menu. + */ + readonly fileMenu: IFileMenu; + + /** + * The application "Edit" menu. + */ + readonly editMenu: IEditMenu; + + /** + * The application "View" menu. + */ + readonly viewMenu: IViewMenu; + + /** + * The application "Help" menu. + */ + readonly helpMenu: IHelpMenu; + + /** + * The application "Kernel" menu. + */ + readonly kernelMenu: IKernelMenu; + + /** + * The application "Run" menu. + */ + readonly runMenu: IRunMenu; } @@ -50,14 +109,65 @@ namespace IMainMenu { */ rank?: number; } -} +} /** * The main menu class. It is intended to be used as a singleton. */ export class MainMenu extends MenuBar implements IMainMenu { + /** + * Construct the main menu bar. + */ + constructor(commands: CommandRegistry) { + super(); + this.editMenu = new EditMenu({ commands }); + this.fileMenu = new FileMenu({ commands }); + this.helpMenu = new HelpMenu({ commands }); + this.kernelMenu = new KernelMenu({ commands }); + this.runMenu = new RunMenu({ commands }); + this.viewMenu = new ViewMenu({ commands }); + + this.addMenu(this.fileMenu.menu, { rank: 0 }); + this.addMenu(this.editMenu.menu, { rank: 1 }); + this.addMenu(this.runMenu.menu, { rank: 2 }); + this.addMenu(this.kernelMenu.menu, { rank: 3 }); + this.addMenu(this.viewMenu.menu, { rank: 4 }); + this.addMenu(this.helpMenu.menu, { rank: 1000 }); + } + + /** + * The application "Edit" menu. + */ + readonly editMenu: EditMenu; + + /** + * The application "File" menu. + */ + readonly fileMenu: FileMenu; + + /** + * The application "Help" menu. + */ + readonly helpMenu: HelpMenu; + + /** + * The application "Kernel" menu. + */ + readonly kernelMenu: KernelMenu; + + /** + * The application "Run" menu. + */ + readonly runMenu: RunMenu; + + /** + * The application "View" menu. + */ + readonly viewMenu: ViewMenu; + + /** * Add a new menu to the main menu bar. */ @@ -74,9 +184,25 @@ class MainMenu extends MenuBar implements IMainMenu { menu.disposed.connect(this._onMenuDisposed, this); ArrayExt.insert(this._items, index, rankItem); + /** + * Create a new menu. + */ this.insertMenu(index, menu); } + /** + * Dispose of the resources held by the menu bar. + */ + dispose(): void { + this.editMenu.dispose(); + this.fileMenu.dispose(); + this.helpMenu.dispose(); + this.kernelMenu.dispose(); + this.runMenu.dispose(); + this.viewMenu.dispose(); + super.dispose(); + } + /** * Handle the disposal of a menu. */ diff --git a/packages/mainmenu/src/run.ts b/packages/mainmenu/src/run.ts new file mode 100644 index 000000000000..025cca53ac1e --- /dev/null +++ b/packages/mainmenu/src/run.ts @@ -0,0 +1,98 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { + Menu, Widget +} from '@phosphor/widgets'; + +import { + IJupyterLabMenu, IMenuExtender, JupyterLabMenu +} from './labmenu'; + +/** + * An interface for a Run menu. + */ +export +interface IRunMenu extends IJupyterLabMenu { + /** + * A map storing ICodeRunner for the Run menu. + * + * ### Notes + * The key for the map may be used in menu labels. + */ + readonly codeRunners: Map>; +} + +/** + * An extensible Run menu for the application. + */ +export +class RunMenu extends JupyterLabMenu implements IRunMenu { + /** + * Construct the run menu. + */ + constructor(options: Menu.IOptions) { + super(options); + this.menu.title.label = 'Run'; + + this.codeRunners = + new Map>(); + } + + /** + * A map storing ICodeRunner for the Run menu. + * + * ### Notes + * The key for the map may be used in menu labels. + */ + readonly codeRunners: Map>; + + /** + * Dispose of the resources held by the run menu. + */ + dispose(): void { + this.codeRunners.clear(); + super.dispose(); + } +} + +/** + * A namespace for RunMenu statics. + */ +export +namespace IRunMenu { + /** + * An object that runs code, which may be + * registered with the Run menu. + */ + export + interface ICodeRunner extends IMenuExtender { + /** + * A string label for the thing that is being run, + * which is used to populate the menu labels. + */ + noun: string; + + /** + * A function to run a chunk of code. + */ + run?: (widget: T) => Promise; + + /** + * A function to run the entirety of the code hosted by the widget. + */ + runAll?: (widget: T) => Promise; + + /** + * A function to run all code above the currently selected + * point (exclusive). + */ + runAbove?: (widget: T) => Promise; + + /** + * A function to run all code below the currently selected + * point (inclusive). + */ + runBelow?: (widget: T) => Promise; + } +} diff --git a/packages/mainmenu/src/view.ts b/packages/mainmenu/src/view.ts new file mode 100644 index 000000000000..076f913602ee --- /dev/null +++ b/packages/mainmenu/src/view.ts @@ -0,0 +1,100 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { + Menu, Widget +} from '@phosphor/widgets'; + +import { + IJupyterLabMenu, IMenuExtender, JupyterLabMenu +} from './labmenu'; + +/** + * An interface for a View menu. + */ +export +interface IViewMenu extends IJupyterLabMenu { + /** + * A map storing IKernelUsers for the Kernel menu. + * + * ### Notes + * The key for the map may be used in menu labels. + */ + readonly editorViewers: Map>; +} + +/** + * An extensible View menu for the application. + */ +export +class ViewMenu extends JupyterLabMenu implements IViewMenu { + /** + * Construct the view menu. + */ + constructor(options: Menu.IOptions) { + super(options); + this.menu.title.label = 'View'; + + this.editorViewers = + new Map>(); + } + + /** + * A map storing IEditorViewers for the View menu. + * + * ### Notes + * The key for the map may be used in menu labels. + */ + readonly editorViewers: Map>; + + /** + * Dispose of the resources held by the view menu. + */ + dispose(): void { + this.editorViewers.clear(); + super.dispose(); + } +} + +/** + * Namespace for IViewMenu. + */ +export +namespace IViewMenu { + /** + * Interface for a text editor viewer to register + * itself with the text editor extension points. + */ + export + interface IEditorViewer extends IMenuExtender { + /** + * Whether to show line numbers in the editor. + */ + toggleLineNumbers?: (widget: T) => void; + + /** + * Whether to word-wrap the editor. + */ + toggleWordWrap?: (widget: T) => void; + + /** + * Whether to match brackets in the editor. + */ + toggleMatchBrackets?: (widget: T) => void; + + /** + * Whether line numbers are toggled. + */ + lineNumbersToggled?: (widget: T) => boolean; + + /** + * Whether word wrap is toggled. + */ + wordWrapToggled?: (widget: T) => boolean; + + /** + * Whether match brackets is toggled. + */ + matchBracketsToggled?: (widget: T) => boolean; + } +} diff --git a/packages/mainmenu/tsconfig.json b/packages/mainmenu/tsconfig.json new file mode 100644 index 000000000000..f0d24f39d29c --- /dev/null +++ b/packages/mainmenu/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "declaration": true, + "noImplicitAny": true, + "noEmitOnError": true, + "noUnusedLocals": true, + "module": "commonjs", + "moduleResolution": "node", + "target": "ES5", + "outDir": "./lib", + "lib": ["ES5", "ES2015.Promise", "DOM", "ES2015.Collection"], + "types": [] + }, + "include": ["src/*"] +} diff --git a/packages/metapackage/package.json b/packages/metapackage/package.json index afa8faae46ed..5f8e26a946c3 100644 --- a/packages/metapackage/package.json +++ b/packages/metapackage/package.json @@ -63,6 +63,8 @@ "@jupyterlab/json-extension": "^0.12.0", "@jupyterlab/launcher": "^0.12.0", "@jupyterlab/launcher-extension": "^0.12.0", + "@jupyterlab/mainmenu": "^0.1.0", + "@jupyterlab/mainmenu-extension": "^0.1.0", "@jupyterlab/markdownviewer-extension": "^0.12.0", "@jupyterlab/notebook": "^0.12.0", "@jupyterlab/notebook-extension": "^0.12.0", diff --git a/packages/metapackage/src/index.ts b/packages/metapackage/src/index.ts index 7230b322626d..22f29160dd26 100644 --- a/packages/metapackage/src/index.ts +++ b/packages/metapackage/src/index.ts @@ -32,6 +32,8 @@ import "@jupyterlab/inspector-extension"; import "@jupyterlab/json-extension"; import "@jupyterlab/launcher"; import "@jupyterlab/launcher-extension"; +import "@jupyterlab/mainmenu"; +import "@jupyterlab/mainmenu-extension"; import "@jupyterlab/markdownviewer-extension"; import "@jupyterlab/notebook"; import "@jupyterlab/notebook-extension"; diff --git a/packages/metapackage/tsconfig.json b/packages/metapackage/tsconfig.json index 89b8a21020c8..ddce24e64aa2 100644 --- a/packages/metapackage/tsconfig.json +++ b/packages/metapackage/tsconfig.json @@ -19,6 +19,9 @@ "paths": { "@jupyterlab/*": [ "../*/src" + ], + "@jupyterlab/mainmenu-extension": [ + "../mainmenu-extension/src" ] }, "jsx": "react", diff --git a/packages/notebook-extension/package.json b/packages/notebook-extension/package.json index 323ece3ab5e5..67855e7ebf21 100644 --- a/packages/notebook-extension/package.json +++ b/packages/notebook-extension/package.json @@ -33,7 +33,9 @@ "@jupyterlab/apputils": "^0.12.4", "@jupyterlab/codeeditor": "^0.12.0", "@jupyterlab/coreutils": "^0.12.0", + "@jupyterlab/filebrowser": "^0.12.1", "@jupyterlab/launcher": "^0.12.0", + "@jupyterlab/mainmenu": "^0.1.0", "@jupyterlab/notebook": "^0.12.0", "@jupyterlab/services": "^0.51.0", "@phosphor/coreutils": "^1.3.0", diff --git a/packages/notebook-extension/src/index.ts b/packages/notebook-extension/src/index.ts index a33ddf85ee4c..43f57245900f 100644 --- a/packages/notebook-extension/src/index.ts +++ b/packages/notebook-extension/src/index.ts @@ -6,7 +6,7 @@ import { } from '@jupyterlab/application'; import { - Dialog, ICommandPalette, IMainMenu, showDialog + Dialog, ICommandPalette, showDialog } from '@jupyterlab/apputils'; import { @@ -17,10 +17,18 @@ import { IStateDB, PageConfig, URLExt, uuid } from '@jupyterlab/coreutils'; +import { + IFileBrowserFactory +} from '@jupyterlab/filebrowser'; + import { ILauncher } from '@jupyterlab/launcher'; +import { + IMainMenu, IEditMenu, IFileMenu, IKernelMenu, IRunMenu, IViewMenu +} from '@jupyterlab/mainmenu'; + import { CellTools, ICellTools, INotebookTracker, NotebookActions, NotebookModelFactory, NotebookPanel, NotebookTracker, NotebookWidgetFactory @@ -48,6 +56,9 @@ import { * The command IDs used by the notebook plugin. */ namespace CommandIDs { + export + const createNew = 'notebook:create-new'; + export const interrupt = 'notebook:interrupt-kernel'; @@ -156,17 +167,14 @@ namespace CommandIDs { export const commandMode = 'notebook:enter-command-mode'; - export - const toggleLines = 'notebook:toggle-cell-line-numbers'; - export const toggleAllLines = 'notebook:toggle-all-cell-line-numbers'; export - const undo = 'notebook:undo-cell-action'; + const undoCellAction = 'notebook:undo-cell-action'; export - const redo = 'notebook:redo-cell-action'; + const redoCellAction = 'notebook:redo-cell-action'; export const markdown1 = 'notebook:change-cell-to-heading-1'; @@ -250,7 +258,7 @@ const tracker: JupyterLabPlugin = { IEditorServices, ILayoutRestorer ], - optional: [ILauncher], + optional: [IFileBrowserFactory, ILauncher], activate: activateNotebookHandler, autoStart: true }; @@ -361,7 +369,7 @@ function activateCellTools(app: JupyterLab, tracker: INotebookTracker, editorSer /** * Activate the notebook handler extension. */ -function activateNotebookHandler(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette, contentFactory: NotebookPanel.IContentFactory, editorServices: IEditorServices, restorer: ILayoutRestorer, launcher: ILauncher | null): INotebookTracker { +function activateNotebookHandler(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette, contentFactory: NotebookPanel.IContentFactory, editorServices: IEditorServices, restorer: ILayoutRestorer, browserFactory: IFileBrowserFactory | null, launcher: ILauncher | null): INotebookTracker { const services = app.serviceManager; const factory = new NotebookWidgetFactory({ name: FACTORY, @@ -385,21 +393,9 @@ function activateNotebookHandler(app: JupyterLab, mainMenu: IMainMenu, palette: when: services.ready }); - // Update the command registry when the notebook state changes. - tracker.currentChanged.connect(() => { - if (tracker.size <= 1) { - commands.notifyCommandChanged(CommandIDs.interrupt); - } - }); - let registry = app.docRegistry; registry.addModelFactory(new NotebookModelFactory({})); registry.addWidgetFactory(factory); - registry.addCreator({ - name: 'Notebook', - fileType: 'Notebook', - widgetName: 'Notebook' - }); addCommands(app, services, tracker); populatePalette(palette); @@ -417,20 +413,32 @@ function activateNotebookHandler(app: JupyterLab, mainMenu: IMainMenu, palette: }); // Add main menu notebook menu. - mainMenu.addMenu(createMenu(app), { rank: 20 }); + populateMenus(app, mainMenu, tracker); - // The launcher callback. - let callback = (cwd: string, name: string) => { + // Utility function to create a new notebook. + const createNew = (cwd: string, kernelName?: string) => { return commands.execute( 'docmanager:new-untitled', { path: cwd, type: 'notebook' } ).then(model => { return commands.execute('docmanager:open', { path: model.path, factory: FACTORY, - kernel: { name } + kernel: { name: kernelName } }); }); }; + // Add a command for creating a new notebook in the File Menu. + commands.addCommand(CommandIDs.createNew, { + label: 'Notebook', + caption: 'Create a new notebook', + execute: () => { + let cwd = browserFactory ? + browserFactory.defaultBrowser.model.path : ''; + return createNew(cwd); + } + }); + + // Add a launcher item if the launcher is available. if (launcher) { services.ready.then(() => { @@ -450,7 +458,7 @@ function activateNotebookHandler(app: JupyterLab, mainMenu: IMainMenu, palette: category: 'Notebook', name, iconClass: 'jp-NotebookRunningIcon', - callback, + callback: createNew, rank, kernelIconUrl }); @@ -476,12 +484,12 @@ function activateNotebookHandler(app: JupyterLab, mainMenu: IMainMenu, palette: rank: 0 }); app.contextMenu.addItem({ - command: CommandIDs.undo, + command: CommandIDs.undoCellAction, selector: '.jp-Notebook', rank: 1 }); app.contextMenu.addItem({ - command: CommandIDs.redo, + command: CommandIDs.redoCellAction, selector: '.jp-Notebook', rank: 2 }); @@ -527,8 +535,21 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo /** * Whether there is an active notebook. */ - function hasWidget(): boolean { - return tracker.currentWidget !== null; + function isEnabled(): boolean { + return tracker.currentWidget !== null && + tracker.currentWidget === app.shell.currentWidget; + } + + /** + * The name of the current notebook widget. + */ + function currentName(): string { + if (tracker.currentWidget && + tracker.currentWidget === app.shell.currentWidget && + tracker.currentWidget.title.label) { + return `"${tracker.currentWidget.title.label}"`; + } + return 'Notebook'; } commands.addCommand(CommandIDs.runAndAdvance, { @@ -542,7 +563,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.runAndAdvance(notebook, context.session); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.run, { label: 'Run Cell(s)', @@ -555,7 +576,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.run(notebook, context.session); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.runAndInsert, { label: 'Run Cell(s) and Insert Below', @@ -568,7 +589,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.runAndInsert(notebook, context.session); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.runAll, { label: 'Run All Cells', @@ -581,7 +602,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.runAll(notebook, context.session); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.restart, { label: 'Restart Kernel', @@ -592,7 +613,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return current.session.restart(); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.closeAndShutdown, { label: 'Close and Shutdown', @@ -616,10 +637,10 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo } }); }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.trust, { - label: 'Trust Notebook', + label: () => `Trust ${currentName()}`, execute: args => { const current = getCurrent(args); @@ -629,13 +650,14 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.trust(notebook).then(() => context.save()); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.exportToFormat, { label: args => { const formatLabel = (args['label']) as string; + const name = currentName(); - return (args['isPalette'] ? 'Export To ' : '') + formatLabel; + return (args['isPalette'] ? `Export ${name} to ` : '') + formatLabel; }, execute: args => { const current = getCurrent(args); @@ -663,7 +685,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo resolve(undefined); }); }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.restartClear, { label: 'Restart Kernel & Clear Outputs', @@ -677,7 +699,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo .then(() => { NotebookActions.clearAllOutputs(notebook); }); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.restartRunAll, { label: 'Restart Kernel & Run All', @@ -691,7 +713,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo .then(() => { NotebookActions.runAll(notebook, context.session); }); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.clearAllOutputs, { label: 'Clear All Outputs', @@ -702,7 +724,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.clearAllOutputs(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.clearOutputs, { label: 'Clear Output(s)', @@ -713,7 +735,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.clearOutputs(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.interrupt, { label: 'Interrupt Kernel', @@ -730,7 +752,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return kernel.interrupt(); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.toCode, { label: 'Change to Code Cell Type', @@ -741,7 +763,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.changeCellType(current.notebook, 'code'); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.toMarkdown, { label: 'Change to Markdown Cell Type', @@ -752,7 +774,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.changeCellType(current.notebook, 'markdown'); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.toRaw, { label: 'Change to Raw Cell Type', @@ -763,7 +785,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.changeCellType(current.notebook, 'raw'); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.cut, { label: 'Cut Cell(s)', @@ -774,7 +796,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.cut(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.copy, { label: 'Copy Cell(s)', @@ -785,7 +807,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.copy(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.paste, { label: 'Paste Cell(s) Below', @@ -796,7 +818,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.paste(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.deleteCell, { label: 'Delete Cell(s)', @@ -807,7 +829,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.deleteCells(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.split, { label: 'Split Cell', @@ -818,7 +840,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.splitCell(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.merge, { label: 'Merge Selected Cell(s)', @@ -829,7 +851,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.mergeCells(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.insertAbove, { label: 'Insert Cell Above', @@ -840,7 +862,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.insertAbove(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.insertBelow, { label: 'Insert Cell Below', @@ -851,7 +873,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.insertBelow(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.selectAbove, { label: 'Select Cell Above', @@ -862,7 +884,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.selectAbove(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.selectBelow, { label: 'Select Cell Below', @@ -873,7 +895,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.selectBelow(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.extendAbove, { label: 'Extend Selection Above', @@ -884,7 +906,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.extendSelectionAbove(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.extendBelow, { label: 'Extend Selection Below', @@ -895,7 +917,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.extendSelectionBelow(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.moveUp, { label: 'Move Cell(s) Up', @@ -906,7 +928,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.moveUp(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.moveDown, { label: 'Move Cell(s) Down', @@ -917,18 +939,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.moveDown(current.notebook); } }, - isEnabled: hasWidget - }); - commands.addCommand(CommandIDs.toggleLines, { - label: 'Toggle Line Numbers', - execute: args => { - const current = getCurrent(args); - - if (current) { - return NotebookActions.toggleLineNumbers(current.notebook); - } - }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.toggleAllLines, { label: 'Toggle All Line Numbers', @@ -939,7 +950,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.toggleAllLineNumbers(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.commandMode, { label: 'Enter Command Mode', @@ -950,7 +961,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo current.notebook.mode = 'command'; } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.editMode, { label: 'Enter Edit Mode', @@ -961,9 +972,9 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo current.notebook.mode = 'edit'; } }, - isEnabled: hasWidget + isEnabled }); - commands.addCommand(CommandIDs.undo, { + commands.addCommand(CommandIDs.undoCellAction, { label: 'Undo Cell Operation', execute: args => { const current = getCurrent(args); @@ -972,9 +983,9 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.undo(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); - commands.addCommand(CommandIDs.redo, { + commands.addCommand(CommandIDs.redoCellAction, { label: 'Redo Cell Operation', execute: args => { const current = getCurrent(args); @@ -983,7 +994,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.redo(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.changeKernel, { label: 'Change Kernel', @@ -994,7 +1005,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return current.context.session.selectKernel(); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.reconnectToKernel, { label: 'Reconnect To Kernel', @@ -1011,7 +1022,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return kernel.reconnect(); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.createCellView, { label: 'Create New View for Cell', @@ -1032,7 +1043,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo p.addWidget(newCell); shell.addToMainArea(p); }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.createConsole, { label: 'Create Console for Notebook', @@ -1052,7 +1063,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return commands.execute('console:create', options); }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.markdown1, { label: 'Change to Heading 1', @@ -1063,7 +1074,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.setMarkdownHeader(current.notebook, 1); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.markdown2, { label: 'Change to Heading 2', @@ -1074,7 +1085,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.setMarkdownHeader(current.notebook, 2); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.markdown3, { label: 'Change to Heading 3', @@ -1085,7 +1096,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.setMarkdownHeader(current.notebook, 3); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.markdown4, { label: 'Change to Heading 4', @@ -1096,7 +1107,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.setMarkdownHeader(current.notebook, 4); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.markdown5, { label: 'Change to Heading 5', @@ -1107,7 +1118,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.setMarkdownHeader(current.notebook, 5); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.markdown6, { label: 'Change to Heading 6', @@ -1118,7 +1129,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.setMarkdownHeader(current.notebook, 6); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.hideCode, { label: 'Hide Code', @@ -1129,7 +1140,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.hideCode(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.showCode, { label: 'Show Code', @@ -1140,7 +1151,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.showCode(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.hideAllCode, { label: 'Hide All Code', @@ -1151,7 +1162,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.hideAllCode(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.showAllCode, { label: 'Show All Code', @@ -1162,7 +1173,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.showAllCode(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.hideOutput, { label: 'Hide Output', @@ -1173,7 +1184,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.hideOutput(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.showOutput, { label: 'Show Output', @@ -1184,7 +1195,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.showOutput(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.hideAllOutputs, { label: 'Hide All Outputs', @@ -1195,7 +1206,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.hideAllOutputs(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand(CommandIDs.showAllOutputs, { label: 'Show All Outputs', @@ -1206,9 +1217,9 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo return NotebookActions.showAllOutputs(current.notebook); } }, - isEnabled: hasWidget + isEnabled }); - } +} /** @@ -1261,9 +1272,8 @@ function populatePalette(palette: ICommandPalette): void { CommandIDs.extendBelow, CommandIDs.moveDown, CommandIDs.moveUp, - CommandIDs.toggleLines, - CommandIDs.undo, - CommandIDs.redo, + CommandIDs.undoCellAction, + CommandIDs.redoCellAction, CommandIDs.markdown1, CommandIDs.markdown2, CommandIDs.markdown3, @@ -1283,52 +1293,151 @@ function populatePalette(palette: ICommandPalette): void { /** - * Creates a menu for the notebook. + * Populates the application menus for the notebook. */ -function createMenu(app: JupyterLab): Menu { +function populateMenus(app: JupyterLab, mainMenu: IMainMenu, tracker: INotebookTracker): void { let { commands } = app; - let menu = new Menu({ commands }); - let settings = new Menu({ commands }); - let exportTo = new Menu({ commands } ); - menu.title.label = 'Notebook'; - settings.title.label = 'Settings'; - settings.addItem({ command: CommandIDs.toggleAllLines }); + // Add undo/redo hooks to the edit menu. + mainMenu.editMenu.undoers.set('Notebook', { + tracker, + undo: widget => { widget.notebook.activeCell.editor.undo(); }, + redo: widget => { widget.notebook.activeCell.editor.redo(); } + } as IEditMenu.IUndoer); + + // Add a clearer to the edit menu + mainMenu.editMenu.clearers.set('Notebook', { + tracker, + noun: 'All Cell Outputs', + clear: (current: NotebookPanel) => { + return NotebookActions.clearAllOutputs(current.notebook); + } + } as IEditMenu.IClearer); + + // Add new notebook creation to the file menu. + mainMenu.fileMenu.newMenu.addItem({ command: CommandIDs.createNew }); + + // Add a close and shutdown command to the file menu. + mainMenu.fileMenu.closeAndCleaners.set('Notebook', { + tracker, + action: 'Shutdown', + closeAndCleanup: (current: NotebookPanel) => { + const fileName = current.title.label; + return showDialog({ + title: 'Shutdown the notebook?', + body: `Are you sure you want to close "${fileName}"?`, + buttons: [Dialog.cancelButton(), Dialog.warnButton()] + }).then(result => { + if (result.button.accept) { + return current.context.session.shutdown() + .then(() => { current.dispose(); }); + } + }); + } + } as IFileMenu.ICloseAndCleaner); + // Add a notebook group to the File menu. + let exportTo = new Menu({ commands } ); exportTo.title.label = 'Export to ...'; EXPORT_TO_FORMATS.forEach(exportToFormat => { exportTo.addItem({ command: CommandIDs.exportToFormat, args: exportToFormat }); }); + const fileGroup = [ + { command: CommandIDs.trust }, + { type: 'submenu', submenu: exportTo } as Menu.IItemOptions + ]; + mainMenu.fileMenu.addGroup(fileGroup, 10); + + // Add a kernel user to the Kernel menu + mainMenu.kernelMenu.kernelUsers.set('Notebook', { + tracker, + interruptKernel: current => { + let kernel = current.session.kernel; + if (kernel) { + return kernel.interrupt(); + } + return Promise.resolve(void 0); + }, + restartKernel: current => current.session.restart(), + changeKernel: current => current.session.selectKernel(), + shutdownKernel: current => current.session.shutdown(), + } as IKernelMenu.IKernelUser); + + // Add a console creator the the Kernel menu + mainMenu.kernelMenu.consoleCreators.set('Notebook', { + tracker, + createConsole: current => { + const options: ReadonlyJSONObject = { + path: current.context.path, + preferredLanguage: current.context.model.defaultKernelLanguage + }; + return commands.execute('console:create', options); + } + } as IKernelMenu.IConsoleCreator); - menu.addItem({ command: CommandIDs.undo }); - menu.addItem({ command: CommandIDs.redo }); - menu.addItem({ type: 'separator' }); - menu.addItem({ command: CommandIDs.cut }); - menu.addItem({ command: CommandIDs.copy }); - menu.addItem({ command: CommandIDs.paste }); - menu.addItem({ command: CommandIDs.deleteCell }); - menu.addItem({ type: 'separator' }); - menu.addItem({ command: CommandIDs.split }); - menu.addItem({ command: CommandIDs.merge }); - menu.addItem({ type: 'separator' }); - menu.addItem({ command: CommandIDs.hideAllCode }); - menu.addItem({ command: CommandIDs.showAllCode }); - menu.addItem({ command: CommandIDs.hideAllOutputs }); - menu.addItem({ command: CommandIDs.showAllOutputs }); - menu.addItem({ command: CommandIDs.clearAllOutputs }); - menu.addItem({ type: 'separator' }); - menu.addItem({ command: CommandIDs.runAll }); - menu.addItem({ command: CommandIDs.interrupt }); - menu.addItem({ command: CommandIDs.restart }); - menu.addItem({ command: CommandIDs.changeKernel }); - menu.addItem({ type: 'separator' }); - menu.addItem({ command: CommandIDs.createConsole }); - menu.addItem({ type: 'separator' }); - menu.addItem({ command: CommandIDs.closeAndShutdown }); - menu.addItem({ command: CommandIDs.trust }); - menu.addItem({ type: 'submenu', submenu: exportTo }); - menu.addItem({ type: 'separator' }); - menu.addItem({ type: 'submenu', submenu: settings }); - - return menu; + // Add some commands to the application view menu. + const viewGroup = [ + CommandIDs.hideAllCode, + CommandIDs.showAllCode, + CommandIDs.hideAllOutputs, + CommandIDs.showAllOutputs + ].map(command => { return { command }; }); + mainMenu.viewMenu.addGroup(viewGroup, 10); + + // Add an IEditorViewer to the application view menu + mainMenu.viewMenu.editorViewers.set('Notebook', { + tracker, + toggleLineNumbers: widget => { + NotebookActions.toggleAllLineNumbers(widget.notebook); + }, + toggleMatchBrackets: widget => { + NotebookActions.toggleAllMatchBrackets(widget.notebook); + }, + lineNumbersToggled: widget => + widget.notebook.activeCell.editor.getOption('lineNumbers'), + matchBracketsToggled: widget => + widget.notebook.activeCell.editor.getOption('matchBrackets'), + } as IViewMenu.IEditorViewer); + + // Add an ICodeRunner to the application run menu + mainMenu.runMenu.codeRunners.set('Notebook', { + tracker, + noun: 'Cell', + run: current => { + const { context, notebook } = current; + return NotebookActions.runAndAdvance(notebook, context.session) + .then(() => void 0); + }, + runAll: current => { + const { context, notebook } = current; + return NotebookActions.runAll(notebook, context.session) + .then(() => void 0); + }, + runAbove: current => { + const { context, notebook } = current; + return NotebookActions.runAllAbove(notebook, context.session) + .then(() => void 0); + }, + runBelow: current => { + const { context, notebook } = current; + return NotebookActions.runAllBelow(notebook, context.session) + .then(() => void 0); + } + } as IRunMenu.ICodeRunner); + + // Add commands to the application edit menu. + const undoCellActionGroup = [ + CommandIDs.undoCellAction, + CommandIDs.redoCellAction + ].map(command => { return { command }; }); + const editGroup = [ + CommandIDs.cut, + CommandIDs.copy, + CommandIDs.paste, + CommandIDs.deleteCell, + CommandIDs.split, + CommandIDs.merge + ].map(command => { return { command }; }); + mainMenu.editMenu.addGroup(undoCellActionGroup, 4); + mainMenu.editMenu.addGroup(editGroup, 5); } diff --git a/packages/notebook/src/actions.ts b/packages/notebook/src/actions.ts index 6ef7257ea838..99d396ddc7d3 100644 --- a/packages/notebook/src/actions.ts +++ b/packages/notebook/src/actions.ts @@ -466,6 +466,64 @@ namespace NotebookActions { return promise; } + /** + * Run all of the cells before the currently active cell (exclusive). + * + * @param widget - The target notebook widget. + * + * @param session - The optional client session object. + * + * #### Notes + * The existing selection will be cleared. + * An execution error will prevent the remaining code cells from executing. + * All markdown cells will be rendered. + * The currently active cell will remain selected. + */ + export + function runAllAbove(widget: Notebook, session?: IClientSession): Promise { + if (!widget.model || !widget.activeCell || widget.activeCellIndex === 0) { + return Promise.resolve(false); + } + let state = Private.getState(widget); + widget.activeCellIndex--; + widget.deselectAll(); + for (let i = 0; i < widget.activeCellIndex; ++i) { + widget.select(widget.widgets[i]); + } + let promise = Private.runSelected(widget, session); + widget.activeCellIndex++; + Private.handleRunState(widget, state, true); + return promise; + } + + /** + * Run all of the cells after the currently active cell (inclusive). + * + * @param widget - The target notebook widget. + * + * @param session - The optional client session object. + * + * #### Notes + * The existing selection will be cleared. + * An execution error will prevent the remaining code cells from executing. + * All markdown cells will be rendered. + * The last cell in the notebook will be activated and scrolled into view. + */ + export + function runAllBelow(widget: Notebook, session?: IClientSession): Promise { + if (!widget.model || !widget.activeCell) { + return Promise.resolve(false); + } + let state = Private.getState(widget); + widget.deselectAll(); + for (let i = widget.activeCellIndex; i < widget.widgets.length; ++i) { + widget.select(widget.widgets[i]); + } + let promise = Private.runSelected(widget, session); + Private.handleRunState(widget, state, true); + return promise; + } + /** * Select the above the active cell. * @@ -706,7 +764,7 @@ namespace NotebookActions { } /** - * Toggle line numbers on the selected cell(s). + * Toggle match-brackets of all cells. * * @param widget - The target notebook widget. * @@ -715,16 +773,14 @@ namespace NotebookActions { * The `mode` of the widget will be preserved. */ export - function toggleLineNumbers(widget: Notebook): void { + function toggleAllMatchBrackets(widget: Notebook): void { if (!widget.model || !widget.activeCell) { return; } let state = Private.getState(widget); - let lineNumbers = widget.activeCell.editor.getOption('lineNumbers'); + let matchBrackets = widget.activeCell.editor.getOption('matchBrackets'); each(widget.widgets, child => { - if (widget.isSelected(child)) { - child.editor.setOption('lineNumbers', !lineNumbers); - } + child.editor.setOption('matchBrackets', !matchBrackets); }); Private.handleState(widget, state); } diff --git a/packages/notebook/src/widget.ts b/packages/notebook/src/widget.ts index 93f1e17fb22e..5b2cc5930ed3 100644 --- a/packages/notebook/src/widget.ts +++ b/packages/notebook/src/widget.ts @@ -60,6 +60,21 @@ import { } from './model'; +/** + * The data attribute added to a widget that has an active kernel. + */ +const KERNEL_USER = 'jpKernelUser'; + +/** + * The data attribute added to a widget that can run code. + */ +const CODE_RUNNER = 'jpCodeRunner'; + +/** + * The data attribute added to a widget that can undo. + */ +const UNDOER = 'jpUndoer'; + /** * The class name added to notebook widgets. */ @@ -169,6 +184,9 @@ class StaticNotebook extends Widget { constructor(options: StaticNotebook.IOptions) { super(); this.addClass(NB_CLASS); + this.node.dataset[KERNEL_USER] = 'true'; + this.node.dataset[CODE_RUNNER] = 'true'; + this.node.dataset[UNDOER] = 'true'; this.rendermime = options.rendermime; this.layout = new Private.NotebookPanelLayout(); this.contentFactory = ( diff --git a/packages/shortcuts-extension/schema/plugin.json b/packages/shortcuts-extension/schema/plugin.json index 6ef1b5ac356f..2ff0f7739be1 100644 --- a/packages/shortcuts-extension/schema/plugin.json +++ b/packages/shortcuts-extension/schema/plugin.json @@ -67,10 +67,10 @@ }, "type": "object" }, - "console:run": { + "console:run-unforced": { "default": { }, "properties": { - "command": { "default": "console:run" }, + "command": { "default": "console:run-unforced" }, "keys": { "default": ["Enter"] }, "selector": { "default": ".jp-CodeConsole-promptCell" } }, @@ -103,6 +103,24 @@ }, "type": "object" }, + "editmenu:undo": { + "default": { }, + "properties": { + "command": { "default": "editmenu:undo" }, + "keys": { "default": ["Ctrl Z"] }, + "selector": { "default": "[data-jp-undoer]" } + }, + "type": "object" + }, + "editmenu:redo": { + "default": { }, + "properties": { + "command": { "default": "editmenu:redo" }, + "keys": { "default": ["Ctrl Shift Z"] }, + "selector": { "default": "[data-jp-undoer]" } + }, + "type": "object" + }, "filebrowser:create-main-launcher": { "default": { }, "properties": { @@ -121,15 +139,6 @@ }, "type": "object" }, - "fileeditor:run-code": { - "default": { }, - "properties": { - "command": { "default": "fileeditor:run-code" }, - "keys": { "default": ["Shift Enter"] }, - "selector": { "default": ".jp-FileEditor" } - }, - "type": "object" - }, "help:toggle": { "default": { }, "properties": { @@ -175,6 +184,24 @@ }, "type": "object" }, + "kernelmenu:interrupt": { + "default": { }, + "properties": { + "command": { "default": "kernelmenu:interrupt" }, + "keys": { "default": ["I", "I"] }, + "selector": { "default": "[data-jp-kernel-user]:focus" } + }, + "type": "object" + }, + "kernelmenu:restart": { + "default": { }, + "properties": { + "command": { "default": "kernelmenu:restart" }, + "keys": { "default": ["0", "0"] }, + "selector": { "default": "[data-jp-kernel-user]:focus" } + }, + "type": "object" + }, "notebook:change-cell-to-code": { "default": { }, "properties": { @@ -366,15 +393,6 @@ }, "type": "object" }, - "notebook:interrupt-kernel": { - "default": { }, - "properties": { - "command": { "default": "notebook:interrupt-kernel" }, - "keys": { "default": ["I", "I"] }, - "selector": { "default": ".jp-Notebook:focus" } - }, - "type": "object" - }, "notebook:merge-cells": { "default": { }, "properties": { @@ -438,15 +456,6 @@ }, "type": "object" }, - "notebook:restart-kernel": { - "default": { }, - "properties": { - "command": { "default": "notebook:restart-kernel" }, - "keys": { "default": ["0", "0"] }, - "selector": { "default": ".jp-Notebook:focus" } - }, - "type": "object" - }, "notebook:run-cell-1": { "default": { }, "properties": { @@ -483,7 +492,7 @@ }, "type": "object" }, - "notebook:run-cell-and-select-next-1": { + "notebook:run-cell-and-select-next": { "default": { }, "properties": { "command": { "default": "notebook:run-cell-and-select-next" }, @@ -492,15 +501,6 @@ }, "type": "object" }, - "notebook:run-cell-and-select-next-2": { - "default": { }, - "properties": { - "command": { "default": "notebook:run-cell-and-select-next" }, - "keys": { "default": ["Shift Enter"] }, - "selector": { "default": ".jp-Notebook:focus" } - }, - "type": "object" - }, "notebook:split-cell-at-cursor": { "default": { }, "properties": { @@ -537,6 +537,15 @@ }, "type": "object" }, + "runmenu:run": { + "default": { }, + "properties": { + "command": { "default": "runmenu:run" }, + "keys": { "default": ["Shift Enter"] }, + "selector": { "default": "[data-jp-code-runner]" } + }, + "type": "object" + }, "settingeditor:debug": { "default": { }, "properties": { diff --git a/packages/terminal-extension/package.json b/packages/terminal-extension/package.json index 6c4a467022a8..ec05498f3375 100644 --- a/packages/terminal-extension/package.json +++ b/packages/terminal-extension/package.json @@ -32,9 +32,9 @@ "@jupyterlab/application": "^0.12.0", "@jupyterlab/apputils": "^0.12.4", "@jupyterlab/launcher": "^0.12.0", + "@jupyterlab/mainmenu": "^0.1.0", "@jupyterlab/services": "^0.51.0", - "@jupyterlab/terminal": "^0.12.0", - "@phosphor/widgets": "^1.5.0" + "@jupyterlab/terminal": "^0.12.0" }, "devDependencies": { "rimraf": "~2.6.2", diff --git a/packages/terminal-extension/src/index.ts b/packages/terminal-extension/src/index.ts index 4ee6ba3fa817..5f05eba91cb4 100644 --- a/packages/terminal-extension/src/index.ts +++ b/packages/terminal-extension/src/index.ts @@ -6,13 +6,17 @@ import { } from '@jupyterlab/application'; import { - ICommandPalette, IMainMenu, InstanceTracker + ICommandPalette, InstanceTracker } from '@jupyterlab/apputils'; import { ILauncher } from '@jupyterlab/launcher'; +import { + IMainMenu +} from '@jupyterlab/mainmenu'; + import { ServiceManager } from '@jupyterlab/services'; @@ -21,10 +25,6 @@ import { Terminal, ITerminalTracker } from '@jupyterlab/terminal'; -import { - Menu -} from '@phosphor/widgets'; - /** * The command IDs used by the terminal plugin. @@ -99,32 +99,29 @@ function activate(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette name: widget => widget.session && widget.session.name }); - // Update the command registry when the terminal state changes. - tracker.currentChanged.connect(() => { - if (tracker.size <= 1) { - commands.notifyCommandChanged(CommandIDs.refresh); - } - }); - addCommands(app, serviceManager, tracker); - // Add command palette and menu items. - const menu = new Menu({ commands }); + // Add some commands to the application view menu. + const viewGroup = [ + CommandIDs.refresh, + CommandIDs.increaseFont, + CommandIDs.decreaseFont, + CommandIDs.toggleTheme + ].map(command => { return { command }; }); + mainMenu.viewMenu.addGroup(viewGroup, 30); - menu.title.label = category; + // Add command palette items. [ - CommandIDs.createNew, CommandIDs.refresh, CommandIDs.increaseFont, CommandIDs.decreaseFont, CommandIDs.toggleTheme ].forEach(command => { palette.addItem({ command, category }); - if (command !== CommandIDs.createNew) { - menu.addItem({ command }); - } }); - mainMenu.addMenu(menu, {rank: 40}); + + // Add terminal creation to the file menu. + mainMenu.fileMenu.newMenu.addItem({ command: CommandIDs.createNew }); // Add a launcher item if the launcher is available. if (launcher) { @@ -155,13 +152,14 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Instanc /** * Whether there is an active terminal. */ - function hasWidget(): boolean { - return tracker.currentWidget !== null; + function isEnabled(): boolean { + return tracker.currentWidget !== null && + tracker.currentWidget === app.shell.currentWidget; } // Add terminal commands. commands.addCommand(CommandIDs.createNew, { - label: 'New Terminal', + label: 'Terminal', caption: 'Start a new terminal session', execute: args => { let name = args['name'] as string; @@ -229,7 +227,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Instanc tracker.forEach(widget => { widget.fontSize = options.fontSize; }); } }, - isEnabled: hasWidget + isEnabled }); commands.addCommand('terminal:decrease-font', { @@ -241,22 +239,23 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Instanc tracker.forEach(widget => { widget.fontSize = options.fontSize; }); } }, - isEnabled: hasWidget + isEnabled }); + let terminalTheme: Terminal.Theme = 'dark'; commands.addCommand('terminal:toggle-theme', { - label: 'Toggle Terminal Theme', - caption: 'Switch Terminal Theme', + label: 'Use Dark Terminal Theme', + caption: 'Whether to use the dark terminal theme', + isToggled: () => terminalTheme === 'dark', execute: () => { + terminalTheme = terminalTheme === 'dark' ? 'light' : 'dark'; tracker.forEach(widget => { - if (widget.theme === 'dark') { - widget.theme = 'light'; - } else { - widget.theme = 'dark'; + if (widget.theme !== terminalTheme) { + widget.theme = terminalTheme; } }); }, - isEnabled: hasWidget + isEnabled }); } diff --git a/test/package.json b/test/package.json index 6c8a3556f5fd..ee8dc177a068 100644 --- a/test/package.json +++ b/test/package.json @@ -32,6 +32,7 @@ "@jupyterlab/fileeditor": "^0.12.0", "@jupyterlab/imageviewer": "^0.12.0", "@jupyterlab/inspector": "^0.12.0", + "@jupyterlab/mainmenu": "^0.1.0", "@jupyterlab/notebook": "^0.12.0", "@jupyterlab/outputarea": "^0.12.0", "@jupyterlab/rendermime": "^0.12.0", diff --git a/test/src/docregistry/registry.spec.ts b/test/src/docregistry/registry.spec.ts index 3168dfd4e9de..339525a9f4ff 100644 --- a/test/src/docregistry/registry.spec.ts +++ b/test/src/docregistry/registry.spec.ts @@ -235,46 +235,6 @@ describe('docregistry/registry', () => { }); - describe('#addCreator()', () => { - - it('should add a file type to the document registry', () => { - let creator = { name: 'notebook', fileType: 'notebook' }; - registry.addCreator(creator); - expect(registry.creators().next()).to.be(creator); - }); - - it('should be removed from the registry when disposed', () => { - let creator = { name: 'notebook', fileType: 'notebook' }; - let disposable = registry.addCreator(creator); - disposable.dispose(); - expect(toArray(registry.creators()).length).to.be(0); - }); - - it('should end up in locale order', () => { - let creators = [ - { name: 'Python Notebook', fileType: 'notebook' }, - { name: 'R Notebook', fileType: 'notebook' }, - { name: 'CSharp Notebook', fileType: 'notebook' } - ]; - registry.addCreator(creators[0]); - registry.addCreator(creators[1]); - registry.addCreator(creators[2]); - let it = registry.creators(); - expect(it.next()).to.be(creators[2]); - expect(it.next()).to.be(creators[0]); - expect(it.next()).to.be(creators[1]); - }); - - it('should be a no-op if a file type of the same name is registered', () => { - let creator = { name: 'notebook', fileType: 'notebook' }; - registry.addCreator(creator); - let disposable = registry.addCreator(creator); - disposable.dispose(); - expect(registry.creators().next()).to.eql(creator); - }); - - }); - describe('#preferredWidgetFactories()', () => { beforeEach(() => { @@ -411,24 +371,6 @@ describe('docregistry/registry', () => { }); - describe('#creators()', () => { - - it('should get the registered file creators', () => { - expect(toArray(registry.creators()).length).to.be(0); - let creators = [ - { name: 'Python Notebook', fileType: 'notebook' }, - { name: 'R Notebook', fileType: 'notebook' }, - { name: 'CSharp Notebook', fileType: 'notebook' } - ]; - registry.addCreator(creators[0]); - registry.addCreator(creators[1]); - registry.addCreator(creators[2]); - expect(toArray(registry.creators()).length).to.be(3); - expect(registry.creators().next().name).to.be('CSharp Notebook'); - }); - - }); - describe('#getFileType()', () => { it('should get a file type by name', () => { @@ -438,25 +380,6 @@ describe('docregistry/registry', () => { }); }); - describe('#getCreator()', () => { - - it('should get a creator by name', () => { - let creators = [ - { name: 'Python Notebook', fileType: 'notebook' }, - { name: 'R Notebook', fileType: 'notebook' }, - { name: 'Shell Notebook', fileType: 'notebook' } - ]; - registry.addCreator(creators[0]); - registry.addCreator(creators[1]); - registry.addCreator(creators[2]); - expect(registry.getCreator('Python Notebook')).to.be(creators[0]); - expect(registry.getCreator('r notebook')).to.be(creators[1]); - expect(registry.getCreator('shell Notebook')).to.be(creators[2]); - expect(registry.getCreator('foo')).to.be(void 0); - }); - - }); - describe('#getKernelPreference()', () => { it('should get a kernel preference', () => { diff --git a/test/src/index.ts b/test/src/index.ts index 3230cd822951..e9fed9a1de26 100644 --- a/test/src/index.ts +++ b/test/src/index.ts @@ -73,6 +73,15 @@ import './imageviewer/widget.spec'; import './inspector/inspector.spec'; +import './mainmenu/mainmenu.spec'; +import './mainmenu/labmenu.spec'; +import './mainmenu/edit.spec'; +import './mainmenu/file.spec'; +import './mainmenu/help.spec'; +import './mainmenu/kernel.spec'; +import './mainmenu/run.spec'; +import './mainmenu/view.spec'; + import './notebook/actions.spec'; import './notebook/celltools.spec'; import './notebook/default-toolbar.spec'; diff --git a/test/src/mainmenu/edit.spec.ts b/test/src/mainmenu/edit.spec.ts new file mode 100644 index 000000000000..5e56ce4e5b85 --- /dev/null +++ b/test/src/mainmenu/edit.spec.ts @@ -0,0 +1,126 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import expect = require('expect.js'); + +import { + CommandRegistry +} from '@phosphor/commands'; + +import { + Widget +} from '@phosphor/widgets'; + +import { + InstanceTracker +} from '@jupyterlab/apputils'; + +import { + EditMenu, IEditMenu +} from '@jupyterlab/mainmenu'; + +class Wodget extends Widget { + state: string; +} + +describe('@jupyterlab/mainmenu', () => { + + describe('EditMenu', () => { + + let commands: CommandRegistry; + let menu: EditMenu; + let tracker: InstanceTracker; + let wodget = new Wodget(); + + before(() => { + commands = new CommandRegistry(); + }); + + beforeEach(() => { + menu = new EditMenu({ commands }); + tracker = new InstanceTracker({ namespace: 'wodget' }); + tracker.add(wodget); + }); + + afterEach(() => { + menu.dispose(); + tracker.dispose(); + wodget.dispose(); + }); + + describe('#constructor()', () => { + + it('should construct a new edit menu', () => { + expect(menu).to.be.an(EditMenu); + expect(menu.menu.title.label).to.be('Edit'); + }); + + }); + + describe('#undoers', () => { + + it('should allow setting of an IUndoer', () => { + const undoer: IEditMenu.IUndoer = { + tracker, + undo: widget => { + widget.state = 'undo'; + return; + }, + redo: widget => { + widget.state = 'redo'; + return; + } + } + menu.undoers.set('Wodget', undoer); + menu.undoers.get('Wodget').undo(wodget); + expect(wodget.state).to.be('undo'); + menu.undoers.get('Wodget').redo(wodget); + expect(wodget.state).to.be('redo'); + }); + + }); + + describe('#clearers', () => { + + it('should allow setting of an IClearer', () => { + const clearer: IEditMenu.IClearer = { + tracker, + noun: 'Nouns', + clear: widget => { + widget.state = 'clear'; + return; + }, + } + menu.clearers.set('Wodget', clearer); + menu.clearers.get('Wodget').clear(wodget); + expect(wodget.state).to.be('clear'); + }); + + }); + + describe('#findReplacers', () => { + + it('should allow setting of an IFindReplacer', () => { + const finder: IEditMenu.IFindReplacer = { + tracker, + find: widget => { + widget.state = 'find'; + return; + }, + findAndReplace: widget => { + widget.state = 'findAndReplace'; + return; + }, + } + menu.findReplacers.set('Wodget', finder); + menu.findReplacers.get('Wodget').find(wodget); + expect(wodget.state).to.be('find'); + menu.findReplacers.get('Wodget').findAndReplace(wodget); + expect(wodget.state).to.be('findAndReplace'); + }); + + }); + + }); + +}); diff --git a/test/src/mainmenu/file.spec.ts b/test/src/mainmenu/file.spec.ts new file mode 100644 index 000000000000..883cbb938433 --- /dev/null +++ b/test/src/mainmenu/file.spec.ts @@ -0,0 +1,88 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import expect = require('expect.js'); + +import { + CommandRegistry +} from '@phosphor/commands'; + +import { + Widget +} from '@phosphor/widgets'; + +import { + InstanceTracker +} from '@jupyterlab/apputils'; + +import { + FileMenu, IFileMenu +} from '@jupyterlab/mainmenu'; + +class Wodget extends Widget { + state: string; +} + +describe('@jupyterlab/mainmenu', () => { + + describe('FileMenu', () => { + + let commands: CommandRegistry; + let menu: FileMenu; + let tracker: InstanceTracker; + let wodget = new Wodget(); + + before(() => { + commands = new CommandRegistry(); + }); + + beforeEach(() => { + menu = new FileMenu({ commands }); + tracker = new InstanceTracker({ namespace: 'wodget' }); + tracker.add(wodget); + }); + + afterEach(() => { + menu.dispose(); + tracker.dispose(); + wodget.dispose(); + }); + + describe('#constructor()', () => { + + it('should construct a new file menu', () => { + expect(menu).to.be.an(FileMenu); + expect(menu.menu.title.label).to.be('File'); + }); + + }); + + describe('#newMenu', () => { + + it('should be a submenu for `New...` commands', () => { + expect(menu.newMenu.title.label).to.be('New'); + }); + + }); + + describe('#cleaners', () => { + + it('should allow setting of an ICloseAndCleaner', () => { + const cleaner: IFileMenu.ICloseAndCleaner = { + tracker, + action: 'Clean', + closeAndCleanup: widget => { + widget.state = 'clean'; + return Promise.resolve(void 0); + } + } + menu.closeAndCleaners.set('Wodget', cleaner); + menu.closeAndCleaners.get('Wodget').closeAndCleanup(wodget); + expect(wodget.state).to.be('clean'); + }); + + }); + + }); + +}); diff --git a/test/src/mainmenu/help.spec.ts b/test/src/mainmenu/help.spec.ts new file mode 100644 index 000000000000..8c84cd06f33a --- /dev/null +++ b/test/src/mainmenu/help.spec.ts @@ -0,0 +1,44 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import expect = require('expect.js'); + +import { + CommandRegistry +} from '@phosphor/commands'; + +import { + HelpMenu, IHelpMenu +} from '@jupyterlab/mainmenu'; + + +describe('@jupyterlab/mainmenu', () => { + + describe('HelpMenu', () => { + + let commands: CommandRegistry; + let menu: HelpMenu; + + before(() => { + commands = new CommandRegistry(); + }); + + beforeEach(() => { + menu = new HelpMenu({ commands }); + }); + + afterEach(() => { + menu.dispose(); + }); + + describe('#constructor()', () => { + it('should construct a new help menu', () => { + expect(menu).to.be.an(HelpMenu); + expect(menu.menu.title.label).to.be('Help'); + }); + + }); + + }); + +}); diff --git a/test/src/mainmenu/kernel.spec.ts b/test/src/mainmenu/kernel.spec.ts new file mode 100644 index 000000000000..dff914707fe7 --- /dev/null +++ b/test/src/mainmenu/kernel.spec.ts @@ -0,0 +1,114 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import expect = require('expect.js'); + +import { + CommandRegistry +} from '@phosphor/commands'; + +import { + Widget +} from '@phosphor/widgets'; + +import { + InstanceTracker +} from '@jupyterlab/apputils'; + +import { + KernelMenu, IKernelMenu +} from '@jupyterlab/mainmenu'; + +class Wodget extends Widget { + state: string; +} + +describe('@jupyterlab/mainmenu', () => { + + describe('KernelMenu', () => { + + let commands: CommandRegistry; + let menu: KernelMenu; + let tracker: InstanceTracker; + let wodget = new Wodget(); + + before(() => { + commands = new CommandRegistry(); + }); + + beforeEach(() => { + menu = new KernelMenu({ commands }); + tracker = new InstanceTracker({ namespace: 'wodget' }); + tracker.add(wodget); + }); + + afterEach(() => { + menu.dispose(); + tracker.dispose(); + wodget.dispose(); + }); + + describe('#constructor()', () => { + + it('should construct a new kernel menu', () => { + expect(menu).to.be.an(KernelMenu); + expect(menu.menu.title.label).to.be('Kernel'); + }); + + }); + + describe('#kernelUsers', () => { + + it('should allow setting of an IKernelUser', () => { + const user: IKernelMenu.IKernelUser = { + tracker, + interruptKernel: widget => { + widget.state = 'interrupt'; + return Promise.resolve(void 0); + }, + restartKernel: widget => { + widget.state = 'restart'; + return Promise.resolve(void 0); + }, + changeKernel: widget => { + widget.state = 'change'; + return Promise.resolve(void 0); + }, + shutdownKernel: widget => { + widget.state = 'shutdown'; + return Promise.resolve(void 0); + }, + } + menu.kernelUsers.set('Wodget', user); + menu.kernelUsers.get('Wodget').interruptKernel(wodget); + expect(wodget.state).to.be('interrupt'); + menu.kernelUsers.get('Wodget').restartKernel(wodget); + expect(wodget.state).to.be('restart'); + menu.kernelUsers.get('Wodget').changeKernel(wodget); + expect(wodget.state).to.be('change'); + menu.kernelUsers.get('Wodget').shutdownKernel(wodget); + expect(wodget.state).to.be('shutdown'); + }); + + }); + + describe('#consoleCreators', () => { + + it('should allow setting of an IConsoleCreator', () => { + const creator: IKernelMenu.IConsoleCreator = { + tracker, + createConsole: widget => { + widget.state = 'create'; + return Promise.resolve(void 0); + }, + } + menu.consoleCreators.set('Wodget', creator); + menu.consoleCreators.get('Wodget').createConsole(wodget); + expect(wodget.state).to.be('create'); + }); + + }); + + }); + +}); diff --git a/test/src/mainmenu/labmenu.spec.ts b/test/src/mainmenu/labmenu.spec.ts new file mode 100644 index 000000000000..43014c54d04b --- /dev/null +++ b/test/src/mainmenu/labmenu.spec.ts @@ -0,0 +1,97 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import expect = require('expect.js'); + +import { + ArrayExt +} from '@phosphor/algorithm'; + +import { + CommandRegistry +} from '@phosphor/commands'; + +import { + JupyterLabMenu +} from '@jupyterlab/mainmenu'; + + +describe('@jupyterlab/mainmenu', () => { + + describe('JupyterLabMenu', () => { + + let commands: CommandRegistry; + let menu: JupyterLabMenu; + + before(() => { + commands = new CommandRegistry(); + commands.addCommand('run1', { + label: 'Run 1', + execute: () => void 0 + }); + commands.addCommand('run2', { + label: 'Run 2', + execute: () => void 0 + }); + commands.addCommand('run3', { + label: 'Run 3', + execute: () => void 0 + }); + commands.addCommand('run4', { + label: 'Run 4', + execute: () => void 0 + }); + }); + + beforeEach(() => { + menu = new JupyterLabMenu({ commands }); + }); + + afterEach(() => { + menu.dispose(); + }); + + describe('#constructor()', () => { + it('should construct a new main menu', () => { + expect(menu).to.be.a(JupyterLabMenu); + }); + + }); + + describe('#addGroup()', () => { + + it('should add a new group to the menu', () => { + menu.addGroup([ { command: 'run1'}, { command: 'run2' }]); + + let idx1 = ArrayExt.findFirstIndex(menu.menu.items, + m => m.command === 'run1'); + let idx2 = ArrayExt.findFirstIndex(menu.menu.items, + m => m.command === 'run2'); + + expect(idx1 === -1).to.be(false); + expect(idx2 === -1).to.be(false); + expect(idx1 > idx2).to.be(false); + }); + + it('should take a rank as an option', () => { + menu.addGroup([ { command: 'run1'}, { command: 'run2' }], 2); + menu.addGroup([ { command: 'run3'}, { command: 'run4' }], 1); + + let idx1 = ArrayExt.findFirstIndex(menu.menu.items, + m => m.command === 'run1'); + let idx2 = ArrayExt.findFirstIndex(menu.menu.items, + m => m.command === 'run2'); + let idx3 = ArrayExt.findFirstIndex(menu.menu.items, + m => m.command === 'run3'); + let idx4 = ArrayExt.findFirstIndex(menu.menu.items, + m => m.command === 'run4'); + expect(idx3 < idx4).to.be(true); + expect(idx4 < idx1).to.be(true); + expect(idx1 < idx2).to.be(true); + }); + + }); + + }); + +}); diff --git a/test/src/mainmenu/mainmenu.spec.ts b/test/src/mainmenu/mainmenu.spec.ts new file mode 100644 index 000000000000..2431b7ced38e --- /dev/null +++ b/test/src/mainmenu/mainmenu.spec.ts @@ -0,0 +1,151 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import expect = require('expect.js'); + +import { + find, ArrayExt +} from '@phosphor/algorithm'; + +import { + CommandRegistry +} from '@phosphor/commands'; + +import { + Menu +} from '@phosphor/widgets'; + +import { + MainMenu, EditMenu, FileMenu, HelpMenu, KernelMenu, RunMenu, ViewMenu +} from '@jupyterlab/mainmenu'; + + +describe('@jupyterlab/mainmenu', () => { + + describe('MainMenu', () => { + + let commands: CommandRegistry; + let mainMenu: MainMenu; + + before(() => { + commands = new CommandRegistry(); + }); + + beforeEach(() => { + mainMenu = new MainMenu(commands); + }); + + afterEach(() => { + mainMenu.dispose(); + }); + + describe('#constructor()', () => { + it('should construct a new main menu', () => { + const menu = new MainMenu(new CommandRegistry()); + expect(menu).to.be.a(MainMenu); + }); + + }); + + + describe('#addMenu()', () => { + + it('should add a new menu', () => { + const menu = new Menu({ commands }); + mainMenu.addMenu(menu); + expect(find(mainMenu.menus, m => menu === m) !== undefined).to.be(true); + }); + + it('should take a rank as an option', () => { + const menu1 = new Menu({ commands }); + const menu2 = new Menu({ commands }); + mainMenu.addMenu(menu1, { rank: 300 }); + mainMenu.addMenu(menu2, { rank: 200 }); + expect(ArrayExt.firstIndexOf(mainMenu.menus, menu1)).to.be(6); + expect(ArrayExt.firstIndexOf(mainMenu.menus, menu2)).to.be(5); + }); + + }); + + describe('#fileMenu', () => { + + it('should be a FileMenu', () => { + expect(mainMenu.fileMenu).to.be.a(FileMenu); + }); + + it('should be the first menu', () => { + expect(ArrayExt.firstIndexOf( + mainMenu.menus, mainMenu.fileMenu.menu)).to.be(0); + }); + + }); + + describe('#editMenu', () => { + + it('should be a EditMenu', () => { + expect(mainMenu.editMenu).to.be.a(EditMenu); + }); + + it('should be the second menu', () => { + expect(ArrayExt.firstIndexOf(mainMenu.menus, + mainMenu.editMenu.menu)).to.be(1); + }); + + }); + + describe('#runMenu', () => { + + it('should be a RunMenu', () => { + expect(mainMenu.runMenu).to.be.a(RunMenu); + }); + + it('should be the third menu', () => { + expect(ArrayExt.firstIndexOf(mainMenu.menus, + mainMenu.runMenu.menu)).to.be(2); + }); + + }); + + describe('#kernelMenu', () => { + + it('should be a KernelMenu', () => { + expect(mainMenu.kernelMenu).to.be.a(KernelMenu); + }); + + it('should be the fourth menu', () => { + expect(ArrayExt.firstIndexOf(mainMenu.menus, + mainMenu.kernelMenu.menu)).to.be(3); + }); + + }); + + describe('#viewMenu', () => { + + it('should be a ViewMenu', () => { + expect(mainMenu.viewMenu).to.be.a(ViewMenu); + }); + + it('should be the fifth menu', () => { + expect(ArrayExt.firstIndexOf(mainMenu.menus, + mainMenu.viewMenu.menu)).to.be(4); + }); + + }); + + describe('#helpMenu', () => { + + it('should be a HelpMenu', () => { + expect(mainMenu.helpMenu).to.be.a(HelpMenu); + }); + + it('should be the sixth menu', () => { + expect(ArrayExt.firstIndexOf(mainMenu.menus, + mainMenu.helpMenu.menu)).to.be(5); + }); + + }); + + + }); + +}); diff --git a/test/src/mainmenu/run.spec.ts b/test/src/mainmenu/run.spec.ts new file mode 100644 index 000000000000..d98b92379f43 --- /dev/null +++ b/test/src/mainmenu/run.spec.ts @@ -0,0 +1,98 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import expect = require('expect.js'); + +import { + CommandRegistry +} from '@phosphor/commands'; + +import { + Widget +} from '@phosphor/widgets'; + +import { + InstanceTracker +} from '@jupyterlab/apputils'; + +import { + RunMenu, IRunMenu +} from '@jupyterlab/mainmenu'; + +class Wodget extends Widget { + state: string; +} + +describe('@jupyterlab/mainmenu', () => { + + describe('RunMenu', () => { + + let commands: CommandRegistry; + let menu: RunMenu; + let tracker: InstanceTracker; + let wodget = new Wodget(); + + before(() => { + commands = new CommandRegistry(); + }); + + beforeEach(() => { + menu = new RunMenu({ commands }); + tracker = new InstanceTracker({ namespace: 'wodget' }); + tracker.add(wodget); + }); + + afterEach(() => { + menu.dispose(); + tracker.dispose(); + wodget.dispose(); + }); + + describe('#constructor()', () => { + + it('should construct a new run menu', () => { + expect(menu).to.be.an(RunMenu); + expect(menu.menu.title.label).to.be('Run'); + }); + + }); + + describe('#codeRunners', () => { + + it('should allow setting of an ICodeRunner', () => { + const runner: IRunMenu.ICodeRunner = { + tracker, + noun: 'Noun', + run: widget => { + widget.state = 'run'; + return Promise.resolve(void 0); + }, + runAll: widget => { + widget.state = 'runAll'; + return Promise.resolve(void 0); + }, + runAbove: widget => { + widget.state = 'runAbove'; + return Promise.resolve(void 0); + }, + runBelow: widget => { + widget.state = 'runBelow'; + return Promise.resolve(void 0); + }, + } + menu.codeRunners.set('Wodget', runner); + menu.codeRunners.get('Wodget').run(wodget); + expect(wodget.state).to.be('run'); + menu.codeRunners.get('Wodget').runAll(wodget); + expect(wodget.state).to.be('runAll'); + menu.codeRunners.get('Wodget').runAbove(wodget); + expect(wodget.state).to.be('runAbove'); + menu.codeRunners.get('Wodget').runBelow(wodget); + expect(wodget.state).to.be('runBelow'); + }); + + }); + + }); + +}); diff --git a/test/src/mainmenu/view.spec.ts b/test/src/mainmenu/view.spec.ts new file mode 100644 index 000000000000..5137cea6927a --- /dev/null +++ b/test/src/mainmenu/view.spec.ts @@ -0,0 +1,105 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import expect = require('expect.js'); + +import { + CommandRegistry +} from '@phosphor/commands'; + +import { + Widget +} from '@phosphor/widgets'; + +import { + InstanceTracker +} from '@jupyterlab/apputils'; + +import { + ViewMenu, IViewMenu +} from '@jupyterlab/mainmenu'; + +class Wodget extends Widget { + wrapped: boolean = false; + matched: boolean = false; + numbered: boolean = false; +} + +describe('@jupyterlab/mainmenu', () => { + + describe('ViewMenu', () => { + + let commands: CommandRegistry; + let menu: ViewMenu; + let tracker: InstanceTracker; + let wodget = new Wodget(); + + before(() => { + commands = new CommandRegistry(); + }); + + beforeEach(() => { + menu = new ViewMenu({ commands }); + tracker = new InstanceTracker({ namespace: 'wodget' }); + tracker.add(wodget); + }); + + afterEach(() => { + menu.dispose(); + tracker.dispose(); + wodget.dispose(); + }); + + describe('#constructor()', () => { + + it('should construct a new view menu', () => { + expect(menu).to.be.an(ViewMenu); + expect(menu.menu.title.label).to.be('View'); + }); + + }); + + describe('#editorViewers', () => { + + it('should allow setting of an IEditorViewer', () => { + const viewer: IViewMenu.IEditorViewer = { + tracker, + toggleLineNumbers: widget => { + widget.numbered = !widget.numbered; + return; + }, + toggleMatchBrackets: widget => { + widget.matched = !widget.matched; + return; + }, + toggleWordWrap: widget => { + widget.wrapped = !widget.wrapped; + return; + }, + matchBracketsToggled: widget => widget.matched, + lineNumbersToggled: widget => widget.numbered, + wordWrapToggled: widget => widget.wrapped + } + menu.editorViewers.set('Wodget', viewer); + expect(menu.editorViewers.get('Wodget').matchBracketsToggled(wodget)) + .to.be(false); + expect(menu.editorViewers.get('Wodget').wordWrapToggled(wodget)) + .to.be(false); + expect(menu.editorViewers.get('Wodget').lineNumbersToggled(wodget)) + .to.be(false); + menu.editorViewers.get('Wodget').toggleLineNumbers(wodget); + menu.editorViewers.get('Wodget').toggleMatchBrackets(wodget); + menu.editorViewers.get('Wodget').toggleWordWrap(wodget); + expect(menu.editorViewers.get('Wodget').matchBracketsToggled(wodget)) + .to.be(true); + expect(menu.editorViewers.get('Wodget').wordWrapToggled(wodget)) + .to.be(true); + expect(menu.editorViewers.get('Wodget').lineNumbersToggled(wodget)) + .to.be(true); + }); + + }); + + }); + +}); diff --git a/test/src/notebook/actions.spec.ts b/test/src/notebook/actions.spec.ts index c273f820cc30..d00afd4846eb 100644 --- a/test/src/notebook/actions.spec.ts +++ b/test/src/notebook/actions.spec.ts @@ -1218,40 +1218,6 @@ describe('@jupyterlab/notebook', () => { }); - describe('#toggleLineNumbers()', () => { - - it('should toggle line numbers on the selected cells', () => { - let state = widget.activeCell.editor.getOption('lineNumbers'); - NotebookActions.toggleLineNumbers(widget); - expect(widget.activeCell.editor.getOption('lineNumbers')).to.be(!state); - }); - - it('should be based on the state of the active cell', () => { - let state = widget.activeCell.editor.getOption('lineNumbers'); - let next = widget.widgets[1]; - next.editor.setOption('lineNumbers', !state); - widget.select(next); - NotebookActions.toggleLineNumbers(widget); - expect(widget.widgets[0].editor.getOption('lineNumbers')).to.be(!state); - expect(widget.widgets[1].editor.getOption('lineNumbers')).to.be(!state); - }); - - it('should preserve the widget mode', () => { - NotebookActions.toggleLineNumbers(widget); - expect(widget.mode).to.be('command'); - widget.mode = 'edit'; - NotebookActions.toggleLineNumbers(widget); - expect(widget.mode).to.be('edit'); - }); - - it('should be a no-op if there is no model', () => { - widget.model = null; - NotebookActions.toggleLineNumbers(widget); - expect(widget.activeCellIndex).to.be(-1); - }); - - }); - describe('#toggleAllLineNumbers()', () => { it('should toggle line numbers on all cells', () => { diff --git a/yarn.lock b/yarn.lock index 21ec910b716e..24f89aca1195 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6114,7 +6114,7 @@ typescript@2.3.4: version "2.3.4" resolved "https://registry.npmjs.org/typescript/-/typescript-2.3.4.tgz#3d38321828231e434f287514959c37a82b629f42" -typescript@latest: +typescript@~2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4"