Skip to content

Commit

Permalink
Merge pull request #3981 from saulshanabrook/persist-collapsed
Browse files Browse the repository at this point in the history
Persist code cell collapsed
  • Loading branch information
ian-r-rose committed May 28, 2018
2 parents e423106 + 6ce4241 commit 3202ec5
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 4 deletions.
37 changes: 35 additions & 2 deletions packages/cells/src/widget.ts
Expand Up @@ -211,6 +211,17 @@ class Cell extends Widget {
this.editor.setOption(key, options.editorConfig[key]);
});
}

}

/**
* Modify some state for initialization.
*
* Should be called at the end of the subclasses's constructor.
*/
protected initializeState() {
const jupyter = this.model.metadata.get('jupyter') || {} as any;
this.inputHidden = jupyter.source_hidden === true;
}

/**
Expand Down Expand Up @@ -572,7 +583,7 @@ class CodeCell extends Cell {
});

// Modify state
this.setPrompt(`${model.executionCount || ''}`);
this.initializeState();
model.stateChanged.connect(this.onStateChanged, this);
model.metadata.changed.connect(this.onMetadataChanged, this);
}
Expand All @@ -582,6 +593,25 @@ class CodeCell extends Cell {
*/
readonly model: ICodeCellModel;

/**
* Modify some state for initialization.
*
* Should be called at the end of the subclasses's constructor.
*/
protected initializeState() {
super.initializeState();

const metadataScrolled = this.model.metadata.get('scrolled');
this.outputsScrolled = metadataScrolled === true;

const jupyter = this.model.metadata.get('jupyter') || {} as any;
const collapsed = this.model.metadata.get('collapsed');
this.outputHidden = collapsed === true || jupyter.outputs_hidden === true;

this.setPrompt(`${this.model.executionCount || ''}`);
}


