@jupyterlab/debugger
Version: 
JupyterLab - Debugger Extension
314 lines • 12.8 kB
JavaScript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { nullTranslator } from '@jupyterlab/translation';
import { bugDotIcon, bugIcon, ToolbarButton } from '@jupyterlab/ui-components';
import { Debugger } from './debugger';
import { ConsoleHandler } from './handlers/console';
import { FileHandler } from './handlers/file';
import { NotebookHandler } from './handlers/notebook';
const TOOLBAR_DEBUGGER_ITEM = 'debugger-icon';
/**
 * Add a bug icon to the widget toolbar to enable and disable debugging.
 *
 * @param widget The widget to add the debug toolbar button to.
 * @param onClick The callback when the toolbar button is clicked.
 */
function updateIconButton(widget, onClick, enabled, pressed, translator = nullTranslator) {
    const trans = translator.load('jupyterlab');
    const icon = new ToolbarButton({
        className: 'jp-DebuggerBugButton',
        icon: bugIcon,
        tooltip: trans.__('Enable Debugger'),
        pressedIcon: bugDotIcon,
        pressedTooltip: trans.__('Disable Debugger'),
        disabledTooltip: trans.__('Select a kernel that supports debugging to enable debugger'),
        enabled,
        pressed,
        onClick
    });
    if (!widget.toolbar.insertBefore('kernelName', TOOLBAR_DEBUGGER_ITEM, icon)) {
        widget.toolbar.addItem(TOOLBAR_DEBUGGER_ITEM, icon);
    }
    return icon;
}
/**
 * Updates button state to on/off,
 * adds/removes css class to update styling
 *
 * @param widget the debug button widget
 * @param pressed true if pressed, false otherwise
 * @param enabled true if widget enabled, false otherwise
 * @param onClick click handler
 */
function updateIconButtonState(widget, pressed, enabled = true, onClick) {
    if (widget) {
        widget.enabled = enabled;
        widget.pressed = pressed;
        if (onClick) {
            widget.onClick = onClick;
        }
    }
}
/**
 * A handler for debugging a widget.
 */
