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