##// END OF EJS Templates
Make shift-tab work as "indent-less" operation, too
Juergen Hasch -
Show More
@@ -1,657 +1,663
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3 /**
4 4 *
5 5 *
6 6 * @module codecell
7 7 * @namespace codecell
8 8 * @class CodeCell
9 9 */
10 10
11 11
12 12 define([
13 13 'base/js/namespace',
14 14 'jquery',
15 15 'base/js/utils',
16 16 'base/js/keyboard',
17 17 'services/config',
18 18 'notebook/js/cell',
19 19 'notebook/js/outputarea',
20 20 'notebook/js/completer',
21 21 'notebook/js/celltoolbar',
22 22 'codemirror/lib/codemirror',
23 23 'codemirror/mode/python/python',
24 24 'notebook/js/codemirror-ipython'
25 25 ], function(IPython,
26 26 $,
27 27 utils,
28 28 keyboard,
29 29 configmod,
30 30 cell,
31 31 outputarea,
32 32 completer,
33 33 celltoolbar,
34 34 CodeMirror,
35 35 cmpython,
36 36 cmip
37 37 ) {
38 38 "use strict";
39 39
40 40 var Cell = cell.Cell;
41 41
42 42 /* local util for codemirror */
43 43 var posEq = function(a, b) {return a.line === b.line && a.ch === b.ch;};
44 44
45 45 /**
46 46 *
47 47 * function to delete until previous non blanking space character
48 48 * or first multiple of 4 tabstop.
49 49 * @private
50 50 */
51 51 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
52 52 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
53 53 if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
54 54 var cur = cm.getCursor(), line = cm.getLine(cur.line);
55 55 var tabsize = cm.getOption('tabSize');
56 56 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
57 57 from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
58 58 var select = cm.getRange(from,cur);
59 59 if( select.match(/^\ +$/) !== null){
60 60 cm.replaceRange("",from,cur);
61 61 } else {
62 62 cm.deleteH(-1,"char");
63 63 }
64 64 };
65 65
66 66 var keycodes = keyboard.keycodes;
67 67
68 68 var CodeCell = function (kernel, options) {
69 69 /**
70 70 * Constructor
71 71 *
72 72 * A Cell conceived to write code.
73 73 *
74 74 * Parameters:
75 75 * kernel: Kernel instance
76 76 * The kernel doesn't have to be set at creation time, in that case
77 77 * it will be null and set_kernel has to be called later.
78 78 * options: dictionary
79 79 * Dictionary of keyword arguments.
80 80 * events: $(Events) instance
81 81 * config: dictionary
82 82 * keyboard_manager: KeyboardManager instance
83 83 * notebook: Notebook instance
84 84 * tooltip: Tooltip instance
85 85 */
86 86 this.kernel = kernel || null;
87 87 this.notebook = options.notebook;
88 88 this.collapsed = false;
89 89 this.events = options.events;
90 90 this.tooltip = options.tooltip;
91 91 this.config = options.config;
92 92 this.class_config = new configmod.ConfigWithDefaults(this.config,
93 93 CodeCell.config_defaults, 'CodeCell');
94 94
95 95 // create all attributed in constructor function
96 96 // even if null for V8 VM optimisation
97 97 this.input_prompt_number = null;
98 98 this.celltoolbar = null;
99 99 this.output_area = null;
100 100
101 101 this.last_msg_id = null;
102 102 this.completer = null;
103 103 this.widget_views = [];
104 104 this._widgets_live = true;
105 105
106 106 Cell.apply(this,[{
107 107 config: $.extend({}, CodeCell.options_default),
108 108 keyboard_manager: options.keyboard_manager,
109 109 events: this.events}]);
110 110
111 111 // Attributes we want to override in this subclass.
112 112 this.cell_type = "code";
113 113 var that = this;
114 114 this.element.focusout(
115 115 function() { that.auto_highlight(); }
116 116 );
117 117 };
118 118
119 119 CodeCell.options_default = {
120 120 cm_config : {
121 121 extraKeys: {
122 122 "Tab" : "indentMore",
123 123 "Shift-Tab" : "indentLess",
124 124 "Backspace" : "delSpaceToPrevTabStop",
125 125 "Cmd-/" : "toggleComment",
126 126 "Ctrl-/" : "toggleComment"
127 127 },
128 128 mode: 'ipython',
129 129 theme: 'ipython',
130 130 matchBrackets: true
131 131 }
132 132 };
133 133
134 134 CodeCell.config_defaults = {
135 135 highlight_modes : {
136 136 'magic_javascript' :{'reg':[/^%%javascript/]},
137 137 'magic_perl' :{'reg':[/^%%perl/]},
138 138 'magic_ruby' :{'reg':[/^%%ruby/]},
139 139 'magic_python' :{'reg':[/^%%python3?/]},
140 140 'magic_shell' :{'reg':[/^%%bash/]},
141 141 'magic_r' :{'reg':[/^%%R/]},
142 142 'magic_text/x-cython' :{'reg':[/^%%cython/]},
143 143 },
144 144 };
145 145
146 146 CodeCell.msg_cells = {};
147 147
148 148 CodeCell.prototype = Object.create(Cell.prototype);
149 149
150 150 /** @method create_element */
151 151 CodeCell.prototype.create_element = function () {
152 152 Cell.prototype.create_element.apply(this, arguments);
153 153 var that = this;
154 154
155 155 var cell = $('<div></div>').addClass('cell code_cell');
156 156 cell.attr('tabindex','2');
157 157
158 158 var input = $('<div></div>').addClass('input');
159 159 var prompt = $('<div/>').addClass('prompt input_prompt');
160 160 var inner_cell = $('<div/>').addClass('inner_cell');
161 161 this.celltoolbar = new celltoolbar.CellToolbar({
162 162 cell: this,
163 163 notebook: this.notebook});
164 164 inner_cell.append(this.celltoolbar.element);
165 165 var input_area = $('<div/>').addClass('input_area');
166 166 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
167 167 // In case of bugs that put the keyboard manager into an inconsistent state,
168 168 // ensure KM is enabled when CodeMirror is focused:
169 169 this.code_mirror.on('focus', function () {
170 170 if (that.keyboard_manager) {
171 171 that.keyboard_manager.enable();
172 172 }
173 173 });
174 174 this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this));
175 175 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
176 176 inner_cell.append(input_area);
177 177 input.append(prompt).append(inner_cell);
178 178
179 179 var widget_area = $('<div/>')
180 180 .addClass('widget-area')
181 181 .hide();
182 182 this.widget_area = widget_area;
183 183 var widget_prompt = $('<div/>')
184 184 .addClass('prompt')
185 185 .appendTo(widget_area);
186 186 var widget_subarea = $('<div/>')
187 187 .addClass('widget-subarea')
188 188 .appendTo(widget_area);
189 189 this.widget_subarea = widget_subarea;
190 190 var that = this;
191 191 var widget_clear_buton = $('<button />')
192 192 .addClass('close')
193 193 .html('&times;')
194 194 .click(function() {
195 195 widget_area.slideUp('', function(){
196 196 for (var i = 0; i < that.widget_views.length; i++) {
197 197 var view = that.widget_views[i];
198 198 view.remove();
199 199
200 200 // Remove widget live events.
201 201 view.off('comm:live', that._widget_live);
202 202 view.off('comm:dead', that._widget_dead);
203 203 }
204 204 that.widget_views = [];
205 205 widget_subarea.html('');
206 206 });
207 207 })
208 208 .appendTo(widget_prompt);
209 209
210 210 var output = $('<div></div>');
211 211 cell.append(input).append(widget_area).append(output);
212 212 this.element = cell;
213 213 this.output_area = new outputarea.OutputArea({
214 214 selector: output,
215 215 prompt_area: true,
216 216 events: this.events,
217 217 keyboard_manager: this.keyboard_manager});
218 218 this.completer = new completer.Completer(this, this.events);
219 219 };
220 220
221 221 /**
222 222 * Display a widget view in the cell.
223 223 */
224 224 CodeCell.prototype.display_widget_view = function(view_promise) {
225 225
226 226 // Display a dummy element
227 227 var dummy = $('<div/>');
228 228 this.widget_subarea.append(dummy);
229 229
230 230 // Display the view.
231 231 var that = this;
232 232 return view_promise.then(function(view) {
233 233 that.widget_area.show();
234 234 dummy.replaceWith(view.$el);
235 235 that.widget_views.push(view);
236 236
237 237 // Check the live state of the view's model.
238 238 if (view.model.comm_live) {
239 239 that._widget_live(view);
240 240 } else {
241 241 that._widget_dead(view);
242 242 }
243 243
244 244 // Listen to comm live events for the view.
245 245 view.on('comm:live', that._widget_live, that);
246 246 view.on('comm:dead', that._widget_dead, that);
247 247 return view;
248 248 });
249 249 };
250 250
251 251 /**
252 252 * Handles when a widget loses it's comm connection.
253 253 * @param {WidgetView} view
254 254 */
255 255 CodeCell.prototype._widget_dead = function(view) {
256 256 if (this._widgets_live) {
257 257 this._widgets_live = false;
258 258 this.widget_area.addClass('connection-problems');
259 259 }
260 260
261 261 };
262 262
263 263 /**
264 264 * Handles when a widget is connected to a live comm.
265 265 * @param {WidgetView} view
266 266 */
267 267 CodeCell.prototype._widget_live = function(view) {
268 268 if (!this._widgets_live) {
269 269 // Check that the other widgets are live too. O(N) operation.
270 270 // Abort the function at the first dead widget found.
271 271 for (var i = 0; i < this.widget_views.length; i++) {
272 272 if (!this.widget_views[i].model.comm_live) return;
273 273 }
274 274 this._widgets_live = true;
275 275 this.widget_area.removeClass('connection-problems');
276 276 }
277 277 };
278 278
279 279 /** @method bind_events */
280 280 CodeCell.prototype.bind_events = function () {
281 281 Cell.prototype.bind_events.apply(this);
282 282 var that = this;
283 283
284 284 this.element.focusout(
285 285 function() { that.auto_highlight(); }
286 286 );
287 287 };
288 288
289 289
290 290 /**
291 291 * This method gets called in CodeMirror's onKeyDown/onKeyPress
292 292 * handlers and is used to provide custom key handling. Its return
293 293 * value is used to determine if CodeMirror should ignore the event:
294 294 * true = ignore, false = don't ignore.
295 295 * @method handle_codemirror_keyevent
296 296 */
297 297
298 298 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
299 299
300 300 var that = this;
301 301 // whatever key is pressed, first, cancel the tooltip request before
302 302 // they are sent, and remove tooltip if any, except for tab again
303 303 var tooltip_closed = null;
304 304 if (event.type === 'keydown' && event.which !== keycodes.tab ) {
305 305 tooltip_closed = this.tooltip.remove_and_cancel_tooltip();
306 306 }
307 307
308 308 var cur = editor.getCursor();
309 309 if (event.keyCode === keycodes.enter){
310 310 this.auto_highlight();
311 311 }
312 312
313 313 if (event.which === keycodes.down && event.type === 'keypress' && this.tooltip.time_before_tooltip >= 0) {
314 314 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
315 315 // browser and keyboard layout !
316 316 // Pressing '(' , request tooltip, don't forget to reappend it
317 317 // The second argument says to hide the tooltip if the docstring
318 318 // is actually empty
319 319 this.tooltip.pending(that, true);
320 320 } else if ( tooltip_closed && event.which === keycodes.esc && event.type === 'keydown') {
321 321 // If tooltip is active, cancel it. The call to
322 322 // remove_and_cancel_tooltip above doesn't pass, force=true.
323 323 // Because of this it won't actually close the tooltip
324 324 // if it is in sticky mode. Thus, we have to check again if it is open
325 325 // and close it with force=true.
326 326 if (!this.tooltip._hidden) {
327 327 this.tooltip.remove_and_cancel_tooltip(true);
328 328 }
329 329 // If we closed the tooltip, don't let CM or the global handlers
330 330 // handle this event.
331 331 event.codemirrorIgnore = true;
332 332 event.preventDefault();
333 333 return true;
334 334 } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
335 335 if (editor.somethingSelected() || editor.getSelections().length !== 1){
336 336 var anchor = editor.getCursor("anchor");
337 337 var head = editor.getCursor("head");
338 338 if( anchor.line !== head.line){
339 339 return false;
340 340 }
341 341 }
342 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
343 if (pre_cursor.trim() === "") {
344 // Don't show tooltip if the part of the line before the cursor
345 // is empty. In this case, let CodeMirror handle indentation.
346 return false;
347 }
342 348 this.tooltip.request(that);
343 349 event.codemirrorIgnore = true;
344 350 event.preventDefault();
345 351 return true;
346 352 } else if (event.keyCode === keycodes.tab && event.type === 'keydown') {
347 353 // Tab completion.
348 354 this.tooltip.remove_and_cancel_tooltip();
349 355
350 356 // completion does not work on multicursor, it might be possible though in some cases
351 357 if (editor.somethingSelected() || editor.getSelections().length > 1) {
352 358 return false;
353 359 }
354 360 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
355 361 if (pre_cursor.trim() === "") {
356 362 // Don't autocomplete if the part of the line before the cursor
357 363 // is empty. In this case, let CodeMirror handle indentation.
358 364 return false;
359 365 } else {
360 366 event.codemirrorIgnore = true;
361 367 event.preventDefault();
362 368 this.completer.startCompletion();
363 369 return true;
364 370 }
365 371 }
366 372
367 373 // keyboard event wasn't one of those unique to code cells, let's see
368 374 // if it's one of the generic ones (i.e. check edit mode shortcuts)
369 375 return Cell.prototype.handle_codemirror_keyevent.apply(this, [editor, event]);
370 376 };
371 377
372 378 // Kernel related calls.
373 379
374 380 CodeCell.prototype.set_kernel = function (kernel) {
375 381 this.kernel = kernel;
376 382 };
377 383
378 384 /**
379 385 * Execute current code cell to the kernel
380 386 * @method execute
381 387 */
382 388 CodeCell.prototype.execute = function (stop_on_error) {
383 389 if (!this.kernel || !this.kernel.is_connected()) {
384 390 console.log("Can't execute, kernel is not connected.");
385 391 return;
386 392 }
387 393
388 394 this.output_area.clear_output(false, true);
389 395
390 396 if (stop_on_error === undefined) {
391 397 stop_on_error = true;
392 398 }
393 399
394 400 // Clear widget area
395 401 for (var i = 0; i < this.widget_views.length; i++) {
396 402 var view = this.widget_views[i];
397 403 view.remove();
398 404
399 405 // Remove widget live events.
400 406 view.off('comm:live', this._widget_live);
401 407 view.off('comm:dead', this._widget_dead);
402 408 }
403 409 this.widget_views = [];
404 410 this.widget_subarea.html('');
405 411 this.widget_subarea.height('');
406 412 this.widget_area.height('');
407 413 this.widget_area.hide();
408 414
409 415 var old_msg_id = this.last_msg_id;
410 416
411 417 if (old_msg_id) {
412 418 this.kernel.clear_callbacks_for_msg(old_msg_id);
413 419 if (old_msg_id) {
414 420 delete CodeCell.msg_cells[old_msg_id];
415 421 }
416 422 }
417 423 if (this.get_text().trim().length === 0) {
418 424 // nothing to do
419 425 this.set_input_prompt(null);
420 426 return;
421 427 }
422 428 this.set_input_prompt('*');
423 429 this.element.addClass("running");
424 430 var callbacks = this.get_callbacks();
425 431
426 432 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true,
427 433 stop_on_error : stop_on_error});
428 434 CodeCell.msg_cells[this.last_msg_id] = this;
429 435 this.render();
430 436 this.events.trigger('execute.CodeCell', {cell: this});
431 437 };
432 438
433 439 /**
434 440 * Construct the default callbacks for
435 441 * @method get_callbacks
436 442 */
437 443 CodeCell.prototype.get_callbacks = function () {
438 444 var that = this;
439 445 return {
440 446 shell : {
441 447 reply : $.proxy(this._handle_execute_reply, this),
442 448 payload : {
443 449 set_next_input : $.proxy(this._handle_set_next_input, this),
444 450 page : $.proxy(this._open_with_pager, this)
445 451 }
446 452 },
447 453 iopub : {
448 454 output : function() {
449 455 that.output_area.handle_output.apply(that.output_area, arguments);
450 456 },
451 457 clear_output : function() {
452 458 that.output_area.handle_clear_output.apply(that.output_area, arguments);
453 459 },
454 460 },
455 461 input : $.proxy(this._handle_input_request, this)
456 462 };
457 463 };
458 464
459 465 CodeCell.prototype._open_with_pager = function (payload) {
460 466 this.events.trigger('open_with_text.Pager', payload);
461 467 };
462 468
463 469 /**
464 470 * @method _handle_execute_reply
465 471 * @private
466 472 */
467 473 CodeCell.prototype._handle_execute_reply = function (msg) {
468 474 this.set_input_prompt(msg.content.execution_count);
469 475 this.element.removeClass("running");
470 476 this.events.trigger('set_dirty.Notebook', {value: true});
471 477 };
472 478
473 479 /**
474 480 * @method _handle_set_next_input
475 481 * @private
476 482 */
477 483 CodeCell.prototype._handle_set_next_input = function (payload) {
478 484 var data = {'cell': this, 'text': payload.text, replace: payload.replace};
479 485 this.events.trigger('set_next_input.Notebook', data);
480 486 };
481 487
482 488 /**
483 489 * @method _handle_input_request
484 490 * @private
485 491 */
486 492 CodeCell.prototype._handle_input_request = function (msg) {
487 493 this.output_area.append_raw_input(msg);
488 494 };
489 495
490 496
491 497 // Basic cell manipulation.
492 498
493 499 CodeCell.prototype.select = function () {
494 500 var cont = Cell.prototype.select.apply(this);
495 501 if (cont) {
496 502 this.code_mirror.refresh();
497 503 this.auto_highlight();
498 504 }
499 505 return cont;
500 506 };
501 507
502 508 CodeCell.prototype.render = function () {
503 509 var cont = Cell.prototype.render.apply(this);
504 510 // Always execute, even if we are already in the rendered state
505 511 return cont;
506 512 };
507 513
508 514 CodeCell.prototype.select_all = function () {
509 515 var start = {line: 0, ch: 0};
510 516 var nlines = this.code_mirror.lineCount();
511 517 var last_line = this.code_mirror.getLine(nlines-1);
512 518 var end = {line: nlines-1, ch: last_line.length};
513 519 this.code_mirror.setSelection(start, end);
514 520 };
515 521
516 522
517 523 CodeCell.prototype.collapse_output = function () {
518 524 this.output_area.collapse();
519 525 };
520 526
521 527
522 528 CodeCell.prototype.expand_output = function () {
523 529 this.output_area.expand();
524 530 this.output_area.unscroll_area();
525 531 };
526 532
527 533 CodeCell.prototype.scroll_output = function () {
528 534 this.output_area.expand();
529 535 this.output_area.scroll_if_long();
530 536 };
531 537
532 538 CodeCell.prototype.toggle_output = function () {
533 539 this.output_area.toggle_output();
534 540 };
535 541
536 542 CodeCell.prototype.toggle_output_scroll = function () {
537 543 this.output_area.toggle_scroll();
538 544 };
539 545
540 546
541 547 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
542 548 var ns;
543 549 if (prompt_value === undefined || prompt_value === null) {
544 550 ns = "&nbsp;";
545 551 } else {
546 552 ns = encodeURIComponent(prompt_value);
547 553 }
548 554 return 'In&nbsp;[' + ns + ']:';
549 555 };
550 556
551 557 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
552 558 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
553 559 for(var i=1; i < lines_number; i++) {
554 560 html.push(['...:']);
555 561 }
556 562 return html.join('<br/>');
557 563 };
558 564
559 565 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
560 566
561 567
562 568 CodeCell.prototype.set_input_prompt = function (number) {
563 569 var nline = 1;
564 570 if (this.code_mirror !== undefined) {
565 571 nline = this.code_mirror.lineCount();
566 572 }
567 573 this.input_prompt_number = number;
568 574 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
569 575 // This HTML call is okay because the user contents are escaped.
570 576 this.element.find('div.input_prompt').html(prompt_html);
571 577 };
572 578
573 579
574 580 CodeCell.prototype.clear_input = function () {
575 581 this.code_mirror.setValue('');
576 582 };
577 583
578 584
579 585 CodeCell.prototype.get_text = function () {
580 586 return this.code_mirror.getValue();
581 587 };
582 588
583 589
584 590 CodeCell.prototype.set_text = function (code) {
585 591 return this.code_mirror.setValue(code);
586 592 };
587 593
588 594
589 595 CodeCell.prototype.clear_output = function (wait) {
590 596 this.output_area.clear_output(wait);
591 597 this.set_input_prompt();
592 598 };
593 599
594 600
595 601 // JSON serialization
596 602
597 603 CodeCell.prototype.fromJSON = function (data) {
598 604 Cell.prototype.fromJSON.apply(this, arguments);
599 605 if (data.cell_type === 'code') {
600 606 if (data.source !== undefined) {
601 607 this.set_text(data.source);
602 608 // make this value the starting point, so that we can only undo
603 609 // to this state, instead of a blank cell
604 610 this.code_mirror.clearHistory();
605 611 this.auto_highlight();
606 612 }
607 613 this.set_input_prompt(data.execution_count);
608 614 this.output_area.trusted = data.metadata.trusted || false;
609 615 this.output_area.fromJSON(data.outputs, data.metadata);
610 616 }
611 617 };
612 618
613 619
614 620 CodeCell.prototype.toJSON = function () {
615 621 var data = Cell.prototype.toJSON.apply(this);
616 622 data.source = this.get_text();
617 623 // is finite protect against undefined and '*' value
618 624 if (isFinite(this.input_prompt_number)) {
619 625 data.execution_count = this.input_prompt_number;
620 626 } else {
621 627 data.execution_count = null;
622 628 }
623 629 var outputs = this.output_area.toJSON();
624 630 data.outputs = outputs;
625 631 data.metadata.trusted = this.output_area.trusted;
626 632 data.metadata.collapsed = this.output_area.collapsed;
627 633 if (this.output_area.scroll_state === 'auto') {
628 634 delete data.metadata.scrolled;
629 635 } else {
630 636 data.metadata.scrolled = this.output_area.scroll_state;
631 637 }
632 638 return data;
633 639 };
634 640
635 641 /**
636 642 * handle cell level logic when a cell is unselected
637 643 * @method unselect
638 644 * @return is the action being taken
639 645 */
640 646 CodeCell.prototype.unselect = function () {
641 647 var cont = Cell.prototype.unselect.apply(this);
642 648 if (cont) {
643 649 // When a code cell is usnelected, make sure that the corresponding
644 650 // tooltip and completer to that cell is closed.
645 651 this.tooltip.remove_and_cancel_tooltip(true);
646 652 if (this.completer !== null) {
647 653 this.completer.close();
648 654 }
649 655 }
650 656 return cont;
651 657 };
652 658
653 659 // Backwards compatability.
654 660 IPython.CodeCell = CodeCell;
655 661
656 662 return {'CodeCell': CodeCell};
657 663 });
General Comments 0
You need to be logged in to leave comments. Login now