/**
* Get the output area for the cell.
*/
Expand Down Expand Up @@ -735,7 +765,7 @@ class CodeCell extends Cell {

private _rendermime: RenderMimeRegistry = null;
private _outputHidden = false;
private _outputsScrolled = false;
private _outputsScrolled: boolean;
private _outputWrapper: Widget = null;
private _outputCollapser: OutputCollapser = null;
private _outputPlaceholder: OutputPlaceholder = null;
Expand Down Expand Up @@ -832,6 +862,8 @@ class MarkdownCell extends Cell {
this._updateRenderedInput().then(() => {
this._ready.resolve(void 0);
});

super.initializeState();
}

/**
Expand Down Expand Up @@ -977,6 +1009,7 @@ class RawCell extends Cell {
constructor(options: Cell.IOptions) {
super(options);
this.addClass(RAW_CELL_CLASS);
super.initializeState();
}

/**
Expand Down
19 changes: 19 additions & 0 deletions packages/mainmenu-extension/src/index.ts
Expand Up @@ -52,6 +52,9 @@ namespace CommandIDs {
export
const closeAndCleanup = 'filemenu:close-and-cleanup';

export
const persistAndSave = 'filemenu:persist-and-save';

export
const createConsole = 'filemenu:create-console';

Expand Down Expand Up @@ -240,6 +243,21 @@ function createFileMenu(app: JupyterLab, menu: FileMenu): void {
Private.delegateExecute(app, menu.closeAndCleaners, 'closeAndCleanup')
});

// Add a delegator command for persisting data then saving.
commands.addCommand(CommandIDs.persistAndSave, {
label: () => {
const action =
Private.delegateLabel(app, menu.persistAndSavers, 'action');
const name =
Private.delegateLabel(app, menu.persistAndSavers, 'name');
return `Save ${name} ${action || 'with Extras'}`;
},
isEnabled:
Private.delegateEnabled(app, menu.persistAndSavers, 'persistAndSave'),
execute:
Private.delegateExecute(app, menu.persistAndSavers, 'persistAndSave')
});

// Add a delegator command for creating a console for an activity.
commands.addCommand(CommandIDs.createConsole, {
label: () => {
Expand Down Expand Up @@ -272,6 +290,7 @@ function createFileMenu(app: JupyterLab, menu: FileMenu): void {
// Add save group.
const saveGroup = [
'docmanager:save',
'filemenu:persist-and-save',
'docmanager:save-as',
'docmanager:save-all'
].map(command => { return { command }; });
Expand Down
36 changes: 36 additions & 0 deletions packages/mainmenu/src/file.ts
Expand Up @@ -24,6 +24,12 @@ interface IFileMenu extends IJupyterLabMenu {
*/
readonly closeAndCleaners: Set<IFileMenu.ICloseAndCleaner<Widget>>;

/**
* The persist and save extension point.
*/
readonly persistAndSavers: Set<IFileMenu.IPersistAndSave<Widget>>;


/**
* A set storing IConsoleCreators for the File menu.
*/
Expand All @@ -45,6 +51,8 @@ class FileMenu extends JupyterLabMenu implements IFileMenu {
this.newMenu.menu.title.label = 'New';
this.closeAndCleaners =
new Set<IFileMenu.ICloseAndCleaner<Widget>>();
this.persistAndSavers =
new Set<IFileMenu.IPersistAndSave<Widget>>();
this.consoleCreators =
new Set<IFileMenu.IConsoleCreator<Widget>>();
}
Expand All @@ -59,6 +67,12 @@ class FileMenu extends JupyterLabMenu implements IFileMenu {
*/
readonly closeAndCleaners: Set<IFileMenu.ICloseAndCleaner<Widget>>;


/**
* The persist and save extension point.
*/
readonly persistAndSavers: Set<IFileMenu.IPersistAndSave<Widget>>;

/**
* A set storing IConsoleCreators for the Kernel menu.
*/
Expand Down Expand Up @@ -102,6 +116,28 @@ namespace IFileMenu {
closeAndCleanup: (widget: T) => Promise<void>;
}

/**
* Interface for an activity that has some persistance action
* before saving.
*/
export
interface IPersistAndSave<T extends Widget> extends IMenuExtender<T> {
/**
* A label to use for the activity that is being saved.
*/
name: string;

/**
* A label to describe what is being persisted before saving.
*/
action: string;

/**
* A function to perform the persistance.
*/
persistAndSave: (widget: T) => Promise<void>;
}

/**
* Interface for a command to create a console for an activity.
*/
Expand Down
29 changes: 28 additions & 1 deletion packages/notebook-extension/src/index.ts
Expand Up @@ -249,6 +249,9 @@ namespace CommandIDs {

export
const disableOutputScrolling = 'notebook:disable-output-scrolling';

export
const saveWithView = 'notebook:save-with-view';
}


Expand Down Expand Up @@ -1493,6 +1496,18 @@ function addCommands(app: JupyterLab, services: ServiceManager, tracker: Noteboo
},
isEnabled
});
commands.addCommand(CommandIDs.saveWithView, {
label: 'Save Notebook with View State',
execute: args => {
const current = getCurrent(args);

if (current) {
NotebookActions.persistViewState(current.content);
app.commands.execute('docmanager:save');
}
},
isEnabled
});
}


Expand All @@ -1519,7 +1534,8 @@ function populatePalette(palette: ICommandPalette): void {
CommandIDs.reconnectToKernel,
CommandIDs.createConsole,
CommandIDs.closeAndShutdown,
CommandIDs.trust
CommandIDs.trust,
CommandIDs.saveWithView
].forEach(command => { palette.addItem({ command, category }); });

EXPORT_TO_FORMATS.forEach(exportToFormat => {
Expand Down Expand Up @@ -1623,6 +1639,17 @@ function populateMenus(app: JupyterLab, mainMenu: IMainMenu, tracker: INotebookT
}
} as IFileMenu.ICloseAndCleaner<NotebookPanel>);

// Add a save with view command to the file menu.
mainMenu.fileMenu.persistAndSavers.add({
tracker,
action: 'with View State',
name: 'Notebook',
persistAndSave: (current: NotebookPanel) => {
NotebookActions.persistViewState(current.content);
return app.commands.execute('docmanager:save');
}
} as IFileMenu.IPersistAndSave<NotebookPanel>);

// Add a notebook group to the File menu.
let exportTo = new Menu({ commands } );
exportTo.title.label = 'Export Notebook As…';
Expand Down
52 changes: 52 additions & 0 deletions packages/notebook/src/actions.tsx
Expand Up @@ -1083,6 +1083,58 @@ namespace NotebookActions {
Private.handleState(widget, state);
}

