<commit_before>// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import {
  ILabShell,
  ILabStatus,
  ILayoutRestorer,
  IRouter,
  JupyterFrontEnd,
  JupyterFrontEndPlugin,
  JupyterLab,
  LabShell,
  LayoutRestorer,
  Router
} from '@jupyterlab/application';

import {
  Dialog,
  ICommandPalette,
  IWindowResolver,
  showDialog,
  showErrorMessage
} from '@jupyterlab/apputils';

import {
  PathExt,
  IStateDB,
  ISettingRegistry,
  URLExt
} from '@jupyterlab/coreutils';

import { each, iter, toArray } from '@phosphor/algorithm';

import { Widget, DockLayout } from '@phosphor/widgets';

import * as React from 'react';

/**
 * The command IDs used by the application plugin.
 */
namespace CommandIDs {
  export const activateNextTab: string = 'application:activate-next-tab';

  export const activatePreviousTab: string =
    'application:activate-previous-tab';

  export const close = 'application:close';

  export const closeOtherTabs = 'application:close-other-tabs';

  export const closeRightTabs = 'application:close-right-tabs';

  export const closeAll: string = 'application:close-all';

  export const setMode: string = 'application:set-mode';

  export const toggleMode: string = 'application:toggle-mode';

  export const toggleLeftArea: string = 'application:toggle-left-area';

  export const toggleRightArea: string = 'application:toggle-right-area';

  export const togglePresentationMode: string =
    'application:toggle-presentation-mode';

  export const tree: string = 'router:tree';

  export const switchSidebar = 'sidebar:switch';
}

/**
 * The main extension.
 */
const main: JupyterFrontEndPlugin<void> = {
  id: '@jupyterlab/application-extension:main',
  requires: [ICommandPalette, IRouter, IWindowResolver],
  activate: (
    app: JupyterFrontEnd,
    palette: ICommandPalette,
    router: IRouter,
    resolver: IWindowResolver
  ) => {
    if (!(app instanceof JupyterLab)) {
      throw new Error(`${main.id} must be activated in JupyterLab.`);
    }

    // Requiring the window resolver guarantees that the application extension
    // only loads if there is a viable window name. Otherwise, the application
    // will short-circuit and ask the user to navigate away.
    const workspace = resolver.name;

    console.log(`Starting application in workspace: ${workspace}`);

    // If there were errors registering plugins, tell the user.
    if (app.registerPluginErrors.length !== 0) {
      const body = (
        <pre>{app.registerPluginErrors.map(e => e.message).join('\n')}</pre>
      );

      void showErrorMessage('Error Registering Plugins', { message: body });
    }

    addCommands(app, palette);

    // If the application shell layout is modified,
    // trigger a refresh of the commands.
    app.shell.layoutModified.connect(() => {
      app.commands.notifyCommandChanged();
    });

    const builder = app.serviceManager.builder;
    const build = () => {
      return builder
        .build()
        .then(() => {
          return showDialog({
            title: 'Build Complete',
            body: 'Build successfully completed, reload page?',
            buttons: [
              Dialog.cancelButton(),
              Dialog.warnButton({ label: 'RELOAD' })
            ]
          });
        })
        .then(result => {
          if (result.button.accept) {
            router.reload();
          }
        })
        .catch(err => {
          void showErrorMessage('Build Failed', {
            message: <pre>{err.message}</pre>
          });
        });
    };

    if (builder.isAvailable && builder.shouldCheck) {
      void builder.getStatus().then(response => {
        if (response.status === 'building') {
          return build();
        }

        if (response.status !== 'needed') {
          return;
        }

        const body = (
          <div>
            JupyterLab build is suggested:
            <br />
            <pre>{response.message}</pre>
          </div>
        );

        void showDialog({
          title: 'Build Recommended',
          body,
          buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'BUILD' })]
        }).then(result => (result.button.accept ? build() : undefined));
      });
    }

    const message =
      'Are you sure you want to exit JupyterLab?\n' +
      'Any unsaved changes will be lost.';

    // The spec for the `beforeunload` event is implemented differently by
    // the different browser vendors. Consequently, the `event.returnValue`
    // attribute needs to set in addition to a return value being returned.
    // For more information, see:
    // https://developer.mozilla.org/en/docs/Web/Events/beforeunload
    window.addEventListener('beforeunload', event => {
      if (app.status.isDirty) {
        return ((event as any).returnValue = message);
      }
    });
  },
  autoStart: true
};

