Merge pull request #266 from takluyver/multicell-selection

Multi-cell selection
Min RK 11 years ago
commit d2b3aa314d

@ -100,6 +100,35 @@ define(function(require){
}
}
},
'extend-selection-previous' : {
help: 'extend selection above',
help_index : 'dc',
handler : function (env) {
var index = env.notebook.get_selected_index();
if (index !== 0 && index !== null) {
env.notebook.extend_selection('up');
env.notebook.focus_cell();
}
}
},
'extend-selection-next' : {
help: 'extend selection below',
help_index : 'dd',
handler : function (env) {
var index = env.notebook.get_selected_index();
if (index !== (env.notebook.ncells()-1) && index !== null) {
env.notebook.extend_selection('down');
env.notebook.focus_cell();
}
}
},
'reset-selection': {
help: 'clear selected cells',
help_index: 'de',
handler: function(env) {
env.notebook.reset_selection();
}
},
'cut-selected-cell' : {
icon: 'fa-cut',
help_index : 'ee',
@ -288,6 +317,13 @@ define(function(require){
env.notebook.merge_cell_below();
}
},
'merge-selected-cells' : {
help : 'merge selected cells',
help_index: 'el',
handler: function(env) {
env.notebook.merge_selected_cells();
}
},
'close-pager' : {
help_index : 'gd',
handler : function (env) {

@ -55,6 +55,8 @@ define([
this.placeholder = config.placeholder || '';
this.selected = false;
this.in_selection = false;
this.selection_anchor = false;
this.rendered = false;
this.mode = 'command';
@ -142,7 +144,7 @@ define([
* Call after this.element exists to initialize the css classes
* related to selected, rendered and mode.
*/
if (this.selected) {
if (this.in_selection) {
this.element.addClass('selected');
} else {
this.element.addClass('unselected');
@ -254,6 +256,7 @@ define([
this.element.addClass('selected');
this.element.removeClass('unselected');
this.selected = true;
this.in_selection = true;
return true;
} else {
return false;
@ -261,19 +264,21 @@ define([
};
/**
* handle cell level logic when a cell is unselected
* handle cell level logic when the cursor moves away from a cell
* @method unselect
* @param {bool} leave_selected - true to move cursor away and extend selection
* @return is the action being taken
*/
Cell.prototype.unselect = function () {
if (this.selected) {
Cell.prototype.unselect = function (leave_selected) {
var was_selected_cell = this.selected;
this.selected = false;
if ((!leave_selected) && this.in_selection) {
this.in_selection = false;
this.selection_anchor = false;
this.element.addClass('unselected');
this.element.removeClass('selected');
this.selected = false;
return true;
} else {
return false;
}
return was_selected_cell;
};
/**

@ -535,14 +535,14 @@ define([
};
/**
* handle cell level logic when a cell is unselected
* handle cell level logic when the cursor moves away from a cell
* @method unselect
* @return is the action being taken
*/
CodeCell.prototype.unselect = function () {
var cont = Cell.prototype.unselect.apply(this);
CodeCell.prototype.unselect = function (leave_selected) {
var cont = Cell.prototype.unselect.apply(this, [leave_selected]);
if (cont) {
// When a code cell is usnelected, make sure that the corresponding
// When a code cell is unselected, make sure that the corresponding
// tooltip and completer to that cell is closed.
this.tooltip.remove_and_cancel_tooltip(true);
if (this.completer !== null) {

@ -86,7 +86,7 @@ define([
return {
'shift-space': 'ipython.scroll-up',
'shift-v' : 'ipython.paste-cell-before',
'shift-m' : 'ipython.merge-selected-cell-with-cell-after',
'shift-m' : 'ipython.merge-selected-cells',
'shift-o' : 'ipython.toggle-output-scrolling-selected-cell',
'enter' : 'ipython.enter-edit-mode',
'space' : 'ipython.scroll-down',
@ -98,6 +98,8 @@ define([
'up' : 'ipython.select-previous-cell',
'k' : 'ipython.select-previous-cell',
'j' : 'ipython.select-next-cell',
'shift-k': 'ipython.extend-selection-previous',
'shift-j': 'ipython.extend-selection-next',
'x' : 'ipython.cut-selected-cell',
'c' : 'ipython.copy-selected-cell',
'v' : 'ipython.paste-cell-after',

@ -600,6 +600,46 @@ define(function (require) {
return result;
};
/**
* Get the index of the anchor cell for range selection
*
* @return {integer} The anchor cell's numeric index
*/
Notebook.prototype.get_selection_anchor = function() {
var result = null;
this.get_cell_elements().filter(function (index) {
if ($(this).data("cell").selection_anchor === true) {
result = index;
}
});
return result;
};
/**
* Get an array of the cells in the currently selected range
*
* @return {Array} The selected cells
*/
Notebook.prototype.get_selected_cells = function () {
return this.get_cells().filter(function(cell) {
return cell.in_selection;
});
};
/**
* Get the indices of the currently selected range of cells.
*
* @return {Array} The selected cells' numeric indices
*/
Notebook.prototype.get_selected_indices = function () {
var result = [];
this.get_cell_elements().filter(function (index) {
if ($(this).data("cell").in_selection === true) {
result.push(index);
}
});
return result;
};
// Cell selection.
@ -618,23 +658,27 @@ define(function (require) {
if (this.mode !== 'command') {
this.command_mode();
}
this.get_cell(sindex).unselect();
}
var cell = this.get_cell(index);
cell.select();
if (cell.cell_type === 'heading') {
this.events.trigger('selected_cell_type_changed.Notebook',
{'cell_type':cell.cell_type,level:cell.level}
);
} else {
this.events.trigger('selected_cell_type_changed.Notebook',
{'cell_type':cell.cell_type}
);
var current_selection = this.get_selected_cells();
for (var i=0; i<current_selection.length; i++) {
current_selection[i].unselect()
}
var cell = this._select(index);
cell.selection_anchor = true
}
return this;
};
Notebook.prototype._select = function(index) {
var cell = this.get_cell(index);
cell.select();
this.events.trigger('selected_cell_type_changed.Notebook',
{'cell_type':cell.cell_type}
);
return cell;
};
/**
* Programmatically select the next cell.
*
@ -657,6 +701,42 @@ define(function (require) {
return this;
};
/**
* Extend the selected range
*
* @param {string} direction - 'up' or 'down
*/
Notebook.prototype.extend_selection = function(direction) {
var anchor_ix = this.get_selection_anchor();
var cursor_ix = this.get_selected_index();
var range_direction = (cursor_ix > anchor_ix) ? 'down' : 'up';
var contracting = (cursor_ix !== anchor_ix) &&
(direction !== range_direction);
var ix_delta = (direction === 'up') ? -1 : 1;
var new_ix = cursor_ix + ix_delta;
if (new_ix < 0 || new_ix >= this.ncells()) {
return false;
}
if (this.mode !== 'command') {
this.command_mode();
}
this.get_cell(cursor_ix).unselect(!contracting);
this._select(new_ix);
return true;
};
/**
* Clear selection of multiple cells (except the cell at the cursor)
*/
Notebook.prototype.reset_selection = function() {
var current_selection = this.get_selected_cells();
for (var i=0; i<current_selection.length; i++) {
if (!current_selection[i].selected) {
current_selection[i].unselect()
}
}
};
// Edit/Command mode
@ -710,6 +790,7 @@ define(function (require) {
if (cell && this.mode !== 'edit') {
cell.edit_mode();
this.mode = 'edit';
this.reset_selection();
this.events.trigger('edit_mode.Notebook');
this.keyboard_manager.edit_mode();
}
@ -1290,36 +1371,71 @@ define(function (require) {
};
/**
* Merge the selected cell into the cell above it.
* Merge a series of cells into one
*
* @param {Array} indices - the numeric indices of the cells to be merged
* @param {bool} into_last - merge into the last cell instead of the first
*/
Notebook.prototype.merge_cell_above = function () {
var index = this.get_selected_index();
var cell = this.get_cell(index);
var render = cell.rendered;
if (!cell.is_mergeable()) {
Notebook.prototype.merge_cells = function(indices, into_last) {
if (indices.length <= 1) {
return;
}
if (index > 0) {
var upper_cell = this.get_cell(index-1);
if (!upper_cell.is_mergeable()) {
for (var i=0; i < indices.length; i++) {
if (!this.get_cell(indices[i]).is_mergeable()) {
return;
}
var upper_text = upper_cell.get_text();
var text = cell.get_text();
if (cell instanceof codecell.CodeCell) {
cell.set_text(upper_text+'\n'+text);
} else {
cell.unrender(); // Must unrender before we set_text.
cell.set_text(upper_text+'\n\n'+text);
if (render) {
// The rendered state of the final cell should match
// that of the original selected cell;
cell.render();
}
}
var target = this.get_cell(into_last ? indices.pop() : indices.shift());
// Get all the cells' contents
var contents = [];
for (i=0; i < indices.length; i++) {
contents.push(this.get_cell(indices[i]).get_text());
}
if (into_last) {
contents.push(target.get_text())
} else {
contents.unshift(target.get_text())
}
// Update the contents of the target cell
if (target instanceof codecell.CodeCell) {
target.set_text(contents.join('\n\n'))
} else {
var was_rendered = target.rendered;
target.unrender(); // Must unrender before we set_text.
target.set_text(contents.join('\n\n'));
if (was_rendered) {
// The rendered state of the final cell should match
// that of the original selected cell;
target.render();
}
this.delete_cell(index-1);
this.select(this.find_cell_index(cell));
}
// Delete the other cells
// If we started deleting cells from the top, the later indices would
// get offset. We sort them into descending order to avoid that.
indices.sort(function(a, b) {return b-a;});
for (i=0; i < indices.length; i++) {
this.delete_cell(indices[i]);
}
this.select(this.find_cell_index(target));
};
/**
* Merge the selected range of cells
*/
Notebook.prototype.merge_selected_cells = function() {
this.merge_cells(this.get_selected_indices());
};
/**
* Merge the selected cell into the cell above it.
*/
Notebook.prototype.merge_cell_above = function () {
var index = this.get_selected_index();
this.merge_cells([index-1, index], true)
};
/**
@ -1327,32 +1443,7 @@ define(function (require) {
*/
Notebook.prototype.merge_cell_below = function () {
var index = this.get_selected_index();
var cell = this.get_cell(index);
var render = cell.rendered;
if (!cell.is_mergeable()) {
return;
}
if (index < this.ncells()-1) {
var lower_cell = this.get_cell(index+1);
if (!lower_cell.is_mergeable()) {
return;
}
var lower_text = lower_cell.get_text();
var text = cell.get_text();
if (cell instanceof codecell.CodeCell) {
cell.set_text(text+'\n'+lower_text);
} else {
cell.unrender(); // Must unrender before we set_text.
cell.set_text(text+'\n\n'+lower_text);
if (render) {
// The rendered state of the final cell should match
// that of the original selected cell;
cell.render();
}
}
this.delete_cell(index+1);
this.select(this.find_cell_index(cell));
}
this.merge_cells([index, index+1], false)
};

@ -1,7 +1,7 @@
// Test
casper.notebook_test(function () {
var a = 'ab\ncd';
var a = 'ab\n\ncd';
var b = 'print("b")';
var c = 'print("c")';
@ -41,6 +41,7 @@ casper.notebook_test(function () {
this.test.assertEquals(this.get_cell_text(1), 'cd', 'split; Verify that cell 1 has the second half.');
this.validate_notebook_state('split', 'edit', 1);
this.select_cell(0); // Move up to cell 0
this.evaluate(function() { IPython.notebook.extend_selection('down');});
this.trigger_keydown('shift-m'); // Merge
this.validate_notebook_state('merge', 'command', 0);
this.test.assertEquals(this.get_cell_text(0), a, 'merge; Verify that cell 0 has the merged contents.');

@ -36,8 +36,8 @@ casper.notebook_test(function() {
return IPython.notebook.get_selected_cell().get_text();
});
this.test.assertEquals(output_above, 'a = 5\nprint(a)',
this.test.assertEquals(output_above, 'a = 5\n\nprint(a)',
'Successful merge_cell_above().');
this.test.assertEquals(output_below, 'a = 5\nprint(a)',
this.test.assertEquals(output_below, 'a = 5\n\nprint(a)',
'Successful merge_cell_below().');
});

Loading…
Cancel
Save