/**
* Persists the collapsed state of all code cell outputs to the model.
*
* @param widget - The target notebook widget.
*/
export
function persistViewState(widget: Notebook): void {
if (!widget.model) {
return;
}
let state = Private.getState(widget);
let cells = widget.widgets;
each(cells, (cell: Cell) => {
const {model, inputHidden} = cell;
const metadata = model.metadata;
const jupyter = metadata.get('jupyter') as any || {};

if (inputHidden) {
jupyter.source_hidden = true;
} else {
delete jupyter.source_hidden;
}

if (cell.model.type === 'code') {
const {outputHidden, outputsScrolled} = (cell as CodeCell);

// set both metadata keys
// https://github.com/jupyterlab/jupyterlab/pull/3981#issuecomment-391139167
if (outputHidden) {
model.metadata.set('collapsed', true);
jupyter.outputs_hidden = true;
} else {
model.metadata.delete('collapsed');
delete jupyter.outputs_hidden;
}

if (outputsScrolled) {
model.metadata.set('scrolled', true);
} else {
model.metadata.delete('scrolled');
}
}

if (Object.keys(jupyter).length === 0) {
metadata.delete('jupyter');
} else {
metadata.set('jupyter', jupyter);
}
});
Private.handleState(widget, state);
}


/**
* Set the markdown header level.
Expand Down
2 changes: 1 addition & 1 deletion packages/notebook/src/celltools.ts
Expand Up @@ -355,7 +355,7 @@ namespace CellTools {
this._cellModel = null;
return;
}
let promptNode = activeCell.promptNode.cloneNode(true) as HTMLElement;
let promptNode = activeCell.promptNode ? activeCell.promptNode.cloneNode(true) as HTMLElement : null;
let prompt = new Widget({ node: promptNode });
let factory = activeCell.contentFactory.editorFactory;

Expand Down
9 changes: 9 additions & 0 deletions packages/shortcuts-extension/schema/plugin.json
Expand Up @@ -157,6 +157,15 @@
},
"type": "object"
},
"filemenu:persist-and-save": {
"default": { },
"properties": {
"command": { "default": "filemenu:persist-and-save" },
"keys": { "default": ["Ctrl Shift S"] },
"selector": { "default": "body" }
},
"type": "object"
},
"help:toggle": {
"default": { },
"properties": {
Expand Down
38 changes: 38 additions & 0 deletions tests/test-cells/src/widget.spec.ts
Expand Up @@ -386,6 +386,22 @@ describe('cells/widget', () => {

describe('#outputCollapsed', () => {

it('should initialize from the model', () => {
const collapsedModel = new CodeCellModel({});
let widget = new CodeCell({ model: collapsedModel, rendermime });
expect(widget.outputHidden).to.be(false);

collapsedModel.metadata.set('collapsed', true);
collapsedModel.metadata.set('jupyter', {outputs_hidden: false});
widget = new CodeCell({ model: collapsedModel, rendermime });
expect(widget.outputHidden).to.be(true);

collapsedModel.metadata.set('collapsed', false);
collapsedModel.metadata.set('jupyter', {outputs_hidden: true});
widget = new CodeCell({ model: collapsedModel, rendermime });
expect(widget.outputHidden).to.be(true);
});

it('should be the view state of the output being collapsed', () => {
let widget = new CodeCell({ model, rendermime });
expect(widget.outputHidden).to.be(false);
Expand All @@ -395,6 +411,28 @@ describe('cells/widget', () => {

});

describe('#outputsScrolled', () => {

it('should initialize from the model', () => {
const collapsedModel = new CodeCellModel({});
let widget = new CodeCell({ model: collapsedModel, rendermime });
expect(widget.outputsScrolled).to.be(false);

collapsedModel.metadata.set('scrolled', false);
widget = new CodeCell({ model: collapsedModel, rendermime });
expect(widget.outputsScrolled).to.be(false);

collapsedModel.metadata.set('scrolled', 'auto');
widget = new CodeCell({ model: collapsedModel, rendermime });
expect(widget.outputsScrolled).to.be(false);

collapsedModel.metadata.set('scrolled', true);
widget = new CodeCell({ model: collapsedModel, rendermime });
expect(widget.outputsScrolled).to.be(true);
});

});

describe('#dispose()', () => {

it('should dispose of the resources held by the widget', () => {
Expand Down

0 comments on commit 3202ec5

Please sign in to comment.