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