/**
 * The default layout restorer provider.
 */
const layout: JupyterFrontEndPlugin<ILayoutRestorer> = {
  id: '@jupyterlab/application-extension:layout',
  requires: [IStateDB, ILabShell],
  activate: (app: JupyterFrontEnd, state: IStateDB, labShell: ILabShell) => {
    const first = app.started;
    const registry = app.commands;
    const restorer = new LayoutRestorer({ first, registry, state });

    void restorer.fetch().then(saved => {
      labShell.restoreLayout(saved);
      labShell.layoutModified.connect(() => {
        void restorer.save(labShell.saveLayout());
      });
    });

    return restorer;
  },
  autoStart: true,
  provides: ILayoutRestorer
};

/**
 * The default URL router provider.
 */
const router: JupyterFrontEndPlugin<IRouter> = {
  id: '@jupyterlab/application-extension:router',
  requires: [JupyterFrontEnd.IPaths],
  activate: (app: JupyterFrontEnd, paths: JupyterFrontEnd.IPaths) => {
    const { commands } = app;
    const base = paths.urls.base;
    const router = new Router({ base, commands });

    void app.started.then(() => {
      // Route the very first request on load.
      void router.route();

      // Route all pop state events.
      window.addEventListener('popstate', () => {
        void router.route();
      });
    });

    return router;
  },
  autoStart: true,
  provides: IRouter
};

/**
 * The tree route handler provider.
 */
const tree: JupyterFrontEndPlugin<void> = {
  id: '@jupyterlab/application-extension:tree',
  autoStart: true,
  requires: [JupyterFrontEnd.IPaths, IRouter, IWindowResolver],
  activate: (
    app: JupyterFrontEnd,
    paths: JupyterFrontEnd.IPaths,
    router: IRouter,
    resolver: IWindowResolver
  ) => {
    const { commands } = app;
    const treePattern = new RegExp(`^${paths.urls.tree}([^?]+)`);
    const workspacePattern = new RegExp(
      `^${paths.urls.workspaces}[^?\/]+/tree/([^?]+)`
    );

    commands.addCommand(CommandIDs.tree, {
      execute: async (args: IRouter.ILocation) => {
        const treeMatch = args.path.match(treePattern);
        const workspaceMatch = args.path.match(workspacePattern);
        const match = treeMatch || workspaceMatch;
        const path = decodeURI(match[1]);
        // const { page, workspaces } = info.urls;
        const workspace = PathExt.basename(resolver.name);
        const url =
          (workspaceMatch
            ? URLExt.join(paths.urls.workspaces, workspace)
            : paths.urls.page) +
          args.search +
          args.hash;
        const immediate = true;
        const silent = true;

        // Silently remove the tree portion of the URL leaving the rest intact.
        router.navigate(url, { silent });

        try {
          await commands.execute('filebrowser:navigate', { path });
          await commands.execute('apputils:save-statedb', { immediate });
        } catch (error) {
          console.warn('Tree routing failed.', error);
        }
      }
    });

    router.register({ command: CommandIDs.tree, pattern: treePattern });
    router.register({ command: CommandIDs.tree, pattern: workspacePattern });
  }
};

/**
 * The default URL not found extension.
 */
const notfound: JupyterFrontEndPlugin<void> = {
  id: '@jupyterlab/application-extension:notfound',
  requires: [JupyterFrontEnd.IPaths, IRouter],
  activate: (
    _: JupyterFrontEnd,
    paths: JupyterFrontEnd.IPaths,
    router: IRouter
  ) => {
    const bad = paths.urls.notFound;

    if (!bad) {
      return;
    }

    const base = router.base;
    const message = `
      The path: ${bad} was not found. JupyterLab redirected to: ${base}
    `;

    // Change the URL back to the base application URL without adding the
    // URL change to the browser history.
    router.navigate('', { silent: true });

    void showErrorMessage('Path Not Found', { message });
  },
  autoStart: true
};

