@jupyterlab/application
Version:
JupyterLab - Application
489 lines (442 loc) • 15.1 kB
text/typescript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { PageConfig } from '@jupyterlab/coreutils';
import { Base64ModelFactory } from '@jupyterlab/docregistry';
import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
import { ServiceManager } from '@jupyterlab/services';
import { PromiseDelegate, Token } from '@lumino/coreutils';
import { JupyterFrontEnd, JupyterFrontEndPlugin } from './frontend';
import { createRendermimePlugins } from './mimerenderers';
import { ILabShell, LabShell } from './shell';
import { LabStatus } from './status';
/**
* JupyterLab is the main application class. It is instantiated once and shared.
*/
export class JupyterLab extends JupyterFrontEnd<ILabShell> {
/**
* Construct a new JupyterLab object.
*/
constructor(options: JupyterLab.IOptions = { shell: new LabShell() }) {
super({
...options,
shell: options.shell || new LabShell(),
serviceManager:
options.serviceManager ||
new ServiceManager({
standby: () => {
return !this._info.isConnected || 'when-hidden';
}
})
});
// Create an IInfo dictionary from the options to override the defaults.
const info = Object.keys(JupyterLab.defaultInfo).reduce((acc, val) => {
if (val in options) {
(acc as any)[val] = JSON.parse(JSON.stringify((options as any)[val]));
}
return acc;
}, {} as Partial<JupyterLab.IInfo>);
// Populate application info.
this._info = { ...JupyterLab.defaultInfo, ...info };
this.restored = this.shell.restored
.then(async () => {
const activated: Promise<void | void[]>[] = [];
const deferred = this.activateDeferredPlugins().catch(error => {
console.error('Error when activating deferred plugins\n:', error);
});
activated.push(deferred);
if (this._info.deferred) {
const customizedDeferred = Promise.all(
this._info.deferred.matches.map(pluginID =>
this.activatePlugin(pluginID)
)
).catch(error => {
console.error(
'Error when activating customized list of deferred plugins:\n',
error
);
});
activated.push(customizedDeferred);
}
Promise.all(activated)
.then(() => {
this._allPluginsActivated.resolve();
})
.catch(() => undefined);
})
.catch(() => undefined);
// Populate application paths override the defaults if necessary.
const defaultURLs = JupyterLab.defaultPaths.urls;
const defaultDirs = JupyterLab.defaultPaths.directories;
const optionURLs = (options.paths && options.paths.urls) || {};
const optionDirs = (options.paths && options.paths.directories) || {};
this._paths = {
urls: Object.keys(defaultURLs).reduce((acc, key) => {
if (key in optionURLs) {
const value = (optionURLs as any)[key];
(acc as any)[key] = value;
} else {
(acc as any)[key] = (defaultURLs as any)[key];
}
return acc;
}, {}),
directories: Object.keys(JupyterLab.defaultPaths.directories).reduce(
(acc, key) => {
if (key in optionDirs) {
const value = (optionDirs as any)[key];
(acc as any)[key] = value;
} else {
(acc as any)[key] = (defaultDirs as any)[key];
}
return acc;
},
{}
)
} as JupyterFrontEnd.IPaths;
if (this._info.devMode) {
this.shell.addClass('jp-mod-devMode');
}
// Add initial model factory.
this.docRegistry.addModelFactory(new Base64ModelFactory());
if (options.mimeExtensions) {
for (const plugin of createRendermimePlugins(options.mimeExtensions)) {
this.registerPlugin(plugin);
}
}
}
/**
* The name of the JupyterLab application.
*/
readonly name = PageConfig.getOption('appName') || 'JupyterLab';
/**
* A namespace/prefix plugins may use to denote their provenance.
*/
readonly namespace = PageConfig.getOption('appNamespace') || this.name;
/**
* A list of all errors encountered when registering plugins.
*/
readonly registerPluginErrors: Array<Error> = [];
/**
* Promise that resolves when state is first restored, returning layout
* description.
*/
readonly restored: Promise<void>;
/**
* The application busy and dirty status signals and flags.
*/
readonly status = new LabStatus(this);
/**
* The version of the JupyterLab application.
*/
readonly version = PageConfig.getOption('appVersion') || 'unknown';
/**
* The JupyterLab application information dictionary.
*/
get info(): JupyterLab.IInfo {
return this._info;
}
/**
* The JupyterLab application paths dictionary.
*/
get paths(): JupyterFrontEnd.IPaths {
return this._paths;
}
/**
* Promise that resolves when all the plugins are activated, including the deferred.
*/
get allPluginsActivated(): Promise<void> {
return this._allPluginsActivated.promise;
}
/**
* Register plugins from a plugin module.
*
* @param mod - The plugin module to register.
*/
registerPluginModule(mod: JupyterLab.IPluginModule): void {
let data = mod.default;
// Handle commonjs exports.
if (!mod.hasOwnProperty('__esModule')) {
data = mod as any;
}
if (!Array.isArray(data)) {
data = [data];
}
data.forEach(item => {
try {
this.registerPlugin(item);
} catch (error) {
this.registerPluginErrors.push(error);
}
});
}
/**
* Register the plugins from multiple plugin modules.
*
* @param mods - The plugin modules to register.
*/
registerPluginModules(mods: JupyterLab.IPluginModule[]): void {
mods.forEach(mod => {
this.registerPluginModule(mod);
});
}
/**
* Override keydown handling to prevent command shortcuts from preventing user input.
*
* This introduces a slight delay to the command invocation, but no delay to user input.
*/
protected evtKeydown(keyDownEvent: KeyboardEvent): void {
const permissionToExecute = new PromiseDelegate<boolean>();
// Hold the execution of any keybinding until we know if this event would cause text insertion
this.commands.holdKeyBindingExecution(
keyDownEvent,
permissionToExecute.promise
);
// Process the key immediately to invoke the prevent default handlers as needed
this.commands.processKeydownEvent(keyDownEvent);
// If we do not know the target, we cannot check if input would be inserted
// as there is no target to attach the `beforeinput` event listener; in that
// case we just permit execution immediately (this may happen for programmatic
// uses of keydown)
const target = keyDownEvent.target;
if (!target) {
return permissionToExecute.resolve(true);
}
let onBeforeInput: ((event: InputEvent) => void) | null = null;
let onBeforeKeyUp: ((event: KeyboardEvent) => void) | null = null;
const disconnectListeners = () => {
if (onBeforeInput) {
target.removeEventListener('beforeinput', onBeforeInput);
}
if (onBeforeKeyUp) {
target.removeEventListener('keyup', onBeforeKeyUp);
}
};
// Permit the execution conditionally, depending on whether the event would lead to text insertion
const causesInputPromise = Promise.race([
new Promise(resolve => {
onBeforeInput = (inputEvent: InputEvent) => {
switch (inputEvent.inputType) {
case 'historyUndo':
case 'historyRedo': {
if (
inputEvent.target instanceof Element &&
inputEvent.target.closest('[data-jp-undoer]')
) {
// Allow to use custom undo/redo bindings on `jpUndoer`s
inputEvent.preventDefault();
disconnectListeners();
return resolve(false);
}
break;
}
case 'insertLineBreak': {
if (
inputEvent.target instanceof Element &&
inputEvent.target.closest('.jp-Cell')
) {
// Allow to override the default action of Shift + Enter on cells as this is used for cell execution
inputEvent.preventDefault();
disconnectListeners();
return resolve(false);
}
break;
}
}
disconnectListeners();
return resolve(true);
};
target.addEventListener('beforeinput', onBeforeInput, { once: true });
}),
new Promise(resolve => {
onBeforeKeyUp = (keyUpEvent: KeyboardEvent) => {
if (keyUpEvent.code === keyDownEvent.code) {
disconnectListeners();
return resolve(false);
}
};
target.addEventListener('keyup', onBeforeKeyUp, { once: true });
}),
new Promise(resolve => {
setTimeout(() => {
disconnectListeners();
return resolve(false);
}, Private.INPUT_GUARD_TIMEOUT);
})
]);
causesInputPromise
.then(willCauseInput => {
permissionToExecute.resolve(!willCauseInput);
})
.catch(console.warn);
}
private _info: JupyterLab.IInfo = JupyterLab.defaultInfo;
private _paths: JupyterFrontEnd.IPaths;
private _allPluginsActivated = new PromiseDelegate<void>();
}
/**
* The namespace for `JupyterLab` class statics.
*/
export namespace JupyterLab {
/**
* The options used to initialize a JupyterLab object.
*/
export interface IOptions
extends Partial<JupyterFrontEnd.IOptions<ILabShell>>,
Partial<IInfo> {
paths?: Partial<JupyterFrontEnd.IPaths>;
}
/**
* The layout restorer token.
*/
export const IInfo = new Token<IInfo>(
'@jupyterlab/application:IInfo',
'A service providing metadata about the current application, including disabled extensions and whether dev mode is enabled.'
);
/**
* The information about a JupyterLab application.
*/
export interface IInfo {
/**
* Whether the application is in dev mode.
*/
readonly devMode: boolean;
/**
* The collection of deferred extension patterns and matched extensions.
*/
readonly deferred: { patterns: string[]; matches: string[] };
/**
* The collection of disabled extension patterns and matched extensions.
*/
readonly disabled: { patterns: string[]; matches: string[] };
/**
* The mime renderer extensions.
*/
readonly mimeExtensions: IRenderMime.IExtensionModule[];
/**
* The information about available plugins.
*/
readonly availablePlugins: IPluginInfo[];
/**
* Whether files are cached on the server.
*/
readonly filesCached: boolean;
/**
* Every periodic network polling should be paused while this is set
* to `false`. Extensions should use this value to decide whether to proceed
* with the polling.
* The extensions may also set this value to `false` if there is no need to
* fetch anything from the server backend basing on some conditions
* (e.g. when an error message dialog is displayed).
* At the same time, the extensions are responsible for setting this value
* back to `true`.
*/
isConnected: boolean;
}
/*
* A read-only subset of the `Token`.
*/
export interface IToken
extends Readonly<Pick<Token<any>, 'name' | 'description'>> {
// no-op
}
/**
* A readonly subset of lumino plugin bundle (excluding activation function,
* service, and state information, and runtime token details).
*/
interface ILuminoPluginData
extends Readonly<
Pick<JupyterFrontEndPlugin<void>, 'id' | 'description' | 'autoStart'>
> {
/**
* The types of required services for the plugin, or `[]`.
*/
readonly requires: IToken[];
/**
* The types of optional services for the the plugin, or `[]`.
*/
readonly optional: IToken[];
/**
* The type of service provided by the plugin, or `null`.
*/
readonly provides: IToken | null;
}
/**
* A subset of plugin bundle enriched with JupyterLab extension metadata.
*/
export interface IPluginInfo extends ILuminoPluginData {
/**
* The name of the extension which provides the plugin.
*/
extension: string;
/**
* Whether the plugin is enabled.
*/
enabled: boolean;
}
/**
* The default JupyterLab application info.
*/
export const defaultInfo: IInfo = {
devMode: PageConfig.getOption('devMode').toLowerCase() === 'true',
deferred: { patterns: [], matches: [] },
disabled: { patterns: [], matches: [] },
mimeExtensions: [],
availablePlugins: [],
filesCached: PageConfig.getOption('cacheFiles').toLowerCase() === 'true',
isConnected: true
};
/**
* The default JupyterLab application paths.
*/
export const defaultPaths: JupyterFrontEnd.IPaths = {
urls: {
base: PageConfig.getOption('baseUrl'),
notFound: PageConfig.getOption('notFoundUrl'),
app: PageConfig.getOption('appUrl'),
doc: PageConfig.getOption('docUrl'),
static: PageConfig.getOption('staticUrl'),
settings: PageConfig.getOption('settingsUrl'),
themes: PageConfig.getOption('themesUrl'),
translations: PageConfig.getOption('translationsApiUrl'),
hubHost: PageConfig.getOption('hubHost') || undefined,
hubPrefix: PageConfig.getOption('hubPrefix') || undefined,
hubUser: PageConfig.getOption('hubUser') || undefined,
hubServerName: PageConfig.getOption('hubServerName') || undefined
},
directories: {
appSettings: PageConfig.getOption('appSettingsDir'),
schemas: PageConfig.getOption('schemasDir'),
static: PageConfig.getOption('staticDir'),
templates: PageConfig.getOption('templatesDir'),
themes: PageConfig.getOption('themesDir'),
userSettings: PageConfig.getOption('userSettingsDir'),
serverRoot: PageConfig.getOption('serverRoot'),
workspaces: PageConfig.getOption('workspacesDir')
}
};
/**
* The interface for a module that exports a plugin or plugins as
* the default value.
*/
export interface IPluginModule {
/**
* The default export.
*/
default:
| JupyterFrontEndPlugin<any, any, any>
| JupyterFrontEndPlugin<any, any, any>[];
}
}
/**
* A namespace for module-private functionality.
*/
namespace Private {
/**
* The delay for invoking a command introduced by user input guard.
* Decreasing this value may lead to commands incorrectly triggering
* on user input. Increasing this value will lead to longer delay for
* command invocation. Note that user input is never delayed.
*
* The value represents the number in milliseconds.
*/
export const INPUT_GUARD_TIMEOUT = 10;
}