diff --git a/packages/console-extension/src/index.ts b/packages/console-extension/src/index.ts index 3f26c3c0bfa0..4b9d9a6b4317 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, InstanceTracker, showDialog + Dialog, IClientSession, ICommandPalette, InstanceTracker, showDialog } from '@jupyterlab/apputils'; import { @@ -164,11 +164,6 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand when: manager.ready }); - // The launcher callback. - let callback = (cwd: string, name: string) => { - return createConsole({ basePath: cwd, kernelPreference: { name }}); - }; - // Add a launcher item if the launcher is available. if (launcher) { manager.ready.then(() => { @@ -178,7 +173,6 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand } let baseUrl = PageConfig.getBaseUrl(); for (let name in specs.kernelspecs) { - let displayName = specs.kernelspecs[name].display_name; let rank = name === specs.default ? 0 : Infinity; let kernelIconUrl = specs.kernelspecs[name].resources['logo-64x64']; if (kernelIconUrl) { @@ -186,11 +180,9 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand kernelIconUrl = baseUrl + kernelIconUrl.slice(index); } launcher.add({ - displayName, + command: CommandIDs.create, + args: { isLauncher: true, kernelPreference: { name } }, category: 'Console', - name, - iconClass: 'jp-CodeConsoleIcon', - callback, rank, kernelIconUrl }); @@ -265,9 +257,20 @@ function activateConsole(app: JupyterLab, mainMenu: IMainMenu, palette: ICommand command = CommandIDs.create; commands.addCommand(command, { - label: args => args['isPalette'] ? 'New Console' : 'Console', - execute: (args: Partial) => { - let basePath = args.basePath || browserFactory.defaultBrowser.model.path; + label: args => { + if (args['isPalette']) { + return 'New Console'; + } else if (args['isLauncher'] && args['kernelPreference']) { + const kernelPreference = + args['kernelPreference'] as IClientSession.IKernelPreference; + return manager.specs.kernelspecs[kernelPreference.name].display_name; + } + return 'Console'; + }, + iconClass: 'jp-CodeConsoleIcon', + execute: args => { + let basePath = args['basePath'] as string || args['cwd'] as string || + browserFactory.defaultBrowser.model.path; return createConsole({ basePath, ...args }); } }); diff --git a/packages/fileeditor-extension/src/index.ts b/packages/fileeditor-extension/src/index.ts index 0e4a85c85e00..90638acc8ab7 100644 --- a/packages/fileeditor-extension/src/index.ts +++ b/packages/fileeditor-extension/src/index.ts @@ -376,20 +376,19 @@ function activate(app: JupyterLab, consoleTracker: IConsoleTracker, editorServic commands.addCommand(CommandIDs.createNew, { label: 'Text File', caption: 'Create a new text file', - execute: () => { - let cwd = browserFactory.defaultBrowser.model.path; - return createNew(cwd); + iconClass: EDITOR_ICON_CLASS, + execute: args => { + let cwd = args['cwd'] || browserFactory.defaultBrowser.model.path; + return createNew(cwd as string); } }); // Add a launcher item if the launcher is available. if (launcher) { launcher.add({ - displayName: 'Text Editor', + command: CommandIDs.createNew, category: 'Other', rank: 1, - iconClass: EDITOR_ICON_CLASS, - callback: createNew }); } diff --git a/packages/launcher-extension/src/index.ts b/packages/launcher-extension/src/index.ts index 7ca167970404..b8c157046456 100644 --- a/packages/launcher-extension/src/index.ts +++ b/packages/launcher-extension/src/index.ts @@ -70,7 +70,7 @@ function activate(app: JupyterLab, palette: ICommandPalette): ILauncher { const callback = (item: Widget) => { shell.addToMainArea(item, { ref: id }); }; - const launcher = new Launcher({ cwd, callback }); + const launcher = new Launcher({ cwd, callback, commands }); launcher.model = model; launcher.title.label = 'Launcher'; diff --git a/packages/launcher/package.json b/packages/launcher/package.json index 49b492b43ee5..5fc1cd6c6baa 100644 --- a/packages/launcher/package.json +++ b/packages/launcher/package.json @@ -32,6 +32,7 @@ "dependencies": { "@jupyterlab/apputils": "^0.16.3", "@phosphor/algorithm": "^1.1.2", + "@phosphor/commands": "^1.5.0", "@phosphor/coreutils": "^1.3.0", "@phosphor/disposable": "^1.1.2", "@phosphor/properties": "^1.1.2", diff --git a/packages/launcher/src/index.tsx b/packages/launcher/src/index.tsx index 4a5f6b0d0090..66fb0ea09198 100644 --- a/packages/launcher/src/index.tsx +++ b/packages/launcher/src/index.tsx @@ -10,7 +10,11 @@ import { } from '@phosphor/algorithm'; import { - Token + CommandRegistry +} from '@phosphor/commands'; + +import { + Token, ReadonlyJSONObject } from '@phosphor/coreutils'; import { @@ -47,17 +51,6 @@ const KNOWN_CATEGORIES = ['Notebook', 'Console', 'Other']; const KERNEL_CATEGORIES = ['Notebook', 'Console']; -/** - * The command IDs used by the launcher plugin. - */ -export -namespace CommandIDs { - export - const show: string = 'launcher:show'; -} - - - /* tslint:disable */ /** * The launcher token. @@ -82,91 +75,7 @@ interface ILauncher { * re-render event for parent widget. * */ - add(options: ILauncherItem): IDisposable; -} - - -/** - * The specification for a launcher item. - */ -export -interface ILauncherItem { - /** - * The display name for the launcher item. - */ - displayName: string; - - /** - * The callback invoked to launch the item. - * - * The callback is invoked with a current working directory and the - * name of the selected launcher item. When the function returns - * the launcher will close. - * - * #### Notes - * The callback must return the widget that was created so the launcher - * can replace itself with the created widget. - */ - callback: (cwd: string, name: string) => Widget | Promise; - - /** - * The icon class for the launcher item. - * - * #### Notes - * This class name will be added to the icon node for the visual - * representation of the launcher item. - * - * Multiple class names can be separated with white space. - * - * The default value is an empty string. - */ - iconClass?: string; - - /** - * The icon label for the launcher item. - * - * #### Notes - * This label will be added as text to the icon node for the visual - * representation of the launcher item. - * - * The default value is an empty string. - */ - iconLabel?: string; - - /** - * The identifier for the launcher item. - * - * The default value is the displayName. - */ - name?: string; - - /** - * The category for the launcher item. - * - * The default value is the an empty string. - */ - category?: string; - - /** - * The rank for the launcher item. - * - * The rank is used when ordering launcher items for display. After grouping - * into categories, items are sorted in the following order: - * 1. Rank (lower is better) - * 3. Display Name (locale order) - * - * The default rank is `Infinity`. - */ - rank?: number; - - /** - * For items that have a kernel associated with them, the URL of the kernel - * icon. - * - * This is not a CSS class, but the URL that points to the icon in the kernel - * spec. - */ - kernelIconUrl?: string; + add(options: ILauncher.IItemOptions): IDisposable; } @@ -193,7 +102,7 @@ class LauncherModel extends VDomModel implements ILauncher { * re-render event for parent widget. * */ - add(options: ILauncherItem): IDisposable { + add(options: ILauncher.IItemOptions): IDisposable { // Create a copy of the options to circumvent mutations to the original. let item = Private.createItem(options); @@ -209,11 +118,11 @@ class LauncherModel extends VDomModel implements ILauncher { /** * Return an iterator of launcher items. */ - items(): IIterator { + items(): IIterator { return new ArrayIterator(this._items); } - private _items: ILauncherItem[] = []; + private _items: ILauncher.IItemOptions[] = []; } @@ -225,10 +134,11 @@ class Launcher extends VDomRenderer { /** * Construct a new launcher widget. */ - constructor(options: Launcher.IOptions) { + constructor(options: ILauncher.IOptions) { super(); this._cwd = options.cwd; this._callback = options.callback; + this._commands = options.commands; this.addClass(LAUNCHER_CLASS); } @@ -273,7 +183,10 @@ class Launcher extends VDomRenderer { }); // Within each category sort by rank for (let cat in categories) { - categories[cat] = categories[cat].sort(Private.sortCmp); + categories[cat] = categories[cat] + .sort((a: ILauncher.IItemOptions, b: ILauncher.IItemOptions) => { + return Private.sortCmp(a, b, this._cwd, this._commands); + }); } // Variable to help create sections @@ -293,8 +206,10 @@ class Launcher extends VDomRenderer { } // Now create the sections for each category - each(orderedCategories, cat => { - let iconClass = `${(categories[cat][0] as ILauncherItem).iconClass} ` + + orderedCategories.forEach(cat => { + const item = categories[cat][0] as ILauncher.IItemOptions; + let iconClass = + `${this._commands.iconClass(item.command, {...item.args, cwd: this.cwd})} ` + 'jp-Launcher-sectionIcon jp-Launcher-icon'; let kernel = KERNEL_CATEGORIES.indexOf(cat) > -1; if (cat in categories) { @@ -305,8 +220,8 @@ class Launcher extends VDomRenderer {