/**
 * Change the favicon changing based on the busy status;
 */
const busy: JupyterFrontEndPlugin<void> = {
  id: '@jupyterlab/application-extension:faviconbusy',
  requires: [ILabStatus],
  activate: async (_: JupyterFrontEnd, status: ILabStatus) => {
    status.busySignal.connect((_, isBusy) => {
      const favicon = document.querySelector(
        `link[rel="icon"]${isBusy ? '.idle.favicon' : '.busy.favicon'}`
      ) as HTMLLinkElement;
      if (!favicon) {
        return;
      }
      const newFavicon = document.querySelector(
        `link${isBusy ? '.busy.favicon' : '.idle.favicon'}`
      ) as HTMLLinkElement;
      if (!newFavicon) {
        return;
      }
      // If we have the two icons with the special classes, then toggle them.
      if (favicon !== newFavicon) {
        favicon.rel = '';
        newFavicon.rel = 'icon';

        // Firefox doesn't seem to recognize just changing rel, so we also
        // reinsert the link into the DOM.
        newFavicon.parentNode.replaceChild(newFavicon, newFavicon);
      }
    });
  },
  autoStart: true
};

const SIDEBAR_ID = '@jupyterlab/application-extension:sidebar';

/**
 * Keep user settings for where to show the side panels.
 */
const sidebar: JupyterFrontEndPlugin<void> = {
  id: SIDEBAR_ID,
  activate: (
    app: JupyterFrontEnd,
    settingRegistry: ISettingRegistry,
    labShell: ILabShell
  ) => {
    type overrideMap = { [id: string]: 'left' | 'right' };
    let overrides: overrideMap = {};
    const handleLayoutOverrides = () => {
      each(labShell.widgets('left'), widget => {
        if (overrides[widget.id] && overrides[widget.id] === 'right') {
          labShell.add(widget, 'right');
        }
      });
      each(labShell.widgets('right'), widget => {
        if (overrides[widget.id] && overrides[widget.id] === 'left') {
          labShell.add(widget, 'left');
        }
      });
    };
    labShell.layoutModified.connect(handleLayoutOverrides);
    // Fetch overrides from the settings system.
    void Promise.all([settingRegistry.load(SIDEBAR_ID), app.restored]).then(
      ([settings]) => {
        overrides = (settings.get('overrides').composite as overrideMap) || {};
        settings.changed.connect(settings => {
          overrides =
            (settings.get('overrides').composite as overrideMap) || {};
          handleLayoutOverrides();
        });
      }
    );

    // Add a command to switch a side panels's side
    app.commands.addCommand(CommandIDs.switchSidebar, {
      label: 'Switch Sidebar Side',
      execute: () => {
        // First, try to find the right panel based on the
        // application context menu click,
        // If we can't find it there, look for use the active
        // left/right widgets.
        const contextNode: HTMLElement = app.contextMenuHitTest(
          node => !!node.dataset.id
        );
        let id: string;
        let side: 'left' | 'right';
        if (contextNode) {
          id = contextNode.dataset['id'];
          const leftPanel = document.getElementById('jp-left-stack');
          const node = document.getElementById(id);
          if (leftPanel && node && leftPanel.contains(node)) {
            side = 'right';
          } else {
            side = 'left';
          }
        } else if (document.body.dataset.leftSidebarWidget) {
          id = document.body.dataset.leftSidebarWidget;
          side = 'right';
        } else if (document.body.dataset.rightSidebarWidget) {
          id = document.body.dataset.rightSidebarWidget;
          side = 'left';
        }

        // Move the panel to the other side.
        const newOverrides = { ...overrides };
        newOverrides[id] = side;
        return settingRegistry.set(SIDEBAR_ID, 'overrides', newOverrides);
      }
    });

    // Add a context menu item to sidebar tabs.
    app.contextMenu.addItem({
      command: CommandIDs.switchSidebar,
      selector: '.jp-SideBar .p-TabBar-tab',
      rank: 500
    });
  },
  requires: [ISettingRegistry, ILabShell],
  autoStart: true
};

/**
 * Add the main application commands.
 */
