##// END OF EJS Templates
avoid race condition when deleting/starting sessions...
MinRK -
Show More
@@ -1,81 +1,91 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 ], function(IPython, $, utils) {
9 9 "use strict";
10 10
11 11 var KernelSelector = function(selector, notebook) {
12 12 this.selector = selector;
13 13 this.notebook = notebook;
14 14 this.events = notebook.events;
15 15 this.current_selection = notebook.default_kernel_name;
16 16 this.kernelspecs = {};
17 17 if (this.selector !== undefined) {
18 18 this.element = $(selector);
19 19 this.request_kernelspecs();
20 20 }
21 21 this.bind_events();
22 22 // Make the object globally available for user convenience & inspection
23 23 IPython.kernelselector = this;
24 24 };
25 25
26 26 KernelSelector.prototype.request_kernelspecs = function() {
27 27 var url = utils.url_join_encode(this.notebook.base_url, 'api/kernelspecs');
28 28 $.ajax(url, {success: $.proxy(this._got_kernelspecs, this)});
29 29 };
30 30
31 31 KernelSelector.prototype._got_kernelspecs = function(data, status, xhr) {
32 32 this.kernelspecs = {};
33 33 var menu = this.element.find("#kernel_selector");
34 34 var change_kernel_submenu = $("#menu-change-kernel-submenu");
35 35 for (var i = 0; i < data.length; i++) {
36 36 var ks = data[i];
37 37 this.kernelspecs[ks.name] = ks;
38 38 var ksentry = $("<li>").attr("id", "kernel-" +ks.name).append($('<a>')
39 39 .attr('href', '#')
40 40 .click($.proxy(this.change_kernel, this, ks.name))
41 41 .text(ks.display_name));
42 42 menu.append(ksentry);
43 43
44 44 var ks_submenu_entry = $("<li>").attr("id", "kernel-submenu-"+ks.name).append($('<a>')
45 45 .attr('href', '#')
46 46 .click($.proxy(this.change_kernel, this, ks.name))
47 47 .text(ks.display_name));
48 48 change_kernel_submenu.append(ks_submenu_entry);
49 49 }
50 50 };
51 51
52 52 KernelSelector.prototype.change_kernel = function(kernel_name) {
53 53 if (kernel_name === this.current_selection) {
54 54 return;
55 55 }
56 56 var ks = this.kernelspecs[kernel_name];
57 try {
58 this.notebook.start_session(kernel_name);
59 } catch (e) {
60 if (e.name === 'SessionAlreadyStarting') {
61 console.log("Cannot change kernel while waiting for pending session start.");
62 } else {
63 // unhandled error
64 throw e;
65 }
66 // only trigger spec_changed if change was successful
67 return;
68 }
57 69 this.events.trigger('spec_changed.Kernel', ks);
58 this.notebook.session.delete();
59 this.notebook.start_session(kernel_name);
60 70 };
61 71
62 72 KernelSelector.prototype.bind_events = function() {
63 73 var that = this;
64 74 this.events.on('spec_changed.Kernel', function(event, data) {
65 75 that.current_selection = data.name;
66 76 that.element.find("#current_kernel_spec").find('.kernel_name').text(data.display_name);
67 77 });
68 78
69 79 this.events.on('started.Session', function(events, session) {
70 80 if (session.kernel_name !== that.current_selection) {
71 81 // If we created a 'python' session, we only know if it's Python
72 82 // 3 or 2 on the server's reply, so we fire the event again to
73 83 // set things up.
74 84 var ks = that.kernelspecs[session.kernel_name];
75 85 that.events.trigger('spec_changed.Kernel', ks);
76 86 }
77 87 });
78 88 };
79 89
80 90 return {'KernelSelector': KernelSelector};
81 91 });
@@ -1,351 +1,352 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'notebook/js/tour',
9 9 'bootstrap',
10 10 ], function(IPython, $, utils, tour) {
11 11 "use strict";
12 12
13 13 var MenuBar = function (selector, options) {
14 14 // Constructor
15 15 //
16 16 // A MenuBar Class to generate the menubar of IPython notebook
17 17 //
18 18 // Parameters:
19 19 // selector: string
20 20 // options: dictionary
21 21 // Dictionary of keyword arguments.
22 22 // notebook: Notebook instance
23 23 // layout_manager: LayoutManager instance
24 24 // events: $(Events) instance
25 25 // save_widget: SaveWidget instance
26 26 // quick_help: QuickHelp instance
27 27 // base_url : string
28 28 // notebook_path : string
29 29 // notebook_name : string
30 30 options = options || {};
31 31 this.base_url = options.base_url || utils.get_body_data("baseUrl");
32 32 this.selector = selector;
33 33 this.notebook = options.notebook;
34 34 this.layout_manager = options.layout_manager;
35 35 this.events = options.events;
36 36 this.save_widget = options.save_widget;
37 37 this.quick_help = options.quick_help;
38 38
39 39 try {
40 40 this.tour = new tour.Tour(this.notebook, this.events);
41 41 } catch (e) {
42 42 this.tour = undefined;
43 43 console.log("Failed to instantiate Notebook Tour", e);
44 44 }
45 45
46 46 if (this.selector !== undefined) {
47 47 this.element = $(selector);
48 48 this.style();
49 49 this.bind_events();
50 50 }
51 51 };
52 52
53 53 // TODO: This has definitively nothing to do with style ...
54 54 MenuBar.prototype.style = function () {
55 55 var that = this;
56 56 this.element.find("li").click(function (event, ui) {
57 57 // The selected cell loses focus when the menu is entered, so we
58 58 // re-select it upon selection.
59 59 var i = that.notebook.get_selected_index();
60 60 that.notebook.select(i);
61 61 }
62 62 );
63 63 };
64 64
65 65 MenuBar.prototype._nbconvert = function (format, download) {
66 66 download = download || false;
67 67 var notebook_path = this.notebook.notebook_path;
68 68 var notebook_name = this.notebook.notebook_name;
69 69 if (this.notebook.dirty) {
70 70 this.notebook.save_notebook({async : false});
71 71 }
72 72 var url = utils.url_join_encode(
73 73 this.base_url,
74 74 'nbconvert',
75 75 format,
76 76 notebook_path,
77 77 notebook_name
78 78 ) + "?download=" + download.toString();
79 79
80 80 window.open(url);
81 81 };
82 82
83 83 MenuBar.prototype.bind_events = function () {
84 84 // File
85 85 var that = this;
86 86 this.element.find('#new_notebook').click(function () {
87 87 that.notebook.new_notebook();
88 88 });
89 89 this.element.find('#open_notebook').click(function () {
90 90 window.open(utils.url_join_encode(
91 91 that.notebook.base_url,
92 92 'tree',
93 93 that.notebook.notebook_path
94 94 ));
95 95 });
96 96 this.element.find('#copy_notebook').click(function () {
97 97 that.notebook.copy_notebook();
98 98 return false;
99 99 });
100 100 this.element.find('#download_ipynb').click(function () {
101 101 var base_url = that.notebook.base_url;
102 102 var notebook_path = that.notebook.notebook_path;
103 103 var notebook_name = that.notebook.notebook_name;
104 104 if (that.notebook.dirty) {
105 105 that.notebook.save_notebook({async : false});
106 106 }
107 107
108 108 var url = utils.url_join_encode(
109 109 base_url,
110 110 'files',
111 111 notebook_path,
112 112 notebook_name
113 113 );
114 114 window.location.assign(url);
115 115 });
116 116
117 117 this.element.find('#print_preview').click(function () {
118 118 that._nbconvert('html', false);
119 119 });
120 120
121 121 this.element.find('#download_py').click(function () {
122 122 that._nbconvert('python', true);
123 123 });
124 124
125 125 this.element.find('#download_html').click(function () {
126 126 that._nbconvert('html', true);
127 127 });
128 128
129 129 this.element.find('#download_rst').click(function () {
130 130 that._nbconvert('rst', true);
131 131 });
132 132
133 133 this.element.find('#download_pdf').click(function () {
134 134 that._nbconvert('pdf', true);
135 135 });
136 136
137 137 this.element.find('#rename_notebook').click(function () {
138 138 that.save_widget.rename_notebook({notebook: that.notebook});
139 139 });
140 140 this.element.find('#save_checkpoint').click(function () {
141 141 that.notebook.save_checkpoint();
142 142 });
143 143 this.element.find('#restore_checkpoint').click(function () {
144 144 });
145 145 this.element.find('#trust_notebook').click(function () {
146 146 that.notebook.trust_notebook();
147 147 });
148 148 this.events.on('trust_changed.Notebook', function (event, trusted) {
149 149 if (trusted) {
150 150 that.element.find('#trust_notebook')
151 151 .addClass("disabled")
152 152 .find("a").text("Trusted Notebook");
153 153 } else {
154 154 that.element.find('#trust_notebook')
155 155 .removeClass("disabled")
156 156 .find("a").text("Trust Notebook");
157 157 }
158 158 });
159 159 this.element.find('#kill_and_exit').click(function () {
160 that.notebook.session.delete();
161 setTimeout(function(){
160 var close_window = function () {
162 161 // allow closing of new tabs in Chromium, impossible in FF
163 162 window.open('', '_self', '');
164 163 window.close();
165 }, 500);
164 };
165 // finish with close on success or failure
166 that.notebook.session.delete(close_window, close_window);
166 167 });
167 168 // Edit
168 169 this.element.find('#cut_cell').click(function () {
169 170 that.notebook.cut_cell();
170 171 });
171 172 this.element.find('#copy_cell').click(function () {
172 173 that.notebook.copy_cell();
173 174 });
174 175 this.element.find('#delete_cell').click(function () {
175 176 that.notebook.delete_cell();
176 177 });
177 178 this.element.find('#undelete_cell').click(function () {
178 179 that.notebook.undelete_cell();
179 180 });
180 181 this.element.find('#split_cell').click(function () {
181 182 that.notebook.split_cell();
182 183 });
183 184 this.element.find('#merge_cell_above').click(function () {
184 185 that.notebook.merge_cell_above();
185 186 });
186 187 this.element.find('#merge_cell_below').click(function () {
187 188 that.notebook.merge_cell_below();
188 189 });
189 190 this.element.find('#move_cell_up').click(function () {
190 191 that.notebook.move_cell_up();
191 192 });
192 193 this.element.find('#move_cell_down').click(function () {
193 194 that.notebook.move_cell_down();
194 195 });
195 196 this.element.find('#edit_nb_metadata').click(function () {
196 197 that.notebook.edit_metadata({
197 198 notebook: that.notebook,
198 199 keyboard_manager: that.notebook.keyboard_manager});
199 200 });
200 201
201 202 // View
202 203 this.element.find('#toggle_header').click(function () {
203 204 $('div#header').toggle();
204 205 that.layout_manager.do_resize();
205 206 });
206 207 this.element.find('#toggle_toolbar').click(function () {
207 208 $('div#maintoolbar').toggle();
208 209 that.layout_manager.do_resize();
209 210 });
210 211 // Insert
211 212 this.element.find('#insert_cell_above').click(function () {
212 213 that.notebook.insert_cell_above('code');
213 214 that.notebook.select_prev();
214 215 });
215 216 this.element.find('#insert_cell_below').click(function () {
216 217 that.notebook.insert_cell_below('code');
217 218 that.notebook.select_next();
218 219 });
219 220 // Cell
220 221 this.element.find('#run_cell').click(function () {
221 222 that.notebook.execute_cell();
222 223 });
223 224 this.element.find('#run_cell_select_below').click(function () {
224 225 that.notebook.execute_cell_and_select_below();
225 226 });
226 227 this.element.find('#run_cell_insert_below').click(function () {
227 228 that.notebook.execute_cell_and_insert_below();
228 229 });
229 230 this.element.find('#run_all_cells').click(function () {
230 231 that.notebook.execute_all_cells();
231 232 });
232 233 this.element.find('#run_all_cells_above').click(function () {
233 234 that.notebook.execute_cells_above();
234 235 });
235 236 this.element.find('#run_all_cells_below').click(function () {
236 237 that.notebook.execute_cells_below();
237 238 });
238 239 this.element.find('#to_code').click(function () {
239 240 that.notebook.to_code();
240 241 });
241 242 this.element.find('#to_markdown').click(function () {
242 243 that.notebook.to_markdown();
243 244 });
244 245 this.element.find('#to_raw').click(function () {
245 246 that.notebook.to_raw();
246 247 });
247 248 this.element.find('#to_heading1').click(function () {
248 249 that.notebook.to_heading(undefined, 1);
249 250 });
250 251 this.element.find('#to_heading2').click(function () {
251 252 that.notebook.to_heading(undefined, 2);
252 253 });
253 254 this.element.find('#to_heading3').click(function () {
254 255 that.notebook.to_heading(undefined, 3);
255 256 });
256 257 this.element.find('#to_heading4').click(function () {
257 258 that.notebook.to_heading(undefined, 4);
258 259 });
259 260 this.element.find('#to_heading5').click(function () {
260 261 that.notebook.to_heading(undefined, 5);
261 262 });
262 263 this.element.find('#to_heading6').click(function () {
263 264 that.notebook.to_heading(undefined, 6);
264 265 });
265 266
266 267 this.element.find('#toggle_current_output').click(function () {
267 268 that.notebook.toggle_output();
268 269 });
269 270 this.element.find('#toggle_current_output_scroll').click(function () {
270 271 that.notebook.toggle_output_scroll();
271 272 });
272 273 this.element.find('#clear_current_output').click(function () {
273 274 that.notebook.clear_output();
274 275 });
275 276
276 277 this.element.find('#toggle_all_output').click(function () {
277 278 that.notebook.toggle_all_output();
278 279 });
279 280 this.element.find('#toggle_all_output_scroll').click(function () {
280 281 that.notebook.toggle_all_output_scroll();
281 282 });
282 283 this.element.find('#clear_all_output').click(function () {
283 284 that.notebook.clear_all_output();
284 285 });
285 286
286 287 // Kernel
287 288 this.element.find('#int_kernel').click(function () {
288 289 that.notebook.session.interrupt_kernel();
289 290 });
290 291 this.element.find('#restart_kernel').click(function () {
291 292 that.notebook.restart_kernel();
292 293 });
293 294 // Help
294 295 if (this.tour) {
295 296 this.element.find('#notebook_tour').click(function () {
296 297 that.tour.start();
297 298 });
298 299 } else {
299 300 this.element.find('#notebook_tour').addClass("disabled");
300 301 }
301 302 this.element.find('#keyboard_shortcuts').click(function () {
302 303 that.quick_help.show_keyboard_shortcuts();
303 304 });
304 305
305 306 this.update_restore_checkpoint(null);
306 307
307 308 this.events.on('checkpoints_listed.Notebook', function (event, data) {
308 309 that.update_restore_checkpoint(that.notebook.checkpoints);
309 310 });
310 311
311 312 this.events.on('checkpoint_created.Notebook', function (event, data) {
312 313 that.update_restore_checkpoint(that.notebook.checkpoints);
313 314 });
314 315 };
315 316
316 317 MenuBar.prototype.update_restore_checkpoint = function(checkpoints) {
317 318 var ul = this.element.find("#restore_checkpoint").find("ul");
318 319 ul.empty();
319 320 if (!checkpoints || checkpoints.length === 0) {
320 321 ul.append(
321 322 $("<li/>")
322 323 .addClass("disabled")
323 324 .append(
324 325 $("<a/>")
325 326 .text("No checkpoints")
326 327 )
327 328 );
328 329 return;
329 330 }
330 331
331 332 var that = this;
332 333 checkpoints.map(function (checkpoint) {
333 334 var d = new Date(checkpoint.last_modified);
334 335 ul.append(
335 336 $("<li/>").append(
336 337 $("<a/>")
337 338 .attr("href", "#")
338 339 .text(d.format("mmm dd HH:MM:ss"))
339 340 .click(function () {
340 341 that.notebook.restore_checkpoint_dialog(checkpoint);
341 342 })
342 343 )
343 344 );
344 345 });
345 346 };
346 347
347 348 // Backwards compatability.
348 349 IPython.MenuBar = MenuBar;
349 350
350 351 return {'MenuBar': MenuBar};
351 352 });
@@ -1,2581 +1,2619 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'base/js/dialog',
9 9 'notebook/js/textcell',
10 10 'notebook/js/codecell',
11 11 'services/sessions/js/session',
12 12 'notebook/js/celltoolbar',
13 13 'components/marked/lib/marked',
14 14 'highlight',
15 15 'notebook/js/mathjaxutils',
16 16 'base/js/keyboard',
17 17 'notebook/js/tooltip',
18 18 'notebook/js/celltoolbarpresets/default',
19 19 'notebook/js/celltoolbarpresets/rawcell',
20 20 'notebook/js/celltoolbarpresets/slideshow',
21 21 ], function (
22 22 IPython,
23 23 $,
24 24 utils,
25 25 dialog,
26 26 textcell,
27 27 codecell,
28 28 session,
29 29 celltoolbar,
30 30 marked,
31 31 hljs,
32 32 mathjaxutils,
33 33 keyboard,
34 34 tooltip,
35 35 default_celltoolbar,
36 36 rawcell_celltoolbar,
37 37 slideshow_celltoolbar
38 38 ) {
39 39
40 40 var Notebook = function (selector, options) {
41 41 // Constructor
42 42 //
43 43 // A notebook contains and manages cells.
44 44 //
45 45 // Parameters:
46 46 // selector: string
47 47 // options: dictionary
48 48 // Dictionary of keyword arguments.
49 49 // events: $(Events) instance
50 50 // keyboard_manager: KeyboardManager instance
51 51 // save_widget: SaveWidget instance
52 52 // config: dictionary
53 53 // base_url : string
54 54 // notebook_path : string
55 55 // notebook_name : string
56 56 this.config = options.config || {};
57 57 this.base_url = options.base_url;
58 58 this.notebook_path = options.notebook_path;
59 59 this.notebook_name = options.notebook_name;
60 60 this.events = options.events;
61 61 this.keyboard_manager = options.keyboard_manager;
62 62 this.save_widget = options.save_widget;
63 63 this.tooltip = new tooltip.Tooltip(this.events);
64 64 this.ws_url = options.ws_url;
65 this._session_starting = false;
65 66 // default_kernel_name is a temporary measure while we implement proper
66 67 // kernel selection and delayed start. Do not rely on it.
67 68 this.default_kernel_name = 'python';
68 69 // TODO: This code smells (and the other `= this` line a couple lines down)
69 70 // We need a better way to deal with circular instance references.
70 71 this.keyboard_manager.notebook = this;
71 72 this.save_widget.notebook = this;
72 73
73 74 mathjaxutils.init();
74 75
75 76 if (marked) {
76 77 marked.setOptions({
77 78 gfm : true,
78 79 tables: true,
79 80 langPrefix: "language-",
80 81 highlight: function(code, lang) {
81 82 if (!lang) {
82 83 // no language, no highlight
83 84 return code;
84 85 }
85 86 var highlighted;
86 87 try {
87 88 highlighted = hljs.highlight(lang, code, false);
88 89 } catch(err) {
89 90 highlighted = hljs.highlightAuto(code);
90 91 }
91 92 return highlighted.value;
92 93 }
93 94 });
94 95 }
95 96
96 97 this.element = $(selector);
97 98 this.element.scroll();
98 99 this.element.data("notebook", this);
99 100 this.next_prompt_number = 1;
100 101 this.session = null;
101 102 this.kernel = null;
102 103 this.clipboard = null;
103 104 this.undelete_backup = null;
104 105 this.undelete_index = null;
105 106 this.undelete_below = false;
106 107 this.paste_enabled = false;
107 108 // It is important to start out in command mode to match the intial mode
108 109 // of the KeyboardManager.
109 110 this.mode = 'command';
110 111 this.set_dirty(false);
111 112 this.metadata = {};
112 113 this._checkpoint_after_save = false;
113 114 this.last_checkpoint = null;
114 115 this.checkpoints = [];
115 116 this.autosave_interval = 0;
116 117 this.autosave_timer = null;
117 118 // autosave *at most* every two minutes
118 119 this.minimum_autosave_interval = 120000;
119 120 // single worksheet for now
120 121 this.worksheet_metadata = {};
121 122 this.notebook_name_blacklist_re = /[\/\\:]/;
122 123 this.nbformat = 3; // Increment this when changing the nbformat
123 124 this.nbformat_minor = 0; // Increment this when changing the nbformat
124 125 this.codemirror_mode = 'ipython';
125 126 this.create_elements();
126 127 this.bind_events();
127 128 this.save_notebook = function() { // don't allow save until notebook_loaded
128 129 this.save_notebook_error(null, null, "Load failed, save is disabled");
129 130 };
130 131
131 132 // Trigger cell toolbar registration.
132 133 default_celltoolbar.register(this);
133 134 rawcell_celltoolbar.register(this);
134 135 slideshow_celltoolbar.register(this);
135 136 };
136 137
137 138
138 139 /**
139 140 * Create an HTML and CSS representation of the notebook.
140 141 *
141 142 * @method create_elements
142 143 */
143 144 Notebook.prototype.create_elements = function () {
144 145 var that = this;
145 146 this.element.attr('tabindex','-1');
146 147 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
147 148 // We add this end_space div to the end of the notebook div to:
148 149 // i) provide a margin between the last cell and the end of the notebook
149 150 // ii) to prevent the div from scrolling up when the last cell is being
150 151 // edited, but is too low on the page, which browsers will do automatically.
151 152 var end_space = $('<div/>').addClass('end_space');
152 153 end_space.dblclick(function (e) {
153 154 var ncells = that.ncells();
154 155 that.insert_cell_below('code',ncells-1);
155 156 });
156 157 this.element.append(this.container);
157 158 this.container.append(end_space);
158 159 };
159 160
160 161 /**
161 162 * Bind JavaScript events: key presses and custom IPython events.
162 163 *
163 164 * @method bind_events
164 165 */
165 166 Notebook.prototype.bind_events = function () {
166 167 var that = this;
167 168
168 169 this.events.on('set_next_input.Notebook', function (event, data) {
169 170 var index = that.find_cell_index(data.cell);
170 171 var new_cell = that.insert_cell_below('code',index);
171 172 new_cell.set_text(data.text);
172 173 that.dirty = true;
173 174 });
174 175
175 176 this.events.on('set_dirty.Notebook', function (event, data) {
176 177 that.dirty = data.value;
177 178 });
178 179
179 180 this.events.on('trust_changed.Notebook', function (event, data) {
180 181 that.trusted = data.value;
181 182 });
182 183
183 184 this.events.on('select.Cell', function (event, data) {
184 185 var index = that.find_cell_index(data.cell);
185 186 that.select(index);
186 187 });
187 188
188 189 this.events.on('edit_mode.Cell', function (event, data) {
189 190 that.handle_edit_mode(data.cell);
190 191 });
191 192
192 193 this.events.on('command_mode.Cell', function (event, data) {
193 194 that.handle_command_mode(data.cell);
194 195 });
195 196
196 197 this.events.on('status_autorestarting.Kernel', function () {
197 198 dialog.modal({
198 199 notebook: that,
199 200 keyboard_manager: that.keyboard_manager,
200 201 title: "Kernel Restarting",
201 202 body: "The kernel appears to have died. It will restart automatically.",
202 203 buttons: {
203 204 OK : {
204 205 class : "btn-primary"
205 206 }
206 207 }
207 208 });
208 209 });
209 210
210 211 this.events.on('spec_changed.Kernel', function(event, data) {
211 212 that.set_kernelspec_metadata(data);
212 213 if (data.codemirror_mode) {
213 214 that.set_codemirror_mode(data.codemirror_mode);
214 215 }
215 216 });
216 217
217 218 var collapse_time = function (time) {
218 219 var app_height = $('#ipython-main-app').height(); // content height
219 220 var splitter_height = $('div#pager_splitter').outerHeight(true);
220 221 var new_height = app_height - splitter_height;
221 222 that.element.animate({height : new_height + 'px'}, time);
222 223 };
223 224
224 225 this.element.bind('collapse_pager', function (event, extrap) {
225 226 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
226 227 collapse_time(time);
227 228 });
228 229
229 230 var expand_time = function (time) {
230 231 var app_height = $('#ipython-main-app').height(); // content height
231 232 var splitter_height = $('div#pager_splitter').outerHeight(true);
232 233 var pager_height = $('div#pager').outerHeight(true);
233 234 var new_height = app_height - pager_height - splitter_height;
234 235 that.element.animate({height : new_height + 'px'}, time);
235 236 };
236 237
237 238 this.element.bind('expand_pager', function (event, extrap) {
238 239 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
239 240 expand_time(time);
240 241 });
241 242
242 243 // Firefox 22 broke $(window).on("beforeunload")
243 244 // I'm not sure why or how.
244 245 window.onbeforeunload = function (e) {
245 246 // TODO: Make killing the kernel configurable.
246 247 var kill_kernel = false;
247 248 if (kill_kernel) {
248 249 that.session.kill_kernel();
249 250 }
250 251 // if we are autosaving, trigger an autosave on nav-away.
251 252 // still warn, because if we don't the autosave may fail.
252 253 if (that.dirty) {
253 254 if ( that.autosave_interval ) {
254 255 // schedule autosave in a timeout
255 256 // this gives you a chance to forcefully discard changes
256 257 // by reloading the page if you *really* want to.
257 258 // the timer doesn't start until you *dismiss* the dialog.
258 259 setTimeout(function () {
259 260 if (that.dirty) {
260 261 that.save_notebook();
261 262 }
262 263 }, 1000);
263 264 return "Autosave in progress, latest changes may be lost.";
264 265 } else {
265 266 return "Unsaved changes will be lost.";
266 267 }
267 268 }
268 269 // Null is the *only* return value that will make the browser not
269 270 // pop up the "don't leave" dialog.
270 271 return null;
271 272 };
272 273 };
273 274
274 275 /**
275 276 * Set the dirty flag, and trigger the set_dirty.Notebook event
276 277 *
277 278 * @method set_dirty
278 279 */
279 280 Notebook.prototype.set_dirty = function (value) {
280 281 if (value === undefined) {
281 282 value = true;
282 283 }
283 284 if (this.dirty == value) {
284 285 return;
285 286 }
286 287 this.events.trigger('set_dirty.Notebook', {value: value});
287 288 };
288 289
289 290 /**
290 291 * Scroll the top of the page to a given cell.
291 292 *
292 293 * @method scroll_to_cell
293 294 * @param {Number} cell_number An index of the cell to view
294 295 * @param {Number} time Animation time in milliseconds
295 296 * @return {Number} Pixel offset from the top of the container
296 297 */
297 298 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
298 299 var cells = this.get_cells();
299 300 time = time || 0;
300 301 cell_number = Math.min(cells.length-1,cell_number);
301 302 cell_number = Math.max(0 ,cell_number);
302 303 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
303 304 this.element.animate({scrollTop:scroll_value}, time);
304 305 return scroll_value;
305 306 };
306 307
307 308 /**
308 309 * Scroll to the bottom of the page.
309 310 *
310 311 * @method scroll_to_bottom
311 312 */
312 313 Notebook.prototype.scroll_to_bottom = function () {
313 314 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
314 315 };
315 316
316 317 /**
317 318 * Scroll to the top of the page.
318 319 *
319 320 * @method scroll_to_top
320 321 */
321 322 Notebook.prototype.scroll_to_top = function () {
322 323 this.element.animate({scrollTop:0}, 0);
323 324 };
324 325
325 326 // Edit Notebook metadata
326 327
327 328 Notebook.prototype.edit_metadata = function () {
328 329 var that = this;
329 330 dialog.edit_metadata({
330 331 md: this.metadata,
331 332 callback: function (md) {
332 333 that.metadata = md;
333 334 },
334 335 name: 'Notebook',
335 336 notebook: this,
336 337 keyboard_manager: this.keyboard_manager});
337 338 };
338 339
339 340 Notebook.prototype.set_kernelspec_metadata = function(ks) {
340 341 var tostore = {};
341 342 $.map(ks, function(value, field) {
342 343 if (field !== 'argv' && field !== 'env') {
343 344 tostore[field] = value;
344 345 }
345 346 });
346 347 this.metadata.kernelspec = tostore;
347 348 }
348 349
349 350 // Cell indexing, retrieval, etc.
350 351
351 352 /**
352 353 * Get all cell elements in the notebook.
353 354 *
354 355 * @method get_cell_elements
355 356 * @return {jQuery} A selector of all cell elements
356 357 */
357 358 Notebook.prototype.get_cell_elements = function () {
358 359 return this.container.children("div.cell");
359 360 };
360 361
361 362 /**
362 363 * Get a particular cell element.
363 364 *
364 365 * @method get_cell_element
365 366 * @param {Number} index An index of a cell to select
366 367 * @return {jQuery} A selector of the given cell.
367 368 */
368 369 Notebook.prototype.get_cell_element = function (index) {
369 370 var result = null;
370 371 var e = this.get_cell_elements().eq(index);
371 372 if (e.length !== 0) {
372 373 result = e;
373 374 }
374 375 return result;
375 376 };
376 377
377 378 /**
378 379 * Try to get a particular cell by msg_id.
379 380 *
380 381 * @method get_msg_cell
381 382 * @param {String} msg_id A message UUID
382 383 * @return {Cell} Cell or null if no cell was found.
383 384 */
384 385 Notebook.prototype.get_msg_cell = function (msg_id) {
385 386 return codecell.CodeCell.msg_cells[msg_id] || null;
386 387 };
387 388
388 389 /**
389 390 * Count the cells in this notebook.
390 391 *
391 392 * @method ncells
392 393 * @return {Number} The number of cells in this notebook
393 394 */
394 395 Notebook.prototype.ncells = function () {
395 396 return this.get_cell_elements().length;
396 397 };
397 398
398 399 /**
399 400 * Get all Cell objects in this notebook.
400 401 *
401 402 * @method get_cells
402 403 * @return {Array} This notebook's Cell objects
403 404 */
404 405 // TODO: we are often calling cells as cells()[i], which we should optimize
405 406 // to cells(i) or a new method.
406 407 Notebook.prototype.get_cells = function () {
407 408 return this.get_cell_elements().toArray().map(function (e) {
408 409 return $(e).data("cell");
409 410 });
410 411 };
411 412
412 413 /**
413 414 * Get a Cell object from this notebook.
414 415 *
415 416 * @method get_cell
416 417 * @param {Number} index An index of a cell to retrieve
417 418 * @return {Cell} A particular cell
418 419 */
419 420 Notebook.prototype.get_cell = function (index) {
420 421 var result = null;
421 422 var ce = this.get_cell_element(index);
422 423 if (ce !== null) {
423 424 result = ce.data('cell');
424 425 }
425 426 return result;
426 427 };
427 428
428 429 /**
429 430 * Get the cell below a given cell.
430 431 *
431 432 * @method get_next_cell
432 433 * @param {Cell} cell The provided cell
433 434 * @return {Cell} The next cell
434 435 */
435 436 Notebook.prototype.get_next_cell = function (cell) {
436 437 var result = null;
437 438 var index = this.find_cell_index(cell);
438 439 if (this.is_valid_cell_index(index+1)) {
439 440 result = this.get_cell(index+1);
440 441 }
441 442 return result;
442 443 };
443 444
444 445 /**
445 446 * Get the cell above a given cell.
446 447 *
447 448 * @method get_prev_cell
448 449 * @param {Cell} cell The provided cell
449 450 * @return {Cell} The previous cell
450 451 */
451 452 Notebook.prototype.get_prev_cell = function (cell) {
452 453 // TODO: off-by-one
453 454 // nb.get_prev_cell(nb.get_cell(1)) is null
454 455 var result = null;
455 456 var index = this.find_cell_index(cell);
456 457 if (index !== null && index > 1) {
457 458 result = this.get_cell(index-1);
458 459 }
459 460 return result;
460 461 };
461 462
462 463 /**
463 464 * Get the numeric index of a given cell.
464 465 *
465 466 * @method find_cell_index
466 467 * @param {Cell} cell The provided cell
467 468 * @return {Number} The cell's numeric index
468 469 */
469 470 Notebook.prototype.find_cell_index = function (cell) {
470 471 var result = null;
471 472 this.get_cell_elements().filter(function (index) {
472 473 if ($(this).data("cell") === cell) {
473 474 result = index;
474 475 }
475 476 });
476 477 return result;
477 478 };
478 479
479 480 /**
480 481 * Get a given index , or the selected index if none is provided.
481 482 *
482 483 * @method index_or_selected
483 484 * @param {Number} index A cell's index
484 485 * @return {Number} The given index, or selected index if none is provided.
485 486 */
486 487 Notebook.prototype.index_or_selected = function (index) {
487 488 var i;
488 489 if (index === undefined || index === null) {
489 490 i = this.get_selected_index();
490 491 if (i === null) {
491 492 i = 0;
492 493 }
493 494 } else {
494 495 i = index;
495 496 }
496 497 return i;
497 498 };
498 499
499 500 /**
500 501 * Get the currently selected cell.
501 502 * @method get_selected_cell
502 503 * @return {Cell} The selected cell
503 504 */
504 505 Notebook.prototype.get_selected_cell = function () {
505 506 var index = this.get_selected_index();
506 507 return this.get_cell(index);
507 508 };
508 509
509 510 /**
510 511 * Check whether a cell index is valid.
511 512 *
512 513 * @method is_valid_cell_index
513 514 * @param {Number} index A cell index
514 515 * @return True if the index is valid, false otherwise
515 516 */
516 517 Notebook.prototype.is_valid_cell_index = function (index) {
517 518 if (index !== null && index >= 0 && index < this.ncells()) {
518 519 return true;
519 520 } else {
520 521 return false;
521 522 }
522 523 };
523 524
524 525 /**
525 526 * Get the index of the currently selected cell.
526 527
527 528 * @method get_selected_index
528 529 * @return {Number} The selected cell's numeric index
529 530 */
530 531 Notebook.prototype.get_selected_index = function () {
531 532 var result = null;
532 533 this.get_cell_elements().filter(function (index) {
533 534 if ($(this).data("cell").selected === true) {
534 535 result = index;
535 536 }
536 537 });
537 538 return result;
538 539 };
539 540
540 541
541 542 // Cell selection.
542 543
543 544 /**
544 545 * Programmatically select a cell.
545 546 *
546 547 * @method select
547 548 * @param {Number} index A cell's index
548 549 * @return {Notebook} This notebook
549 550 */
550 551 Notebook.prototype.select = function (index) {
551 552 if (this.is_valid_cell_index(index)) {
552 553 var sindex = this.get_selected_index();
553 554 if (sindex !== null && index !== sindex) {
554 555 // If we are about to select a different cell, make sure we are
555 556 // first in command mode.
556 557 if (this.mode !== 'command') {
557 558 this.command_mode();
558 559 }
559 560 this.get_cell(sindex).unselect();
560 561 }
561 562 var cell = this.get_cell(index);
562 563 cell.select();
563 564 if (cell.cell_type === 'heading') {
564 565 this.events.trigger('selected_cell_type_changed.Notebook',
565 566 {'cell_type':cell.cell_type,level:cell.level}
566 567 );
567 568 } else {
568 569 this.events.trigger('selected_cell_type_changed.Notebook',
569 570 {'cell_type':cell.cell_type}
570 571 );
571 572 }
572 573 }
573 574 return this;
574 575 };
575 576
576 577 /**
577 578 * Programmatically select the next cell.
578 579 *
579 580 * @method select_next
580 581 * @return {Notebook} This notebook
581 582 */
582 583 Notebook.prototype.select_next = function () {
583 584 var index = this.get_selected_index();
584 585 this.select(index+1);
585 586 return this;
586 587 };
587 588
588 589 /**
589 590 * Programmatically select the previous cell.
590 591 *
591 592 * @method select_prev
592 593 * @return {Notebook} This notebook
593 594 */
594 595 Notebook.prototype.select_prev = function () {
595 596 var index = this.get_selected_index();
596 597 this.select(index-1);
597 598 return this;
598 599 };
599 600
600 601
601 602 // Edit/Command mode
602 603
603 604 /**
604 605 * Gets the index of the cell that is in edit mode.
605 606 *
606 607 * @method get_edit_index
607 608 *
608 609 * @return index {int}
609 610 **/
610 611 Notebook.prototype.get_edit_index = function () {
611 612 var result = null;
612 613 this.get_cell_elements().filter(function (index) {
613 614 if ($(this).data("cell").mode === 'edit') {
614 615 result = index;
615 616 }
616 617 });
617 618 return result;
618 619 };
619 620
620 621 /**
621 622 * Handle when a a cell blurs and the notebook should enter command mode.
622 623 *
623 624 * @method handle_command_mode
624 625 * @param [cell] {Cell} Cell to enter command mode on.
625 626 **/
626 627 Notebook.prototype.handle_command_mode = function (cell) {
627 628 if (this.mode !== 'command') {
628 629 cell.command_mode();
629 630 this.mode = 'command';
630 631 this.events.trigger('command_mode.Notebook');
631 632 this.keyboard_manager.command_mode();
632 633 }
633 634 };
634 635
635 636 /**
636 637 * Make the notebook enter command mode.
637 638 *
638 639 * @method command_mode
639 640 **/
640 641 Notebook.prototype.command_mode = function () {
641 642 var cell = this.get_cell(this.get_edit_index());
642 643 if (cell && this.mode !== 'command') {
643 644 // We don't call cell.command_mode, but rather call cell.focus_cell()
644 645 // which will blur and CM editor and trigger the call to
645 646 // handle_command_mode.
646 647 cell.focus_cell();
647 648 }
648 649 };
649 650
650 651 /**
651 652 * Handle when a cell fires it's edit_mode event.
652 653 *
653 654 * @method handle_edit_mode
654 655 * @param [cell] {Cell} Cell to enter edit mode on.
655 656 **/
656 657 Notebook.prototype.handle_edit_mode = function (cell) {
657 658 if (cell && this.mode !== 'edit') {
658 659 cell.edit_mode();
659 660 this.mode = 'edit';
660 661 this.events.trigger('edit_mode.Notebook');
661 662 this.keyboard_manager.edit_mode();
662 663 }
663 664 };
664 665
665 666 /**
666 667 * Make a cell enter edit mode.
667 668 *
668 669 * @method edit_mode
669 670 **/
670 671 Notebook.prototype.edit_mode = function () {
671 672 var cell = this.get_selected_cell();
672 673 if (cell && this.mode !== 'edit') {
673 674 cell.unrender();
674 675 cell.focus_editor();
675 676 }
676 677 };
677 678
678 679 /**
679 680 * Focus the currently selected cell.
680 681 *
681 682 * @method focus_cell
682 683 **/
683 684 Notebook.prototype.focus_cell = function () {
684 685 var cell = this.get_selected_cell();
685 686 if (cell === null) {return;} // No cell is selected
686 687 cell.focus_cell();
687 688 };
688 689
689 690 // Cell movement
690 691
691 692 /**
692 693 * Move given (or selected) cell up and select it.
693 694 *
694 695 * @method move_cell_up
695 696 * @param [index] {integer} cell index
696 697 * @return {Notebook} This notebook
697 698 **/
698 699 Notebook.prototype.move_cell_up = function (index) {
699 700 var i = this.index_or_selected(index);
700 701 if (this.is_valid_cell_index(i) && i > 0) {
701 702 var pivot = this.get_cell_element(i-1);
702 703 var tomove = this.get_cell_element(i);
703 704 if (pivot !== null && tomove !== null) {
704 705 tomove.detach();
705 706 pivot.before(tomove);
706 707 this.select(i-1);
707 708 var cell = this.get_selected_cell();
708 709 cell.focus_cell();
709 710 }
710 711 this.set_dirty(true);
711 712 }
712 713 return this;
713 714 };
714 715
715 716
716 717 /**
717 718 * Move given (or selected) cell down and select it
718 719 *
719 720 * @method move_cell_down
720 721 * @param [index] {integer} cell index
721 722 * @return {Notebook} This notebook
722 723 **/
723 724 Notebook.prototype.move_cell_down = function (index) {
724 725 var i = this.index_or_selected(index);
725 726 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
726 727 var pivot = this.get_cell_element(i+1);
727 728 var tomove = this.get_cell_element(i);
728 729 if (pivot !== null && tomove !== null) {
729 730 tomove.detach();
730 731 pivot.after(tomove);
731 732 this.select(i+1);
732 733 var cell = this.get_selected_cell();
733 734 cell.focus_cell();
734 735 }
735 736 }
736 737 this.set_dirty();
737 738 return this;
738 739 };
739 740
740 741
741 742 // Insertion, deletion.
742 743
743 744 /**
744 745 * Delete a cell from the notebook.
745 746 *
746 747 * @method delete_cell
747 748 * @param [index] A cell's numeric index
748 749 * @return {Notebook} This notebook
749 750 */
750 751 Notebook.prototype.delete_cell = function (index) {
751 752 var i = this.index_or_selected(index);
752 753 var cell = this.get_selected_cell();
753 754 this.undelete_backup = cell.toJSON();
754 755 $('#undelete_cell').removeClass('disabled');
755 756 if (this.is_valid_cell_index(i)) {
756 757 var old_ncells = this.ncells();
757 758 var ce = this.get_cell_element(i);
758 759 ce.remove();
759 760 if (i === 0) {
760 761 // Always make sure we have at least one cell.
761 762 if (old_ncells === 1) {
762 763 this.insert_cell_below('code');
763 764 }
764 765 this.select(0);
765 766 this.undelete_index = 0;
766 767 this.undelete_below = false;
767 768 } else if (i === old_ncells-1 && i !== 0) {
768 769 this.select(i-1);
769 770 this.undelete_index = i - 1;
770 771 this.undelete_below = true;
771 772 } else {
772 773 this.select(i);
773 774 this.undelete_index = i;
774 775 this.undelete_below = false;
775 776 }
776 777 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
777 778 this.set_dirty(true);
778 779 }
779 780 return this;
780 781 };
781 782
782 783 /**
783 784 * Restore the most recently deleted cell.
784 785 *
785 786 * @method undelete
786 787 */
787 788 Notebook.prototype.undelete_cell = function() {
788 789 if (this.undelete_backup !== null && this.undelete_index !== null) {
789 790 var current_index = this.get_selected_index();
790 791 if (this.undelete_index < current_index) {
791 792 current_index = current_index + 1;
792 793 }
793 794 if (this.undelete_index >= this.ncells()) {
794 795 this.select(this.ncells() - 1);
795 796 }
796 797 else {
797 798 this.select(this.undelete_index);
798 799 }
799 800 var cell_data = this.undelete_backup;
800 801 var new_cell = null;
801 802 if (this.undelete_below) {
802 803 new_cell = this.insert_cell_below(cell_data.cell_type);
803 804 } else {
804 805 new_cell = this.insert_cell_above(cell_data.cell_type);
805 806 }
806 807 new_cell.fromJSON(cell_data);
807 808 if (this.undelete_below) {
808 809 this.select(current_index+1);
809 810 } else {
810 811 this.select(current_index);
811 812 }
812 813 this.undelete_backup = null;
813 814 this.undelete_index = null;
814 815 }
815 816 $('#undelete_cell').addClass('disabled');
816 817 };
817 818
818 819 /**
819 820 * Insert a cell so that after insertion the cell is at given index.
820 821 *
821 822 * If cell type is not provided, it will default to the type of the
822 823 * currently active cell.
823 824 *
824 825 * Similar to insert_above, but index parameter is mandatory
825 826 *
826 827 * Index will be brought back into the accessible range [0,n]
827 828 *
828 829 * @method insert_cell_at_index
829 830 * @param [type] {string} in ['code','markdown','heading'], defaults to 'code'
830 831 * @param [index] {int} a valid index where to insert cell
831 832 *
832 833 * @return cell {cell|null} created cell or null
833 834 **/
834 835 Notebook.prototype.insert_cell_at_index = function(type, index){
835 836
836 837 var ncells = this.ncells();
837 838 index = Math.min(index,ncells);
838 839 index = Math.max(index,0);
839 840 var cell = null;
840 841 type = type || this.get_selected_cell().cell_type;
841 842
842 843 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
843 844 var cell_options = {
844 845 events: this.events,
845 846 config: this.config,
846 847 keyboard_manager: this.keyboard_manager,
847 848 notebook: this,
848 849 tooltip: this.tooltip,
849 850 };
850 851 if (type === 'code') {
851 852 cell = new codecell.CodeCell(this.kernel, cell_options);
852 853 cell.set_input_prompt();
853 854 } else if (type === 'markdown') {
854 855 cell = new textcell.MarkdownCell(cell_options);
855 856 } else if (type === 'raw') {
856 857 cell = new textcell.RawCell(cell_options);
857 858 } else if (type === 'heading') {
858 859 cell = new textcell.HeadingCell(cell_options);
859 860 }
860 861
861 862 if(this._insert_element_at_index(cell.element,index)) {
862 863 cell.render();
863 864 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
864 865 cell.refresh();
865 866 // We used to select the cell after we refresh it, but there
866 867 // are now cases were this method is called where select is
867 868 // not appropriate. The selection logic should be handled by the
868 869 // caller of the the top level insert_cell methods.
869 870 this.set_dirty(true);
870 871 }
871 872 }
872 873 return cell;
873 874
874 875 };
875 876
876 877 /**
877 878 * Insert an element at given cell index.
878 879 *
879 880 * @method _insert_element_at_index
880 881 * @param element {dom element} a cell element
881 882 * @param [index] {int} a valid index where to inser cell
882 883 * @private
883 884 *
884 885 * return true if everything whent fine.
885 886 **/
886 887 Notebook.prototype._insert_element_at_index = function(element, index){
887 888 if (element === undefined){
888 889 return false;
889 890 }
890 891
891 892 var ncells = this.ncells();
892 893
893 894 if (ncells === 0) {
894 895 // special case append if empty
895 896 this.element.find('div.end_space').before(element);
896 897 } else if ( ncells === index ) {
897 898 // special case append it the end, but not empty
898 899 this.get_cell_element(index-1).after(element);
899 900 } else if (this.is_valid_cell_index(index)) {
900 901 // otherwise always somewhere to append to
901 902 this.get_cell_element(index).before(element);
902 903 } else {
903 904 return false;
904 905 }
905 906
906 907 if (this.undelete_index !== null && index <= this.undelete_index) {
907 908 this.undelete_index = this.undelete_index + 1;
908 909 this.set_dirty(true);
909 910 }
910 911 return true;
911 912 };
912 913
913 914 /**
914 915 * Insert a cell of given type above given index, or at top
915 916 * of notebook if index smaller than 0.
916 917 *
917 918 * default index value is the one of currently selected cell
918 919 *
919 920 * @method insert_cell_above
920 921 * @param [type] {string} cell type
921 922 * @param [index] {integer}
922 923 *
923 924 * @return handle to created cell or null
924 925 **/
925 926 Notebook.prototype.insert_cell_above = function (type, index) {
926 927 index = this.index_or_selected(index);
927 928 return this.insert_cell_at_index(type, index);
928 929 };
929 930
930 931 /**
931 932 * Insert a cell of given type below given index, or at bottom
932 933 * of notebook if index greater than number of cells
933 934 *
934 935 * default index value is the one of currently selected cell
935 936 *
936 937 * @method insert_cell_below
937 938 * @param [type] {string} cell type
938 939 * @param [index] {integer}
939 940 *
940 941 * @return handle to created cell or null
941 942 *
942 943 **/
943 944 Notebook.prototype.insert_cell_below = function (type, index) {
944 945 index = this.index_or_selected(index);
945 946 return this.insert_cell_at_index(type, index+1);
946 947 };
947 948
948 949
949 950 /**
950 951 * Insert cell at end of notebook
951 952 *
952 953 * @method insert_cell_at_bottom
953 954 * @param {String} type cell type
954 955 *
955 956 * @return the added cell; or null
956 957 **/
957 958 Notebook.prototype.insert_cell_at_bottom = function (type){
958 959 var len = this.ncells();
959 960 return this.insert_cell_below(type,len-1);
960 961 };
961 962
962 963 /**
963 964 * Turn a cell into a code cell.
964 965 *
965 966 * @method to_code
966 967 * @param {Number} [index] A cell's index
967 968 */
968 969 Notebook.prototype.to_code = function (index) {
969 970 var i = this.index_or_selected(index);
970 971 if (this.is_valid_cell_index(i)) {
971 972 var source_element = this.get_cell_element(i);
972 973 var source_cell = source_element.data("cell");
973 974 if (!(source_cell instanceof codecell.CodeCell)) {
974 975 var target_cell = this.insert_cell_below('code',i);
975 976 var text = source_cell.get_text();
976 977 if (text === source_cell.placeholder) {
977 978 text = '';
978 979 }
979 980 target_cell.set_text(text);
980 981 // make this value the starting point, so that we can only undo
981 982 // to this state, instead of a blank cell
982 983 target_cell.code_mirror.clearHistory();
983 984 source_element.remove();
984 985 this.select(i);
985 986 var cursor = source_cell.code_mirror.getCursor();
986 987 target_cell.code_mirror.setCursor(cursor);
987 988 this.set_dirty(true);
988 989 }
989 990 }
990 991 };
991 992
992 993 /**
993 994 * Turn a cell into a Markdown cell.
994 995 *
995 996 * @method to_markdown
996 997 * @param {Number} [index] A cell's index
997 998 */
998 999 Notebook.prototype.to_markdown = function (index) {
999 1000 var i = this.index_or_selected(index);
1000 1001 if (this.is_valid_cell_index(i)) {
1001 1002 var source_element = this.get_cell_element(i);
1002 1003 var source_cell = source_element.data("cell");
1003 1004 if (!(source_cell instanceof textcell.MarkdownCell)) {
1004 1005 var target_cell = this.insert_cell_below('markdown',i);
1005 1006 var text = source_cell.get_text();
1006 1007 if (text === source_cell.placeholder) {
1007 1008 text = '';
1008 1009 }
1009 1010 // We must show the editor before setting its contents
1010 1011 target_cell.unrender();
1011 1012 target_cell.set_text(text);
1012 1013 // make this value the starting point, so that we can only undo
1013 1014 // to this state, instead of a blank cell
1014 1015 target_cell.code_mirror.clearHistory();
1015 1016 source_element.remove();
1016 1017 this.select(i);
1017 1018 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1018 1019 target_cell.render();
1019 1020 }
1020 1021 var cursor = source_cell.code_mirror.getCursor();
1021 1022 target_cell.code_mirror.setCursor(cursor);
1022 1023 this.set_dirty(true);
1023 1024 }
1024 1025 }
1025 1026 };
1026 1027
1027 1028 /**
1028 1029 * Turn a cell into a raw text cell.
1029 1030 *
1030 1031 * @method to_raw
1031 1032 * @param {Number} [index] A cell's index
1032 1033 */
1033 1034 Notebook.prototype.to_raw = function (index) {
1034 1035 var i = this.index_or_selected(index);
1035 1036 if (this.is_valid_cell_index(i)) {
1036 1037 var source_element = this.get_cell_element(i);
1037 1038 var source_cell = source_element.data("cell");
1038 1039 var target_cell = null;
1039 1040 if (!(source_cell instanceof textcell.RawCell)) {
1040 1041 target_cell = this.insert_cell_below('raw',i);
1041 1042 var text = source_cell.get_text();
1042 1043 if (text === source_cell.placeholder) {
1043 1044 text = '';
1044 1045 }
1045 1046 // We must show the editor before setting its contents
1046 1047 target_cell.unrender();
1047 1048 target_cell.set_text(text);
1048 1049 // make this value the starting point, so that we can only undo
1049 1050 // to this state, instead of a blank cell
1050 1051 target_cell.code_mirror.clearHistory();
1051 1052 source_element.remove();
1052 1053 this.select(i);
1053 1054 var cursor = source_cell.code_mirror.getCursor();
1054 1055 target_cell.code_mirror.setCursor(cursor);
1055 1056 this.set_dirty(true);
1056 1057 }
1057 1058 }
1058 1059 };
1059 1060
1060 1061 /**
1061 1062 * Turn a cell into a heading cell.
1062 1063 *
1063 1064 * @method to_heading
1064 1065 * @param {Number} [index] A cell's index
1065 1066 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
1066 1067 */
1067 1068 Notebook.prototype.to_heading = function (index, level) {
1068 1069 level = level || 1;
1069 1070 var i = this.index_or_selected(index);
1070 1071 if (this.is_valid_cell_index(i)) {
1071 1072 var source_element = this.get_cell_element(i);
1072 1073 var source_cell = source_element.data("cell");
1073 1074 var target_cell = null;
1074 1075 if (source_cell instanceof textcell.HeadingCell) {
1075 1076 source_cell.set_level(level);
1076 1077 } else {
1077 1078 target_cell = this.insert_cell_below('heading',i);
1078 1079 var text = source_cell.get_text();
1079 1080 if (text === source_cell.placeholder) {
1080 1081 text = '';
1081 1082 }
1082 1083 // We must show the editor before setting its contents
1083 1084 target_cell.set_level(level);
1084 1085 target_cell.unrender();
1085 1086 target_cell.set_text(text);
1086 1087 // make this value the starting point, so that we can only undo
1087 1088 // to this state, instead of a blank cell
1088 1089 target_cell.code_mirror.clearHistory();
1089 1090 source_element.remove();
1090 1091 this.select(i);
1091 1092 var cursor = source_cell.code_mirror.getCursor();
1092 1093 target_cell.code_mirror.setCursor(cursor);
1093 1094 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1094 1095 target_cell.render();
1095 1096 }
1096 1097 }
1097 1098 this.set_dirty(true);
1098 1099 this.events.trigger('selected_cell_type_changed.Notebook',
1099 1100 {'cell_type':'heading',level:level}
1100 1101 );
1101 1102 }
1102 1103 };
1103 1104
1104 1105
1105 1106 // Cut/Copy/Paste
1106 1107
1107 1108 /**
1108 1109 * Enable UI elements for pasting cells.
1109 1110 *
1110 1111 * @method enable_paste
1111 1112 */
1112 1113 Notebook.prototype.enable_paste = function () {
1113 1114 var that = this;
1114 1115 if (!this.paste_enabled) {
1115 1116 $('#paste_cell_replace').removeClass('disabled')
1116 1117 .on('click', function () {that.paste_cell_replace();});
1117 1118 $('#paste_cell_above').removeClass('disabled')
1118 1119 .on('click', function () {that.paste_cell_above();});
1119 1120 $('#paste_cell_below').removeClass('disabled')
1120 1121 .on('click', function () {that.paste_cell_below();});
1121 1122 this.paste_enabled = true;
1122 1123 }
1123 1124 };
1124 1125
1125 1126 /**
1126 1127 * Disable UI elements for pasting cells.
1127 1128 *
1128 1129 * @method disable_paste
1129 1130 */
1130 1131 Notebook.prototype.disable_paste = function () {
1131 1132 if (this.paste_enabled) {
1132 1133 $('#paste_cell_replace').addClass('disabled').off('click');
1133 1134 $('#paste_cell_above').addClass('disabled').off('click');
1134 1135 $('#paste_cell_below').addClass('disabled').off('click');
1135 1136 this.paste_enabled = false;
1136 1137 }
1137 1138 };
1138 1139
1139 1140 /**
1140 1141 * Cut a cell.
1141 1142 *
1142 1143 * @method cut_cell
1143 1144 */
1144 1145 Notebook.prototype.cut_cell = function () {
1145 1146 this.copy_cell();
1146 1147 this.delete_cell();
1147 1148 };
1148 1149
1149 1150 /**
1150 1151 * Copy a cell.
1151 1152 *
1152 1153 * @method copy_cell
1153 1154 */
1154 1155 Notebook.prototype.copy_cell = function () {
1155 1156 var cell = this.get_selected_cell();
1156 1157 this.clipboard = cell.toJSON();
1157 1158 this.enable_paste();
1158 1159 };
1159 1160
1160 1161 /**
1161 1162 * Replace the selected cell with a cell in the clipboard.
1162 1163 *
1163 1164 * @method paste_cell_replace
1164 1165 */
1165 1166 Notebook.prototype.paste_cell_replace = function () {
1166 1167 if (this.clipboard !== null && this.paste_enabled) {
1167 1168 var cell_data = this.clipboard;
1168 1169 var new_cell = this.insert_cell_above(cell_data.cell_type);
1169 1170 new_cell.fromJSON(cell_data);
1170 1171 var old_cell = this.get_next_cell(new_cell);
1171 1172 this.delete_cell(this.find_cell_index(old_cell));
1172 1173 this.select(this.find_cell_index(new_cell));
1173 1174 }
1174 1175 };
1175 1176
1176 1177 /**
1177 1178 * Paste a cell from the clipboard above the selected cell.
1178 1179 *
1179 1180 * @method paste_cell_above
1180 1181 */
1181 1182 Notebook.prototype.paste_cell_above = function () {
1182 1183 if (this.clipboard !== null && this.paste_enabled) {
1183 1184 var cell_data = this.clipboard;
1184 1185 var new_cell = this.insert_cell_above(cell_data.cell_type);
1185 1186 new_cell.fromJSON(cell_data);
1186 1187 new_cell.focus_cell();
1187 1188 }
1188 1189 };
1189 1190
1190 1191 /**
1191 1192 * Paste a cell from the clipboard below the selected cell.
1192 1193 *
1193 1194 * @method paste_cell_below
1194 1195 */
1195 1196 Notebook.prototype.paste_cell_below = function () {
1196 1197 if (this.clipboard !== null && this.paste_enabled) {
1197 1198 var cell_data = this.clipboard;
1198 1199 var new_cell = this.insert_cell_below(cell_data.cell_type);
1199 1200 new_cell.fromJSON(cell_data);
1200 1201 new_cell.focus_cell();
1201 1202 }
1202 1203 };
1203 1204
1204 1205 // Split/merge
1205 1206
1206 1207 /**
1207 1208 * Split the selected cell into two, at the cursor.
1208 1209 *
1209 1210 * @method split_cell
1210 1211 */
1211 1212 Notebook.prototype.split_cell = function () {
1212 1213 var mdc = textcell.MarkdownCell;
1213 1214 var rc = textcell.RawCell;
1214 1215 var cell = this.get_selected_cell();
1215 1216 if (cell.is_splittable()) {
1216 1217 var texta = cell.get_pre_cursor();
1217 1218 var textb = cell.get_post_cursor();
1218 1219 cell.set_text(textb);
1219 1220 var new_cell = this.insert_cell_above(cell.cell_type);
1220 1221 // Unrender the new cell so we can call set_text.
1221 1222 new_cell.unrender();
1222 1223 new_cell.set_text(texta);
1223 1224 }
1224 1225 };
1225 1226
1226 1227 /**
1227 1228 * Combine the selected cell into the cell above it.
1228 1229 *
1229 1230 * @method merge_cell_above
1230 1231 */
1231 1232 Notebook.prototype.merge_cell_above = function () {
1232 1233 var mdc = textcell.MarkdownCell;
1233 1234 var rc = textcell.RawCell;
1234 1235 var index = this.get_selected_index();
1235 1236 var cell = this.get_cell(index);
1236 1237 var render = cell.rendered;
1237 1238 if (!cell.is_mergeable()) {
1238 1239 return;
1239 1240 }
1240 1241 if (index > 0) {
1241 1242 var upper_cell = this.get_cell(index-1);
1242 1243 if (!upper_cell.is_mergeable()) {
1243 1244 return;
1244 1245 }
1245 1246 var upper_text = upper_cell.get_text();
1246 1247 var text = cell.get_text();
1247 1248 if (cell instanceof codecell.CodeCell) {
1248 1249 cell.set_text(upper_text+'\n'+text);
1249 1250 } else {
1250 1251 cell.unrender(); // Must unrender before we set_text.
1251 1252 cell.set_text(upper_text+'\n\n'+text);
1252 1253 if (render) {
1253 1254 // The rendered state of the final cell should match
1254 1255 // that of the original selected cell;
1255 1256 cell.render();
1256 1257 }
1257 1258 }
1258 1259 this.delete_cell(index-1);
1259 1260 this.select(this.find_cell_index(cell));
1260 1261 }
1261 1262 };
1262 1263
1263 1264 /**
1264 1265 * Combine the selected cell into the cell below it.
1265 1266 *
1266 1267 * @method merge_cell_below
1267 1268 */
1268 1269 Notebook.prototype.merge_cell_below = function () {
1269 1270 var mdc = textcell.MarkdownCell;
1270 1271 var rc = textcell.RawCell;
1271 1272 var index = this.get_selected_index();
1272 1273 var cell = this.get_cell(index);
1273 1274 var render = cell.rendered;
1274 1275 if (!cell.is_mergeable()) {
1275 1276 return;
1276 1277 }
1277 1278 if (index < this.ncells()-1) {
1278 1279 var lower_cell = this.get_cell(index+1);
1279 1280 if (!lower_cell.is_mergeable()) {
1280 1281 return;
1281 1282 }
1282 1283 var lower_text = lower_cell.get_text();
1283 1284 var text = cell.get_text();
1284 1285 if (cell instanceof codecell.CodeCell) {
1285 1286 cell.set_text(text+'\n'+lower_text);
1286 1287 } else {
1287 1288 cell.unrender(); // Must unrender before we set_text.
1288 1289 cell.set_text(text+'\n\n'+lower_text);
1289 1290 if (render) {
1290 1291 // The rendered state of the final cell should match
1291 1292 // that of the original selected cell;
1292 1293 cell.render();
1293 1294 }
1294 1295 }
1295 1296 this.delete_cell(index+1);
1296 1297 this.select(this.find_cell_index(cell));
1297 1298 }
1298 1299 };
1299 1300
1300 1301
1301 1302 // Cell collapsing and output clearing
1302 1303
1303 1304 /**
1304 1305 * Hide a cell's output.
1305 1306 *
1306 1307 * @method collapse_output
1307 1308 * @param {Number} index A cell's numeric index
1308 1309 */
1309 1310 Notebook.prototype.collapse_output = function (index) {
1310 1311 var i = this.index_or_selected(index);
1311 1312 var cell = this.get_cell(i);
1312 1313 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1313 1314 cell.collapse_output();
1314 1315 this.set_dirty(true);
1315 1316 }
1316 1317 };
1317 1318
1318 1319 /**
1319 1320 * Hide each code cell's output area.
1320 1321 *
1321 1322 * @method collapse_all_output
1322 1323 */
1323 1324 Notebook.prototype.collapse_all_output = function () {
1324 1325 $.map(this.get_cells(), function (cell, i) {
1325 1326 if (cell instanceof codecell.CodeCell) {
1326 1327 cell.collapse_output();
1327 1328 }
1328 1329 });
1329 1330 // this should not be set if the `collapse` key is removed from nbformat
1330 1331 this.set_dirty(true);
1331 1332 };
1332 1333
1333 1334 /**
1334 1335 * Show a cell's output.
1335 1336 *
1336 1337 * @method expand_output
1337 1338 * @param {Number} index A cell's numeric index
1338 1339 */
1339 1340 Notebook.prototype.expand_output = function (index) {
1340 1341 var i = this.index_or_selected(index);
1341 1342 var cell = this.get_cell(i);
1342 1343 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1343 1344 cell.expand_output();
1344 1345 this.set_dirty(true);
1345 1346 }
1346 1347 };
1347 1348
1348 1349 /**
1349 1350 * Expand each code cell's output area, and remove scrollbars.
1350 1351 *
1351 1352 * @method expand_all_output
1352 1353 */
1353 1354 Notebook.prototype.expand_all_output = function () {
1354 1355 $.map(this.get_cells(), function (cell, i) {
1355 1356 if (cell instanceof codecell.CodeCell) {
1356 1357 cell.expand_output();
1357 1358 }
1358 1359 });
1359 1360 // this should not be set if the `collapse` key is removed from nbformat
1360 1361 this.set_dirty(true);
1361 1362 };
1362 1363
1363 1364 /**
1364 1365 * Clear the selected CodeCell's output area.
1365 1366 *
1366 1367 * @method clear_output
1367 1368 * @param {Number} index A cell's numeric index
1368 1369 */
1369 1370 Notebook.prototype.clear_output = function (index) {
1370 1371 var i = this.index_or_selected(index);
1371 1372 var cell = this.get_cell(i);
1372 1373 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1373 1374 cell.clear_output();
1374 1375 this.set_dirty(true);
1375 1376 }
1376 1377 };
1377 1378
1378 1379 /**
1379 1380 * Clear each code cell's output area.
1380 1381 *
1381 1382 * @method clear_all_output
1382 1383 */
1383 1384 Notebook.prototype.clear_all_output = function () {
1384 1385 $.map(this.get_cells(), function (cell, i) {
1385 1386 if (cell instanceof codecell.CodeCell) {
1386 1387 cell.clear_output();
1387 1388 }
1388 1389 });
1389 1390 this.set_dirty(true);
1390 1391 };
1391 1392
1392 1393 /**
1393 1394 * Scroll the selected CodeCell's output area.
1394 1395 *
1395 1396 * @method scroll_output
1396 1397 * @param {Number} index A cell's numeric index
1397 1398 */
1398 1399 Notebook.prototype.scroll_output = function (index) {
1399 1400 var i = this.index_or_selected(index);
1400 1401 var cell = this.get_cell(i);
1401 1402 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1402 1403 cell.scroll_output();
1403 1404 this.set_dirty(true);
1404 1405 }
1405 1406 };
1406 1407
1407 1408 /**
1408 1409 * Expand each code cell's output area, and add a scrollbar for long output.
1409 1410 *
1410 1411 * @method scroll_all_output
1411 1412 */
1412 1413 Notebook.prototype.scroll_all_output = function () {
1413 1414 $.map(this.get_cells(), function (cell, i) {
1414 1415 if (cell instanceof codecell.CodeCell) {
1415 1416 cell.scroll_output();
1416 1417 }
1417 1418 });
1418 1419 // this should not be set if the `collapse` key is removed from nbformat
1419 1420 this.set_dirty(true);
1420 1421 };
1421 1422
1422 1423 /** Toggle whether a cell's output is collapsed or expanded.
1423 1424 *
1424 1425 * @method toggle_output
1425 1426 * @param {Number} index A cell's numeric index
1426 1427 */
1427 1428 Notebook.prototype.toggle_output = function (index) {
1428 1429 var i = this.index_or_selected(index);
1429 1430 var cell = this.get_cell(i);
1430 1431 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1431 1432 cell.toggle_output();
1432 1433 this.set_dirty(true);
1433 1434 }
1434 1435 };
1435 1436
1436 1437 /**
1437 1438 * Hide/show the output of all cells.
1438 1439 *
1439 1440 * @method toggle_all_output
1440 1441 */
1441 1442 Notebook.prototype.toggle_all_output = function () {
1442 1443 $.map(this.get_cells(), function (cell, i) {
1443 1444 if (cell instanceof codecell.CodeCell) {
1444 1445 cell.toggle_output();
1445 1446 }
1446 1447 });
1447 1448 // this should not be set if the `collapse` key is removed from nbformat
1448 1449 this.set_dirty(true);
1449 1450 };
1450 1451
1451 1452 /**
1452 1453 * Toggle a scrollbar for long cell outputs.
1453 1454 *
1454 1455 * @method toggle_output_scroll
1455 1456 * @param {Number} index A cell's numeric index
1456 1457 */
1457 1458 Notebook.prototype.toggle_output_scroll = function (index) {
1458 1459 var i = this.index_or_selected(index);
1459 1460 var cell = this.get_cell(i);
1460 1461 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1461 1462 cell.toggle_output_scroll();
1462 1463 this.set_dirty(true);
1463 1464 }
1464 1465 };
1465 1466
1466 1467 /**
1467 1468 * Toggle the scrolling of long output on all cells.
1468 1469 *
1469 1470 * @method toggle_all_output_scrolling
1470 1471 */
1471 1472 Notebook.prototype.toggle_all_output_scroll = function () {
1472 1473 $.map(this.get_cells(), function (cell, i) {
1473 1474 if (cell instanceof codecell.CodeCell) {
1474 1475 cell.toggle_output_scroll();
1475 1476 }
1476 1477 });
1477 1478 // this should not be set if the `collapse` key is removed from nbformat
1478 1479 this.set_dirty(true);
1479 1480 };
1480 1481
1481 1482 // Other cell functions: line numbers, ...
1482 1483
1483 1484 /**
1484 1485 * Toggle line numbers in the selected cell's input area.
1485 1486 *
1486 1487 * @method cell_toggle_line_numbers
1487 1488 */
1488 1489 Notebook.prototype.cell_toggle_line_numbers = function() {
1489 1490 this.get_selected_cell().toggle_line_numbers();
1490 1491 };
1491 1492
1492 1493 /**
1493 1494 * Set the codemirror mode for all code cells, including the default for
1494 1495 * new code cells.
1495 1496 *
1496 1497 * @method set_codemirror_mode
1497 1498 */
1498 1499 Notebook.prototype.set_codemirror_mode = function(newmode){
1499 1500 if (newmode === this.codemirror_mode) {
1500 1501 return;
1501 1502 }
1502 1503 this.codemirror_mode = newmode;
1503 1504 codecell.CodeCell.options_default.cm_config.mode = newmode;
1504 1505 modename = newmode.name || newmode
1505 1506
1506 1507 that = this;
1507 1508 CodeMirror.requireMode(modename, function(){
1508 1509 $.map(that.get_cells(), function(cell, i) {
1509 1510 if (cell.cell_type === 'code'){
1510 1511 cell.code_mirror.setOption('mode', newmode);
1511 1512 // This is currently redundant, because cm_config ends up as
1512 1513 // codemirror's own .options object, but I don't want to
1513 1514 // rely on that.
1514 1515 cell.cm_config.mode = newmode;
1515 1516 }
1516 1517 });
1517 1518 })
1518 1519 };
1519 1520
1520 1521 // Session related things
1521 1522
1522 1523 /**
1523 1524 * Start a new session and set it on each code cell.
1524 1525 *
1525 1526 * @method start_session
1526 1527 */
1527 1528 Notebook.prototype.start_session = function (kernel_name) {
1529 var that = this;
1528 1530 if (kernel_name === undefined) {
1529 1531 kernel_name = this.default_kernel_name;
1530 1532 }
1533 if (this._session_starting) {
1534 throw new session.SessionAlreadyStarting();
1535 }
1536 this._session_starting = true;
1537
1538 if (this.session !== null) {
1539 var s = this.session;
1540 this.session = null;
1541 // need to start the new session in a callback after delete,
1542 // because javascript does not guarantee the ordering of AJAX requests (?!)
1543 s.delete(function () {
1544 // on successful delete, start new session
1545 that._session_starting = false;
1546 that.start_session(kernel_name);
1547 }, function (jqXHR, status, error) {
1548 // log the failed delete, but still create a new session
1549 // 404 just means it was already deleted by someone else,
1550 // but other errors are possible.
1551 utils.log_ajax_error(jqXHR, status, error);
1552 that._session_starting = false;
1553 that.start_session(kernel_name);
1554 }
1555 );
1556 return;
1557 }
1558
1559
1560
1531 1561 this.session = new session.Session({
1532 1562 base_url: this.base_url,
1533 1563 ws_url: this.ws_url,
1534 1564 notebook_path: this.notebook_path,
1535 1565 notebook_name: this.notebook_name,
1536 1566 // For now, create all sessions with the 'python' kernel, which is the
1537 1567 // default. Later, the user will be able to select kernels. This is
1538 1568 // overridden if KernelManager.kernel_cmd is specified for the server.
1539 1569 kernel_name: kernel_name,
1540 1570 notebook: this});
1541 1571
1542 this.session.start($.proxy(this._session_started, this));
1572 this.session.start(
1573 $.proxy(this._session_started, this),
1574 $.proxy(this._session_start_failed, this)
1575 );
1543 1576 };
1544 1577
1545 1578
1546 1579 /**
1547 1580 * Once a session is started, link the code cells to the kernel and pass the
1548 1581 * comm manager to the widget manager
1549 1582 *
1550 1583 */
1551 Notebook.prototype._session_started = function(){
1584 Notebook.prototype._session_started = function (){
1585 this._session_starting = false;
1552 1586 this.kernel = this.session.kernel;
1553 1587 var ncells = this.ncells();
1554 1588 for (var i=0; i<ncells; i++) {
1555 1589 var cell = this.get_cell(i);
1556 1590 if (cell instanceof codecell.CodeCell) {
1557 1591 cell.set_kernel(this.session.kernel);
1558 1592 }
1559 1593 }
1560 1594 };
1561
1595 Notebook.prototype._session_start_failed = function (jqxhr, status, error){
1596 this._session_starting = false;
1597 utils.log_ajax_error(jqxhr, status, error);
1598 };
1599
1562 1600 /**
1563 1601 * Prompt the user to restart the IPython kernel.
1564 1602 *
1565 1603 * @method restart_kernel
1566 1604 */
1567 1605 Notebook.prototype.restart_kernel = function () {
1568 1606 var that = this;
1569 1607 dialog.modal({
1570 1608 notebook: this,
1571 1609 keyboard_manager: this.keyboard_manager,
1572 1610 title : "Restart kernel or continue running?",
1573 1611 body : $("<p/>").text(
1574 1612 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1575 1613 ),
1576 1614 buttons : {
1577 1615 "Continue running" : {},
1578 1616 "Restart" : {
1579 1617 "class" : "btn-danger",
1580 1618 "click" : function() {
1581 1619 that.session.restart_kernel();
1582 1620 }
1583 1621 }
1584 1622 }
1585 1623 });
1586 1624 };
1587 1625
1588 1626 /**
1589 1627 * Execute or render cell outputs and go into command mode.
1590 1628 *
1591 1629 * @method execute_cell
1592 1630 */
1593 1631 Notebook.prototype.execute_cell = function () {
1594 1632 // mode = shift, ctrl, alt
1595 1633 var cell = this.get_selected_cell();
1596 1634 var cell_index = this.find_cell_index(cell);
1597 1635
1598 1636 cell.execute();
1599 1637 this.command_mode();
1600 1638 this.set_dirty(true);
1601 1639 };
1602 1640
1603 1641 /**
1604 1642 * Execute or render cell outputs and insert a new cell below.
1605 1643 *
1606 1644 * @method execute_cell_and_insert_below
1607 1645 */
1608 1646 Notebook.prototype.execute_cell_and_insert_below = function () {
1609 1647 var cell = this.get_selected_cell();
1610 1648 var cell_index = this.find_cell_index(cell);
1611 1649
1612 1650 cell.execute();
1613 1651
1614 1652 // If we are at the end always insert a new cell and return
1615 1653 if (cell_index === (this.ncells()-1)) {
1616 1654 this.command_mode();
1617 1655 this.insert_cell_below();
1618 1656 this.select(cell_index+1);
1619 1657 this.edit_mode();
1620 1658 this.scroll_to_bottom();
1621 1659 this.set_dirty(true);
1622 1660 return;
1623 1661 }
1624 1662
1625 1663 this.command_mode();
1626 1664 this.insert_cell_below();
1627 1665 this.select(cell_index+1);
1628 1666 this.edit_mode();
1629 1667 this.set_dirty(true);
1630 1668 };
1631 1669
1632 1670 /**
1633 1671 * Execute or render cell outputs and select the next cell.
1634 1672 *
1635 1673 * @method execute_cell_and_select_below
1636 1674 */
1637 1675 Notebook.prototype.execute_cell_and_select_below = function () {
1638 1676
1639 1677 var cell = this.get_selected_cell();
1640 1678 var cell_index = this.find_cell_index(cell);
1641 1679
1642 1680 cell.execute();
1643 1681
1644 1682 // If we are at the end always insert a new cell and return
1645 1683 if (cell_index === (this.ncells()-1)) {
1646 1684 this.command_mode();
1647 1685 this.insert_cell_below();
1648 1686 this.select(cell_index+1);
1649 1687 this.edit_mode();
1650 1688 this.scroll_to_bottom();
1651 1689 this.set_dirty(true);
1652 1690 return;
1653 1691 }
1654 1692
1655 1693 this.command_mode();
1656 1694 this.select(cell_index+1);
1657 1695 this.focus_cell();
1658 1696 this.set_dirty(true);
1659 1697 };
1660 1698
1661 1699 /**
1662 1700 * Execute all cells below the selected cell.
1663 1701 *
1664 1702 * @method execute_cells_below
1665 1703 */
1666 1704 Notebook.prototype.execute_cells_below = function () {
1667 1705 this.execute_cell_range(this.get_selected_index(), this.ncells());
1668 1706 this.scroll_to_bottom();
1669 1707 };
1670 1708
1671 1709 /**
1672 1710 * Execute all cells above the selected cell.
1673 1711 *
1674 1712 * @method execute_cells_above
1675 1713 */
1676 1714 Notebook.prototype.execute_cells_above = function () {
1677 1715 this.execute_cell_range(0, this.get_selected_index());
1678 1716 };
1679 1717
1680 1718 /**
1681 1719 * Execute all cells.
1682 1720 *
1683 1721 * @method execute_all_cells
1684 1722 */
1685 1723 Notebook.prototype.execute_all_cells = function () {
1686 1724 this.execute_cell_range(0, this.ncells());
1687 1725 this.scroll_to_bottom();
1688 1726 };
1689 1727
1690 1728 /**
1691 1729 * Execute a contiguous range of cells.
1692 1730 *
1693 1731 * @method execute_cell_range
1694 1732 * @param {Number} start Index of the first cell to execute (inclusive)
1695 1733 * @param {Number} end Index of the last cell to execute (exclusive)
1696 1734 */
1697 1735 Notebook.prototype.execute_cell_range = function (start, end) {
1698 1736 this.command_mode();
1699 1737 for (var i=start; i<end; i++) {
1700 1738 this.select(i);
1701 1739 this.execute_cell();
1702 1740 }
1703 1741 };
1704 1742
1705 1743 // Persistance and loading
1706 1744
1707 1745 /**
1708 1746 * Getter method for this notebook's name.
1709 1747 *
1710 1748 * @method get_notebook_name
1711 1749 * @return {String} This notebook's name (excluding file extension)
1712 1750 */
1713 1751 Notebook.prototype.get_notebook_name = function () {
1714 1752 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1715 1753 return nbname;
1716 1754 };
1717 1755
1718 1756 /**
1719 1757 * Setter method for this notebook's name.
1720 1758 *
1721 1759 * @method set_notebook_name
1722 1760 * @param {String} name A new name for this notebook
1723 1761 */
1724 1762 Notebook.prototype.set_notebook_name = function (name) {
1725 1763 this.notebook_name = name;
1726 1764 };
1727 1765
1728 1766 /**
1729 1767 * Check that a notebook's name is valid.
1730 1768 *
1731 1769 * @method test_notebook_name
1732 1770 * @param {String} nbname A name for this notebook
1733 1771 * @return {Boolean} True if the name is valid, false if invalid
1734 1772 */
1735 1773 Notebook.prototype.test_notebook_name = function (nbname) {
1736 1774 nbname = nbname || '';
1737 1775 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1738 1776 return true;
1739 1777 } else {
1740 1778 return false;
1741 1779 }
1742 1780 };
1743 1781
1744 1782 /**
1745 1783 * Load a notebook from JSON (.ipynb).
1746 1784 *
1747 1785 * This currently handles one worksheet: others are deleted.
1748 1786 *
1749 1787 * @method fromJSON
1750 1788 * @param {Object} data JSON representation of a notebook
1751 1789 */
1752 1790 Notebook.prototype.fromJSON = function (data) {
1753 1791 var content = data.content;
1754 1792 var ncells = this.ncells();
1755 1793 var i;
1756 1794 for (i=0; i<ncells; i++) {
1757 1795 // Always delete cell 0 as they get renumbered as they are deleted.
1758 1796 this.delete_cell(0);
1759 1797 }
1760 1798 // Save the metadata and name.
1761 1799 this.metadata = content.metadata;
1762 1800 this.notebook_name = data.name;
1763 1801 var trusted = true;
1764 1802
1765 1803 // Trigger an event changing the kernel spec - this will set the default
1766 1804 // codemirror mode
1767 1805 if (this.metadata.kernelspec !== undefined) {
1768 1806 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1769 1807 }
1770 1808
1771 1809 // Only handle 1 worksheet for now.
1772 1810 var worksheet = content.worksheets[0];
1773 1811 if (worksheet !== undefined) {
1774 1812 if (worksheet.metadata) {
1775 1813 this.worksheet_metadata = worksheet.metadata;
1776 1814 }
1777 1815 var new_cells = worksheet.cells;
1778 1816 ncells = new_cells.length;
1779 1817 var cell_data = null;
1780 1818 var new_cell = null;
1781 1819 for (i=0; i<ncells; i++) {
1782 1820 cell_data = new_cells[i];
1783 1821 // VERSIONHACK: plaintext -> raw
1784 1822 // handle never-released plaintext name for raw cells
1785 1823 if (cell_data.cell_type === 'plaintext'){
1786 1824 cell_data.cell_type = 'raw';
1787 1825 }
1788 1826
1789 1827 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1790 1828 new_cell.fromJSON(cell_data);
1791 1829 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1792 1830 trusted = false;
1793 1831 }
1794 1832 }
1795 1833 }
1796 1834 if (trusted != this.trusted) {
1797 1835 this.trusted = trusted;
1798 1836 this.events.trigger("trust_changed.Notebook", trusted);
1799 1837 }
1800 1838 if (content.worksheets.length > 1) {
1801 1839 dialog.modal({
1802 1840 notebook: this,
1803 1841 keyboard_manager: this.keyboard_manager,
1804 1842 title : "Multiple worksheets",
1805 1843 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1806 1844 "but this version of IPython can only handle the first. " +
1807 1845 "If you save this notebook, worksheets after the first will be lost.",
1808 1846 buttons : {
1809 1847 OK : {
1810 1848 class : "btn-danger"
1811 1849 }
1812 1850 }
1813 1851 });
1814 1852 }
1815 1853 };
1816 1854
1817 1855 /**
1818 1856 * Dump this notebook into a JSON-friendly object.
1819 1857 *
1820 1858 * @method toJSON
1821 1859 * @return {Object} A JSON-friendly representation of this notebook.
1822 1860 */
1823 1861 Notebook.prototype.toJSON = function () {
1824 1862 var cells = this.get_cells();
1825 1863 var ncells = cells.length;
1826 1864 var cell_array = new Array(ncells);
1827 1865 var trusted = true;
1828 1866 for (var i=0; i<ncells; i++) {
1829 1867 var cell = cells[i];
1830 1868 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1831 1869 trusted = false;
1832 1870 }
1833 1871 cell_array[i] = cell.toJSON();
1834 1872 }
1835 1873 var data = {
1836 1874 // Only handle 1 worksheet for now.
1837 1875 worksheets : [{
1838 1876 cells: cell_array,
1839 1877 metadata: this.worksheet_metadata
1840 1878 }],
1841 1879 metadata : this.metadata
1842 1880 };
1843 1881 if (trusted != this.trusted) {
1844 1882 this.trusted = trusted;
1845 1883 this.events.trigger("trust_changed.Notebook", trusted);
1846 1884 }
1847 1885 return data;
1848 1886 };
1849 1887
1850 1888 /**
1851 1889 * Start an autosave timer, for periodically saving the notebook.
1852 1890 *
1853 1891 * @method set_autosave_interval
1854 1892 * @param {Integer} interval the autosave interval in milliseconds
1855 1893 */
1856 1894 Notebook.prototype.set_autosave_interval = function (interval) {
1857 1895 var that = this;
1858 1896 // clear previous interval, so we don't get simultaneous timers
1859 1897 if (this.autosave_timer) {
1860 1898 clearInterval(this.autosave_timer);
1861 1899 }
1862 1900
1863 1901 this.autosave_interval = this.minimum_autosave_interval = interval;
1864 1902 if (interval) {
1865 1903 this.autosave_timer = setInterval(function() {
1866 1904 if (that.dirty) {
1867 1905 that.save_notebook();
1868 1906 }
1869 1907 }, interval);
1870 1908 this.events.trigger("autosave_enabled.Notebook", interval);
1871 1909 } else {
1872 1910 this.autosave_timer = null;
1873 1911 this.events.trigger("autosave_disabled.Notebook");
1874 1912 }
1875 1913 };
1876 1914
1877 1915 /**
1878 1916 * Save this notebook on the server. This becomes a notebook instance's
1879 1917 * .save_notebook method *after* the entire notebook has been loaded.
1880 1918 *
1881 1919 * @method save_notebook
1882 1920 */
1883 1921 Notebook.prototype.save_notebook = function (extra_settings) {
1884 1922 // Create a JSON model to be sent to the server.
1885 1923 var model = {};
1886 1924 model.name = this.notebook_name;
1887 1925 model.path = this.notebook_path;
1888 1926 model.type = 'notebook';
1889 1927 model.format = 'json';
1890 1928 model.content = this.toJSON();
1891 1929 model.content.nbformat = this.nbformat;
1892 1930 model.content.nbformat_minor = this.nbformat_minor;
1893 1931 // time the ajax call for autosave tuning purposes.
1894 1932 var start = new Date().getTime();
1895 1933 // We do the call with settings so we can set cache to false.
1896 1934 var settings = {
1897 1935 processData : false,
1898 1936 cache : false,
1899 1937 type : "PUT",
1900 1938 data : JSON.stringify(model),
1901 1939 headers : {'Content-Type': 'application/json'},
1902 1940 success : $.proxy(this.save_notebook_success, this, start),
1903 1941 error : $.proxy(this.save_notebook_error, this)
1904 1942 };
1905 1943 if (extra_settings) {
1906 1944 for (var key in extra_settings) {
1907 1945 settings[key] = extra_settings[key];
1908 1946 }
1909 1947 }
1910 1948 this.events.trigger('notebook_saving.Notebook');
1911 1949 var url = utils.url_join_encode(
1912 1950 this.base_url,
1913 1951 'api/contents',
1914 1952 this.notebook_path,
1915 1953 this.notebook_name
1916 1954 );
1917 1955 $.ajax(url, settings);
1918 1956 };
1919 1957
1920 1958 /**
1921 1959 * Success callback for saving a notebook.
1922 1960 *
1923 1961 * @method save_notebook_success
1924 1962 * @param {Integer} start the time when the save request started
1925 1963 * @param {Object} data JSON representation of a notebook
1926 1964 * @param {String} status Description of response status
1927 1965 * @param {jqXHR} xhr jQuery Ajax object
1928 1966 */
1929 1967 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1930 1968 this.set_dirty(false);
1931 1969 this.events.trigger('notebook_saved.Notebook');
1932 1970 this._update_autosave_interval(start);
1933 1971 if (this._checkpoint_after_save) {
1934 1972 this.create_checkpoint();
1935 1973 this._checkpoint_after_save = false;
1936 1974 }
1937 1975 };
1938 1976
1939 1977 /**
1940 1978 * update the autosave interval based on how long the last save took
1941 1979 *
1942 1980 * @method _update_autosave_interval
1943 1981 * @param {Integer} timestamp when the save request started
1944 1982 */
1945 1983 Notebook.prototype._update_autosave_interval = function (start) {
1946 1984 var duration = (new Date().getTime() - start);
1947 1985 if (this.autosave_interval) {
1948 1986 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1949 1987 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1950 1988 // round to 10 seconds, otherwise we will be setting a new interval too often
1951 1989 interval = 10000 * Math.round(interval / 10000);
1952 1990 // set new interval, if it's changed
1953 1991 if (interval != this.autosave_interval) {
1954 1992 this.set_autosave_interval(interval);
1955 1993 }
1956 1994 }
1957 1995 };
1958 1996
1959 1997 /**
1960 1998 * Failure callback for saving a notebook.
1961 1999 *
1962 2000 * @method save_notebook_error
1963 2001 * @param {jqXHR} xhr jQuery Ajax object
1964 2002 * @param {String} status Description of response status
1965 2003 * @param {String} error HTTP error message
1966 2004 */
1967 2005 Notebook.prototype.save_notebook_error = function (xhr, status, error) {
1968 2006 this.events.trigger('notebook_save_failed.Notebook', [xhr, status, error]);
1969 2007 };
1970 2008
1971 2009 /**
1972 2010 * Explicitly trust the output of this notebook.
1973 2011 *
1974 2012 * @method trust_notebook
1975 2013 */
1976 2014 Notebook.prototype.trust_notebook = function (extra_settings) {
1977 2015 var body = $("<div>").append($("<p>")
1978 2016 .text("A trusted IPython notebook may execute hidden malicious code ")
1979 2017 .append($("<strong>")
1980 2018 .append(
1981 2019 $("<em>").text("when you open it")
1982 2020 )
1983 2021 ).append(".").append(
1984 2022 " Selecting trust will immediately reload this notebook in a trusted state."
1985 2023 ).append(
1986 2024 " For more information, see the "
1987 2025 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
1988 2026 .text("IPython security documentation")
1989 2027 ).append(".")
1990 2028 );
1991 2029
1992 2030 var nb = this;
1993 2031 dialog.modal({
1994 2032 notebook: this,
1995 2033 keyboard_manager: this.keyboard_manager,
1996 2034 title: "Trust this notebook?",
1997 2035 body: body,
1998 2036
1999 2037 buttons: {
2000 2038 Cancel : {},
2001 2039 Trust : {
2002 2040 class : "btn-danger",
2003 2041 click : function () {
2004 2042 var cells = nb.get_cells();
2005 2043 for (var i = 0; i < cells.length; i++) {
2006 2044 var cell = cells[i];
2007 2045 if (cell.cell_type == 'code') {
2008 2046 cell.output_area.trusted = true;
2009 2047 }
2010 2048 }
2011 2049 this.events.on('notebook_saved.Notebook', function () {
2012 2050 window.location.reload();
2013 2051 });
2014 2052 nb.save_notebook();
2015 2053 }
2016 2054 }
2017 2055 }
2018 2056 });
2019 2057 };
2020 2058
2021 2059 Notebook.prototype.new_notebook = function(){
2022 2060 var path = this.notebook_path;
2023 2061 var base_url = this.base_url;
2024 2062 var settings = {
2025 2063 processData : false,
2026 2064 cache : false,
2027 2065 type : "POST",
2028 2066 dataType : "json",
2029 2067 async : false,
2030 2068 success : function (data, status, xhr){
2031 2069 var notebook_name = data.name;
2032 2070 window.open(
2033 2071 utils.url_join_encode(
2034 2072 base_url,
2035 2073 'notebooks',
2036 2074 path,
2037 2075 notebook_name
2038 2076 ),
2039 2077 '_blank'
2040 2078 );
2041 2079 },
2042 2080 error : utils.log_ajax_error,
2043 2081 };
2044 2082 var url = utils.url_join_encode(
2045 2083 base_url,
2046 2084 'api/contents',
2047 2085 path
2048 2086 );
2049 2087 $.ajax(url,settings);
2050 2088 };
2051 2089
2052 2090
2053 2091 Notebook.prototype.copy_notebook = function(){
2054 2092 var path = this.notebook_path;
2055 2093 var base_url = this.base_url;
2056 2094 var settings = {
2057 2095 processData : false,
2058 2096 cache : false,
2059 2097 type : "POST",
2060 2098 dataType : "json",
2061 2099 data : JSON.stringify({copy_from : this.notebook_name}),
2062 2100 async : false,
2063 2101 success : function (data, status, xhr) {
2064 2102 window.open(utils.url_join_encode(
2065 2103 base_url,
2066 2104 'notebooks',
2067 2105 data.path,
2068 2106 data.name
2069 2107 ), '_blank');
2070 2108 },
2071 2109 error : utils.log_ajax_error,
2072 2110 };
2073 2111 var url = utils.url_join_encode(
2074 2112 base_url,
2075 2113 'api/contents',
2076 2114 path
2077 2115 );
2078 2116 $.ajax(url,settings);
2079 2117 };
2080 2118
2081 2119 Notebook.prototype.rename = function (nbname) {
2082 2120 var that = this;
2083 2121 if (!nbname.match(/\.ipynb$/)) {
2084 2122 nbname = nbname + ".ipynb";
2085 2123 }
2086 2124 var data = {name: nbname};
2087 2125 var settings = {
2088 2126 processData : false,
2089 2127 cache : false,
2090 2128 type : "PATCH",
2091 2129 data : JSON.stringify(data),
2092 2130 dataType: "json",
2093 2131 headers : {'Content-Type': 'application/json'},
2094 2132 success : $.proxy(that.rename_success, this),
2095 2133 error : $.proxy(that.rename_error, this)
2096 2134 };
2097 2135 this.events.trigger('rename_notebook.Notebook', data);
2098 2136 var url = utils.url_join_encode(
2099 2137 this.base_url,
2100 2138 'api/contents',
2101 2139 this.notebook_path,
2102 2140 this.notebook_name
2103 2141 );
2104 2142 $.ajax(url, settings);
2105 2143 };
2106 2144
2107 2145 Notebook.prototype.delete = function () {
2108 2146 var that = this;
2109 2147 var settings = {
2110 2148 processData : false,
2111 2149 cache : false,
2112 2150 type : "DELETE",
2113 2151 dataType: "json",
2114 2152 error : utils.log_ajax_error,
2115 2153 };
2116 2154 var url = utils.url_join_encode(
2117 2155 this.base_url,
2118 2156 'api/contents',
2119 2157 this.notebook_path,
2120 2158 this.notebook_name
2121 2159 );
2122 2160 $.ajax(url, settings);
2123 2161 };
2124 2162
2125 2163
2126 2164 Notebook.prototype.rename_success = function (json, status, xhr) {
2127 2165 var name = this.notebook_name = json.name;
2128 2166 var path = json.path;
2129 2167 this.session.rename_notebook(name, path);
2130 2168 this.events.trigger('notebook_renamed.Notebook', json);
2131 2169 };
2132 2170
2133 2171 Notebook.prototype.rename_error = function (xhr, status, error) {
2134 2172 var that = this;
2135 2173 var dialog_body = $('<div/>').append(
2136 2174 $("<p/>").text('This notebook name already exists.')
2137 2175 );
2138 2176 this.events.trigger('notebook_rename_failed.Notebook', [xhr, status, error]);
2139 2177 dialog.modal({
2140 2178 notebook: this,
2141 2179 keyboard_manager: this.keyboard_manager,
2142 2180 title: "Notebook Rename Error!",
2143 2181 body: dialog_body,
2144 2182 buttons : {
2145 2183 "Cancel": {},
2146 2184 "OK": {
2147 2185 class: "btn-primary",
2148 2186 click: function () {
2149 2187 this.save_widget.rename_notebook({notebook:that});
2150 2188 }}
2151 2189 },
2152 2190 open : function (event, ui) {
2153 2191 var that = $(this);
2154 2192 // Upon ENTER, click the OK button.
2155 2193 that.find('input[type="text"]').keydown(function (event, ui) {
2156 2194 if (event.which === this.keyboard.keycodes.enter) {
2157 2195 that.find('.btn-primary').first().click();
2158 2196 }
2159 2197 });
2160 2198 that.find('input[type="text"]').focus();
2161 2199 }
2162 2200 });
2163 2201 };
2164 2202
2165 2203 /**
2166 2204 * Request a notebook's data from the server.
2167 2205 *
2168 2206 * @method load_notebook
2169 2207 * @param {String} notebook_name and path A notebook to load
2170 2208 */
2171 2209 Notebook.prototype.load_notebook = function (notebook_name, notebook_path) {
2172 2210 var that = this;
2173 2211 this.notebook_name = notebook_name;
2174 2212 this.notebook_path = notebook_path;
2175 2213 // We do the call with settings so we can set cache to false.
2176 2214 var settings = {
2177 2215 processData : false,
2178 2216 cache : false,
2179 2217 type : "GET",
2180 2218 dataType : "json",
2181 2219 success : $.proxy(this.load_notebook_success,this),
2182 2220 error : $.proxy(this.load_notebook_error,this),
2183 2221 };
2184 2222 this.events.trigger('notebook_loading.Notebook');
2185 2223 var url = utils.url_join_encode(
2186 2224 this.base_url,
2187 2225 'api/contents',
2188 2226 this.notebook_path,
2189 2227 this.notebook_name
2190 2228 );
2191 2229 $.ajax(url, settings);
2192 2230 };
2193 2231
2194 2232 /**
2195 2233 * Success callback for loading a notebook from the server.
2196 2234 *
2197 2235 * Load notebook data from the JSON response.
2198 2236 *
2199 2237 * @method load_notebook_success
2200 2238 * @param {Object} data JSON representation of a notebook
2201 2239 * @param {String} status Description of response status
2202 2240 * @param {jqXHR} xhr jQuery Ajax object
2203 2241 */
2204 2242 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
2205 2243 this.fromJSON(data);
2206 2244 if (this.ncells() === 0) {
2207 2245 this.insert_cell_below('code');
2208 2246 this.edit_mode(0);
2209 2247 } else {
2210 2248 this.select(0);
2211 2249 this.handle_command_mode(this.get_cell(0));
2212 2250 }
2213 2251 this.set_dirty(false);
2214 2252 this.scroll_to_top();
2215 2253 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
2216 2254 var msg = "This notebook has been converted from an older " +
2217 2255 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
2218 2256 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
2219 2257 "newer notebook format will be used and older versions of IPython " +
2220 2258 "may not be able to read it. To keep the older version, close the " +
2221 2259 "notebook without saving it.";
2222 2260 dialog.modal({
2223 2261 notebook: this,
2224 2262 keyboard_manager: this.keyboard_manager,
2225 2263 title : "Notebook converted",
2226 2264 body : msg,
2227 2265 buttons : {
2228 2266 OK : {
2229 2267 class : "btn-primary"
2230 2268 }
2231 2269 }
2232 2270 });
2233 2271 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
2234 2272 var that = this;
2235 2273 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
2236 2274 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
2237 2275 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
2238 2276 this_vs + ". You can still work with this notebook, but some features " +
2239 2277 "introduced in later notebook versions may not be available.";
2240 2278
2241 2279 dialog.modal({
2242 2280 notebook: this,
2243 2281 keyboard_manager: this.keyboard_manager,
2244 2282 title : "Newer Notebook",
2245 2283 body : msg,
2246 2284 buttons : {
2247 2285 OK : {
2248 2286 class : "btn-danger"
2249 2287 }
2250 2288 }
2251 2289 });
2252 2290
2253 2291 }
2254 2292
2255 2293 // Create the session after the notebook is completely loaded to prevent
2256 2294 // code execution upon loading, which is a security risk.
2257 2295 if (this.session === null) {
2258 2296 var kernelspec = this.metadata.kernelspec || {};
2259 2297 var kernel_name = kernelspec.name || this.default_kernel_name;
2260 2298
2261 2299 this.start_session(kernel_name);
2262 2300 }
2263 2301 // load our checkpoint list
2264 2302 this.list_checkpoints();
2265 2303
2266 2304 // load toolbar state
2267 2305 if (this.metadata.celltoolbar) {
2268 2306 celltoolbar.CellToolbar.global_show();
2269 2307 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2270 2308 } else {
2271 2309 celltoolbar.CellToolbar.global_hide();
2272 2310 }
2273 2311
2274 2312 // now that we're fully loaded, it is safe to restore save functionality
2275 2313 delete(this.save_notebook);
2276 2314 this.events.trigger('notebook_loaded.Notebook');
2277 2315 };
2278 2316
2279 2317 /**
2280 2318 * Failure callback for loading a notebook from the server.
2281 2319 *
2282 2320 * @method load_notebook_error
2283 2321 * @param {jqXHR} xhr jQuery Ajax object
2284 2322 * @param {String} status Description of response status
2285 2323 * @param {String} error HTTP error message
2286 2324 */
2287 2325 Notebook.prototype.load_notebook_error = function (xhr, status, error) {
2288 2326 this.events.trigger('notebook_load_failed.Notebook', [xhr, status, error]);
2289 2327 var msg;
2290 2328 if (xhr.status === 400) {
2291 2329 msg = error;
2292 2330 } else if (xhr.status === 500) {
2293 2331 msg = "An unknown error occurred while loading this notebook. " +
2294 2332 "This version can load notebook formats " +
2295 2333 "v" + this.nbformat + " or earlier.";
2296 2334 }
2297 2335 dialog.modal({
2298 2336 notebook: this,
2299 2337 keyboard_manager: this.keyboard_manager,
2300 2338 title: "Error loading notebook",
2301 2339 body : msg,
2302 2340 buttons : {
2303 2341 "OK": {}
2304 2342 }
2305 2343 });
2306 2344 };
2307 2345
2308 2346 /********************* checkpoint-related *********************/
2309 2347
2310 2348 /**
2311 2349 * Save the notebook then immediately create a checkpoint.
2312 2350 *
2313 2351 * @method save_checkpoint
2314 2352 */
2315 2353 Notebook.prototype.save_checkpoint = function () {
2316 2354 this._checkpoint_after_save = true;
2317 2355 this.save_notebook();
2318 2356 };
2319 2357
2320 2358 /**
2321 2359 * Add a checkpoint for this notebook.
2322 2360 * for use as a callback from checkpoint creation.
2323 2361 *
2324 2362 * @method add_checkpoint
2325 2363 */
2326 2364 Notebook.prototype.add_checkpoint = function (checkpoint) {
2327 2365 var found = false;
2328 2366 for (var i = 0; i < this.checkpoints.length; i++) {
2329 2367 var existing = this.checkpoints[i];
2330 2368 if (existing.id == checkpoint.id) {
2331 2369 found = true;
2332 2370 this.checkpoints[i] = checkpoint;
2333 2371 break;
2334 2372 }
2335 2373 }
2336 2374 if (!found) {
2337 2375 this.checkpoints.push(checkpoint);
2338 2376 }
2339 2377 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2340 2378 };
2341 2379
2342 2380 /**
2343 2381 * List checkpoints for this notebook.
2344 2382 *
2345 2383 * @method list_checkpoints
2346 2384 */
2347 2385 Notebook.prototype.list_checkpoints = function () {
2348 2386 var url = utils.url_join_encode(
2349 2387 this.base_url,
2350 2388 'api/contents',
2351 2389 this.notebook_path,
2352 2390 this.notebook_name,
2353 2391 'checkpoints'
2354 2392 );
2355 2393 $.get(url).done(
2356 2394 $.proxy(this.list_checkpoints_success, this)
2357 2395 ).fail(
2358 2396 $.proxy(this.list_checkpoints_error, this)
2359 2397 );
2360 2398 };
2361 2399
2362 2400 /**
2363 2401 * Success callback for listing checkpoints.
2364 2402 *
2365 2403 * @method list_checkpoint_success
2366 2404 * @param {Object} data JSON representation of a checkpoint
2367 2405 * @param {String} status Description of response status
2368 2406 * @param {jqXHR} xhr jQuery Ajax object
2369 2407 */
2370 2408 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
2371 2409 data = $.parseJSON(data);
2372 2410 this.checkpoints = data;
2373 2411 if (data.length) {
2374 2412 this.last_checkpoint = data[data.length - 1];
2375 2413 } else {
2376 2414 this.last_checkpoint = null;
2377 2415 }
2378 2416 this.events.trigger('checkpoints_listed.Notebook', [data]);
2379 2417 };
2380 2418
2381 2419 /**
2382 2420 * Failure callback for listing a checkpoint.
2383 2421 *
2384 2422 * @method list_checkpoint_error
2385 2423 * @param {jqXHR} xhr jQuery Ajax object
2386 2424 * @param {String} status Description of response status
2387 2425 * @param {String} error_msg HTTP error message
2388 2426 */
2389 2427 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
2390 2428 this.events.trigger('list_checkpoints_failed.Notebook');
2391 2429 };
2392 2430
2393 2431 /**
2394 2432 * Create a checkpoint of this notebook on the server from the most recent save.
2395 2433 *
2396 2434 * @method create_checkpoint
2397 2435 */
2398 2436 Notebook.prototype.create_checkpoint = function () {
2399 2437 var url = utils.url_join_encode(
2400 2438 this.base_url,
2401 2439 'api/contents',
2402 2440 this.notebook_path,
2403 2441 this.notebook_name,
2404 2442 'checkpoints'
2405 2443 );
2406 2444 $.post(url).done(
2407 2445 $.proxy(this.create_checkpoint_success, this)
2408 2446 ).fail(
2409 2447 $.proxy(this.create_checkpoint_error, this)
2410 2448 );
2411 2449 };
2412 2450
2413 2451 /**
2414 2452 * Success callback for creating a checkpoint.
2415 2453 *
2416 2454 * @method create_checkpoint_success
2417 2455 * @param {Object} data JSON representation of a checkpoint
2418 2456 * @param {String} status Description of response status
2419 2457 * @param {jqXHR} xhr jQuery Ajax object
2420 2458 */
2421 2459 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
2422 2460 data = $.parseJSON(data);
2423 2461 this.add_checkpoint(data);
2424 2462 this.events.trigger('checkpoint_created.Notebook', data);
2425 2463 };
2426 2464
2427 2465 /**
2428 2466 * Failure callback for creating a checkpoint.
2429 2467 *
2430 2468 * @method create_checkpoint_error
2431 2469 * @param {jqXHR} xhr jQuery Ajax object
2432 2470 * @param {String} status Description of response status
2433 2471 * @param {String} error_msg HTTP error message
2434 2472 */
2435 2473 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
2436 2474 this.events.trigger('checkpoint_failed.Notebook');
2437 2475 };
2438 2476
2439 2477 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2440 2478 var that = this;
2441 2479 checkpoint = checkpoint || this.last_checkpoint;
2442 2480 if ( ! checkpoint ) {
2443 2481 console.log("restore dialog, but no checkpoint to restore to!");
2444 2482 return;
2445 2483 }
2446 2484 var body = $('<div/>').append(
2447 2485 $('<p/>').addClass("p-space").text(
2448 2486 "Are you sure you want to revert the notebook to " +
2449 2487 "the latest checkpoint?"
2450 2488 ).append(
2451 2489 $("<strong/>").text(
2452 2490 " This cannot be undone."
2453 2491 )
2454 2492 )
2455 2493 ).append(
2456 2494 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2457 2495 ).append(
2458 2496 $('<p/>').addClass("p-space").text(
2459 2497 Date(checkpoint.last_modified)
2460 2498 ).css("text-align", "center")
2461 2499 );
2462 2500
2463 2501 dialog.modal({
2464 2502 notebook: this,
2465 2503 keyboard_manager: this.keyboard_manager,
2466 2504 title : "Revert notebook to checkpoint",
2467 2505 body : body,
2468 2506 buttons : {
2469 2507 Revert : {
2470 2508 class : "btn-danger",
2471 2509 click : function () {
2472 2510 that.restore_checkpoint(checkpoint.id);
2473 2511 }
2474 2512 },
2475 2513 Cancel : {}
2476 2514 }
2477 2515 });
2478 2516 };
2479 2517
2480 2518 /**
2481 2519 * Restore the notebook to a checkpoint state.
2482 2520 *
2483 2521 * @method restore_checkpoint
2484 2522 * @param {String} checkpoint ID
2485 2523 */
2486 2524 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2487 2525 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2488 2526 var url = utils.url_join_encode(
2489 2527 this.base_url,
2490 2528 'api/contents',
2491 2529 this.notebook_path,
2492 2530 this.notebook_name,
2493 2531 'checkpoints',
2494 2532 checkpoint
2495 2533 );
2496 2534 $.post(url).done(
2497 2535 $.proxy(this.restore_checkpoint_success, this)
2498 2536 ).fail(
2499 2537 $.proxy(this.restore_checkpoint_error, this)
2500 2538 );
2501 2539 };
2502 2540
2503 2541 /**
2504 2542 * Success callback for restoring a notebook to a checkpoint.
2505 2543 *
2506 2544 * @method restore_checkpoint_success
2507 2545 * @param {Object} data (ignored, should be empty)
2508 2546 * @param {String} status Description of response status
2509 2547 * @param {jqXHR} xhr jQuery Ajax object
2510 2548 */
2511 2549 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2512 2550 this.events.trigger('checkpoint_restored.Notebook');
2513 2551 this.load_notebook(this.notebook_name, this.notebook_path);
2514 2552 };
2515 2553
2516 2554 /**
2517 2555 * Failure callback for restoring a notebook to a checkpoint.
2518 2556 *
2519 2557 * @method restore_checkpoint_error
2520 2558 * @param {jqXHR} xhr jQuery Ajax object
2521 2559 * @param {String} status Description of response status
2522 2560 * @param {String} error_msg HTTP error message
2523 2561 */
2524 2562 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2525 2563 this.events.trigger('checkpoint_restore_failed.Notebook');
2526 2564 };
2527 2565
2528 2566 /**
2529 2567 * Delete a notebook checkpoint.
2530 2568 *
2531 2569 * @method delete_checkpoint
2532 2570 * @param {String} checkpoint ID
2533 2571 */
2534 2572 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2535 2573 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2536 2574 var url = utils.url_join_encode(
2537 2575 this.base_url,
2538 2576 'api/contents',
2539 2577 this.notebook_path,
2540 2578 this.notebook_name,
2541 2579 'checkpoints',
2542 2580 checkpoint
2543 2581 );
2544 2582 $.ajax(url, {
2545 2583 type: 'DELETE',
2546 2584 success: $.proxy(this.delete_checkpoint_success, this),
2547 2585 error: $.proxy(this.delete_checkpoint_error, this)
2548 2586 });
2549 2587 };
2550 2588
2551 2589 /**
2552 2590 * Success callback for deleting a notebook checkpoint
2553 2591 *
2554 2592 * @method delete_checkpoint_success
2555 2593 * @param {Object} data (ignored, should be empty)
2556 2594 * @param {String} status Description of response status
2557 2595 * @param {jqXHR} xhr jQuery Ajax object
2558 2596 */
2559 2597 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2560 2598 this.events.trigger('checkpoint_deleted.Notebook', data);
2561 2599 this.load_notebook(this.notebook_name, this.notebook_path);
2562 2600 };
2563 2601
2564 2602 /**
2565 2603 * Failure callback for deleting a notebook checkpoint.
2566 2604 *
2567 2605 * @method delete_checkpoint_error
2568 2606 * @param {jqXHR} xhr jQuery Ajax object
2569 2607 * @param {String} status Description of response status
2570 2608 * @param {String} error_msg HTTP error message
2571 2609 */
2572 2610 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2573 2611 this.events.trigger('checkpoint_delete_failed.Notebook');
2574 2612 };
2575 2613
2576 2614
2577 2615 // For backwards compatability.
2578 2616 IPython.Notebook = Notebook;
2579 2617
2580 2618 return {'Notebook': Notebook};
2581 2619 });
@@ -1,617 +1,618 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'services/kernels/js/comm',
9 9 'widgets/js/init',
10 10 ], function(IPython, $, utils, comm, widgetmanager) {
11 11 "use strict";
12 12
13 13 // Initialization and connection.
14 14 /**
15 15 * A Kernel Class to communicate with the Python kernel
16 16 * @Class Kernel
17 17 */
18 18 var Kernel = function (kernel_service_url, ws_url, notebook, name) {
19 19 this.events = notebook.events;
20 20 this.kernel_id = null;
21 21 this.shell_channel = null;
22 22 this.iopub_channel = null;
23 23 this.stdin_channel = null;
24 24 this.kernel_service_url = kernel_service_url;
25 25 this.name = name;
26 26 this.ws_url = ws_url || IPython.utils.get_body_data("wsUrl");
27 27 if (!this.ws_url) {
28 28 // trailing 's' in https will become wss for secure web sockets
29 29 this.ws_url = location.protocol.replace('http', 'ws') + "//" + location.host;
30 30 }
31 31 this.running = false;
32 32 this.username = "username";
33 33 this.session_id = utils.uuid();
34 34 this._msg_callbacks = {};
35 35 this.post = $.post;
36 36
37 37 if (typeof(WebSocket) !== 'undefined') {
38 38 this.WebSocket = WebSocket;
39 39 } else if (typeof(MozWebSocket) !== 'undefined') {
40 40 this.WebSocket = MozWebSocket;
41 41 } else {
42 42 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.');
43 43 }
44 44
45 45 this.bind_events();
46 46 this.init_iopub_handlers();
47 47 this.comm_manager = new comm.CommManager(this);
48 48 this.widget_manager = new widgetmanager.WidgetManager(this.comm_manager, notebook);
49 49
50 50 this.last_msg_id = null;
51 51 this.last_msg_callbacks = {};
52 52 };
53 53
54 54
55 55 Kernel.prototype._get_msg = function (msg_type, content, metadata) {
56 56 var msg = {
57 57 header : {
58 58 msg_id : utils.uuid(),
59 59 username : this.username,
60 60 session : this.session_id,
61 61 msg_type : msg_type,
62 62 version : "5.0"
63 63 },
64 64 metadata : metadata || {},
65 65 content : content,
66 66 parent_header : {}
67 67 };
68 68 return msg;
69 69 };
70 70
71 71 Kernel.prototype.bind_events = function () {
72 72 var that = this;
73 73 this.events.on('send_input_reply.Kernel', function(evt, data) {
74 74 that.send_input_reply(data);
75 75 });
76 76 };
77 77
78 78 // Initialize the iopub handlers
79 79
80 80 Kernel.prototype.init_iopub_handlers = function () {
81 81 var output_msg_types = ['stream', 'display_data', 'execute_result', 'error'];
82 82 this._iopub_handlers = {};
83 83 this.register_iopub_handler('status', $.proxy(this._handle_status_message, this));
84 84 this.register_iopub_handler('clear_output', $.proxy(this._handle_clear_output, this));
85 85
86 86 for (var i=0; i < output_msg_types.length; i++) {
87 87 this.register_iopub_handler(output_msg_types[i], $.proxy(this._handle_output_message, this));
88 88 }
89 89 };
90 90
91 91 /**
92 92 * Start the Python kernel
93 93 * @method start
94 94 */
95 95 Kernel.prototype.start = function (params) {
96 96 params = params || {};
97 97 if (!this.running) {
98 98 var qs = $.param(params);
99 99 this.post(utils.url_join_encode(this.kernel_service_url) + '?' + qs,
100 100 $.proxy(this._kernel_started, this),
101 101 'json'
102 102 );
103 103 }
104 104 };
105 105
106 106 /**
107 107 * Restart the python kernel.
108 108 *
109 109 * Emit a 'status_restarting.Kernel' event with
110 110 * the current object as parameter
111 111 *
112 112 * @method restart
113 113 */
114 114 Kernel.prototype.restart = function () {
115 115 this.events.trigger('status_restarting.Kernel', {kernel: this});
116 116 if (this.running) {
117 117 this.stop_channels();
118 118 this.post(utils.url_join_encode(this.kernel_url, "restart"),
119 119 $.proxy(this._kernel_started, this),
120 120 'json'
121 121 );
122 122 }
123 123 };
124 124
125 125
126 126 Kernel.prototype._kernel_started = function (json) {
127 127 console.log("Kernel started: ", json.id);
128 128 this.running = true;
129 129 this.kernel_id = json.id;
130 130 this.kernel_url = utils.url_path_join(this.kernel_service_url, this.kernel_id);
131 131 this.start_channels();
132 132 };
133 133
134 134
135 135 Kernel.prototype._websocket_closed = function(ws_url, early) {
136 136 this.stop_channels();
137 137 this.events.trigger('websocket_closed.Kernel',
138 138 {ws_url: ws_url, kernel: this, early: early}
139 139 );
140 140 };
141 141
142 142 /**
143 143 * Start the `shell`and `iopub` channels.
144 144 * Will stop and restart them if they already exist.
145 145 *
146 146 * @method start_channels
147 147 */
148 148 Kernel.prototype.start_channels = function () {
149 149 var that = this;
150 150 this.stop_channels();
151 151 var ws_host_url = this.ws_url + this.kernel_url;
152 152 console.log("Starting WebSockets:", ws_host_url);
153 153 this.shell_channel = new this.WebSocket(
154 154 this.ws_url + utils.url_join_encode(this.kernel_url, "shell")
155 155 );
156 156 this.stdin_channel = new this.WebSocket(
157 157 this.ws_url + utils.url_join_encode(this.kernel_url, "stdin")
158 158 );
159 159 this.iopub_channel = new this.WebSocket(
160 160 this.ws_url + utils.url_join_encode(this.kernel_url, "iopub")
161 161 );
162 162
163 163 var already_called_onclose = false; // only alert once
164 164 var ws_closed_early = function(evt){
165 165 if (already_called_onclose){
166 166 return;
167 167 }
168 168 already_called_onclose = true;
169 169 if ( ! evt.wasClean ){
170 170 that._websocket_closed(ws_host_url, true);
171 171 }
172 172 };
173 173 var ws_closed_late = function(evt){
174 174 if (already_called_onclose){
175 175 return;
176 176 }
177 177 already_called_onclose = true;
178 178 if ( ! evt.wasClean ){
179 179 that._websocket_closed(ws_host_url, false);
180 180 }
181 181 };
182 182 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
183 183 for (var i=0; i < channels.length; i++) {
184 184 channels[i].onopen = $.proxy(this._ws_opened, this);
185 185 channels[i].onclose = ws_closed_early;
186 186 }
187 187 // switch from early-close to late-close message after 1s
188 188 setTimeout(function() {
189 189 for (var i=0; i < channels.length; i++) {
190 190 if (channels[i] !== null) {
191 191 channels[i].onclose = ws_closed_late;
192 192 }
193 193 }
194 194 }, 1000);
195 195 this.shell_channel.onmessage = $.proxy(this._handle_shell_reply, this);
196 196 this.iopub_channel.onmessage = $.proxy(this._handle_iopub_message, this);
197 197 this.stdin_channel.onmessage = $.proxy(this._handle_input_request, this);
198 198 };
199 199
200 200 /**
201 201 * Handle a websocket entering the open state
202 202 * sends session and cookie authentication info as first message.
203 203 * Once all sockets are open, signal the Kernel.status_started event.
204 204 * @method _ws_opened
205 205 */
206 206 Kernel.prototype._ws_opened = function (evt) {
207 207 // send the session id so the Session object Python-side
208 208 // has the same identity
209 209 evt.target.send(this.session_id + ':' + document.cookie);
210 210
211 211 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
212 212 for (var i=0; i < channels.length; i++) {
213 213 // if any channel is not ready, don't trigger event.
214 214 if ( !channels[i].readyState ) return;
215 215 }
216 216 // all events ready, trigger started event.
217 217 this.events.trigger('status_started.Kernel', {kernel: this});
218 218 };
219 219
220 220 /**
221 221 * Stop the websocket channels.
222 222 * @method stop_channels
223 223 */
224 224 Kernel.prototype.stop_channels = function () {
225 225 var channels = [this.shell_channel, this.iopub_channel, this.stdin_channel];
226 226 for (var i=0; i < channels.length; i++) {
227 227 if ( channels[i] !== null ) {
228 228 channels[i].onclose = null;
229 229 channels[i].close();
230 230 }
231 231 }
232 232 this.shell_channel = this.iopub_channel = this.stdin_channel = null;
233 233 };
234 234
235 235 // Main public methods.
236 236
237 237 // send a message on the Kernel's shell channel
238 238 Kernel.prototype.send_shell_message = function (msg_type, content, callbacks, metadata) {
239 239 var msg = this._get_msg(msg_type, content, metadata);
240 240 this.shell_channel.send(JSON.stringify(msg));
241 241 this.set_callbacks_for_msg(msg.header.msg_id, callbacks);
242 242 return msg.header.msg_id;
243 243 };
244 244
245 245 /**
246 246 * Get kernel info
247 247 *
248 248 * @param callback {function}
249 249 * @method kernel_info
250 250 *
251 251 * When calling this method, pass a callback function that expects one argument.
252 252 * The callback will be passed the complete `kernel_info_reply` message documented
253 253 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#kernel-info)
254 254 */
255 255 Kernel.prototype.kernel_info = function (callback) {
256 256 var callbacks;
257 257 if (callback) {
258 258 callbacks = { shell : { reply : callback } };
259 259 }
260 260 return this.send_shell_message("kernel_info_request", {}, callbacks);
261 261 };
262 262
263 263 /**
264 264 * Get info on an object
265 265 *
266 266 * @param code {string}
267 267 * @param cursor_pos {integer}
268 268 * @param callback {function}
269 269 * @method inspect
270 270 *
271 271 * When calling this method, pass a callback function that expects one argument.
272 272 * The callback will be passed the complete `inspect_reply` message documented
273 273 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#object-information)
274 274 */
275 275 Kernel.prototype.inspect = function (code, cursor_pos, callback) {
276 276 var callbacks;
277 277 if (callback) {
278 278 callbacks = { shell : { reply : callback } };
279 279 }
280 280
281 281 var content = {
282 282 code : code,
283 283 cursor_pos : cursor_pos,
284 284 detail_level : 0,
285 285 };
286 286 return this.send_shell_message("inspect_request", content, callbacks);
287 287 };
288 288
289 289 /**
290 290 * Execute given code into kernel, and pass result to callback.
291 291 *
292 292 * @async
293 293 * @method execute
294 294 * @param {string} code
295 295 * @param [callbacks] {Object} With the following keys (all optional)
296 296 * @param callbacks.shell.reply {function}
297 297 * @param callbacks.shell.payload.[payload_name] {function}
298 298 * @param callbacks.iopub.output {function}
299 299 * @param callbacks.iopub.clear_output {function}
300 300 * @param callbacks.input {function}
301 301 * @param {object} [options]
302 302 * @param [options.silent=false] {Boolean}
303 303 * @param [options.user_expressions=empty_dict] {Dict}
304 304 * @param [options.allow_stdin=false] {Boolean} true|false
305 305 *
306 306 * @example
307 307 *
308 308 * The options object should contain the options for the execute call. Its default
309 309 * values are:
310 310 *
311 311 * options = {
312 312 * silent : true,
313 313 * user_expressions : {},
314 314 * allow_stdin : false
315 315 * }
316 316 *
317 317 * When calling this method pass a callbacks structure of the form:
318 318 *
319 319 * callbacks = {
320 320 * shell : {
321 321 * reply : execute_reply_callback,
322 322 * payload : {
323 323 * set_next_input : set_next_input_callback,
324 324 * }
325 325 * },
326 326 * iopub : {
327 327 * output : output_callback,
328 328 * clear_output : clear_output_callback,
329 329 * },
330 330 * input : raw_input_callback
331 331 * }
332 332 *
333 333 * Each callback will be passed the entire message as a single arugment.
334 334 * Payload handlers will be passed the corresponding payload and the execute_reply message.
335 335 */
336 336 Kernel.prototype.execute = function (code, callbacks, options) {
337 337
338 338 var content = {
339 339 code : code,
340 340 silent : true,
341 341 store_history : false,
342 342 user_expressions : {},
343 343 allow_stdin : false
344 344 };
345 345 callbacks = callbacks || {};
346 346 if (callbacks.input !== undefined) {
347 347 content.allow_stdin = true;
348 348 }
349 349 $.extend(true, content, options);
350 350 this.events.trigger('execution_request.Kernel', {kernel: this, content:content});
351 351 return this.send_shell_message("execute_request", content, callbacks);
352 352 };
353 353
354 354 /**
355 355 * When calling this method, pass a function to be called with the `complete_reply` message
356 356 * as its only argument when it arrives.
357 357 *
358 358 * `complete_reply` is documented
359 359 * [here](http://ipython.org/ipython-doc/dev/development/messaging.html#complete)
360 360 *
361 361 * @method complete
362 362 * @param code {string}
363 363 * @param cursor_pos {integer}
364 364 * @param callback {function}
365 365 *
366 366 */
367 367 Kernel.prototype.complete = function (code, cursor_pos, callback) {
368 368 var callbacks;
369 369 if (callback) {
370 370 callbacks = { shell : { reply : callback } };
371 371 }
372 372 var content = {
373 373 code : code,
374 374 cursor_pos : cursor_pos,
375 375 };
376 376 return this.send_shell_message("complete_request", content, callbacks);
377 377 };
378 378
379 379
380 380 Kernel.prototype.interrupt = function () {
381 381 if (this.running) {
382 382 this.events.trigger('status_interrupting.Kernel', {kernel: this});
383 383 this.post(utils.url_join_encode(this.kernel_url, "interrupt"));
384 384 }
385 385 };
386 386
387 387
388 Kernel.prototype.kill = function () {
388 Kernel.prototype.kill = function (success, faiure) {
389 389 if (this.running) {
390 390 this.running = false;
391 391 var settings = {
392 392 cache : false,
393 393 type : "DELETE",
394 error : utils.log_ajax_error,
394 success : success,
395 error : error || utils.log_ajax_error,
395 396 };
396 397 $.ajax(utils.url_join_encode(this.kernel_url), settings);
397 398 this.stop_channels();
398 399 }
399 400 };
400 401
401 402 Kernel.prototype.send_input_reply = function (input) {
402 403 var content = {
403 404 value : input,
404 405 };
405 406 this.events.trigger('input_reply.Kernel', {kernel: this, content:content});
406 407 var msg = this._get_msg("input_reply", content);
407 408 this.stdin_channel.send(JSON.stringify(msg));
408 409 return msg.header.msg_id;
409 410 };
410 411
411 412
412 413 // Reply handlers
413 414
414 415 Kernel.prototype.register_iopub_handler = function (msg_type, callback) {
415 416 this._iopub_handlers[msg_type] = callback;
416 417 };
417 418
418 419 Kernel.prototype.get_iopub_handler = function (msg_type) {
419 420 // get iopub handler for a specific message type
420 421 return this._iopub_handlers[msg_type];
421 422 };
422 423
423 424
424 425 Kernel.prototype.get_callbacks_for_msg = function (msg_id) {
425 426 // get callbacks for a specific message
426 427 if (msg_id == this.last_msg_id) {
427 428 return this.last_msg_callbacks;
428 429 } else {
429 430 return this._msg_callbacks[msg_id];
430 431 }
431 432 };
432 433
433 434
434 435 Kernel.prototype.clear_callbacks_for_msg = function (msg_id) {
435 436 if (this._msg_callbacks[msg_id] !== undefined ) {
436 437 delete this._msg_callbacks[msg_id];
437 438 }
438 439 };
439 440
440 441 Kernel.prototype._finish_shell = function (msg_id) {
441 442 var callbacks = this._msg_callbacks[msg_id];
442 443 if (callbacks !== undefined) {
443 444 callbacks.shell_done = true;
444 445 if (callbacks.iopub_done) {
445 446 this.clear_callbacks_for_msg(msg_id);
446 447 }
447 448 }
448 449 };
449 450
450 451 Kernel.prototype._finish_iopub = function (msg_id) {
451 452 var callbacks = this._msg_callbacks[msg_id];
452 453 if (callbacks !== undefined) {
453 454 callbacks.iopub_done = true;
454 455 if (callbacks.shell_done) {
455 456 this.clear_callbacks_for_msg(msg_id);
456 457 }
457 458 }
458 459 };
459 460
460 461 /* Set callbacks for a particular message.
461 462 * Callbacks should be a struct of the following form:
462 463 * shell : {
463 464 *
464 465 * }
465 466
466 467 */
467 468 Kernel.prototype.set_callbacks_for_msg = function (msg_id, callbacks) {
468 469 this.last_msg_id = msg_id;
469 470 if (callbacks) {
470 471 // shallow-copy mapping, because we will modify it at the top level
471 472 var cbcopy = this._msg_callbacks[msg_id] = this.last_msg_callbacks = {};
472 473 cbcopy.shell = callbacks.shell;
473 474 cbcopy.iopub = callbacks.iopub;
474 475 cbcopy.input = callbacks.input;
475 476 cbcopy.shell_done = (!callbacks.shell);
476 477 cbcopy.iopub_done = (!callbacks.iopub);
477 478 } else {
478 479 this.last_msg_callbacks = {};
479 480 }
480 481 };
481 482
482 483
483 484 Kernel.prototype._handle_shell_reply = function (e) {
484 485 var reply = $.parseJSON(e.data);
485 486 this.events.trigger('shell_reply.Kernel', {kernel: this, reply:reply});
486 487 var content = reply.content;
487 488 var metadata = reply.metadata;
488 489 var parent_id = reply.parent_header.msg_id;
489 490 var callbacks = this.get_callbacks_for_msg(parent_id);
490 491 if (!callbacks || !callbacks.shell) {
491 492 return;
492 493 }
493 494 var shell_callbacks = callbacks.shell;
494 495
495 496 // signal that shell callbacks are done
496 497 this._finish_shell(parent_id);
497 498
498 499 if (shell_callbacks.reply !== undefined) {
499 500 shell_callbacks.reply(reply);
500 501 }
501 502 if (content.payload && shell_callbacks.payload) {
502 503 this._handle_payloads(content.payload, shell_callbacks.payload, reply);
503 504 }
504 505 };
505 506
506 507
507 508 Kernel.prototype._handle_payloads = function (payloads, payload_callbacks, msg) {
508 509 var l = payloads.length;
509 510 // Payloads are handled by triggering events because we don't want the Kernel
510 511 // to depend on the Notebook or Pager classes.
511 512 for (var i=0; i<l; i++) {
512 513 var payload = payloads[i];
513 514 var callback = payload_callbacks[payload.source];
514 515 if (callback) {
515 516 callback(payload, msg);
516 517 }
517 518 }
518 519 };
519 520
520 521 Kernel.prototype._handle_status_message = function (msg) {
521 522 var execution_state = msg.content.execution_state;
522 523 var parent_id = msg.parent_header.msg_id;
523 524
524 525 // dispatch status msg callbacks, if any
525 526 var callbacks = this.get_callbacks_for_msg(parent_id);
526 527 if (callbacks && callbacks.iopub && callbacks.iopub.status) {
527 528 try {
528 529 callbacks.iopub.status(msg);
529 530 } catch (e) {
530 531 console.log("Exception in status msg handler", e, e.stack);
531 532 }
532 533 }
533 534
534 535 if (execution_state === 'busy') {
535 536 this.events.trigger('status_busy.Kernel', {kernel: this});
536 537 } else if (execution_state === 'idle') {
537 538 // signal that iopub callbacks are (probably) done
538 539 // async output may still arrive,
539 540 // but only for the most recent request
540 541 this._finish_iopub(parent_id);
541 542
542 543 // trigger status_idle event
543 544 this.events.trigger('status_idle.Kernel', {kernel: this});
544 545 } else if (execution_state === 'restarting') {
545 546 // autorestarting is distinct from restarting,
546 547 // in that it means the kernel died and the server is restarting it.
547 548 // status_restarting sets the notification widget,
548 549 // autorestart shows the more prominent dialog.
549 550 this.events.trigger('status_autorestarting.Kernel', {kernel: this});
550 551 this.events.trigger('status_restarting.Kernel', {kernel: this});
551 552 } else if (execution_state === 'dead') {
552 553 this.stop_channels();
553 554 this.events.trigger('status_dead.Kernel', {kernel: this});
554 555 }
555 556 };
556 557
557 558
558 559 // handle clear_output message
559 560 Kernel.prototype._handle_clear_output = function (msg) {
560 561 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
561 562 if (!callbacks || !callbacks.iopub) {
562 563 return;
563 564 }
564 565 var callback = callbacks.iopub.clear_output;
565 566 if (callback) {
566 567 callback(msg);
567 568 }
568 569 };
569 570
570 571
571 572 // handle an output message (execute_result, display_data, etc.)
572 573 Kernel.prototype._handle_output_message = function (msg) {
573 574 var callbacks = this.get_callbacks_for_msg(msg.parent_header.msg_id);
574 575 if (!callbacks || !callbacks.iopub) {
575 576 return;
576 577 }
577 578 var callback = callbacks.iopub.output;
578 579 if (callback) {
579 580 callback(msg);
580 581 }
581 582 };
582 583
583 584 // dispatch IOPub messages to respective handlers.
584 585 // each message type should have a handler.
585 586 Kernel.prototype._handle_iopub_message = function (e) {
586 587 var msg = $.parseJSON(e.data);
587 588
588 589 var handler = this.get_iopub_handler(msg.header.msg_type);
589 590 if (handler !== undefined) {
590 591 handler(msg);
591 592 }
592 593 };
593 594
594 595
595 596 Kernel.prototype._handle_input_request = function (e) {
596 597 var request = $.parseJSON(e.data);
597 598 var header = request.header;
598 599 var content = request.content;
599 600 var metadata = request.metadata;
600 601 var msg_type = header.msg_type;
601 602 if (msg_type !== 'input_request') {
602 603 console.log("Invalid input request!", request);
603 604 return;
604 605 }
605 606 var callbacks = this.get_callbacks_for_msg(request.parent_header.msg_id);
606 607 if (callbacks) {
607 608 if (callbacks.input) {
608 609 callbacks.input(request);
609 610 }
610 611 }
611 612 };
612 613
613 614 // Backwards compatability.
614 615 IPython.Kernel = Kernel;
615 616
616 617 return {'Kernel': Kernel};
617 618 });
@@ -1,126 +1,137 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'services/kernels/js/kernel',
9 9 ], function(IPython, $, utils, kernel) {
10 10 "use strict";
11 11
12 12 var Session = function(options){
13 13 this.kernel = null;
14 14 this.id = null;
15 15 this.notebook = options.notebook;
16 16 this.events = options.notebook.events;
17 17 this.name = options.notebook_name;
18 18 this.path = options.notebook_path;
19 19 this.kernel_name = options.kernel_name;
20 20 this.base_url = options.base_url;
21 21 this.ws_url = options.ws_url;
22 22 };
23 23
24 Session.prototype.start = function(callback) {
24 Session.prototype.start = function (success, error) {
25 25 var that = this;
26 26 var model = {
27 27 notebook : {
28 28 name : this.name,
29 29 path : this.path
30 30 },
31 31 kernel : {
32 32 name : this.kernel_name
33 33 }
34 34 };
35 35 var settings = {
36 36 processData : false,
37 37 cache : false,
38 38 type : "POST",
39 39 data: JSON.stringify(model),
40 40 dataType : "json",
41 41 success : function (data, status, xhr) {
42 42 that._handle_start_success(data);
43 if (callback) {
44 callback(data, status, xhr);
43 if (success) {
44 success(data, status, xhr);
45 45 }
46 46 },
47 error : utils.log_ajax_error,
47 error : error || utils.log_ajax_error,
48 48 };
49 49 var url = utils.url_join_encode(this.base_url, 'api/sessions');
50 50 $.ajax(url, settings);
51 51 };
52 52
53 53 Session.prototype.rename_notebook = function (name, path) {
54 54 this.name = name;
55 55 this.path = path;
56 56 var model = {
57 57 notebook : {
58 58 name : this.name,
59 59 path : this.path
60 60 }
61 61 };
62 62 var settings = {
63 63 processData : false,
64 64 cache : false,
65 65 type : "PATCH",
66 66 data: JSON.stringify(model),
67 67 dataType : "json",
68 68 error : utils.log_ajax_error,
69 69 };
70 70 var url = utils.url_join_encode(this.base_url, 'api/sessions', this.id);
71 71 $.ajax(url, settings);
72 72 };
73 73
74 Session.prototype.delete = function() {
74 Session.prototype.delete = function (success, error) {
75 75 var settings = {
76 76 processData : false,
77 77 cache : false,
78 78 type : "DELETE",
79 79 dataType : "json",
80 error : utils.log_ajax_error,
80 success : success,
81 error : error || utils.log_ajax_error,
81 82 };
82 83 this.kernel.running = false;
83 84 this.kernel.stop_channels();
84 85 var url = utils.url_join_encode(this.base_url, 'api/sessions', this.id);
85 86 $.ajax(url, settings);
86 87 };
87 88
88 89 // Kernel related things
89 90 /**
90 91 * Create the Kernel object associated with this Session.
91 92 *
92 93 * @method _handle_start_success
93 94 */
94 95 Session.prototype._handle_start_success = function (data, status, xhr) {
95 96 this.id = data.id;
96 97 // If we asked for 'python', the response will have 'python3' or 'python2'.
97 98 this.kernel_name = data.kernel.name;
98 99 this.events.trigger('started.Session', this);
99 100 var kernel_service_url = utils.url_path_join(this.base_url, "api/kernels");
100 101 this.kernel = new kernel.Kernel(kernel_service_url, this.ws_url, this.notebook, this.kernel_name);
101 102 this.kernel._kernel_started(data.kernel);
102 103 };
103 104
104 105 /**
105 106 * Prompt the user to restart the IPython kernel.
106 107 *
107 108 * @method restart_kernel
108 109 */
109 110 Session.prototype.restart_kernel = function () {
110 111 this.kernel.restart();
111 112 };
112 113
113 114 Session.prototype.interrupt_kernel = function() {
114 115 this.kernel.interrupt();
115 116 };
116 117
117 118
118 119 Session.prototype.kill_kernel = function() {
119 120 this.kernel.kill();
120 121 };
121 122
123 var SessionAlreadyStarting = function (message) {
124 this.name = "SessionAlreadyStarting";
125 this.message = (message || "");
126 };
127
128 SessionAlreadyStarting.prototype = Error.prototype;
129
122 130 // For backwards compatability.
123 131 IPython.Session = Session;
124 132
125 return {'Session': Session};
133 return {
134 Session: Session,
135 SessionAlreadyStarting: SessionAlreadyStarting,
136 };
126 137 });
General Comments 0
You need to be logged in to leave comments. Login now