|
@@ -23,7 +23,7 @@ import * as nbformat from '@jupyterlab/nbformat';
|
|
|
|
|
|
import { KernelMessage } from '@jupyterlab/services';
|
|
|
|
|
|
-import { ArrayExt, each, toArray } from '@lumino/algorithm';
|
|
|
+import { ArrayExt, each, findIndex, toArray } from '@lumino/algorithm';
|
|
|
|
|
|
import { JSONObject, JSONExt } from '@lumino/coreutils';
|
|
|
|
|
@@ -725,22 +725,20 @@ export namespace NotebookActions {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- let possibleNextCell = notebook.activeCellIndex - 1;
|
|
|
+ let possibleNextCellIndex = notebook.activeCellIndex - 1;
|
|
|
|
|
|
// find first non hidden cell above current cell
|
|
|
- if (notebook.mode === 'edit') {
|
|
|
- while (notebook.widgets[possibleNextCell].inputHidden) {
|
|
|
- // If we are at the top cell, we cannot change selection.
|
|
|
- if (possibleNextCell === 0) {
|
|
|
- return;
|
|
|
- }
|
|
|
- possibleNextCell -= 1;
|
|
|
+ while (possibleNextCellIndex >= 0) {
|
|
|
+ const possibleNextCell = notebook.widgets[possibleNextCellIndex];
|
|
|
+ if (!possibleNextCell.inputHidden && !possibleNextCell.isHidden) {
|
|
|
+ break;
|
|
|
}
|
|
|
+ possibleNextCellIndex -= 1;
|
|
|
}
|
|
|
|
|
|
const state = Private.getState(notebook);
|
|
|
|
|
|
- notebook.activeCellIndex = possibleNextCell;
|
|
|
+ notebook.activeCellIndex = possibleNextCellIndex;
|
|
|
notebook.deselectAll();
|
|
|
Private.handleState(notebook, state, true);
|
|
|
}
|
|
@@ -760,27 +758,32 @@ export namespace NotebookActions {
|
|
|
if (!notebook.model || !notebook.activeCell) {
|
|
|
return;
|
|
|
}
|
|
|
- const maxCellIndex = notebook.widgets.length - 1;
|
|
|
+ let maxCellIndex = notebook.widgets.length - 1;
|
|
|
+ // Find last non-hidden cell
|
|
|
+ while (
|
|
|
+ notebook.widgets[maxCellIndex].isHidden ||
|
|
|
+ notebook.widgets[maxCellIndex].inputHidden
|
|
|
+ ) {
|
|
|
+ maxCellIndex -= 1;
|
|
|
+ }
|
|
|
if (notebook.activeCellIndex === maxCellIndex) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- let possibleNextCell = notebook.activeCellIndex + 1;
|
|
|
+ let possibleNextCellIndex = notebook.activeCellIndex + 1;
|
|
|
|
|
|
// find first non hidden cell below current cell
|
|
|
- if (notebook.mode === 'edit') {
|
|
|
- while (notebook.widgets[possibleNextCell].inputHidden) {
|
|
|
- // If we are at the bottom cell, we cannot change selection.
|
|
|
- if (possibleNextCell === maxCellIndex) {
|
|
|
- return;
|
|
|
- }
|
|
|
- possibleNextCell += 1;
|
|
|
+ while (possibleNextCellIndex < maxCellIndex) {
|
|
|
+ let possibleNextCell = notebook.widgets[possibleNextCellIndex];
|
|
|
+ if (!possibleNextCell.inputHidden && !possibleNextCell.isHidden) {
|
|
|
+ break;
|
|
|
}
|
|
|
+ possibleNextCellIndex += 1;
|
|
|
}
|
|
|
|
|
|
const state = Private.getState(notebook);
|
|
|
|
|
|
- notebook.activeCellIndex = possibleNextCell;
|
|
|
+ notebook.activeCellIndex = possibleNextCellIndex;
|
|
|
notebook.deselectAll();
|
|
|
Private.handleState(notebook, state, true);
|
|
|
}
|
|
@@ -1390,6 +1393,233 @@ export namespace NotebookActions {
|
|
|
Private.handleState(notebook, state);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Collapse all cells in given notebook.
|
|
|
+ *
|
|
|
+ * @param notebook - The target notebook widget.
|
|
|
+ */
|
|
|
+ export function collapseAll(notebook: Notebook): any {
|
|
|
+ for (const cell of notebook.widgets) {
|
|
|
+ if (NotebookActions.getHeadingInfo(cell).isHeading) {
|
|
|
+ NotebookActions.setHeadingCollapse(cell, true, notebook);
|
|
|
+ NotebookActions.setCellCollapse(cell, true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Un-collapse all cells in given notebook.
|
|
|
+ *
|
|
|
+ * @param notebook - The target notebook widget.
|
|
|
+ */
|
|
|
+ export function expandAllHeadings(notebook: Notebook): any {
|
|
|
+ for (const cell of notebook.widgets) {
|
|
|
+ if (NotebookActions.getHeadingInfo(cell).isHeading) {
|
|
|
+ NotebookActions.setHeadingCollapse(cell, false, notebook);
|
|
|
+ // similar to collapseAll.
|
|
|
+ NotebookActions.setCellCollapse(cell, false);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function findNearestParentHeader(
|
|
|
+ cell: Cell,
|
|
|
+ notebook: Notebook
|
|
|
+ ): Cell | undefined {
|
|
|
+ const index = findIndex(
|
|
|
+ notebook.widgets,
|
|
|
+ (possibleCell: Cell, index: number) => {
|
|
|
+ return cell.model.id === possibleCell.model.id;
|
|
|
+ }
|
|
|
+ );
|
|
|
+ if (index === -1) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // Finds the nearest header above the given cell. If the cell is a header itself, it does not return itself;
|
|
|
+ // this can be checked directly by calling functions.
|
|
|
+ if (index >= notebook.widgets.length) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ let childHeaderInfo = getHeadingInfo(notebook.widgets[index]);
|
|
|
+ for (let cellN = index - 1; cellN >= 0; cellN--) {
|
|
|
+ if (cellN < notebook.widgets.length) {
|
|
|
+ let hInfo = getHeadingInfo(notebook.widgets[cellN]);
|
|
|
+ if (
|
|
|
+ hInfo.isHeading &&
|
|
|
+ hInfo.headingLevel < childHeaderInfo.headingLevel
|
|
|
+ ) {
|
|
|
+ return notebook.widgets[cellN];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // else no parent header found.
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Finds the "parent" heading of the given cell and expands.
|
|
|
+ * Used for the case that a cell becomes active that is within a collapsed heading.
|
|
|
+ * @param cell - "Child" cell that has become the active cell
|
|
|
+ * @param notebook - The target notebook widget.
|
|
|
+ */
|
|
|
+ export function expandParent(cell: Cell, notebook: Notebook): void {
|
|
|
+ let nearestParentCell = findNearestParentHeader(cell, notebook);
|
|
|
+ if (!nearestParentCell) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (
|
|
|
+ !getHeadingInfo(nearestParentCell).collapsed &&
|
|
|
+ !nearestParentCell.isHidden
|
|
|
+ ) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (nearestParentCell.isHidden) {
|
|
|
+ expandParent(nearestParentCell, notebook);
|
|
|
+ }
|
|
|
+ if (getHeadingInfo(nearestParentCell).collapsed) {
|
|
|
+ setHeadingCollapse(nearestParentCell, false, notebook);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Set the given cell and ** all "child" cells **
|
|
|
+ * to the given collapse / expand if cell is
|
|
|
+ * a markdown header.
|
|
|
+ *
|
|
|
+ * @param cell - The cell
|
|
|
+ * @param collapsing - Whether to collapse or expand the cell
|
|
|
+ * @param notebook - The target notebook widget.
|
|
|
+ */
|
|
|
+ export function setHeadingCollapse(
|
|
|
+ cell: Cell,
|
|
|
+ collapsing: boolean,
|
|
|
+ notebook: Notebook
|
|
|
+ ): number {
|
|
|
+ const which = findIndex(
|
|
|
+ notebook.widgets,
|
|
|
+ (possibleCell: Cell, index: number) => {
|
|
|
+ return cell.model.id === possibleCell.model.id;
|
|
|
+ }
|
|
|
+ );
|
|
|
+ if (which === -1) {
|
|
|
+ return -1;
|
|
|
+ }
|
|
|
+ if (!notebook.widgets.length) {
|
|
|
+ return which + 1;
|
|
|
+ }
|
|
|
+ let selectedheadingInfo = NotebookActions.getHeadingInfo(cell);
|
|
|
+ if (
|
|
|
+ cell.isHidden ||
|
|
|
+ !(cell instanceof MarkdownCell) ||
|
|
|
+ !selectedheadingInfo.isHeading
|
|
|
+ ) {
|
|
|
+ // otherwise collapsing and uncollapsing already hidden stuff can
|
|
|
+ // cause some funny looking bugs.
|
|
|
+ return which + 1;
|
|
|
+ }
|
|
|
+ let localCollapsed = false;
|
|
|
+ let localCollapsedLevel = 0;
|
|
|
+ // iterate through all cells after the active cell.
|
|
|
+ let cellNum;
|
|
|
+ for (cellNum = which + 1; cellNum < notebook.widgets.length; cellNum++) {
|
|
|
+ let subCell = notebook.widgets[cellNum];
|
|
|
+ let subCellheadingInfo = NotebookActions.getHeadingInfo(subCell);
|
|
|
+ if (
|
|
|
+ subCellheadingInfo.isHeading &&
|
|
|
+ subCellheadingInfo.headingLevel <= selectedheadingInfo.headingLevel
|
|
|
+ ) {
|
|
|
+ // then reached an equivalent or higher heading level than the
|
|
|
+ // original the end of the collapse.
|
|
|
+ cellNum -= 1;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ if (
|
|
|
+ localCollapsed &&
|
|
|
+ subCellheadingInfo.isHeading &&
|
|
|
+ subCellheadingInfo.headingLevel <= localCollapsedLevel
|
|
|
+ ) {
|
|
|
+ // then reached the end of the local collapsed, so unset NotebookActions.
|
|
|
+ localCollapsed = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (collapsing || localCollapsed) {
|
|
|
+ // then no extra handling is needed for further locally collapsed
|
|
|
+ // headings.
|
|
|
+ subCell.setHidden(true);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (subCellheadingInfo.collapsed && subCellheadingInfo.isHeading) {
|
|
|
+ localCollapsed = true;
|
|
|
+ localCollapsedLevel = subCellheadingInfo.headingLevel;
|
|
|
+ // but don't collapse the locally collapsed heading, so continue to
|
|
|
+ // expand the heading. This will get noticed in the next round.
|
|
|
+ }
|
|
|
+ subCell.setHidden(false);
|
|
|
+ }
|
|
|
+ if (cellNum === notebook.widgets.length) {
|
|
|
+ cell.numberChildNodes = cellNum - which - 1;
|
|
|
+ } else {
|
|
|
+ cell.numberChildNodes = cellNum - which;
|
|
|
+ }
|
|
|
+ NotebookActions.setCellCollapse(cell, collapsing);
|
|
|
+ return cellNum + 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Toggles the collapse state of the active cell of the given notebook
|
|
|
+ * and ** all of its "child" cells ** if the cell is a heading.
|
|
|
+ *
|
|
|
+ * @param notebook - The target notebook widget.
|
|
|
+ */
|
|
|
+ export function toggleCurrentHeadingCollapse(notebook: Notebook): any {
|
|
|
+ if (!notebook.activeCell || notebook.activeCellIndex === undefined) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ let headingInfo = NotebookActions.getHeadingInfo(notebook.activeCell);
|
|
|
+ if (headingInfo.isHeading) {
|
|
|
+ // Then toggle!
|
|
|
+ NotebookActions.setHeadingCollapse(
|
|
|
+ notebook.activeCell,
|
|
|
+ !headingInfo.collapsed,
|
|
|
+ notebook
|
|
|
+ );
|
|
|
+ }
|
|
|
+ ElementExt.scrollIntoViewIfNeeded(notebook.node, notebook.activeCell.node);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * If cell is a markdown heading, sets the headingCollapsed field,
|
|
|
+ * and otherwise hides the cell.
|
|
|
+ *
|
|
|
+ * @param cell - The cell to collapse / expand
|
|
|
+ * @param collapsing - Whether to collapse or expand the given cell
|
|
|
+ */
|
|
|
+ export function setCellCollapse(cell: Cell, collapsing: boolean): any {
|
|
|
+ if (cell instanceof MarkdownCell) {
|
|
|
+ cell.headingCollapsed = collapsing;
|
|
|
+ } else {
|
|
|
+ cell.setHidden(collapsing);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * If given cell is a markdown heading, returns the heading level.
|
|
|
+ * If given cell is not markdown, returns 7 (there are only 6 levels of markdown headings)
|
|
|
+ *
|
|
|
+ * @param cell - The target cell widget.
|
|
|
+ */
|
|
|
+ export function getHeadingInfo(
|
|
|
+ cell: Cell
|
|
|
+ ): { isHeading: boolean; headingLevel: number; collapsed?: boolean } {
|
|
|
+ if (!(cell instanceof MarkdownCell)) {
|
|
|
+ return { isHeading: false, headingLevel: 7 };
|
|
|
+ }
|
|
|
+ let level = cell.headingInfo.level;
|
|
|
+ let collapsed = cell.headingCollapsed;
|
|
|
+ return { isHeading: level > 0, headingLevel: level, collapsed: collapsed };
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Trust the notebook after prompting the user.
|
|
|
*
|