function addCommands(app: JupyterLab, palette: ICommandPalette): void {
  const { commands, contextMenu, shell } = app;
  const category = 'Main Area';

  // Returns the widget associated with the most recent contextmenu event.
  const contextMenuWidget = (): Widget => {
    const test = (node: HTMLElement) => !!node.dataset.id;
    const node = app.contextMenuHitTest(test);

    if (!node) {
      // Fall back to active widget if path cannot be obtained from event.
      return shell.currentWidget;
    }

    const matches = toArray(shell.widgets('main')).filter(
      widget => widget.id === node.dataset.id
    );

    if (matches.length < 1) {
      return shell.currentWidget;
    }

    return matches[0];
  };

  // Closes an array of widgets.
  const closeWidgets = (widgets: Array<Widget>): void => {
    widgets.forEach(widget => widget.close());
  };

  // Find the tab area for a widget within a specific dock area.
  const findTab = (
    area: DockLayout.AreaConfig,
    widget: Widget
  ): DockLayout.ITabAreaConfig | null => {
    switch (area.type) {
      case 'split-area':
        const iterator = iter(area.children);
        let tab: DockLayout.ITabAreaConfig | null = null;
        let value: DockLayout.AreaConfig | null = null;
        do {
          value = iterator.next();
          if (value) {
            tab = findTab(value, widget);
          }
        } while (!tab && value);
        return tab;
      case 'tab-area':
        const { id } = widget;
        return area.widgets.some(widget => widget.id === id) ? area : null;
      default:
        return null;
    }
  };

  // Find the tab area for a widget within the main dock area.
  const tabAreaFor = (widget: Widget): DockLayout.ITabAreaConfig | null => {
    const { mainArea } = shell.saveLayout();
    if (mainArea.mode !== 'multiple-document') {
      return null;
    }
    let area = mainArea.dock.main;
    if (!area) {
      return null;
    }
    return findTab(area, widget);
  };

  // Returns an array of all widgets to the right of a widget in a tab area.
  const widgetsRightOf = (widget: Widget): Array<Widget> => {
    const { id } = widget;
    const tabArea = tabAreaFor(widget);
    const widgets = tabArea ? tabArea.widgets || [] : [];
    const index = widgets.findIndex(widget => widget.id === id);
    if (index < 0) {
      return [];
    }
    return widgets.slice(index + 1);
  };

  commands.addCommand(CommandIDs.activateNextTab, {
    label: 'Activate Next Tab',
    execute: () => {
      shell.activateNextTab();
    }
  });
  palette.addItem({ command: CommandIDs.activateNextTab, category });

  commands.addCommand(CommandIDs.activatePreviousTab, {
    label: 'Activate Previous Tab',
    execute: () => {
      shell.activatePreviousTab();
    }
  });
  palette.addItem({ command: CommandIDs.activatePreviousTab, category });

  commands.addCommand(CommandIDs.close, {
    label: () => 'Close Tab',
    isEnabled: () =>
      !!shell.currentWidget && !!shell.currentWidget.title.closable,
    execute: () => {
      if (shell.currentWidget) {
        shell.currentWidget.close();
      }
    }
  });
  palette.addItem({ command: CommandIDs.close, category });

  commands.addCommand(CommandIDs.closeAll, {
    label: 'Close All Tabs',
    execute: () => {
      shell.closeAll();
    }
  });
  palette.addItem({ command: CommandIDs.closeAll, category });

  commands.addCommand(CommandIDs.closeOtherTabs, {
    label: () => `Close Other Tabs`,
    isEnabled: () => {
      // Ensure there are at least two widgets.
      const iterator = shell.widgets('main');
      return !!iterator.next() && !!iterator.next();
    },
    execute: () => {
      const widget = contextMenuWidget();
      if (!widget) {
        return;
      }
      const { id } = widget;
      const otherWidgets = toArray(shell.widgets('main')).filter(
        widget => widget.id !== id
      );<commit_msg>Add close to tab context menu<commit_after>// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import {
  ILabShell,
  ILabStatus,
  ILayoutRestorer,
  IRouter,
  JupyterFrontEnd,
  JupyterFrontEndPlugin,
  JupyterLab,
  LabShell,
  LayoutRestorer,
  Router
} from '@jupyterlab/application';

import {
  Dialog,
  ICommandPalette,
  IWindowResolver,
  showDialog,
  showErrorMessage
} from '@jupyterlab/apputils';

import {
  PathExt,
  IStateDB,
  ISettingRegistry,
  URLExt
} from '@jupyterlab/coreutils';

import { each, iter, toArray } from '@phosphor/algorithm';

import { Widget, DockLayout } from '@phosphor/widgets';

import * as React from 'react';

/**
 * The command IDs used by the application plugin.
 */
namespace CommandIDs {
  export const activateNextTab: string = 'application:activate-next-tab';

  export const activatePreviousTab: string =
    'application:activate-previous-tab';

  export const close = 'application:close';

  export const closeOtherTabs = 'application:close-other-tabs';

  export const closeRightTabs = 'application:close-right-tabs';

  export const closeAll: string = 'application:close-all';

  export const setMode: string = 'application:set-mode';

  export const toggleMode: string = 'application:toggle-mode';

  export const toggleLeftArea: string = 'application:toggle-left-area';

  export const toggleRightArea: string = 'application:toggle-right-area';

  export const togglePresentationMode: string =
    'application:toggle-presentation-mode';

  export const tree: string = 'router:tree';

  export const switchSidebar = 'sidebar:switch';
}

/**
 * The main extension.
 */
const main: JupyterFrontEndPlugin<void> = {
  id: '@jupyterlab/application-extension:main',
  requires: [ICommandPalette, IRouter, IWindowResolver],
  activate: (
    app: JupyterFrontEnd,
    palette: ICommandPalette,
    router: IRouter,
    resolver: IWindowResolver
  ) => {
    if (!(app instanceof JupyterLab)) {
      throw new Error(`${main.id} must be activated in JupyterLab.`);
    }

    // Requiring the window resolver guarantees that the application extension
    // only loads if there is a viable window name. Otherwise, the application
    // will short-circuit and ask the user to navigate away.
    const workspace = resolver.name;

    console.log(`Starting application in workspace: ${workspace}`);

    // If there were errors registering plugins, tell the user.
    if (app.registerPluginErrors.length !== 0) {
      const body = (
        <pre>{app.registerPluginErrors.map(e => e.message).join('\n')}</pre>
      );

      void showErrorMessage('Error Registering Plugins', { message: body });
    }

    addCommands(app, palette);

    // If the application shell layout is modified,
    // trigger a refresh of the commands.
    app.shell.layoutModified.connect(() => {
      app.commands.notifyCommandChanged();
    });

    const builder = app.serviceManager.builder;
    const build = () => {
      return builder
        .build()
        .then(() => {
          return showDialog({
            title: 'Build Complete',
            body: 'Build successfully completed, reload page?',
            buttons: [
              Dialog.cancelButton(),
              Dialog.warnButton({ label: 'RELOAD' })
            ]
          });
        })
        .then(result => {
          if (result.button.accept) {
            router.reload();
          }
        })
        .catch(err => {
          void showErrorMessage('Build Failed', {
            message: <pre>{err.message}</pre>
          });
        });
    };

    if (builder.isAvailable && builder.shouldCheck) {
      void builder.getStatus().then(response => {
        if (response.status === 'building') {
          return build();
        }

        if (response.status !== 'needed') {
          return;
        }

        const body = (
          <div>
            JupyterLab build is suggested:
            <br />
            <pre>{response.message}</pre>
          </div>
        );

        void showDialog({
          title: 'Build Recommended',
          body,
          buttons: [Dialog.cancelButton(), Dialog.okButton({ label: 'BUILD' })]
        }).then(result => (result.button.accept ? build() : undefined));
      });
    }

    const message =
      'Are you sure you want to exit JupyterLab?\n' +
      'Any unsaved changes will be lost.';

    // The spec for the `beforeunload` event is implemented differently by
    // the different browser vendors. Consequently, the `event.returnValue`
    // attribute needs to set in addition to a return value being returned.
    // For more information, see:
    // https://developer.mozilla.org/en/docs/Web/Events/beforeunload
    window.addEventListener('beforeunload', event => {
      if (app.status.isDirty) {
        return ((event as any).returnValue = message);
      }
    });
  },
  autoStart: true
};

/**
 * The default layout restorer provider.
 */
const layout: JupyterFrontEndPlugin<ILayoutRestorer> = {
  id: '@jupyterlab/application-extension:layout',
  requires: [IStateDB, ILabShell],
  activate: (app: JupyterFrontEnd, state: IStateDB, labShell: ILabShell) => {
    const first = app.started;
    const registry = app.commands;
    const restorer = new LayoutRestorer({ first, registry, state });

    void restorer.fetch().then(saved => {
      labShell.restoreLayout(saved);
      labShell.layoutModified.connect(() => {
        void restorer.save(labShell.saveLayout());
      });
    });

    return restorer;
  },
  autoStart: true,
  provides: ILayoutRestorer
};

/**
 * The default URL router provider.
 */
const router: JupyterFrontEndPlugin<IRouter> = {
  id: '@jupyterlab/application-extension:router',
  requires: [JupyterFrontEnd.IPaths],
  activate: (app: JupyterFrontEnd, paths: JupyterFrontEnd.IPaths) => {
    const { commands } = app;
    const base = paths.urls.base;
    const router = new Router({ base, commands });

    void app.started.then(() => {
      // Route the very first request on load.
      void router.route();

      // Route all pop state events.
      window.addEventListener('popstate', () => {
        void router.route();
      });
    });

    return router;
  },
  autoStart: true,
  provides: IRouter
};

/**
 * The tree route handler provider.
 */
const tree: JupyterFrontEndPlugin<void> = {
  id: '@jupyterlab/application-extension:tree',
  autoStart: true,
  requires: [JupyterFrontEnd.IPaths, IRouter, IWindowResolver],
  activate: (
    app: JupyterFrontEnd,
    paths: JupyterFrontEnd.IPaths,
    router: IRouter,
    resolver: IWindowResolver
  ) => {
    const { commands } = app;
    const treePattern = new RegExp(`^${paths.urls.tree}([^?]+)`);
    const workspacePattern = new RegExp(
      `^${paths.urls.workspaces}[^?\/]+/tree/([^?]+)`
    );

    commands.addCommand(CommandIDs.tree, {
      execute: async (args: IRouter.ILocation) => {
        const treeMatch = args.path.match(treePattern);
        const workspaceMatch = args.path.match(workspacePattern);
        const match = treeMatch || workspaceMatch;
        const path = decodeURI(match[1]);
        // const { page, workspaces } = info.urls;
        const workspace = PathExt.basename(resolver.name);
        const url =
          (workspaceMatch
            ? URLExt.join(paths.urls.workspaces, workspace)
            : paths.urls.page) +
          args.search +
          args.hash;
        const immediate = true;
        const silent = true;

        // Silently remove the tree portion of the URL leaving the rest intact.
        router.navigate(url, { silent });

        try {
          await commands.execute('filebrowser:navigate', { path });
          await commands.execute('apputils:save-statedb', { immediate });
        } catch (error) {
          console.warn('Tree routing failed.', error);
        }
      }
    });

    router.register({ command: CommandIDs.tree, pattern: treePattern });
    router.register({ command: CommandIDs.tree, pattern: workspacePattern });
  }
};

/**
 * The default URL not found extension.
 */
const notfound: JupyterFrontEndPlugin<void> = {
  id: '@jupyterlab/application-extension:notfound',
  requires: [JupyterFrontEnd.IPaths, IRouter],
  activate: (
    _: JupyterFrontEnd,
    paths: JupyterFrontEnd.IPaths,
    router: IRouter
  ) => {
    const bad = paths.urls.notFound;

    if (!bad) {
      return;
    }

    const base = router.base;
    const message = `
      The path: ${bad} was not found. JupyterLab redirected to: ${base}
    `;

    // Change the URL back to the base application URL without adding the
    // URL change to the browser history.
    router.navigate('', { silent: true });

    void showErrorMessage('Path Not Found', { message });
  },
  autoStart: true
};

/**
 * Change the favicon changing based on the busy status;
 */
const busy: JupyterFrontEndPlugin<void> = {
  id: '@jupyterlab/application-extension:faviconbusy',
  requires: [ILabStatus],
  activate: async (_: JupyterFrontEnd, status: ILabStatus) => {
    status.busySignal.connect((_, isBusy) => {
      const favicon = document.querySelector(
        `link[rel="icon"]${isBusy ? '.idle.favicon' : '.busy.favicon'}`
      ) as HTMLLinkElement;
      if (!favicon) {
        return;
      }
      const newFavicon = document.querySelector(
        `link${isBusy ? '.busy.favicon' : '.idle.favicon'}`
      ) as HTMLLinkElement;
      if (!newFavicon) {
        return;
      }
      // If we have the two icons with the special classes, then toggle them.
      if (favicon !== newFavicon) {
        favicon.rel = '';
        newFavicon.rel = 'icon';

        // Firefox doesn't seem to recognize just changing rel, so we also
        // reinsert the link into the DOM.
        newFavicon.parentNode.replaceChild(newFavicon, newFavicon);
      }
    });
  },
  autoStart: true
};

const SIDEBAR_ID = '@jupyterlab/application-extension:sidebar';

/**
 * Keep user settings for where to show the side panels.
 */
const sidebar: JupyterFrontEndPlugin<void> = {
  id: SIDEBAR_ID,
  activate: (
    app: JupyterFrontEnd,
    settingRegistry: ISettingRegistry,
    labShell: ILabShell
  ) => {
    type overrideMap = { [id: string]: 'left' | 'right' };
    let overrides: overrideMap = {};
    const handleLayoutOverrides = () => {
      each(labShell.widgets('left'), widget => {
        if (overrides[widget.id] && overrides[widget.id] === 'right') {
          labShell.add(widget, 'right');
        }
      });
      each(labShell.widgets('right'), widget => {
        if (overrides[widget.id] && overrides[widget.id] === 'left') {
          labShell.add(widget, 'left');
        }
      });
    };
    labShell.layoutModified.connect(handleLayoutOverrides);
    // Fetch overrides from the settings system.
    void Promise.all([settingRegistry.load(SIDEBAR_ID), app.restored]).then(
      ([settings]) => {
        overrides = (settings.get('overrides').composite as overrideMap) || {};
        settings.changed.connect(settings => {
          overrides =
            (settings.get('overrides').composite as overrideMap) || {};
          handleLayoutOverrides();
        });
      }
    );

    // Add a command to switch a side panels's side
    app.commands.addCommand(CommandIDs.switchSidebar, {
      label: 'Switch Sidebar Side',
      execute: () => {
        // First, try to find the right panel based on the
        // application context menu click,
        // If we can't find it there, look for use the active
        // left/right widgets.
        const contextNode: HTMLElement = app.contextMenuHitTest(
          node => !!node.dataset.id
        );
        let id: string;
        let side: 'left' | 'right';
        if (contextNode) {
          id = contextNode.dataset['id'];
          const leftPanel = document.getElementById('jp-left-stack');
          const node = document.getElementById(id);
          if (leftPanel && node && leftPanel.contains(node)) {
            side = 'right';
          } else {
            side = 'left';
          }
        } else if (document.body.dataset.leftSidebarWidget) {
          id = document.body.dataset.leftSidebarWidget;
          side = 'right';
        } else if (document.body.dataset.rightSidebarWidget) {
          id = document.body.dataset.rightSidebarWidget;
          side = 'left';
        }

        // Move the panel to the other side.
        const newOverrides = { ...overrides };
        newOverrides[id] = side;
        return settingRegistry.set(SIDEBAR_ID, 'overrides', newOverrides);
      }
    });

    // Add a context menu item to sidebar tabs.
    app.contextMenu.addItem({
      command: CommandIDs.switchSidebar,
      selector: '.jp-SideBar .p-TabBar-tab',
      rank: 500
    });
  },
  requires: [ISettingRegistry, ILabShell],
  autoStart: true
};

/**
 * Add the main application commands.
 */
function addCommands(app: JupyterLab, palette: ICommandPalette): void {
  const { commands, contextMenu, shell } = app;
  const category = 'Main Area';

  // Returns the widget associated with the most recent contextmenu event.
  const contextMenuWidget = (): Widget => {
    const test = (node: HTMLElement) => !!node.dataset.id;
    const node = app.contextMenuHitTest(test);

    if (!node) {
      // Fall back to active widget if path cannot be obtained from event.
      return shell.currentWidget;
    }

    const matches = toArray(shell.widgets('main')).filter(
      widget => widget.id === node.dataset.id
    );

    if (matches.length < 1) {
      return shell.currentWidget;
    }

    return matches[0];
  };

  // Closes an array of widgets.
  const closeWidgets = (widgets: Array<Widget>): void => {
    widgets.forEach(widget => widget.close());
  };

  // Find the tab area for a widget within a specific dock area.
  const findTab = (
    area: DockLayout.AreaConfig,
    widget: Widget
  ): DockLayout.ITabAreaConfig | null => {
    switch (area.type) {
      case 'split-area':
        const iterator = iter(area.children);
        let tab: DockLayout.ITabAreaConfig | null = null;
        let value: DockLayout.AreaConfig | null = null;
        do {
          value = iterator.next();
          if (value) {
            tab = findTab(value, widget);
          }
        } while (!tab && value);
        return tab;
      case 'tab-area':
        const { id } = widget;
        return area.widgets.some(widget => widget.id === id) ? area : null;
      default:
        return null;
    }
  };

  // Find the tab area for a widget within the main dock area.
  const tabAreaFor = (widget: Widget): DockLayout.ITabAreaConfig | null => {
    const { mainArea } = shell.saveLayout();
    if (mainArea.mode !== 'multiple-document') {
      return null;
    }
    let area = mainArea.dock.main;
    if (!area) {
      return null;
    }
    return findTab(area, widget);
  };

  // Returns an array of all widgets to the right of a widget in a tab area.
  const widgetsRightOf = (widget: Widget): Array<Widget> => {
    const { id } = widget;
    const tabArea = tabAreaFor(widget);
    const widgets = tabArea ? tabArea.widgets || [] : [];
    const index = widgets.findIndex(widget => widget.id === id);
    if (index < 0) {
      return [];
    }
    return widgets.slice(index + 1);
  };

  commands.addCommand(CommandIDs.activateNextTab, {
    label: 'Activate Next Tab',
    execute: () => {
      shell.activateNextTab();
    }
  });
  palette.addItem({ command: CommandIDs.activateNextTab, category });

  commands.addCommand(CommandIDs.activatePreviousTab, {
    label: 'Activate Previous Tab',
    execute: () => {
      shell.activatePreviousTab();
    }
  });
  palette.addItem({ command: CommandIDs.activatePreviousTab, category });

  commands.addCommand(CommandIDs.close, {
    label: () => 'Close Tab',
    isEnabled: () =>
      !!shell.currentWidget && !!shell.currentWidget.title.closable,
    execute: () => {
      if (shell.currentWidget) {
        shell.currentWidget.close();
      }
    }
  });
  palette.addItem({ command: CommandIDs.close, category });
  contextMenu.addItem({
    command: CommandIDs.close,
    selector: '.p-TabBar-tab',
    rank: 4
  });

  commands.addCommand(CommandIDs.closeAll, {
    label: 'Close All Tabs',
    execute: () => {
      shell.closeAll();
    }
  });
  palette.addItem({ command: CommandIDs.closeAll, category });

  commands.addCommand(CommandIDs.closeOtherTabs, {
    label: () => `Close Other Tabs`,
    isEnabled: () => {
      // Ensure there are at least two widgets.
      const iterator = shell.widgets('main');
      return !!iterator.next() && !!iterator.next();
    },
    execute: () => {
      const widget = contextMenuWidget();
      if (!widget) {
        return;
      }
      const { id } = widget;
      const otherWidgets = toArray(shell.widgets('main')).filter(
        widget => widget.id !== id
      );
{'fraction_non_alphanumeric': 0.13034568781183178, 'fraction_numerical': 0.0005642670468044666, 'mean_word_length': 2.5940868822713203, 'pattern_counts': {'":': 0, '<': 43, '<?xml version=': 0, '>': 137, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '967796', 'n_tokens_mistral': 10078, 'n_tokens_neox': 9894, 'n_words': 2508}