export class DebuggerHandler {
    /**
     * Instantiate a new DebuggerHandler.
     *
     * @param options The instantiation options for a DebuggerHandler.
     */
    constructor(options) {
        this._handlers = {};
        this._contextKernelChangedHandlers = {};
        this._kernelChangedHandlers = {};
        this._statusChangedHandlers = {};
        this._iopubMessageHandlers = {};
        this._iconButtons = {};
        this._type = options.type;
        this._shell = options.shell;
        this._service = options.service;
    }
    /**
     * Get the active widget.
     */
    get activeWidget() {
        return this._activeWidget;
    }
    /**
     * Update a debug handler for the given widget, and
     * handle kernel changed events.
     *
     * @param widget The widget to update.
     * @param connection The session connection.
     */
    async update(widget, connection) {
        if (!connection) {
            delete this._kernelChangedHandlers[widget.id];
            delete this._statusChangedHandlers[widget.id];
            delete this._iopubMessageHandlers[widget.id];
            return this.updateWidget(widget, connection);
        }
        const kernelChanged = () => {
            void this.updateWidget(widget, connection);
        };
        const kernelChangedHandler = this._kernelChangedHandlers[widget.id];
        if (kernelChangedHandler) {
            connection.kernelChanged.disconnect(kernelChangedHandler);
        }
        this._kernelChangedHandlers[widget.id] = kernelChanged;
        connection.kernelChanged.connect(kernelChanged);
        const statusChanged = (_, status) => {
            if (status.endsWith('restarting')) {
                void this.updateWidget(widget, connection);
            }
        };
        const statusChangedHandler = this._statusChangedHandlers[widget.id];
        if (statusChangedHandler) {
            connection.statusChanged.disconnect(statusChangedHandler);
        }
        connection.statusChanged.connect(statusChanged);
        this._statusChangedHandlers[widget.id] = statusChanged;
        const iopubMessage = (_, msg) => {
            if (this._service.isStarted &&
                !this._service.hasStoppedThreads() &&
                msg.parent_header.msg_type ===
                    'execute_request') {
                void this._service.displayDefinedVariables();
            }
        };
        const iopubMessageHandler = this._iopubMessageHandlers[widget.id];
        if (iopubMessageHandler) {
            connection.iopubMessage.disconnect(iopubMessageHandler);
        }
        connection.iopubMessage.connect(iopubMessage);
        this._iopubMessageHandlers[widget.id] = iopubMessage;
        this._activeWidget = widget;
        return this.updateWidget(widget, connection);
    }
    /**
     * Update a debug handler for the given widget, and
     * handle connection kernel changed events.
     *
     * @param widget The widget to update.
     * @param sessionContext The session context.
     */
    async updateContext(widget, sessionContext) {
        const connectionChanged = () => {
            const { session: connection } = sessionContext;
            void this.update(widget, connection);
        };
        const contextKernelChangedHandlers = this._contextKernelChangedHandlers[widget.id];
        if (contextKernelChangedHandlers) {
            sessionContext.kernelChanged.disconnect(contextKernelChangedHandlers);
        }
        this._contextKernelChangedHandlers[widget.id] = connectionChanged;
        sessionContext.kernelChanged.connect(connectionChanged);
        return this.update(widget, sessionContext.session);
    }
    /**
     * Update a debug handler for the given widget.
     *
     * @param widget The widget to update.
     * @param connection The session connection.
     */
    async updateWidget(widget, connection) {
        var _a, _b, _c, _d;
        if (!this._service.model || !connection) {
            return;
        }
        const hasFocus = () => {
            return this._shell.currentWidget === widget;
        };
        const updateAttribute = () => {
            if (!this._handlers[widget.id]) {
                widget.node.removeAttribute('data-jp-debugger');
                return;
            }
            widget.node.setAttribute('data-jp-debugger', 'true');
        };
        const createHandler = () => {
            if (this._handlers[widget.id]) {
                return;
            }
            switch (this._type) {
                case 'notebook':
                    this._handlers[widget.id] = new NotebookHandler({
                        debuggerService: this._service,
                        widget: widget
                    });
                    break;
                case 'console':
                    this._handlers[widget.id] = new ConsoleHandler({
                        debuggerService: this._service,
                        widget: widget
                    });
                    break;
                case 'file':
                    this._handlers[widget.id] = new FileHandler({
                        debuggerService: this._service,
                        widget: widget
                    });
                    break;
                default:
                    throw Error(`No handler for the type ${this._type}`);
            }
            updateAttribute();
        };
        const removeHandlers = () => {
            var _a, _b, _c, _d;
            const handler = this._handlers[widget.id];
            if (!handler) {
                return;
            }
            handler.dispose();
            delete this._handlers[widget.id];
            delete this._kernelChangedHandlers[widget.id];
            delete this._statusChangedHandlers[widget.id];
            delete this._iopubMessageHandlers[widget.id];
            delete this._contextKernelChangedHandlers[widget.id];
            // Clear the model if the handler being removed corresponds
            // to the current active debug session, or if the connection
            // does not have a kernel.
            if (((_b = (_a = this._service.session) === null || _a === void 0 ? void 0 : _a.connection) === null || _b === void 0 ? void 0 : _b.path) === (connection === null || connection === void 0 ? void 0 : connection.path) ||
                !((_d = (_c = this._service.session) === null || _c === void 0 ? void 0 : _c.connection) === null || _d === void 0 ? void 0 : _d.kernel)) {
                const model = this._service.model;
                model.clear();
            }
            updateAttribute();
        };
        const addToolbarButton = (enabled = true) => {
            const debugButton = this._iconButtons[widget.id];
            if (!debugButton) {
                this._iconButtons[widget.id] = updateIconButton(widget, toggleDebugging, this._service.isStarted, enabled);
            }
            else {
                updateIconButtonState(debugButton, this._service.isStarted, enabled, toggleDebugging);
            }
        };
        const isDebuggerOn = () => {
            var _a;
            return (this._service.isStarted &&
                ((_a = this._previousConnection) === null || _a === void 0 ? void 0 : _a.id) === (connection === null || connection === void 0 ? void 0 : connection.id));
        };
        const stopDebugger = async () => {
            this._service.session.connection = connection;
            await this._service.stop();
        };
        const startDebugger = async () => {
            var _a, _b;
            this._service.session.connection = connection;
            this._previousConnection = connection;
            await this._service.restoreState(true);
            await this._service.displayDefinedVariables();
            if ((_b = (_a = this._service.session) === null || _a === void 0 ? void 0 : _a.capabilities) === null || _b === void 0 ? void 0 : _b.supportsModulesRequest) {
                await this._service.displayModules();
            }
        };
        const toggleDebugging = async () => {
            // bail if the widget doesn't have focus
            if (!hasFocus()) {
                return;
            }
            const debugButton = this._iconButtons[widget.id];
            if (isDebuggerOn()) {
                await stopDebugger();
                removeHandlers();
                updateIconButtonState(debugButton, false);
            }
            else {
                await startDebugger();
                createHandler();
                updateIconButtonState(debugButton, true);
            }
        };
        addToolbarButton(false);
        // listen to the disposed signals
        widget.disposed.connect(async () => {
            if (isDebuggerOn()) {
                await stopDebugger();
            }
            removeHandlers();
            delete this._iconButtons[widget.id];
            delete this._contextKernelChangedHandlers[widget.id];
        });
        const debuggingEnabled = await this._service.isAvailable(connection);
        if (!debuggingEnabled) {
            removeHandlers();
            updateIconButtonState(this._iconButtons[widget.id], false, false);
            return;
        }
        // update the active debug session
        if (!this._service.session) {
            this._service.session = new Debugger.Session({
                connection,
                config: this._service.config
            });
        }
        else {
            this._previousConnection = ((_a = this._service.session.connection) === null || _a === void 0 ? void 0 : _a.kernel)
                ? this._service.session.connection
                : null;
            this._service.session.connection = connection;
        }
        await this._service.restoreState(false);
        if (this._service.isStarted && !this._service.hasStoppedThreads()) {
            await this._service.displayDefinedVariables();
            if ((_c = (_b = this._service.session) === null || _b === void 0 ? void 0 : _b.capabilities) === null || _c === void 0 ? void 0 : _c.supportsModulesRequest) {
                await this._service.displayModules();
            }
        }
        updateIconButtonState(this._iconButtons[widget.id], this._service.isStarted, true);
        // check the state of the debug session
        if (!this._service.isStarted) {
            removeHandlers();
            this._service.session.connection = (_d = this._previousConnection) !== null && _d !== void 0 ? _d : connection;
            await this._service.restoreState(false);
            return;
        }
        // if the debugger is started but there is no handler, create a new one
        createHandler();
        this._previousConnection = connection;
    }
}
//# sourceMappingURL=handler.js.map