You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

282 lines
9.6 KiB

// Allow codefolding in code cells
//
// This extension enables the CodeMirror feature
// It works by adding a gutter area to each code cell.
// Fold-able code is marked using small triangles in the gutter.
//
// The current folding state is saved in the cell metadata as an array
// of line numbers.
// Format: cell.metadata.code_folding = [ line1, line2, line3, ...]
//
define([
'base/js/namespace',
'jquery',
'require',
'base/js/events',
'services/config',
'notebook/js/codecell',
'codemirror/lib/codemirror',
'codemirror/addon/fold/foldcode',
'codemirror/addon/fold/foldgutter',
'codemirror/addon/fold/brace-fold',
'codemirror/addon/fold/indent-fold'
], function (Jupyter, $, requirejs, events, configmod, codecell, CodeMirror) {
"use strict";
// define default config parameter values
var params = {
codefolding_hotkey : 'Alt-f',
init_delay : 1000
};
// updates default params with any specified in the provided config data
var update_params = function (config_data) {
for (var key in params) {
if (config_data.hasOwnProperty(key)) {
params[key] = config_data[key];
}
}
};
var on_config_loaded = function () {
if (Jupyter.notebook !== undefined) {
// register actions with ActionHandler instance
var prefix = 'auto';
var name = 'toggle-codefolding';
var action = {
icon: 'fa-comment-o',
help : 'Toggle codefolding',
help_index : 'ec',
id : 'toggle_codefolding',
handler : toggleFolding
};
var action_full_name = Jupyter.keyboard_manager.actions.register(action, name, prefix);
// define keyboard shortcuts
var edit_mode_shortcuts = {};
edit_mode_shortcuts[params.codefolding_hotkey] = action_full_name;
// register keyboard shortcuts with keyboard_manager
Jupyter.notebook.keyboard_manager.edit_shortcuts.add_shortcuts(edit_mode_shortcuts);
Jupyter.notebook.keyboard_manager.command_shortcuts.add_shortcuts(edit_mode_shortcuts);
}
else {
// we're in edit view
var extraKeys = Jupyter.editor.codemirror.getOption('extraKeys');
extraKeys[params.codefolding_hotkey] = toggleFolding;
CodeMirror.normalizeKeyMap(extraKeys);
console.log('[codefolding] binding hotkey', params.codefolding_hotkey);
Jupyter.editor.codemirror.setOption('extraKeys', extraKeys);
}
};
/*
* Toggle folding on/off at current line
*
* @param cm CodeMirror instance
*
*/
function toggleFolding () {
var cm;
var pos = {line: 0, ch: 0, xRel: 0};
if (Jupyter.notebook !== undefined) {
cm = Jupyter.notebook.get_selected_cell().code_mirror;
if (Jupyter.notebook.mode === 'edit') {
pos = cm.getCursor();
}
}
else {
cm = Jupyter.editor.codemirror;
pos = cm.getCursor();
}
var opts = cm.state.foldGutter.options;
cm.foldCode(pos, opts.rangeFinder);
}
/**
* Update cell metadata with folding info, so folding state can be restored after reloading notebook
*
* @param cm CodeMirror instance
*/
function updateMetadata (cm) {
var list = cm.getAllMarks();
var lines = [];
for (var i = 0; i < list.length; i++) {
if (list[i].__isFold) {
var range = list[i].find();
lines.push(range.from.line);
}
}
/* User can click on gutter of unselected cells, so make sure we store metadata in the correct cell */
var cell = Jupyter.notebook.get_selected_cell();
if (cell.code_mirror !== cm) {
var cells = Jupyter.notebook.get_cells();
var ncells = Jupyter.notebook.ncells();
for (var k = 0; k < ncells; k++) {
var _cell = cells[k];
if (_cell.code_mirror === cm ) { cell = _cell; break; }
}
}
cell.metadata.code_folding = lines;
}
/**
* Activate codefolding in CodeMirror options, don't overwrite other settings
*
* @param cm codemirror instance
*/
function activate_cm_folding (cm) {
var gutters = cm.getOption('gutters').slice();
if ( $.inArray("CodeMirror-foldgutter", gutters) < 0) {
gutters.push('CodeMirror-foldgutter');
cm.setOption('gutters', gutters);
}
/* set indent or brace folding */
var opts = true;
if (Jupyter.notebook) {
opts = {
rangeFinder: new CodeMirror.fold.combine(
CodeMirror.fold.firstline,
CodeMirror.fold.magic,
cm.getMode().fold === 'indent' ? CodeMirror.fold.indent : CodeMirror.fold.brace
)
};
}
cm.setOption('foldGutter', opts);
}
/**
* Restore folding status from metadata
* @param cell
*/
var restoreFolding = function (cell) {
if (cell.metadata.code_folding === undefined || !(cell instanceof codecell.CodeCell)) {
return;
}
// visit in reverse order, as otherwise nested folds un-fold outer ones
var lines = cell.metadata.code_folding.slice().sort();
for (var idx = lines.length - 1; idx >= 0; idx--) {
var line = lines[idx];
var opts = cell.code_mirror.state.foldGutter.options;
var linetext = cell.code_mirror.getLine(line);
if (linetext !== undefined) {
cell.code_mirror.foldCode(CodeMirror.Pos(line, 0), opts.rangeFinder);
}
else {
// the line doesn't exist, so we should remove it from metadata
cell.metadata.code_folding = lines.slice(0, idx);
}
cell.code_mirror.refresh();
}
};
/**
* Add codefolding gutter to a new cell
*
* @param event
* @param nbcell
*
*/
var createCell = function (event, nbcell) {
var cell = nbcell.cell;
if ((cell instanceof codecell.CodeCell)) {
activate_cm_folding(cell.code_mirror);
cell.code_mirror.on('fold', updateMetadata);
cell.code_mirror.on('unfold', updateMetadata);
// queue restoring folding, to run once metadata is set, hopefully.
// This can be useful if cells are un-deleted, for example.
setTimeout(function () { restoreFolding(cell); }, 500);
}
};
/*
* Initialize gutter in existing cells
*
*/
var initExistingCells = function () {
var cells = Jupyter.notebook.get_cells();
var ncells = Jupyter.notebook.ncells();
for (var i = 0; i < ncells; i++) {
var cell = cells[i];
if ((cell instanceof codecell.CodeCell)) {
activate_cm_folding(cell.code_mirror);
/* restore folding state if previously saved */
restoreFolding(cell);
cell.code_mirror.on('fold', updateMetadata);
cell.code_mirror.on('unfold', updateMetadata);
}
}
events.on('create.Cell', createCell);
};
/**
* Load my own CSS file
*
* @param name off CSS file
*
*/
var load_css = function (name) {
var link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
link.href = requirejs.toUrl(name, 'css');
document.getElementsByTagName("head")[0].appendChild(link);
};
/**
* Initialize extension
*
*/
var load_extension = function () {
// first, check which view we're in, in order to decide whether to load
var conf_sect;
if (Jupyter.notebook) {
// we're in notebook view
conf_sect = Jupyter.notebook.config;
}
else if (Jupyter.editor) {
// we're in file-editor view
conf_sect = new configmod.ConfigSection('notebook', {base_url: Jupyter.editor.base_url});
conf_sect.load();
}
else {
// we're some other view like dashboard, terminal, etc, so bail now
return;
}
load_css('codemirror/addon/fold/foldgutter.css');
/* change default gutter width */
load_css( './foldgutter.css');
conf_sect.loaded
.then(function () { update_params(conf_sect.data); })
.then(on_config_loaded);
if (Jupyter.notebook) {
/* require our additional custom codefolding modes before initialising fully */
requirejs(['./firstline-fold', './magic-fold'], function () {
if (Jupyter.notebook._fully_loaded) {
setTimeout(function () {
console.log('Codefolding: Wait for', params.init_delay, 'ms');
initExistingCells();
}, params.init_delay);
}
else {
events.one('notebook_loaded.Notebook', initExistingCells);
}
});
}
else {
activate_cm_folding(Jupyter.editor.codemirror);
setTimeout(function () {
console.log('Codefolding: Wait for', params.init_delay, 'ms');
Jupyter.editor.codemirror.refresh();
}, params.init_delay);
}
};
return {load_ipython_extension : load_extension};
});