{cat}

- {toArray(map(categories[cat], (item: ILauncherItem) => { - return Card(kernel, item, this, this._callback); + {toArray(map(categories[cat], (item: ILauncher.IItemOptions) => { + return Card(kernel, item, this, this._commands, this._callback); }))}
@@ -328,6 +243,7 @@ class Launcher extends VDomRenderer { ); } + private _commands: CommandRegistry; private _callback: (widget: Widget) => void; private _pending = false; private _cwd = ''; @@ -335,10 +251,10 @@ class Launcher extends VDomRenderer { /** - * The namespace for `Launcher` class statics. + * The namespace for `ILauncher` class statics. */ export -namespace Launcher { +namespace ILauncher { /** * The options used to create a Launcher. */ @@ -349,11 +265,77 @@ namespace Launcher { */ cwd: string; + /** + * The command registry used by the launcher. + */ + commands: CommandRegistry; + /** * The callback used when an item is launched. */ callback: (widget: Widget) => void; } + + /** + * The options used to create a launcher item. + */ + export + interface IItemOptions { + /** + * The command ID for the launcher item. + * + * #### Notes + * If the command's `execute` method returns a `Widget` or + * a promise that resolves with a `Widget`, then that widget will + * replace the launcher in the same location of the application + * shell. If the `execute` method does something else + * (i.e., create a modal dialog), then the launcher will not be + * disposed. + */ + command: string; + + /** + * The arguments given to the command for + * creating the launcher item. + * + * ### Notes + * The launcher will also add the current working + * directory of the filebrowser in the `cwd` field + * of the args, which a command may use to create + * the activity with respect to the right directory. + */ + args?: ReadonlyJSONObject; + + /** + * The category for the launcher item. + * + * The default value is the an empty string. + */ + category?: string; + + /** + * The rank for the launcher item. + * + * The rank is used when ordering launcher items for display. After grouping + * into categories, items are sorted in the following order: + * 1. Rank (lower is better) + * 3. Display Name (locale order) + * + * The default rank is `Infinity`. + */ + rank?: number; + + /** + * For items that have a kernel associated with them, the URL of the kernel + * icon. + * + * This is not a CSS class, but the URL that points to the icon in the kernel + * spec. + */ + kernelIconUrl?: string; + } + + } @@ -370,7 +352,12 @@ namespace Launcher { * * @returns a vdom `VirtualElement` for the launcher card. */ -function Card(kernel: boolean, item: ILauncherItem, launcher: Launcher, launcherCallback: (widget: Widget) => void): React.ReactElement { +function Card(kernel: boolean, item: ILauncher.IItemOptions, launcher: Launcher, commands: CommandRegistry, launcherCallback: (widget: Widget) => void): React.ReactElement { + // Get some properties of the command + const command = item.command; + const args = {...item.args, cwd: launcher.cwd}; + const label = commands.label(command, args); + // Build the onclick handler. let onclick = () => { // If an item has already been launched, @@ -379,14 +366,15 @@ function Card(kernel: boolean, item: ILauncherItem, launcher: Launcher, launcher return; } launcher.pending = true; - let callback = item.callback as any; - let value = callback(launcher.cwd, item.name); - Promise.resolve(value).then(widget => { - if (!widget) { - throw new Error('Launcher callbacks must resolve with a widget'); + commands.execute(command, { + ...item.args, + cwd: launcher.cwd + }).then(value => { + launcher.pending = false; + if (value instanceof Widget) { + launcherCallback(value); + launcher.dispose(); } - launcherCallback(widget); - launcher.dispose(); }).catch(err => { launcher.pending = false; showErrorMessage('Launcher Error', err); @@ -396,7 +384,7 @@ function Card(kernel: boolean, item: ILauncherItem, launcher: Launcher, launcher // Return the VDOM element. return (
@@ -404,14 +392,14 @@ function Card(kernel: boolean, item: ILauncherItem, launcher: Launcher, launcher {(item.kernelIconUrl && kernel) && } {(!item.kernelIconUrl && !kernel) && -
} +
} {(!item.kernelIconUrl && kernel) &&
- {item.displayName[0].toUpperCase()} + {label[0].toUpperCase()}
}
-
- {item.displayName} +
+ {label}
); @@ -431,22 +419,19 @@ namespace Private { * An attached property for an item's key. */ export - const keyProperty = new AttachedProperty({ + const keyProperty = new AttachedProperty({ name: 'key', create: () => id++ }); /** - * Create an item given item options. + * Create a fully specified item given item options. */ export - function createItem(options: ILauncherItem): ILauncherItem { + function createItem(options: ILauncher.IItemOptions): ILauncher.IItemOptions { return { ...options, category: options.category || '', - name: options.name || options.name, - iconClass: options.iconClass || '', - iconLabel: options.iconLabel || '', rank: options.rank !== undefined ? options.rank : Infinity }; } @@ -455,7 +440,7 @@ namespace Private { * A sort comparison function for a launcher item. */ export - function sortCmp(a: ILauncherItem, b: ILauncherItem): number { + function sortCmp(a: ILauncher.IItemOptions, b: ILauncher.IItemOptions, cwd: string, commands: CommandRegistry): number { // First, compare by rank. let r1 = a.rank; let r2 = b.rank; @@ -464,6 +449,8 @@ namespace Private { } // Finally, compare by display name. - return a.displayName.localeCompare(b.displayName); + const aLabel = commands.label(a.command, { ...a.args, cwd }); + const bLabel = commands.label(a.command, { ...b.args, cwd }); + return aLabel.localeCompare(bLabel); } } diff --git a/packages/notebook-extension/src/index.ts b/packages/notebook-extension/src/index.ts index 498c4b64bb95..21c7f8bf97ad 100644 --- a/packages/notebook-extension/src/index.ts +++ b/packages/notebook-extension/src/index.ts @@ -515,14 +515,22 @@ function activateNotebookHandler(app: JupyterLab, mainMenu: IMainMenu, palette: }); }; - // Add a command for creating a new notebook in the File Menu. + // Add a command for creating a new notebook. commands.addCommand(CommandIDs.createNew, { - label: 'Notebook', + label: args => { + const kernelName = args['kernelName'] as string || ''; + if (args['isLauncher'] && args['kernelName']) { + return services.specs.kernelspecs[kernelName].display_name; + } + return 'Notebook'; + }, caption: 'Create a new notebook', - execute: () => { - let cwd = browserFactory ? + iconClass: 'jp-NotebookIcon', + execute: args => { + const cwd = args['cwd'] || browserFactory ? browserFactory.defaultBrowser.model.path : ''; - return createNew(cwd); + const kernelName = args['kernelName'] as string || ''; + return createNew(cwd, kernelName); } }); @@ -534,7 +542,6 @@ function activateNotebookHandler(app: JupyterLab, mainMenu: IMainMenu, palette: const baseUrl = PageConfig.getBaseUrl(); for (let name in specs.kernelspecs) { - let displayName = specs.kernelspecs[name].display_name; let rank = name === specs.default ? 0 : Infinity; let kernelIconUrl = specs.kernelspecs[name].resources['logo-64x64']; if (kernelIconUrl) { @@ -542,11 +549,9 @@ function activateNotebookHandler(app: JupyterLab, mainMenu: IMainMenu, palette: kernelIconUrl = baseUrl + kernelIconUrl.slice(index); } launcher.add({ - displayName, + command: CommandIDs.createNew, + args: { isLauncher: true, kernelName: name }, category: 'Notebook', - name, - iconClass: 'jp-NotebookIcon', - callback: createNew, rank, kernelIconUrl }); diff --git a/packages/terminal-extension/src/index.ts b/packages/terminal-extension/src/index.ts index 57d34e91ae04..7ffe9d05730b 100644 --- a/packages/terminal-extension/src/index.ts +++ b/packages/terminal-extension/src/index.ts @@ -79,7 +79,7 @@ export default plugin; * Activate the terminal plugin. */ function activate(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette, restorer: ILayoutRestorer, launcher: ILauncher | null): ITerminalTracker { - const { commands, serviceManager } = app; + const { serviceManager } = app; const category = 'Terminal'; const namespace = 'terminal'; const tracker = new InstanceTracker>({ namespace }); @@ -124,11 +124,9 @@ function activate(app: JupyterLab, mainMenu: IMainMenu, palette: ICommandPalette // Add a launcher item if the launcher is available. if (launcher) { launcher.add({ - displayName: 'Terminal', + command: CommandIDs.createNew, category: 'Other', rank: 0, - iconClass: TERMINAL_ICON_CLASS, - callback: () => commands.execute(CommandIDs.createNew) }); } @@ -157,6 +155,7 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Instanc commands.addCommand(CommandIDs.createNew, { label: args => args['isPalette'] ? 'New Terminal' : 'Terminal', caption: 'Start a new terminal session', + iconClass: TERMINAL_ICON_CLASS, execute: args => { const name = args['name'] as string; const initialCommand = args['initialCommand'] as string;