##// END OF EJS Templates
Merge pull request #3493 from minrk/restart-dialog...
Brian E. Granger -
r11101:f93cd111 merge
parent child Browse files
Show More
@@ -1,2010 +1,2022 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Notebook
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13
14 14 var utils = IPython.utils;
15 15 var key = IPython.utils.keycodes;
16 16
17 17 /**
18 18 * A notebook contains and manages cells.
19 19 *
20 20 * @class Notebook
21 21 * @constructor
22 22 * @param {String} selector A jQuery selector for the notebook's DOM element
23 23 * @param {Object} [options] A config object
24 24 */
25 25 var Notebook = function (selector, options) {
26 26 var options = options || {};
27 27 this._baseProjectUrl = options.baseProjectUrl;
28 28 this.read_only = options.read_only || IPython.read_only;
29 29
30 30 this.element = $(selector);
31 31 this.element.scroll();
32 32 this.element.data("notebook", this);
33 33 this.next_prompt_number = 1;
34 34 this.kernel = null;
35 35 this.clipboard = null;
36 36 this.undelete_backup = null;
37 37 this.undelete_index = null;
38 38 this.undelete_below = false;
39 39 this.paste_enabled = false;
40 40 this.set_dirty(false);
41 41 this.metadata = {};
42 42 this._checkpoint_after_save = false;
43 43 this.last_checkpoint = null;
44 44 this.autosave_interval = 0;
45 45 this.autosave_timer = null;
46 46 // autosave *at most* every two minutes
47 47 this.minimum_autosave_interval = 120000;
48 48 // single worksheet for now
49 49 this.worksheet_metadata = {};
50 50 this.control_key_active = false;
51 51 this.notebook_id = null;
52 52 this.notebook_name = null;
53 53 this.notebook_name_blacklist_re = /[\/\\:]/;
54 54 this.nbformat = 3 // Increment this when changing the nbformat
55 55 this.nbformat_minor = 0 // Increment this when changing the nbformat
56 56 this.style();
57 57 this.create_elements();
58 58 this.bind_events();
59 59 };
60 60
61 61 /**
62 62 * Tweak the notebook's CSS style.
63 63 *
64 64 * @method style
65 65 */
66 66 Notebook.prototype.style = function () {
67 67 $('div#notebook').addClass('border-box-sizing');
68 68 };
69 69
70 70 /**
71 71 * Get the root URL of the notebook server.
72 72 *
73 73 * @method baseProjectUrl
74 74 * @return {String} The base project URL
75 75 */
76 76 Notebook.prototype.baseProjectUrl = function(){
77 77 return this._baseProjectUrl || $('body').data('baseProjectUrl');
78 78 };
79 79
80 80 /**
81 81 * Create an HTML and CSS representation of the notebook.
82 82 *
83 83 * @method create_elements
84 84 */
85 85 Notebook.prototype.create_elements = function () {
86 86 // We add this end_space div to the end of the notebook div to:
87 87 // i) provide a margin between the last cell and the end of the notebook
88 88 // ii) to prevent the div from scrolling up when the last cell is being
89 89 // edited, but is too low on the page, which browsers will do automatically.
90 90 var that = this;
91 91 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
92 92 var end_space = $('<div/>').addClass('end_space');
93 93 end_space.dblclick(function (e) {
94 94 if (that.read_only) return;
95 95 var ncells = that.ncells();
96 96 that.insert_cell_below('code',ncells-1);
97 97 });
98 98 this.element.append(this.container);
99 99 this.container.append(end_space);
100 100 $('div#notebook').addClass('border-box-sizing');
101 101 };
102 102
103 103 /**
104 104 * Bind JavaScript events: key presses and custom IPython events.
105 105 *
106 106 * @method bind_events
107 107 */
108 108 Notebook.prototype.bind_events = function () {
109 109 var that = this;
110 110
111 111 $([IPython.events]).on('set_next_input.Notebook', function (event, data) {
112 112 var index = that.find_cell_index(data.cell);
113 113 var new_cell = that.insert_cell_below('code',index);
114 114 new_cell.set_text(data.text);
115 115 that.dirty = true;
116 116 });
117 117
118 118 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
119 119 that.dirty = data.value;
120 120 });
121 121
122 122 $([IPython.events]).on('select.Cell', function (event, data) {
123 123 var index = that.find_cell_index(data.cell);
124 124 that.select(index);
125 125 });
126
127 $([IPython.events]).on('status_autorestarting.Kernel', function () {
128 IPython.dialog.modal({
129 title: "Kernel Restarting",
130 body: "The kernel appears to have died. It will restart automatically.",
131 buttons: {
132 OK : {
133 class : "btn-primary"
134 }
135 }
136 });
137 });
126 138
127 139
128 140 $(document).keydown(function (event) {
129 141 // console.log(event);
130 142 if (that.read_only) return true;
131 143
132 144 // Save (CTRL+S) or (AppleKey+S)
133 145 //metaKey = applekey on mac
134 146 if ((event.ctrlKey || event.metaKey) && event.keyCode==83) {
135 147 that.save_checkpoint();
136 148 event.preventDefault();
137 149 return false;
138 150 } else if (event.which === key.ESC) {
139 151 // Intercept escape at highest level to avoid closing
140 152 // websocket connection with firefox
141 153 IPython.pager.collapse();
142 154 event.preventDefault();
143 155 } else if (event.which === key.SHIFT) {
144 156 // ignore shift keydown
145 157 return true;
146 158 }
147 159 if (event.which === key.UPARROW && !event.shiftKey) {
148 160 var cell = that.get_selected_cell();
149 161 if (cell && cell.at_top()) {
150 162 event.preventDefault();
151 163 that.select_prev();
152 164 };
153 165 } else if (event.which === key.DOWNARROW && !event.shiftKey) {
154 166 var cell = that.get_selected_cell();
155 167 if (cell && cell.at_bottom()) {
156 168 event.preventDefault();
157 169 that.select_next();
158 170 };
159 171 } else if (event.which === key.ENTER && event.shiftKey) {
160 172 that.execute_selected_cell();
161 173 return false;
162 174 } else if (event.which === key.ENTER && event.altKey) {
163 175 // Execute code cell, and insert new in place
164 176 that.execute_selected_cell();
165 177 // Only insert a new cell, if we ended up in an already populated cell
166 178 if (/\S/.test(that.get_selected_cell().get_text()) == true) {
167 179 that.insert_cell_above('code');
168 180 }
169 181 return false;
170 182 } else if (event.which === key.ENTER && event.ctrlKey) {
171 183 that.execute_selected_cell({terminal:true});
172 184 return false;
173 185 } else if (event.which === 77 && event.ctrlKey && that.control_key_active == false) {
174 186 that.control_key_active = true;
175 187 return false;
176 188 } else if (event.which === 88 && that.control_key_active) {
177 189 // Cut selected cell = x
178 190 that.cut_cell();
179 191 that.control_key_active = false;
180 192 return false;
181 193 } else if (event.which === 67 && that.control_key_active) {
182 194 // Copy selected cell = c
183 195 that.copy_cell();
184 196 that.control_key_active = false;
185 197 return false;
186 198 } else if (event.which === 86 && that.control_key_active) {
187 199 // Paste below selected cell = v
188 200 that.paste_cell_below();
189 201 that.control_key_active = false;
190 202 return false;
191 203 } else if (event.which === 68 && that.control_key_active) {
192 204 // Delete selected cell = d
193 205 that.delete_cell();
194 206 that.control_key_active = false;
195 207 return false;
196 208 } else if (event.which === 65 && that.control_key_active) {
197 209 // Insert code cell above selected = a
198 210 that.insert_cell_above('code');
199 211 that.control_key_active = false;
200 212 return false;
201 213 } else if (event.which === 66 && that.control_key_active) {
202 214 // Insert code cell below selected = b
203 215 that.insert_cell_below('code');
204 216 that.control_key_active = false;
205 217 return false;
206 218 } else if (event.which === 89 && that.control_key_active) {
207 219 // To code = y
208 220 that.to_code();
209 221 that.control_key_active = false;
210 222 return false;
211 223 } else if (event.which === 77 && that.control_key_active) {
212 224 // To markdown = m
213 225 that.to_markdown();
214 226 that.control_key_active = false;
215 227 return false;
216 228 } else if (event.which === 84 && that.control_key_active) {
217 229 // To Raw = t
218 230 that.to_raw();
219 231 that.control_key_active = false;
220 232 return false;
221 233 } else if (event.which === 49 && that.control_key_active) {
222 234 // To Heading 1 = 1
223 235 that.to_heading(undefined, 1);
224 236 that.control_key_active = false;
225 237 return false;
226 238 } else if (event.which === 50 && that.control_key_active) {
227 239 // To Heading 2 = 2
228 240 that.to_heading(undefined, 2);
229 241 that.control_key_active = false;
230 242 return false;
231 243 } else if (event.which === 51 && that.control_key_active) {
232 244 // To Heading 3 = 3
233 245 that.to_heading(undefined, 3);
234 246 that.control_key_active = false;
235 247 return false;
236 248 } else if (event.which === 52 && that.control_key_active) {
237 249 // To Heading 4 = 4
238 250 that.to_heading(undefined, 4);
239 251 that.control_key_active = false;
240 252 return false;
241 253 } else if (event.which === 53 && that.control_key_active) {
242 254 // To Heading 5 = 5
243 255 that.to_heading(undefined, 5);
244 256 that.control_key_active = false;
245 257 return false;
246 258 } else if (event.which === 54 && that.control_key_active) {
247 259 // To Heading 6 = 6
248 260 that.to_heading(undefined, 6);
249 261 that.control_key_active = false;
250 262 return false;
251 263 } else if (event.which === 79 && that.control_key_active) {
252 264 // Toggle output = o
253 265 if (event.shiftKey){
254 266 that.toggle_output_scroll();
255 267 } else {
256 268 that.toggle_output();
257 269 }
258 270 that.control_key_active = false;
259 271 return false;
260 272 } else if (event.which === 83 && that.control_key_active) {
261 273 // Save notebook = s
262 274 that.save_checkpoint();
263 275 that.control_key_active = false;
264 276 return false;
265 277 } else if (event.which === 74 && that.control_key_active) {
266 278 // Move cell down = j
267 279 that.move_cell_down();
268 280 that.control_key_active = false;
269 281 return false;
270 282 } else if (event.which === 75 && that.control_key_active) {
271 283 // Move cell up = k
272 284 that.move_cell_up();
273 285 that.control_key_active = false;
274 286 return false;
275 287 } else if (event.which === 80 && that.control_key_active) {
276 288 // Select previous = p
277 289 that.select_prev();
278 290 that.control_key_active = false;
279 291 return false;
280 292 } else if (event.which === 78 && that.control_key_active) {
281 293 // Select next = n
282 294 that.select_next();
283 295 that.control_key_active = false;
284 296 return false;
285 297 } else if (event.which === 76 && that.control_key_active) {
286 298 // Toggle line numbers = l
287 299 that.cell_toggle_line_numbers();
288 300 that.control_key_active = false;
289 301 return false;
290 302 } else if (event.which === 73 && that.control_key_active) {
291 303 // Interrupt kernel = i
292 304 that.kernel.interrupt();
293 305 that.control_key_active = false;
294 306 return false;
295 307 } else if (event.which === 190 && that.control_key_active) {
296 308 // Restart kernel = . # matches qt console
297 309 that.restart_kernel();
298 310 that.control_key_active = false;
299 311 return false;
300 312 } else if (event.which === 72 && that.control_key_active) {
301 313 // Show keyboard shortcuts = h
302 314 IPython.quick_help.show_keyboard_shortcuts();
303 315 that.control_key_active = false;
304 316 return false;
305 317 } else if (event.which === 90 && that.control_key_active) {
306 318 // Undo last cell delete = z
307 319 that.undelete();
308 320 that.control_key_active = false;
309 321 return false;
310 322 } else if (that.control_key_active) {
311 323 that.control_key_active = false;
312 324 return true;
313 325 }
314 326 return true;
315 327 });
316 328
317 329 var collapse_time = function(time){
318 330 var app_height = $('#ipython-main-app').height(); // content height
319 331 var splitter_height = $('div#pager_splitter').outerHeight(true);
320 332 var new_height = app_height - splitter_height;
321 333 that.element.animate({height : new_height + 'px'}, time);
322 334 }
323 335
324 336 this.element.bind('collapse_pager', function (event,extrap) {
325 337 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
326 338 collapse_time(time);
327 339 });
328 340
329 341 var expand_time = function(time) {
330 342 var app_height = $('#ipython-main-app').height(); // content height
331 343 var splitter_height = $('div#pager_splitter').outerHeight(true);
332 344 var pager_height = $('div#pager').outerHeight(true);
333 345 var new_height = app_height - pager_height - splitter_height;
334 346 that.element.animate({height : new_height + 'px'}, time);
335 347 }
336 348
337 349 this.element.bind('expand_pager', function (event, extrap) {
338 350 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
339 351 expand_time(time);
340 352 });
341 353
342 354 $(window).bind('beforeunload', function () {
343 355 // TODO: Make killing the kernel configurable.
344 356 var kill_kernel = false;
345 357 if (kill_kernel) {
346 358 that.kernel.kill();
347 359 }
348 360 // if we are autosaving, trigger an autosave on nav-away
349 361 if (that.dirty && that.autosave_interval && ! that.read_only) {
350 362 that.save_notebook();
351 363 };
352 364 // Null is the *only* return value that will make the browser not
353 365 // pop up the "don't leave" dialog.
354 366 return null;
355 367 });
356 368 };
357 369
358 370 /**
359 371 * Set the dirty flag, and trigger the set_dirty.Notebook event
360 372 *
361 373 * @method set_dirty
362 374 */
363 375 Notebook.prototype.set_dirty = function (value) {
364 376 if (value === undefined) {
365 377 value = true;
366 378 }
367 379 if (this.dirty == value) {
368 380 return;
369 381 }
370 382 $([IPython.events]).trigger('set_dirty.Notebook', {value: value});
371 383 };
372 384
373 385 /**
374 386 * Scroll the top of the page to a given cell.
375 387 *
376 388 * @method scroll_to_cell
377 389 * @param {Number} cell_number An index of the cell to view
378 390 * @param {Number} time Animation time in milliseconds
379 391 * @return {Number} Pixel offset from the top of the container
380 392 */
381 393 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
382 394 var cells = this.get_cells();
383 395 var time = time || 0;
384 396 cell_number = Math.min(cells.length-1,cell_number);
385 397 cell_number = Math.max(0 ,cell_number);
386 398 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
387 399 this.element.animate({scrollTop:scroll_value}, time);
388 400 return scroll_value;
389 401 };
390 402
391 403 /**
392 404 * Scroll to the bottom of the page.
393 405 *
394 406 * @method scroll_to_bottom
395 407 */
396 408 Notebook.prototype.scroll_to_bottom = function () {
397 409 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
398 410 };
399 411
400 412 /**
401 413 * Scroll to the top of the page.
402 414 *
403 415 * @method scroll_to_top
404 416 */
405 417 Notebook.prototype.scroll_to_top = function () {
406 418 this.element.animate({scrollTop:0}, 0);
407 419 };
408 420
409 421
410 422 // Cell indexing, retrieval, etc.
411 423
412 424 /**
413 425 * Get all cell elements in the notebook.
414 426 *
415 427 * @method get_cell_elements
416 428 * @return {jQuery} A selector of all cell elements
417 429 */
418 430 Notebook.prototype.get_cell_elements = function () {
419 431 return this.container.children("div.cell");
420 432 };
421 433
422 434 /**
423 435 * Get a particular cell element.
424 436 *
425 437 * @method get_cell_element
426 438 * @param {Number} index An index of a cell to select
427 439 * @return {jQuery} A selector of the given cell.
428 440 */
429 441 Notebook.prototype.get_cell_element = function (index) {
430 442 var result = null;
431 443 var e = this.get_cell_elements().eq(index);
432 444 if (e.length !== 0) {
433 445 result = e;
434 446 }
435 447 return result;
436 448 };
437 449
438 450 /**
439 451 * Count the cells in this notebook.
440 452 *
441 453 * @method ncells
442 454 * @return {Number} The number of cells in this notebook
443 455 */
444 456 Notebook.prototype.ncells = function () {
445 457 return this.get_cell_elements().length;
446 458 };
447 459
448 460 /**
449 461 * Get all Cell objects in this notebook.
450 462 *
451 463 * @method get_cells
452 464 * @return {Array} This notebook's Cell objects
453 465 */
454 466 // TODO: we are often calling cells as cells()[i], which we should optimize
455 467 // to cells(i) or a new method.
456 468 Notebook.prototype.get_cells = function () {
457 469 return this.get_cell_elements().toArray().map(function (e) {
458 470 return $(e).data("cell");
459 471 });
460 472 };
461 473
462 474 /**
463 475 * Get a Cell object from this notebook.
464 476 *
465 477 * @method get_cell
466 478 * @param {Number} index An index of a cell to retrieve
467 479 * @return {Cell} A particular cell
468 480 */
469 481 Notebook.prototype.get_cell = function (index) {
470 482 var result = null;
471 483 var ce = this.get_cell_element(index);
472 484 if (ce !== null) {
473 485 result = ce.data('cell');
474 486 }
475 487 return result;
476 488 }
477 489
478 490 /**
479 491 * Get the cell below a given cell.
480 492 *
481 493 * @method get_next_cell
482 494 * @param {Cell} cell The provided cell
483 495 * @return {Cell} The next cell
484 496 */
485 497 Notebook.prototype.get_next_cell = function (cell) {
486 498 var result = null;
487 499 var index = this.find_cell_index(cell);
488 500 if (this.is_valid_cell_index(index+1)) {
489 501 result = this.get_cell(index+1);
490 502 }
491 503 return result;
492 504 }
493 505
494 506 /**
495 507 * Get the cell above a given cell.
496 508 *
497 509 * @method get_prev_cell
498 510 * @param {Cell} cell The provided cell
499 511 * @return {Cell} The previous cell
500 512 */
501 513 Notebook.prototype.get_prev_cell = function (cell) {
502 514 // TODO: off-by-one
503 515 // nb.get_prev_cell(nb.get_cell(1)) is null
504 516 var result = null;
505 517 var index = this.find_cell_index(cell);
506 518 if (index !== null && index > 1) {
507 519 result = this.get_cell(index-1);
508 520 }
509 521 return result;
510 522 }
511 523
512 524 /**
513 525 * Get the numeric index of a given cell.
514 526 *
515 527 * @method find_cell_index
516 528 * @param {Cell} cell The provided cell
517 529 * @return {Number} The cell's numeric index
518 530 */
519 531 Notebook.prototype.find_cell_index = function (cell) {
520 532 var result = null;
521 533 this.get_cell_elements().filter(function (index) {
522 534 if ($(this).data("cell") === cell) {
523 535 result = index;
524 536 };
525 537 });
526 538 return result;
527 539 };
528 540
529 541 /**
530 542 * Get a given index , or the selected index if none is provided.
531 543 *
532 544 * @method index_or_selected
533 545 * @param {Number} index A cell's index
534 546 * @return {Number} The given index, or selected index if none is provided.
535 547 */
536 548 Notebook.prototype.index_or_selected = function (index) {
537 549 var i;
538 550 if (index === undefined || index === null) {
539 551 i = this.get_selected_index();
540 552 if (i === null) {
541 553 i = 0;
542 554 }
543 555 } else {
544 556 i = index;
545 557 }
546 558 return i;
547 559 };
548 560
549 561 /**
550 562 * Get the currently selected cell.
551 563 * @method get_selected_cell
552 564 * @return {Cell} The selected cell
553 565 */
554 566 Notebook.prototype.get_selected_cell = function () {
555 567 var index = this.get_selected_index();
556 568 return this.get_cell(index);
557 569 };
558 570
559 571 /**
560 572 * Check whether a cell index is valid.
561 573 *
562 574 * @method is_valid_cell_index
563 575 * @param {Number} index A cell index
564 576 * @return True if the index is valid, false otherwise
565 577 */
566 578 Notebook.prototype.is_valid_cell_index = function (index) {
567 579 if (index !== null && index >= 0 && index < this.ncells()) {
568 580 return true;
569 581 } else {
570 582 return false;
571 583 };
572 584 }
573 585
574 586 /**
575 587 * Get the index of the currently selected cell.
576 588
577 589 * @method get_selected_index
578 590 * @return {Number} The selected cell's numeric index
579 591 */
580 592 Notebook.prototype.get_selected_index = function () {
581 593 var result = null;
582 594 this.get_cell_elements().filter(function (index) {
583 595 if ($(this).data("cell").selected === true) {
584 596 result = index;
585 597 };
586 598 });
587 599 return result;
588 600 };
589 601
590 602
591 603 // Cell selection.
592 604
593 605 /**
594 606 * Programmatically select a cell.
595 607 *
596 608 * @method select
597 609 * @param {Number} index A cell's index
598 610 * @return {Notebook} This notebook
599 611 */
600 612 Notebook.prototype.select = function (index) {
601 613 if (this.is_valid_cell_index(index)) {
602 614 var sindex = this.get_selected_index()
603 615 if (sindex !== null && index !== sindex) {
604 616 this.get_cell(sindex).unselect();
605 617 };
606 618 var cell = this.get_cell(index);
607 619 cell.select();
608 620 if (cell.cell_type === 'heading') {
609 621 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
610 622 {'cell_type':cell.cell_type,level:cell.level}
611 623 );
612 624 } else {
613 625 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
614 626 {'cell_type':cell.cell_type}
615 627 );
616 628 };
617 629 };
618 630 return this;
619 631 };
620 632
621 633 /**
622 634 * Programmatically select the next cell.
623 635 *
624 636 * @method select_next
625 637 * @return {Notebook} This notebook
626 638 */
627 639 Notebook.prototype.select_next = function () {
628 640 var index = this.get_selected_index();
629 641 this.select(index+1);
630 642 return this;
631 643 };
632 644
633 645 /**
634 646 * Programmatically select the previous cell.
635 647 *
636 648 * @method select_prev
637 649 * @return {Notebook} This notebook
638 650 */
639 651 Notebook.prototype.select_prev = function () {
640 652 var index = this.get_selected_index();
641 653 this.select(index-1);
642 654 return this;
643 655 };
644 656
645 657
646 658 // Cell movement
647 659
648 660 /**
649 661 * Move given (or selected) cell up and select it.
650 662 *
651 663 * @method move_cell_up
652 664 * @param [index] {integer} cell index
653 665 * @return {Notebook} This notebook
654 666 **/
655 667 Notebook.prototype.move_cell_up = function (index) {
656 668 var i = this.index_or_selected(index);
657 669 if (this.is_valid_cell_index(i) && i > 0) {
658 670 var pivot = this.get_cell_element(i-1);
659 671 var tomove = this.get_cell_element(i);
660 672 if (pivot !== null && tomove !== null) {
661 673 tomove.detach();
662 674 pivot.before(tomove);
663 675 this.select(i-1);
664 676 };
665 677 this.set_dirty(true);
666 678 };
667 679 return this;
668 680 };
669 681
670 682
671 683 /**
672 684 * Move given (or selected) cell down and select it
673 685 *
674 686 * @method move_cell_down
675 687 * @param [index] {integer} cell index
676 688 * @return {Notebook} This notebook
677 689 **/
678 690 Notebook.prototype.move_cell_down = function (index) {
679 691 var i = this.index_or_selected(index);
680 692 if ( this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
681 693 var pivot = this.get_cell_element(i+1);
682 694 var tomove = this.get_cell_element(i);
683 695 if (pivot !== null && tomove !== null) {
684 696 tomove.detach();
685 697 pivot.after(tomove);
686 698 this.select(i+1);
687 699 };
688 700 };
689 701 this.set_dirty();
690 702 return this;
691 703 };
692 704
693 705
694 706 // Insertion, deletion.
695 707
696 708 /**
697 709 * Delete a cell from the notebook.
698 710 *
699 711 * @method delete_cell
700 712 * @param [index] A cell's numeric index
701 713 * @return {Notebook} This notebook
702 714 */
703 715 Notebook.prototype.delete_cell = function (index) {
704 716 var i = this.index_or_selected(index);
705 717 var cell = this.get_selected_cell();
706 718 this.undelete_backup = cell.toJSON();
707 719 $('#undelete_cell').removeClass('ui-state-disabled');
708 720 if (this.is_valid_cell_index(i)) {
709 721 var ce = this.get_cell_element(i);
710 722 ce.remove();
711 723 if (i === (this.ncells())) {
712 724 this.select(i-1);
713 725 this.undelete_index = i - 1;
714 726 this.undelete_below = true;
715 727 } else {
716 728 this.select(i);
717 729 this.undelete_index = i;
718 730 this.undelete_below = false;
719 731 };
720 732 this.set_dirty(true);
721 733 };
722 734 return this;
723 735 };
724 736
725 737 /**
726 738 * Insert a cell so that after insertion the cell is at given index.
727 739 *
728 740 * Similar to insert_above, but index parameter is mandatory
729 741 *
730 742 * Index will be brought back into the accissible range [0,n]
731 743 *
732 744 * @method insert_cell_at_index
733 745 * @param type {string} in ['code','markdown','heading']
734 746 * @param [index] {int} a valid index where to inser cell
735 747 *
736 748 * @return cell {cell|null} created cell or null
737 749 **/
738 750 Notebook.prototype.insert_cell_at_index = function(type, index){
739 751
740 752 var ncells = this.ncells();
741 753 var index = Math.min(index,ncells);
742 754 index = Math.max(index,0);
743 755 var cell = null;
744 756
745 757 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
746 758 if (type === 'code') {
747 759 cell = new IPython.CodeCell(this.kernel);
748 760 cell.set_input_prompt();
749 761 } else if (type === 'markdown') {
750 762 cell = new IPython.MarkdownCell();
751 763 } else if (type === 'raw') {
752 764 cell = new IPython.RawCell();
753 765 } else if (type === 'heading') {
754 766 cell = new IPython.HeadingCell();
755 767 }
756 768
757 769 if(this._insert_element_at_index(cell.element,index)){
758 770 cell.render();
759 771 this.select(this.find_cell_index(cell));
760 772 this.set_dirty(true);
761 773 }
762 774 }
763 775 return cell;
764 776
765 777 };
766 778
767 779 /**
768 780 * Insert an element at given cell index.
769 781 *
770 782 * @method _insert_element_at_index
771 783 * @param element {dom element} a cell element
772 784 * @param [index] {int} a valid index where to inser cell
773 785 * @private
774 786 *
775 787 * return true if everything whent fine.
776 788 **/
777 789 Notebook.prototype._insert_element_at_index = function(element, index){
778 790 if (element === undefined){
779 791 return false;
780 792 }
781 793
782 794 var ncells = this.ncells();
783 795
784 796 if (ncells === 0) {
785 797 // special case append if empty
786 798 this.element.find('div.end_space').before(element);
787 799 } else if ( ncells === index ) {
788 800 // special case append it the end, but not empty
789 801 this.get_cell_element(index-1).after(element);
790 802 } else if (this.is_valid_cell_index(index)) {
791 803 // otherwise always somewhere to append to
792 804 this.get_cell_element(index).before(element);
793 805 } else {
794 806 return false;
795 807 }
796 808
797 809 if (this.undelete_index !== null && index <= this.undelete_index) {
798 810 this.undelete_index = this.undelete_index + 1;
799 811 this.set_dirty(true);
800 812 }
801 813 return true;
802 814 };
803 815
804 816 /**
805 817 * Insert a cell of given type above given index, or at top
806 818 * of notebook if index smaller than 0.
807 819 *
808 820 * default index value is the one of currently selected cell
809 821 *
810 822 * @method insert_cell_above
811 823 * @param type {string} cell type
812 824 * @param [index] {integer}
813 825 *
814 826 * @return handle to created cell or null
815 827 **/
816 828 Notebook.prototype.insert_cell_above = function (type, index) {
817 829 index = this.index_or_selected(index);
818 830 return this.insert_cell_at_index(type, index);
819 831 };
820 832
821 833 /**
822 834 * Insert a cell of given type below given index, or at bottom
823 835 * of notebook if index greater thatn number of cell
824 836 *
825 837 * default index value is the one of currently selected cell
826 838 *
827 839 * @method insert_cell_below
828 840 * @param type {string} cell type
829 841 * @param [index] {integer}
830 842 *
831 843 * @return handle to created cell or null
832 844 *
833 845 **/
834 846 Notebook.prototype.insert_cell_below = function (type, index) {
835 847 index = this.index_or_selected(index);
836 848 return this.insert_cell_at_index(type, index+1);
837 849 };
838 850
839 851
840 852 /**
841 853 * Insert cell at end of notebook
842 854 *
843 855 * @method insert_cell_at_bottom
844 856 * @param {String} type cell type
845 857 *
846 858 * @return the added cell; or null
847 859 **/
848 860 Notebook.prototype.insert_cell_at_bottom = function (type){
849 861 var len = this.ncells();
850 862 return this.insert_cell_below(type,len-1);
851 863 };
852 864
853 865 /**
854 866 * Turn a cell into a code cell.
855 867 *
856 868 * @method to_code
857 869 * @param {Number} [index] A cell's index
858 870 */
859 871 Notebook.prototype.to_code = function (index) {
860 872 var i = this.index_or_selected(index);
861 873 if (this.is_valid_cell_index(i)) {
862 874 var source_element = this.get_cell_element(i);
863 875 var source_cell = source_element.data("cell");
864 876 if (!(source_cell instanceof IPython.CodeCell)) {
865 877 var target_cell = this.insert_cell_below('code',i);
866 878 var text = source_cell.get_text();
867 879 if (text === source_cell.placeholder) {
868 880 text = '';
869 881 }
870 882 target_cell.set_text(text);
871 883 // make this value the starting point, so that we can only undo
872 884 // to this state, instead of a blank cell
873 885 target_cell.code_mirror.clearHistory();
874 886 source_element.remove();
875 887 this.set_dirty(true);
876 888 };
877 889 };
878 890 };
879 891
880 892 /**
881 893 * Turn a cell into a Markdown cell.
882 894 *
883 895 * @method to_markdown
884 896 * @param {Number} [index] A cell's index
885 897 */
886 898 Notebook.prototype.to_markdown = function (index) {
887 899 var i = this.index_or_selected(index);
888 900 if (this.is_valid_cell_index(i)) {
889 901 var source_element = this.get_cell_element(i);
890 902 var source_cell = source_element.data("cell");
891 903 if (!(source_cell instanceof IPython.MarkdownCell)) {
892 904 var target_cell = this.insert_cell_below('markdown',i);
893 905 var text = source_cell.get_text();
894 906 if (text === source_cell.placeholder) {
895 907 text = '';
896 908 };
897 909 // The edit must come before the set_text.
898 910 target_cell.edit();
899 911 target_cell.set_text(text);
900 912 // make this value the starting point, so that we can only undo
901 913 // to this state, instead of a blank cell
902 914 target_cell.code_mirror.clearHistory();
903 915 source_element.remove();
904 916 this.set_dirty(true);
905 917 };
906 918 };
907 919 };
908 920
909 921 /**
910 922 * Turn a cell into a raw text cell.
911 923 *
912 924 * @method to_raw
913 925 * @param {Number} [index] A cell's index
914 926 */
915 927 Notebook.prototype.to_raw = function (index) {
916 928 var i = this.index_or_selected(index);
917 929 if (this.is_valid_cell_index(i)) {
918 930 var source_element = this.get_cell_element(i);
919 931 var source_cell = source_element.data("cell");
920 932 var target_cell = null;
921 933 if (!(source_cell instanceof IPython.RawCell)) {
922 934 target_cell = this.insert_cell_below('raw',i);
923 935 var text = source_cell.get_text();
924 936 if (text === source_cell.placeholder) {
925 937 text = '';
926 938 };
927 939 // The edit must come before the set_text.
928 940 target_cell.edit();
929 941 target_cell.set_text(text);
930 942 // make this value the starting point, so that we can only undo
931 943 // to this state, instead of a blank cell
932 944 target_cell.code_mirror.clearHistory();
933 945 source_element.remove();
934 946 this.set_dirty(true);
935 947 };
936 948 };
937 949 };
938 950
939 951 /**
940 952 * Turn a cell into a heading cell.
941 953 *
942 954 * @method to_heading
943 955 * @param {Number} [index] A cell's index
944 956 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
945 957 */
946 958 Notebook.prototype.to_heading = function (index, level) {
947 959 level = level || 1;
948 960 var i = this.index_or_selected(index);
949 961 if (this.is_valid_cell_index(i)) {
950 962 var source_element = this.get_cell_element(i);
951 963 var source_cell = source_element.data("cell");
952 964 var target_cell = null;
953 965 if (source_cell instanceof IPython.HeadingCell) {
954 966 source_cell.set_level(level);
955 967 } else {
956 968 target_cell = this.insert_cell_below('heading',i);
957 969 var text = source_cell.get_text();
958 970 if (text === source_cell.placeholder) {
959 971 text = '';
960 972 };
961 973 // The edit must come before the set_text.
962 974 target_cell.set_level(level);
963 975 target_cell.edit();
964 976 target_cell.set_text(text);
965 977 // make this value the starting point, so that we can only undo
966 978 // to this state, instead of a blank cell
967 979 target_cell.code_mirror.clearHistory();
968 980 source_element.remove();
969 981 this.set_dirty(true);
970 982 };
971 983 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
972 984 {'cell_type':'heading',level:level}
973 985 );
974 986 };
975 987 };
976 988
977 989
978 990 // Cut/Copy/Paste
979 991
980 992 /**
981 993 * Enable UI elements for pasting cells.
982 994 *
983 995 * @method enable_paste
984 996 */
985 997 Notebook.prototype.enable_paste = function () {
986 998 var that = this;
987 999 if (!this.paste_enabled) {
988 1000 $('#paste_cell_replace').removeClass('ui-state-disabled')
989 1001 .on('click', function () {that.paste_cell_replace();});
990 1002 $('#paste_cell_above').removeClass('ui-state-disabled')
991 1003 .on('click', function () {that.paste_cell_above();});
992 1004 $('#paste_cell_below').removeClass('ui-state-disabled')
993 1005 .on('click', function () {that.paste_cell_below();});
994 1006 this.paste_enabled = true;
995 1007 };
996 1008 };
997 1009
998 1010 /**
999 1011 * Disable UI elements for pasting cells.
1000 1012 *
1001 1013 * @method disable_paste
1002 1014 */
1003 1015 Notebook.prototype.disable_paste = function () {
1004 1016 if (this.paste_enabled) {
1005 1017 $('#paste_cell_replace').addClass('ui-state-disabled').off('click');
1006 1018 $('#paste_cell_above').addClass('ui-state-disabled').off('click');
1007 1019 $('#paste_cell_below').addClass('ui-state-disabled').off('click');
1008 1020 this.paste_enabled = false;
1009 1021 };
1010 1022 };
1011 1023
1012 1024 /**
1013 1025 * Cut a cell.
1014 1026 *
1015 1027 * @method cut_cell
1016 1028 */
1017 1029 Notebook.prototype.cut_cell = function () {
1018 1030 this.copy_cell();
1019 1031 this.delete_cell();
1020 1032 }
1021 1033
1022 1034 /**
1023 1035 * Copy a cell.
1024 1036 *
1025 1037 * @method copy_cell
1026 1038 */
1027 1039 Notebook.prototype.copy_cell = function () {
1028 1040 var cell = this.get_selected_cell();
1029 1041 this.clipboard = cell.toJSON();
1030 1042 this.enable_paste();
1031 1043 };
1032 1044
1033 1045 /**
1034 1046 * Replace the selected cell with a cell in the clipboard.
1035 1047 *
1036 1048 * @method paste_cell_replace
1037 1049 */
1038 1050 Notebook.prototype.paste_cell_replace = function () {
1039 1051 if (this.clipboard !== null && this.paste_enabled) {
1040 1052 var cell_data = this.clipboard;
1041 1053 var new_cell = this.insert_cell_above(cell_data.cell_type);
1042 1054 new_cell.fromJSON(cell_data);
1043 1055 var old_cell = this.get_next_cell(new_cell);
1044 1056 this.delete_cell(this.find_cell_index(old_cell));
1045 1057 this.select(this.find_cell_index(new_cell));
1046 1058 };
1047 1059 };
1048 1060
1049 1061 /**
1050 1062 * Paste a cell from the clipboard above the selected cell.
1051 1063 *
1052 1064 * @method paste_cell_above
1053 1065 */
1054 1066 Notebook.prototype.paste_cell_above = function () {
1055 1067 if (this.clipboard !== null && this.paste_enabled) {
1056 1068 var cell_data = this.clipboard;
1057 1069 var new_cell = this.insert_cell_above(cell_data.cell_type);
1058 1070 new_cell.fromJSON(cell_data);
1059 1071 };
1060 1072 };
1061 1073
1062 1074 /**
1063 1075 * Paste a cell from the clipboard below the selected cell.
1064 1076 *
1065 1077 * @method paste_cell_below
1066 1078 */
1067 1079 Notebook.prototype.paste_cell_below = function () {
1068 1080 if (this.clipboard !== null && this.paste_enabled) {
1069 1081 var cell_data = this.clipboard;
1070 1082 var new_cell = this.insert_cell_below(cell_data.cell_type);
1071 1083 new_cell.fromJSON(cell_data);
1072 1084 };
1073 1085 };
1074 1086
1075 1087 // Cell undelete
1076 1088
1077 1089 /**
1078 1090 * Restore the most recently deleted cell.
1079 1091 *
1080 1092 * @method undelete
1081 1093 */
1082 1094 Notebook.prototype.undelete = function() {
1083 1095 if (this.undelete_backup !== null && this.undelete_index !== null) {
1084 1096 var current_index = this.get_selected_index();
1085 1097 if (this.undelete_index < current_index) {
1086 1098 current_index = current_index + 1;
1087 1099 }
1088 1100 if (this.undelete_index >= this.ncells()) {
1089 1101 this.select(this.ncells() - 1);
1090 1102 }
1091 1103 else {
1092 1104 this.select(this.undelete_index);
1093 1105 }
1094 1106 var cell_data = this.undelete_backup;
1095 1107 var new_cell = null;
1096 1108 if (this.undelete_below) {
1097 1109 new_cell = this.insert_cell_below(cell_data.cell_type);
1098 1110 } else {
1099 1111 new_cell = this.insert_cell_above(cell_data.cell_type);
1100 1112 }
1101 1113 new_cell.fromJSON(cell_data);
1102 1114 this.select(current_index);
1103 1115 this.undelete_backup = null;
1104 1116 this.undelete_index = null;
1105 1117 }
1106 1118 $('#undelete_cell').addClass('ui-state-disabled');
1107 1119 }
1108 1120
1109 1121 // Split/merge
1110 1122
1111 1123 /**
1112 1124 * Split the selected cell into two, at the cursor.
1113 1125 *
1114 1126 * @method split_cell
1115 1127 */
1116 1128 Notebook.prototype.split_cell = function () {
1117 1129 // Todo: implement spliting for other cell types.
1118 1130 var cell = this.get_selected_cell();
1119 1131 if (cell.is_splittable()) {
1120 1132 var texta = cell.get_pre_cursor();
1121 1133 var textb = cell.get_post_cursor();
1122 1134 if (cell instanceof IPython.CodeCell) {
1123 1135 cell.set_text(texta);
1124 1136 var new_cell = this.insert_cell_below('code');
1125 1137 new_cell.set_text(textb);
1126 1138 } else if (cell instanceof IPython.MarkdownCell) {
1127 1139 cell.set_text(texta);
1128 1140 cell.render();
1129 1141 var new_cell = this.insert_cell_below('markdown');
1130 1142 new_cell.edit(); // editor must be visible to call set_text
1131 1143 new_cell.set_text(textb);
1132 1144 new_cell.render();
1133 1145 }
1134 1146 };
1135 1147 };
1136 1148
1137 1149 /**
1138 1150 * Combine the selected cell into the cell above it.
1139 1151 *
1140 1152 * @method merge_cell_above
1141 1153 */
1142 1154 Notebook.prototype.merge_cell_above = function () {
1143 1155 var index = this.get_selected_index();
1144 1156 var cell = this.get_cell(index);
1145 1157 if (index > 0) {
1146 1158 var upper_cell = this.get_cell(index-1);
1147 1159 var upper_text = upper_cell.get_text();
1148 1160 var text = cell.get_text();
1149 1161 if (cell instanceof IPython.CodeCell) {
1150 1162 cell.set_text(upper_text+'\n'+text);
1151 1163 } else if (cell instanceof IPython.MarkdownCell) {
1152 1164 cell.edit();
1153 1165 cell.set_text(upper_text+'\n'+text);
1154 1166 cell.render();
1155 1167 };
1156 1168 this.delete_cell(index-1);
1157 1169 this.select(this.find_cell_index(cell));
1158 1170 };
1159 1171 };
1160 1172
1161 1173 /**
1162 1174 * Combine the selected cell into the cell below it.
1163 1175 *
1164 1176 * @method merge_cell_below
1165 1177 */
1166 1178 Notebook.prototype.merge_cell_below = function () {
1167 1179 var index = this.get_selected_index();
1168 1180 var cell = this.get_cell(index);
1169 1181 if (index < this.ncells()-1) {
1170 1182 var lower_cell = this.get_cell(index+1);
1171 1183 var lower_text = lower_cell.get_text();
1172 1184 var text = cell.get_text();
1173 1185 if (cell instanceof IPython.CodeCell) {
1174 1186 cell.set_text(text+'\n'+lower_text);
1175 1187 } else if (cell instanceof IPython.MarkdownCell) {
1176 1188 cell.edit();
1177 1189 cell.set_text(text+'\n'+lower_text);
1178 1190 cell.render();
1179 1191 };
1180 1192 this.delete_cell(index+1);
1181 1193 this.select(this.find_cell_index(cell));
1182 1194 };
1183 1195 };
1184 1196
1185 1197
1186 1198 // Cell collapsing and output clearing
1187 1199
1188 1200 /**
1189 1201 * Hide a cell's output.
1190 1202 *
1191 1203 * @method collapse
1192 1204 * @param {Number} index A cell's numeric index
1193 1205 */
1194 1206 Notebook.prototype.collapse = function (index) {
1195 1207 var i = this.index_or_selected(index);
1196 1208 this.get_cell(i).collapse();
1197 1209 this.set_dirty(true);
1198 1210 };
1199 1211
1200 1212 /**
1201 1213 * Show a cell's output.
1202 1214 *
1203 1215 * @method expand
1204 1216 * @param {Number} index A cell's numeric index
1205 1217 */
1206 1218 Notebook.prototype.expand = function (index) {
1207 1219 var i = this.index_or_selected(index);
1208 1220 this.get_cell(i).expand();
1209 1221 this.set_dirty(true);
1210 1222 };
1211 1223
1212 1224 /** Toggle whether a cell's output is collapsed or expanded.
1213 1225 *
1214 1226 * @method toggle_output
1215 1227 * @param {Number} index A cell's numeric index
1216 1228 */
1217 1229 Notebook.prototype.toggle_output = function (index) {
1218 1230 var i = this.index_or_selected(index);
1219 1231 this.get_cell(i).toggle_output();
1220 1232 this.set_dirty(true);
1221 1233 };
1222 1234
1223 1235 /**
1224 1236 * Toggle a scrollbar for long cell outputs.
1225 1237 *
1226 1238 * @method toggle_output_scroll
1227 1239 * @param {Number} index A cell's numeric index
1228 1240 */
1229 1241 Notebook.prototype.toggle_output_scroll = function (index) {
1230 1242 var i = this.index_or_selected(index);
1231 1243 this.get_cell(i).toggle_output_scroll();
1232 1244 };
1233 1245
1234 1246 /**
1235 1247 * Hide each code cell's output area.
1236 1248 *
1237 1249 * @method collapse_all_output
1238 1250 */
1239 1251 Notebook.prototype.collapse_all_output = function () {
1240 1252 var ncells = this.ncells();
1241 1253 var cells = this.get_cells();
1242 1254 for (var i=0; i<ncells; i++) {
1243 1255 if (cells[i] instanceof IPython.CodeCell) {
1244 1256 cells[i].output_area.collapse();
1245 1257 }
1246 1258 };
1247 1259 // this should not be set if the `collapse` key is removed from nbformat
1248 1260 this.set_dirty(true);
1249 1261 };
1250 1262
1251 1263 /**
1252 1264 * Expand each code cell's output area, and add a scrollbar for long output.
1253 1265 *
1254 1266 * @method scroll_all_output
1255 1267 */
1256 1268 Notebook.prototype.scroll_all_output = function () {
1257 1269 var ncells = this.ncells();
1258 1270 var cells = this.get_cells();
1259 1271 for (var i=0; i<ncells; i++) {
1260 1272 if (cells[i] instanceof IPython.CodeCell) {
1261 1273 cells[i].output_area.expand();
1262 1274 cells[i].output_area.scroll_if_long();
1263 1275 }
1264 1276 };
1265 1277 // this should not be set if the `collapse` key is removed from nbformat
1266 1278 this.set_dirty(true);
1267 1279 };
1268 1280
1269 1281 /**
1270 1282 * Expand each code cell's output area, and remove scrollbars.
1271 1283 *
1272 1284 * @method expand_all_output
1273 1285 */
1274 1286 Notebook.prototype.expand_all_output = function () {
1275 1287 var ncells = this.ncells();
1276 1288 var cells = this.get_cells();
1277 1289 for (var i=0; i<ncells; i++) {
1278 1290 if (cells[i] instanceof IPython.CodeCell) {
1279 1291 cells[i].output_area.expand();
1280 1292 cells[i].output_area.unscroll_area();
1281 1293 }
1282 1294 };
1283 1295 // this should not be set if the `collapse` key is removed from nbformat
1284 1296 this.set_dirty(true);
1285 1297 };
1286 1298
1287 1299 /**
1288 1300 * Clear each code cell's output area.
1289 1301 *
1290 1302 * @method clear_all_output
1291 1303 */
1292 1304 Notebook.prototype.clear_all_output = function () {
1293 1305 var ncells = this.ncells();
1294 1306 var cells = this.get_cells();
1295 1307 for (var i=0; i<ncells; i++) {
1296 1308 if (cells[i] instanceof IPython.CodeCell) {
1297 1309 cells[i].clear_output(true,true,true);
1298 1310 // Make all In[] prompts blank, as well
1299 1311 // TODO: make this configurable (via checkbox?)
1300 1312 cells[i].set_input_prompt();
1301 1313 }
1302 1314 };
1303 1315 this.set_dirty(true);
1304 1316 };
1305 1317
1306 1318
1307 1319 // Other cell functions: line numbers, ...
1308 1320
1309 1321 /**
1310 1322 * Toggle line numbers in the selected cell's input area.
1311 1323 *
1312 1324 * @method cell_toggle_line_numbers
1313 1325 */
1314 1326 Notebook.prototype.cell_toggle_line_numbers = function() {
1315 1327 this.get_selected_cell().toggle_line_numbers();
1316 1328 };
1317 1329
1318 1330 // Kernel related things
1319 1331
1320 1332 /**
1321 1333 * Start a new kernel and set it on each code cell.
1322 1334 *
1323 1335 * @method start_kernel
1324 1336 */
1325 1337 Notebook.prototype.start_kernel = function () {
1326 1338 var base_url = $('body').data('baseKernelUrl') + "kernels";
1327 1339 this.kernel = new IPython.Kernel(base_url);
1328 1340 this.kernel.start(this.notebook_id);
1329 1341 // Now that the kernel has been created, tell the CodeCells about it.
1330 1342 var ncells = this.ncells();
1331 1343 for (var i=0; i<ncells; i++) {
1332 1344 var cell = this.get_cell(i);
1333 1345 if (cell instanceof IPython.CodeCell) {
1334 1346 cell.set_kernel(this.kernel)
1335 1347 };
1336 1348 };
1337 1349 };
1338 1350
1339 1351 /**
1340 1352 * Prompt the user to restart the IPython kernel.
1341 1353 *
1342 1354 * @method restart_kernel
1343 1355 */
1344 1356 Notebook.prototype.restart_kernel = function () {
1345 1357 var that = this;
1346 1358 IPython.dialog.modal({
1347 1359 title : "Restart kernel or continue running?",
1348 1360 body : $("<p/>").html(
1349 1361 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1350 1362 ),
1351 1363 buttons : {
1352 1364 "Continue running" : {},
1353 1365 "Restart" : {
1354 1366 "class" : "btn-danger",
1355 1367 "click" : function() {
1356 1368 that.kernel.restart();
1357 1369 }
1358 1370 }
1359 1371 }
1360 1372 });
1361 1373 };
1362 1374
1363 1375 /**
1364 1376 * Run the selected cell.
1365 1377 *
1366 1378 * Execute or render cell outputs.
1367 1379 *
1368 1380 * @method execute_selected_cell
1369 1381 * @param {Object} options Customize post-execution behavior
1370 1382 */
1371 1383 Notebook.prototype.execute_selected_cell = function (options) {
1372 1384 // add_new: should a new cell be added if we are at the end of the nb
1373 1385 // terminal: execute in terminal mode, which stays in the current cell
1374 1386 var default_options = {terminal: false, add_new: true};
1375 1387 $.extend(default_options, options);
1376 1388 var that = this;
1377 1389 var cell = that.get_selected_cell();
1378 1390 var cell_index = that.find_cell_index(cell);
1379 1391 if (cell instanceof IPython.CodeCell) {
1380 1392 cell.execute();
1381 1393 }
1382 1394 if (default_options.terminal) {
1383 1395 cell.select_all();
1384 1396 } else {
1385 1397 if ((cell_index === (that.ncells()-1)) && default_options.add_new) {
1386 1398 that.insert_cell_below('code');
1387 1399 // If we are adding a new cell at the end, scroll down to show it.
1388 1400 that.scroll_to_bottom();
1389 1401 } else {
1390 1402 that.select(cell_index+1);
1391 1403 };
1392 1404 };
1393 1405 this.set_dirty(true);
1394 1406 };
1395 1407
1396 1408 /**
1397 1409 * Execute all cells below the selected cell.
1398 1410 *
1399 1411 * @method execute_cells_below
1400 1412 */
1401 1413 Notebook.prototype.execute_cells_below = function () {
1402 1414 this.execute_cell_range(this.get_selected_index(), this.ncells());
1403 1415 this.scroll_to_bottom();
1404 1416 };
1405 1417
1406 1418 /**
1407 1419 * Execute all cells above the selected cell.
1408 1420 *
1409 1421 * @method execute_cells_above
1410 1422 */
1411 1423 Notebook.prototype.execute_cells_above = function () {
1412 1424 this.execute_cell_range(0, this.get_selected_index());
1413 1425 };
1414 1426
1415 1427 /**
1416 1428 * Execute all cells.
1417 1429 *
1418 1430 * @method execute_all_cells
1419 1431 */
1420 1432 Notebook.prototype.execute_all_cells = function () {
1421 1433 this.execute_cell_range(0, this.ncells());
1422 1434 this.scroll_to_bottom();
1423 1435 };
1424 1436
1425 1437 /**
1426 1438 * Execute a contiguous range of cells.
1427 1439 *
1428 1440 * @method execute_cell_range
1429 1441 * @param {Number} start Index of the first cell to execute (inclusive)
1430 1442 * @param {Number} end Index of the last cell to execute (exclusive)
1431 1443 */
1432 1444 Notebook.prototype.execute_cell_range = function (start, end) {
1433 1445 for (var i=start; i<end; i++) {
1434 1446 this.select(i);
1435 1447 this.execute_selected_cell({add_new:false});
1436 1448 };
1437 1449 };
1438 1450
1439 1451 // Persistance and loading
1440 1452
1441 1453 /**
1442 1454 * Getter method for this notebook's ID.
1443 1455 *
1444 1456 * @method get_notebook_id
1445 1457 * @return {String} This notebook's ID
1446 1458 */
1447 1459 Notebook.prototype.get_notebook_id = function () {
1448 1460 return this.notebook_id;
1449 1461 };
1450 1462
1451 1463 /**
1452 1464 * Getter method for this notebook's name.
1453 1465 *
1454 1466 * @method get_notebook_name
1455 1467 * @return {String} This notebook's name
1456 1468 */
1457 1469 Notebook.prototype.get_notebook_name = function () {
1458 1470 return this.notebook_name;
1459 1471 };
1460 1472
1461 1473 /**
1462 1474 * Setter method for this notebook's name.
1463 1475 *
1464 1476 * @method set_notebook_name
1465 1477 * @param {String} name A new name for this notebook
1466 1478 */
1467 1479 Notebook.prototype.set_notebook_name = function (name) {
1468 1480 this.notebook_name = name;
1469 1481 };
1470 1482
1471 1483 /**
1472 1484 * Check that a notebook's name is valid.
1473 1485 *
1474 1486 * @method test_notebook_name
1475 1487 * @param {String} nbname A name for this notebook
1476 1488 * @return {Boolean} True if the name is valid, false if invalid
1477 1489 */
1478 1490 Notebook.prototype.test_notebook_name = function (nbname) {
1479 1491 nbname = nbname || '';
1480 1492 if (this.notebook_name_blacklist_re.test(nbname) == false && nbname.length>0) {
1481 1493 return true;
1482 1494 } else {
1483 1495 return false;
1484 1496 };
1485 1497 };
1486 1498
1487 1499 /**
1488 1500 * Load a notebook from JSON (.ipynb).
1489 1501 *
1490 1502 * This currently handles one worksheet: others are deleted.
1491 1503 *
1492 1504 * @method fromJSON
1493 1505 * @param {Object} data JSON representation of a notebook
1494 1506 */
1495 1507 Notebook.prototype.fromJSON = function (data) {
1496 1508 var ncells = this.ncells();
1497 1509 var i;
1498 1510 for (i=0; i<ncells; i++) {
1499 1511 // Always delete cell 0 as they get renumbered as they are deleted.
1500 1512 this.delete_cell(0);
1501 1513 };
1502 1514 // Save the metadata and name.
1503 1515 this.metadata = data.metadata;
1504 1516 this.notebook_name = data.metadata.name;
1505 1517 // Only handle 1 worksheet for now.
1506 1518 var worksheet = data.worksheets[0];
1507 1519 if (worksheet !== undefined) {
1508 1520 if (worksheet.metadata) {
1509 1521 this.worksheet_metadata = worksheet.metadata;
1510 1522 }
1511 1523 var new_cells = worksheet.cells;
1512 1524 ncells = new_cells.length;
1513 1525 var cell_data = null;
1514 1526 var new_cell = null;
1515 1527 for (i=0; i<ncells; i++) {
1516 1528 cell_data = new_cells[i];
1517 1529 // VERSIONHACK: plaintext -> raw
1518 1530 // handle never-released plaintext name for raw cells
1519 1531 if (cell_data.cell_type === 'plaintext'){
1520 1532 cell_data.cell_type = 'raw';
1521 1533 }
1522 1534
1523 1535 new_cell = this.insert_cell_below(cell_data.cell_type);
1524 1536 new_cell.fromJSON(cell_data);
1525 1537 };
1526 1538 };
1527 1539 if (data.worksheets.length > 1) {
1528 1540 IPython.dialog.modal({
1529 1541 title : "Multiple worksheets",
1530 1542 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1531 1543 "but this version of IPython can only handle the first. " +
1532 1544 "If you save this notebook, worksheets after the first will be lost.",
1533 1545 buttons : {
1534 1546 OK : {
1535 1547 class : "btn-danger"
1536 1548 }
1537 1549 }
1538 1550 });
1539 1551 }
1540 1552 };
1541 1553
1542 1554 /**
1543 1555 * Dump this notebook into a JSON-friendly object.
1544 1556 *
1545 1557 * @method toJSON
1546 1558 * @return {Object} A JSON-friendly representation of this notebook.
1547 1559 */
1548 1560 Notebook.prototype.toJSON = function () {
1549 1561 var cells = this.get_cells();
1550 1562 var ncells = cells.length;
1551 1563 var cell_array = new Array(ncells);
1552 1564 for (var i=0; i<ncells; i++) {
1553 1565 cell_array[i] = cells[i].toJSON();
1554 1566 };
1555 1567 var data = {
1556 1568 // Only handle 1 worksheet for now.
1557 1569 worksheets : [{
1558 1570 cells: cell_array,
1559 1571 metadata: this.worksheet_metadata
1560 1572 }],
1561 1573 metadata : this.metadata
1562 1574 };
1563 1575 return data;
1564 1576 };
1565 1577
1566 1578 /**
1567 1579 * Start an autosave timer, for periodically saving the notebook.
1568 1580 *
1569 1581 * @method set_autosave_interval
1570 1582 * @param {Integer} interval the autosave interval in milliseconds
1571 1583 */
1572 1584 Notebook.prototype.set_autosave_interval = function (interval) {
1573 1585 var that = this;
1574 1586 // clear previous interval, so we don't get simultaneous timers
1575 1587 if (this.autosave_timer) {
1576 1588 clearInterval(this.autosave_timer);
1577 1589 }
1578 1590
1579 1591 this.autosave_interval = this.minimum_autosave_interval = interval;
1580 1592 if (interval) {
1581 1593 this.autosave_timer = setInterval(function() {
1582 1594 if (that.dirty) {
1583 1595 that.save_notebook();
1584 1596 }
1585 1597 }, interval);
1586 1598 $([IPython.events]).trigger("autosave_enabled.Notebook", interval);
1587 1599 } else {
1588 1600 this.autosave_timer = null;
1589 1601 $([IPython.events]).trigger("autosave_disabled.Notebook");
1590 1602 };
1591 1603 };
1592 1604
1593 1605 /**
1594 1606 * Save this notebook on the server.
1595 1607 *
1596 1608 * @method save_notebook
1597 1609 */
1598 1610 Notebook.prototype.save_notebook = function () {
1599 1611 // We may want to move the name/id/nbformat logic inside toJSON?
1600 1612 var data = this.toJSON();
1601 1613 data.metadata.name = this.notebook_name;
1602 1614 data.nbformat = this.nbformat;
1603 1615 data.nbformat_minor = this.nbformat_minor;
1604 1616
1605 1617 // time the ajax call for autosave tuning purposes.
1606 1618 var start = new Date().getTime();
1607 1619
1608 1620 // We do the call with settings so we can set cache to false.
1609 1621 var settings = {
1610 1622 processData : false,
1611 1623 cache : false,
1612 1624 type : "PUT",
1613 1625 data : JSON.stringify(data),
1614 1626 headers : {'Content-Type': 'application/json'},
1615 1627 success : $.proxy(this.save_notebook_success, this, start),
1616 1628 error : $.proxy(this.save_notebook_error, this)
1617 1629 };
1618 1630 $([IPython.events]).trigger('notebook_saving.Notebook');
1619 1631 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id;
1620 1632 $.ajax(url, settings);
1621 1633 };
1622 1634
1623 1635 /**
1624 1636 * Success callback for saving a notebook.
1625 1637 *
1626 1638 * @method save_notebook_success
1627 1639 * @param {Integer} start the time when the save request started
1628 1640 * @param {Object} data JSON representation of a notebook
1629 1641 * @param {String} status Description of response status
1630 1642 * @param {jqXHR} xhr jQuery Ajax object
1631 1643 */
1632 1644 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1633 1645 this.set_dirty(false);
1634 1646 $([IPython.events]).trigger('notebook_saved.Notebook');
1635 1647 this._update_autosave_interval(start);
1636 1648 if (this._checkpoint_after_save) {
1637 1649 this.create_checkpoint();
1638 1650 this._checkpoint_after_save = false;
1639 1651 };
1640 1652 };
1641 1653
1642 1654 /**
1643 1655 * update the autosave interval based on how long the last save took
1644 1656 *
1645 1657 * @method _update_autosave_interval
1646 1658 * @param {Integer} timestamp when the save request started
1647 1659 */
1648 1660 Notebook.prototype._update_autosave_interval = function (start) {
1649 1661 var duration = (new Date().getTime() - start);
1650 1662 if (this.autosave_interval) {
1651 1663 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1652 1664 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1653 1665 // round to 10 seconds, otherwise we will be setting a new interval too often
1654 1666 interval = 10000 * Math.round(interval / 10000);
1655 1667 // set new interval, if it's changed
1656 1668 if (interval != this.autosave_interval) {
1657 1669 this.set_autosave_interval(interval);
1658 1670 }
1659 1671 }
1660 1672 };
1661 1673
1662 1674 /**
1663 1675 * Failure callback for saving a notebook.
1664 1676 *
1665 1677 * @method save_notebook_error
1666 1678 * @param {jqXHR} xhr jQuery Ajax object
1667 1679 * @param {String} status Description of response status
1668 1680 * @param {String} error_msg HTTP error message
1669 1681 */
1670 1682 Notebook.prototype.save_notebook_error = function (xhr, status, error_msg) {
1671 1683 $([IPython.events]).trigger('notebook_save_failed.Notebook');
1672 1684 };
1673 1685
1674 1686 /**
1675 1687 * Request a notebook's data from the server.
1676 1688 *
1677 1689 * @method load_notebook
1678 1690 * @param {String} notebook_id A notebook to load
1679 1691 */
1680 1692 Notebook.prototype.load_notebook = function (notebook_id) {
1681 1693 var that = this;
1682 1694 this.notebook_id = notebook_id;
1683 1695 // We do the call with settings so we can set cache to false.
1684 1696 var settings = {
1685 1697 processData : false,
1686 1698 cache : false,
1687 1699 type : "GET",
1688 1700 dataType : "json",
1689 1701 success : $.proxy(this.load_notebook_success,this),
1690 1702 error : $.proxy(this.load_notebook_error,this),
1691 1703 };
1692 1704 $([IPython.events]).trigger('notebook_loading.Notebook');
1693 1705 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id;
1694 1706 $.ajax(url, settings);
1695 1707 };
1696 1708
1697 1709 /**
1698 1710 * Success callback for loading a notebook from the server.
1699 1711 *
1700 1712 * Load notebook data from the JSON response.
1701 1713 *
1702 1714 * @method load_notebook_success
1703 1715 * @param {Object} data JSON representation of a notebook
1704 1716 * @param {String} status Description of response status
1705 1717 * @param {jqXHR} xhr jQuery Ajax object
1706 1718 */
1707 1719 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
1708 1720 this.fromJSON(data);
1709 1721 if (this.ncells() === 0) {
1710 1722 this.insert_cell_below('code');
1711 1723 };
1712 1724 this.set_dirty(false);
1713 1725 this.select(0);
1714 1726 this.scroll_to_top();
1715 1727 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
1716 1728 var msg = "This notebook has been converted from an older " +
1717 1729 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
1718 1730 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
1719 1731 "newer notebook format will be used and older versions of IPython " +
1720 1732 "may not be able to read it. To keep the older version, close the " +
1721 1733 "notebook without saving it.";
1722 1734 IPython.dialog.modal({
1723 1735 title : "Notebook converted",
1724 1736 body : msg,
1725 1737 buttons : {
1726 1738 OK : {
1727 1739 class : "btn-primary"
1728 1740 }
1729 1741 }
1730 1742 });
1731 1743 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
1732 1744 var that = this;
1733 1745 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
1734 1746 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
1735 1747 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
1736 1748 this_vs + ". You can still work with this notebook, but some features " +
1737 1749 "introduced in later notebook versions may not be available."
1738 1750
1739 1751 IPython.dialog.modal({
1740 1752 title : "Newer Notebook",
1741 1753 body : msg,
1742 1754 buttons : {
1743 1755 OK : {
1744 1756 class : "btn-danger"
1745 1757 }
1746 1758 }
1747 1759 });
1748 1760
1749 1761 }
1750 1762
1751 1763 // Create the kernel after the notebook is completely loaded to prevent
1752 1764 // code execution upon loading, which is a security risk.
1753 1765 if (! this.read_only) {
1754 1766 this.start_kernel();
1755 1767 // load our checkpoint list
1756 1768 IPython.notebook.list_checkpoints();
1757 1769 }
1758 1770 $([IPython.events]).trigger('notebook_loaded.Notebook');
1759 1771 };
1760 1772
1761 1773 /**
1762 1774 * Failure callback for loading a notebook from the server.
1763 1775 *
1764 1776 * @method load_notebook_error
1765 1777 * @param {jqXHR} xhr jQuery Ajax object
1766 1778 * @param {String} textStatus Description of response status
1767 1779 * @param {String} errorThrow HTTP error message
1768 1780 */
1769 1781 Notebook.prototype.load_notebook_error = function (xhr, textStatus, errorThrow) {
1770 1782 if (xhr.status === 500) {
1771 1783 var msg = "An error occurred while loading this notebook. Most likely " +
1772 1784 "this notebook is in a newer format than is supported by this " +
1773 1785 "version of IPython. This version can load notebook formats " +
1774 1786 "v"+this.nbformat+" or earlier.";
1775 1787
1776 1788 IPython.dialog.modal({
1777 1789 title: "Error loading notebook",
1778 1790 body : msg,
1779 1791 buttons : {
1780 1792 "OK": {}
1781 1793 }
1782 1794 });
1783 1795 }
1784 1796 }
1785 1797
1786 1798 /********************* checkpoint-related *********************/
1787 1799
1788 1800 /**
1789 1801 * Save the notebook then immediately create a checkpoint.
1790 1802 *
1791 1803 * @method save_checkpoint
1792 1804 */
1793 1805 Notebook.prototype.save_checkpoint = function () {
1794 1806 this._checkpoint_after_save = true;
1795 1807 this.save_notebook();
1796 1808 };
1797 1809
1798 1810 /**
1799 1811 * List checkpoints for this notebook.
1800 1812 *
1801 1813 * @method list_checkpoint
1802 1814 */
1803 1815 Notebook.prototype.list_checkpoints = function () {
1804 1816 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints';
1805 1817 $.get(url).done(
1806 1818 $.proxy(this.list_checkpoints_success, this)
1807 1819 ).fail(
1808 1820 $.proxy(this.list_checkpoints_error, this)
1809 1821 );
1810 1822 };
1811 1823
1812 1824 /**
1813 1825 * Success callback for listing checkpoints.
1814 1826 *
1815 1827 * @method list_checkpoint_success
1816 1828 * @param {Object} data JSON representation of a checkpoint
1817 1829 * @param {String} status Description of response status
1818 1830 * @param {jqXHR} xhr jQuery Ajax object
1819 1831 */
1820 1832 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
1821 1833 var data = $.parseJSON(data);
1822 1834 if (data.length) {
1823 1835 this.last_checkpoint = data[0];
1824 1836 } else {
1825 1837 this.last_checkpoint = null;
1826 1838 }
1827 1839 $([IPython.events]).trigger('checkpoints_listed.Notebook', [data]);
1828 1840 };
1829 1841
1830 1842 /**
1831 1843 * Failure callback for listing a checkpoint.
1832 1844 *
1833 1845 * @method list_checkpoint_error
1834 1846 * @param {jqXHR} xhr jQuery Ajax object
1835 1847 * @param {String} status Description of response status
1836 1848 * @param {String} error_msg HTTP error message
1837 1849 */
1838 1850 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
1839 1851 $([IPython.events]).trigger('list_checkpoints_failed.Notebook');
1840 1852 };
1841 1853
1842 1854 /**
1843 1855 * Create a checkpoint of this notebook on the server from the most recent save.
1844 1856 *
1845 1857 * @method create_checkpoint
1846 1858 */
1847 1859 Notebook.prototype.create_checkpoint = function () {
1848 1860 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints';
1849 1861 $.post(url).done(
1850 1862 $.proxy(this.create_checkpoint_success, this)
1851 1863 ).fail(
1852 1864 $.proxy(this.create_checkpoint_error, this)
1853 1865 );
1854 1866 };
1855 1867
1856 1868 /**
1857 1869 * Success callback for creating a checkpoint.
1858 1870 *
1859 1871 * @method create_checkpoint_success
1860 1872 * @param {Object} data JSON representation of a checkpoint
1861 1873 * @param {String} status Description of response status
1862 1874 * @param {jqXHR} xhr jQuery Ajax object
1863 1875 */
1864 1876 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
1865 1877 var data = $.parseJSON(data);
1866 1878 this.last_checkpoint = data;
1867 1879 $([IPython.events]).trigger('checkpoint_created.Notebook', data);
1868 1880 };
1869 1881
1870 1882 /**
1871 1883 * Failure callback for creating a checkpoint.
1872 1884 *
1873 1885 * @method create_checkpoint_error
1874 1886 * @param {jqXHR} xhr jQuery Ajax object
1875 1887 * @param {String} status Description of response status
1876 1888 * @param {String} error_msg HTTP error message
1877 1889 */
1878 1890 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
1879 1891 $([IPython.events]).trigger('checkpoint_failed.Notebook');
1880 1892 };
1881 1893
1882 1894 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
1883 1895 var that = this;
1884 1896 var checkpoint = checkpoint || this.last_checkpoint;
1885 1897 if ( ! checkpoint ) {
1886 1898 console.log("restore dialog, but no checkpoint to restore to!");
1887 1899 return;
1888 1900 }
1889 1901 var body = $('<div/>').append(
1890 1902 $('<p/>').addClass("p-space").text(
1891 1903 "Are you sure you want to revert the notebook to " +
1892 1904 "the latest checkpoint?"
1893 1905 ).append(
1894 1906 $("<strong/>").text(
1895 1907 " This cannot be undone."
1896 1908 )
1897 1909 )
1898 1910 ).append(
1899 1911 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
1900 1912 ).append(
1901 1913 $('<p/>').addClass("p-space").text(
1902 1914 Date(checkpoint.last_modified)
1903 1915 ).css("text-align", "center")
1904 1916 );
1905 1917
1906 1918 IPython.dialog.modal({
1907 1919 title : "Revert notebook to checkpoint",
1908 1920 body : body,
1909 1921 buttons : {
1910 1922 Revert : {
1911 1923 class : "btn-danger",
1912 1924 click : function () {
1913 1925 that.restore_checkpoint(checkpoint.checkpoint_id);
1914 1926 }
1915 1927 },
1916 1928 Cancel : {}
1917 1929 }
1918 1930 });
1919 1931 }
1920 1932
1921 1933 /**
1922 1934 * Restore the notebook to a checkpoint state.
1923 1935 *
1924 1936 * @method restore_checkpoint
1925 1937 * @param {String} checkpoint ID
1926 1938 */
1927 1939 Notebook.prototype.restore_checkpoint = function (checkpoint) {
1928 1940 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
1929 1941 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints/' + checkpoint;
1930 1942 $.post(url).done(
1931 1943 $.proxy(this.restore_checkpoint_success, this)
1932 1944 ).fail(
1933 1945 $.proxy(this.restore_checkpoint_error, this)
1934 1946 );
1935 1947 };
1936 1948
1937 1949 /**
1938 1950 * Success callback for restoring a notebook to a checkpoint.
1939 1951 *
1940 1952 * @method restore_checkpoint_success
1941 1953 * @param {Object} data (ignored, should be empty)
1942 1954 * @param {String} status Description of response status
1943 1955 * @param {jqXHR} xhr jQuery Ajax object
1944 1956 */
1945 1957 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
1946 1958 $([IPython.events]).trigger('checkpoint_restored.Notebook');
1947 1959 this.load_notebook(this.notebook_id);
1948 1960 };
1949 1961
1950 1962 /**
1951 1963 * Failure callback for restoring a notebook to a checkpoint.
1952 1964 *
1953 1965 * @method restore_checkpoint_error
1954 1966 * @param {jqXHR} xhr jQuery Ajax object
1955 1967 * @param {String} status Description of response status
1956 1968 * @param {String} error_msg HTTP error message
1957 1969 */
1958 1970 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
1959 1971 $([IPython.events]).trigger('checkpoint_restore_failed.Notebook');
1960 1972 };
1961 1973
1962 1974 /**
1963 1975 * Delete a notebook checkpoint.
1964 1976 *
1965 1977 * @method delete_checkpoint
1966 1978 * @param {String} checkpoint ID
1967 1979 */
1968 1980 Notebook.prototype.delete_checkpoint = function (checkpoint) {
1969 1981 $([IPython.events]).trigger('notebook_restoring.Notebook', checkpoint);
1970 1982 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints/' + checkpoint;
1971 1983 $.ajax(url, {
1972 1984 type: 'DELETE',
1973 1985 success: $.proxy(this.delete_checkpoint_success, this),
1974 1986 error: $.proxy(this.delete_notebook_error,this)
1975 1987 });
1976 1988 };
1977 1989
1978 1990 /**
1979 1991 * Success callback for deleting a notebook checkpoint
1980 1992 *
1981 1993 * @method delete_checkpoint_success
1982 1994 * @param {Object} data (ignored, should be empty)
1983 1995 * @param {String} status Description of response status
1984 1996 * @param {jqXHR} xhr jQuery Ajax object
1985 1997 */
1986 1998 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
1987 1999 $([IPython.events]).trigger('checkpoint_deleted.Notebook', data);
1988 2000 this.load_notebook(this.notebook_id);
1989 2001 };
1990 2002
1991 2003 /**
1992 2004 * Failure callback for deleting a notebook checkpoint.
1993 2005 *
1994 2006 * @method delete_checkpoint_error
1995 2007 * @param {jqXHR} xhr jQuery Ajax object
1996 2008 * @param {String} status Description of response status
1997 2009 * @param {String} error_msg HTTP error message
1998 2010 */
1999 2011 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2000 2012 $([IPython.events]).trigger('checkpoint_delete_failed.Notebook');
2001 2013 };
2002 2014
2003 2015
2004 2016 IPython.Notebook = Notebook;
2005 2017
2006 2018
2007 2019 return IPython;
2008 2020
2009 2021 }(IPython));
2010 2022
@@ -1,490 +1,495 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Kernel
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule Kernel
16 16 */
17 17
18 18 var IPython = (function (IPython) {
19 19
20 20 var utils = IPython.utils;
21 21
22 22 // Initialization and connection.
23 23 /**
24 24 * A Kernel Class to communicate with the Python kernel
25 25 * @Class Kernel
26 26 */
27 27 var Kernel = function (base_url) {
28 28 this.kernel_id = null;
29 29 this.shell_channel = null;
30 30 this.iopub_channel = null;
31 31 this.stdin_channel = null;
32 32 this.base_url = base_url;
33 33 this.running = false;
34 34 this.username = "username";
35 35 this.session_id = utils.uuid();
36 36 this._msg_callbacks = {};
37 37
38 38 if (typeof(WebSocket) !== 'undefined') {
39 39 this.WebSocket = WebSocket;
40 40 } else if (typeof(MozWebSocket) !== 'undefined') {
41 41 this.WebSocket = MozWebSocket;
42 42 } else {
43 43 alert('Your browser does not have WebSocket support, please try Chrome, Safari or Firefox β‰₯ 6. Firefox 4 and 5 are also supported by you have to enable WebSockets in about:config.');
44 44 };
45 45 };
46 46
47 47
48 48 Kernel.prototype._get_msg = function (msg_type, content) {
49 49 var msg = {
50 50 header : {
51 51 msg_id : utils.uuid(),
52 52 username : this.username,
53 53 session : this.session_id,
54 54 msg_type : msg_type
55 55 },
56 56 metadata : {},
57 57 content : content,
58 58 parent_header : {}
59 59 };
60 60 return msg;
61 61 };
62 62
63 63 /**
64 64 * Start the Python kernel
65 65 * @method start
66 66 */
67 67 Kernel.prototype.start = function (notebook_id) {
68 68 var that = this;
69 69 if (!this.running) {
70 70 var qs = $.param({notebook:notebook_id});
71 71 var url = this.base_url + '?' + qs;
72 72 $.post(url,
73 73 $.proxy(that._kernel_started,that),
74 74 'json'
75 75 );
76 76 };
77 77 };
78 78
79 79 /**
80 80 * Restart the python kernel.
81 81 *
82 82 * Emit a 'status_restarting.Kernel' event with
83 83 * the current object as parameter
84 84 *
85 85 * @method restart
86 86 */
87 87 Kernel.prototype.restart = function () {
88 88 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
89 89 var that = this;
90 90 if (this.running) {
91 91 this.stop_channels();
92 92 var url = this.kernel_url + "/restart";
93 93 $.post(url,
94 94 $.proxy(that._kernel_started, that),
95 95 'json'
96 96 );
97 97 };
98 98 };
99 99
100 100
101 101 Kernel.prototype._kernel_started = function (json) {
102 102 console.log("Kernel started: ", json.kernel_id);
103 103 this.running = true;
104 104 this.kernel_id = json.kernel_id;
105 105 var ws_url = json.ws_url;
106 106 if (ws_url.match(/wss?:\/\//) == null) {
107 107 // trailing 's' in https will become wss for secure web sockets
108 108 prot = location.protocol.replace('http', 'ws') + "//";
109 109 ws_url = prot + location.host + ws_url;
110 110 };
111 111 this.ws_url = ws_url;
112 112 this.kernel_url = this.base_url + "/" + this.kernel_id;
113 113 this.start_channels();
114 114 $([IPython.events]).trigger('status_started.Kernel', {kernel: this});
115 115 };
116 116
117 117
118 118 Kernel.prototype._websocket_closed = function(ws_url, early) {
119 119 this.stop_channels();
120 120 $([IPython.events]).trigger('websocket_closed.Kernel',
121 121 {ws_url: ws_url, kernel: this, early: early}
122 122 );
123 123 };
124 124
125 125 /**
126 126 * Start the `shell`and `iopub` channels.
127 127 * Will stop and restart them if they already exist.
128 128 *
129 129 * @method start_channels
130 130 */
131 131 Kernel.prototype.start_channels = function () {
132 132 var that = this;
133 133 this.stop_channels();
134 134 var ws_url = this.ws_url + this.kernel_url;
135 135 console.log("Starting WebSockets:", ws_url);
136 136 this.shell_channel = new this.WebSocket(ws_url + "/shell");
137 137 this.stdin_channel = new this.WebSocket(ws_url + "/stdin");
138 138 this.iopub_channel = new this.WebSocket(ws_url + "/iopub");
139 139 send_cookie = function(){
140 140 // send the session id so the Session object Python-side
141 141 // has the same identity
142 142 this.send(that.session_id + ':' + document.cookie);
143 143 };
144 144 var already_called_onclose = false; // only alert once
145 145 var ws_closed_early = function(evt){
146 146 if (already_called_onclose){
147 147 return;
148 148 }
149 149 already_called_onclose = true;
150 150 if ( ! evt.wasClean ){
151 151 that._websocket_closed(ws_url, true);
152 152 }
153 153 };
154 154 var ws_closed_late = function(evt){
155 155 if (already_called_onclose){
156 156 return;
157 157 }
158 158 already_called_onclose = true;
159 159 if ( ! evt.wasClean ){
160 160 that._websocket_closed(ws_url, false);
161 161 }
162 162 };
163 163 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
164 164 for (var i=0; i < channels.length; i++) {
165 165 channels[i].onopen = send_cookie;
166 166 channels[i].onclose = ws_closed_early;
167 167 }
168 168 // switch from early-close to late-close message after 1s
169 169 setTimeout(function() {
170 170 for (var i=0; i < channels.length; i++) {
171 171 if (channels[i] !== null) {
172 172 channels[i].onclose = ws_closed_late;
173 173 }
174 174 }
175 175 }, 1000);
176 176 this.shell_channel.onmessage = $.proxy(this._handle_shell_reply, this);
177 177 this.iopub_channel.onmessage = $.proxy(this._handle_iopub_reply, this);
178 178 this.stdin_channel.onmessage = $.proxy(this._handle_input_request, this);
179 179
180 180 $([IPython.events]).on('send_input_reply.Kernel', function(evt, data) {
181 181 that.send_input_reply(data);
182 182 });
183 183 };
184 184
185 185 /**
186 186 * Start the `shell`and `iopub` channels.
187 187 * @method stop_channels
188 188 */
189 189 Kernel.prototype.stop_channels = function () {
190 190 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
191 191 for (var i=0; i < channels.length; i++) {
192 192 if ( channels[i] !== null ) {
193 193 channels[i].onclose = function (evt) {};
194 194 channels[i].close();
195 195 }
196 196 };
197 197 this.shell_channel = this.iopub_channel = this.stdin_channel = null;
198 198 };
199 199
200 200 // Main public methods.
201 201
202 202 /**
203 203 * Get info on object asynchronoulsy
204 204 *
205 205 * @async
206 206 * @param objname {string}
207 207 * @param callback {dict}
208 208 * @method object_info_request
209 209 *
210 210 * @example
211 211 *
212 212 * When calling this method pass a callbacks structure of the form:
213 213 *
214 214 * callbacks = {
215 215 * 'object_info_reply': object_info_reply_callback
216 216 * }
217 217 *
218 218 * The `object_info_reply_callback` will be passed the content object of the
219 219 *
220 220 * `object_into_reply` message documented in
221 221 * [IPython dev documentation](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information)
222 222 */
223 223 Kernel.prototype.object_info_request = function (objname, callbacks) {
224 224 if(typeof(objname)!=null && objname!=null)
225 225 {
226 226 var content = {
227 227 oname : objname.toString(),
228 228 };
229 229 var msg = this._get_msg("object_info_request", content);
230 230 this.shell_channel.send(JSON.stringify(msg));
231 231 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
232 232 return msg.header.msg_id;
233 233 }
234 234 return;
235 235 }
236 236
237 237 /**
238 238 * Execute given code into kernel, and pass result to callback.
239 239 *
240 240 * TODO: document input_request in callbacks
241 241 *
242 242 * @async
243 243 * @method execute
244 244 * @param {string} code
245 245 * @param [callbacks] {Object} With the optional following keys
246 246 * @param callbacks.'execute_reply' {function}
247 247 * @param callbacks.'output' {function}
248 248 * @param callbacks.'clear_output' {function}
249 249 * @param callbacks.'set_next_input' {function}
250 250 * @param {object} [options]
251 251 * @param [options.silent=false] {Boolean}
252 252 * @param [options.user_expressions=empty_dict] {Dict}
253 253 * @param [options.user_variables=empty_list] {List od Strings}
254 254 * @param [options.allow_stdin=false] {Boolean} true|false
255 255 *
256 256 * @example
257 257 *
258 258 * The options object should contain the options for the execute call. Its default
259 259 * values are:
260 260 *
261 261 * options = {
262 262 * silent : true,
263 263 * user_variables : [],
264 264 * user_expressions : {},
265 265 * allow_stdin : false
266 266 * }
267 267 *
268 268 * When calling this method pass a callbacks structure of the form:
269 269 *
270 270 * callbacks = {
271 271 * 'execute_reply': execute_reply_callback,
272 272 * 'output': output_callback,
273 273 * 'clear_output': clear_output_callback,
274 274 * 'set_next_input': set_next_input_callback
275 275 * }
276 276 *
277 277 * The `execute_reply_callback` will be passed the content and metadata
278 278 * objects of the `execute_reply` message documented
279 279 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#execute)
280 280 *
281 281 * The `output_callback` will be passed `msg_type` ('stream','display_data','pyout','pyerr')
282 282 * of the output and the content and metadata objects of the PUB/SUB channel that contains the
283 283 * output:
284 284 *
285 285 * http://ipython.org/ipython-doc/dev/development/messaging.html#messages-on-the-pub-sub-socket
286 286 *
287 287 * The `clear_output_callback` will be passed a content object that contains
288 288 * stdout, stderr and other fields that are booleans, as well as the metadata object.
289 289 *
290 290 * The `set_next_input_callback` will be passed the text that should become the next
291 291 * input cell.
292 292 */
293 293 Kernel.prototype.execute = function (code, callbacks, options) {
294 294
295 295 var content = {
296 296 code : code,
297 297 silent : true,
298 298 user_variables : [],
299 299 user_expressions : {},
300 300 allow_stdin : false
301 301 };
302 302 callbacks = callbacks || {};
303 303 if (callbacks.input_request !== undefined) {
304 304 content.allow_stdin = true;
305 305 }
306 306 $.extend(true, content, options)
307 307 $([IPython.events]).trigger('execution_request.Kernel', {kernel: this, content:content});
308 308 var msg = this._get_msg("execute_request", content);
309 309 this.shell_channel.send(JSON.stringify(msg));
310 310 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
311 311 return msg.header.msg_id;
312 312 };
313 313
314 314 /**
315 315 * When calling this method pass a callbacks structure of the form:
316 316 *
317 317 * callbacks = {
318 318 * 'complete_reply': complete_reply_callback
319 319 * }
320 320 *
321 321 * The `complete_reply_callback` will be passed the content object of the
322 322 * `complete_reply` message documented
323 323 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete)
324 324 *
325 325 * @method complete
326 326 * @param line {integer}
327 327 * @param cursor_pos {integer}
328 328 * @param {dict} callbacks
329 329 * @param callbacks.complete_reply {function} `complete_reply_callback`
330 330 *
331 331 */
332 332 Kernel.prototype.complete = function (line, cursor_pos, callbacks) {
333 333 callbacks = callbacks || {};
334 334 var content = {
335 335 text : '',
336 336 line : line,
337 337 cursor_pos : cursor_pos
338 338 };
339 339 var msg = this._get_msg("complete_request", content);
340 340 this.shell_channel.send(JSON.stringify(msg));
341 341 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
342 342 return msg.header.msg_id;
343 343 };
344 344
345 345
346 346 Kernel.prototype.interrupt = function () {
347 347 if (this.running) {
348 348 $([IPython.events]).trigger('status_interrupting.Kernel', {kernel: this});
349 349 $.post(this.kernel_url + "/interrupt");
350 350 };
351 351 };
352 352
353 353
354 354 Kernel.prototype.kill = function () {
355 355 if (this.running) {
356 356 this.running = false;
357 357 var settings = {
358 358 cache : false,
359 359 type : "DELETE"
360 360 };
361 361 $.ajax(this.kernel_url, settings);
362 362 };
363 363 };
364 364
365 365 Kernel.prototype.send_input_reply = function (input) {
366 366 var content = {
367 367 value : input,
368 368 };
369 369 $([IPython.events]).trigger('input_reply.Kernel', {kernel: this, content:content});
370 370 var msg = this._get_msg("input_reply", content);
371 371 this.stdin_channel.send(JSON.stringify(msg));
372 372 return msg.header.msg_id;
373 373 };
374 374
375 375
376 376 // Reply handlers
377 377
378 378 Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
379 379 var callbacks = this._msg_callbacks[msg_id];
380 380 return callbacks;
381 381 };
382 382
383 383
384 384 Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
385 385 this._msg_callbacks[msg_id] = callbacks || {};
386 386 }
387 387
388 388
389 389 Kernel.prototype._handle_shell_reply = function (e) {
390 390 var reply = $.parseJSON(e.data);
391 391 $([IPython.events]).trigger('shell_reply.Kernel', {kernel: this, reply:reply});
392 392 var header = reply.header;
393 393 var content = reply.content;
394 394 var metadata = reply.metadata;
395 395 var msg_type = header.msg_type;
396 396 var callbacks = this.get_callbacks_for_msg(reply.parent_header.msg_id);
397 397 if (callbacks !== undefined) {
398 398 var cb = callbacks[msg_type];
399 399 if (cb !== undefined) {
400 400 cb(content, metadata);
401 401 }
402 402 };
403 403
404 404 if (content.payload !== undefined) {
405 405 var payload = content.payload || [];
406 406 this._handle_payload(callbacks, payload);
407 407 }
408 408 };
409 409
410 410
411 411 Kernel.prototype._handle_payload = function (callbacks, payload) {
412 412 var l = payload.length;
413 413 // Payloads are handled by triggering events because we don't want the Kernel
414 414 // to depend on the Notebook or Pager classes.
415 415 for (var i=0; i<l; i++) {
416 416 if (payload[i].source === 'IPython.kernel.zmq.page.page') {
417 417 var data = {'text':payload[i].text}
418 418 $([IPython.events]).trigger('open_with_text.Pager', data);
419 419 } else if (payload[i].source === 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell.set_next_input') {
420 420 if (callbacks.set_next_input !== undefined) {
421 421 callbacks.set_next_input(payload[i].text)
422 422 }
423 423 }
424 424 };
425 425 };
426 426
427 427
428 428 Kernel.prototype._handle_iopub_reply = function (e) {
429 429 var reply = $.parseJSON(e.data);
430 430 var content = reply.content;
431 431 var msg_type = reply.header.msg_type;
432 432 var metadata = reply.metadata;
433 433 var callbacks = this.get_callbacks_for_msg(reply.parent_header.msg_id);
434 434 if (msg_type !== 'status' && callbacks === undefined) {
435 435 // Message not from one of this notebook's cells and there are no
436 436 // callbacks to handle it.
437 437 return;
438 438 }
439 439 var output_types = ['stream','display_data','pyout','pyerr'];
440 440 if (output_types.indexOf(msg_type) >= 0) {
441 441 var cb = callbacks['output'];
442 442 if (cb !== undefined) {
443 443 cb(msg_type, content, metadata);
444 444 }
445 445 } else if (msg_type === 'status') {
446 446 if (content.execution_state === 'busy') {
447 447 $([IPython.events]).trigger('status_busy.Kernel', {kernel: this});
448 448 } else if (content.execution_state === 'idle') {
449 449 $([IPython.events]).trigger('status_idle.Kernel', {kernel: this});
450 450 } else if (content.execution_state === 'restarting') {
451 // autorestarting is distinct from restarting,
452 // in that it means the kernel died and the server is restarting it.
453 // status_restarting sets the notification widget,
454 // autorestart shows the more prominent dialog.
455 $([IPython.events]).trigger('status_autorestarting.Kernel', {kernel: this});
451 456 $([IPython.events]).trigger('status_restarting.Kernel', {kernel: this});
452 457 } else if (content.execution_state === 'dead') {
453 458 this.stop_channels();
454 459 $([IPython.events]).trigger('status_dead.Kernel', {kernel: this});
455 460 };
456 461 } else if (msg_type === 'clear_output') {
457 462 var cb = callbacks['clear_output'];
458 463 if (cb !== undefined) {
459 464 cb(content, metadata);
460 465 }
461 466 };
462 467 };
463 468
464 469
465 470 Kernel.prototype._handle_input_request = function (e) {
466 471 var request = $.parseJSON(e.data);
467 472 var header = request.header;
468 473 var content = request.content;
469 474 var metadata = request.metadata;
470 475 var msg_type = header.msg_type;
471 476 if (msg_type !== 'input_request') {
472 477 console.log("Invalid input request!", request);
473 478 return;
474 479 }
475 480 var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
476 481 if (callbacks !== undefined) {
477 482 var cb = callbacks[msg_type];
478 483 if (cb !== undefined) {
479 484 cb(content, metadata);
480 485 }
481 486 };
482 487 };
483 488
484 489
485 490 IPython.Kernel = Kernel;
486 491
487 492 return IPython;
488 493
489 494 }(IPython));
490 495
General Comments 0
You need to be logged in to leave comments. Login now