##// END OF EJS Templates
handle somehting selected and multiple cursors and 4 tabs show content in pager
Matthias Bussonnier -
Show More
@@ -1,530 +1,532 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 'notebook/js/cell',
18 18 'notebook/js/outputarea',
19 19 'notebook/js/completer',
20 20 'notebook/js/celltoolbar',
21 21 'codemirror/lib/codemirror',
22 22 'codemirror/mode/python/python',
23 23 'notebook/js/codemirror-ipython'
24 24 ], function(IPython, $, utils, keyboard, cell, outputarea, completer, celltoolbar, CodeMirror, cmpython, cmip) {
25 25 "use strict";
26 26 var Cell = cell.Cell;
27 27
28 28 /* local util for codemirror */
29 29 var posEq = function(a, b) {return a.line == b.line && a.ch == b.ch;};
30 30
31 31 /**
32 32 *
33 33 * function to delete until previous non blanking space character
34 34 * or first multiple of 4 tabstop.
35 35 * @private
36 36 */
37 37 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
38 38 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
39 39 if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
40 40 var cur = cm.getCursor(), line = cm.getLine(cur.line);
41 41 var tabsize = cm.getOption('tabSize');
42 42 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
43 43 from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
44 44 var select = cm.getRange(from,cur);
45 45 if( select.match(/^\ +$/) !== null){
46 46 cm.replaceRange("",from,cur);
47 47 } else {
48 48 cm.deleteH(-1,"char");
49 49 }
50 50 };
51 51
52 52 var keycodes = keyboard.keycodes;
53 53
54 54 var CodeCell = function (kernel, options) {
55 55 // Constructor
56 56 //
57 57 // A Cell conceived to write code.
58 58 //
59 59 // Parameters:
60 60 // kernel: Kernel instance
61 61 // The kernel doesn't have to be set at creation time, in that case
62 62 // it will be null and set_kernel has to be called later.
63 63 // options: dictionary
64 64 // Dictionary of keyword arguments.
65 65 // events: $(Events) instance
66 66 // config: dictionary
67 67 // keyboard_manager: KeyboardManager instance
68 68 // notebook: Notebook instance
69 69 // tooltip: Tooltip instance
70 70 this.kernel = kernel || null;
71 71 this.notebook = options.notebook;
72 72 this.collapsed = false;
73 73 this.events = options.events;
74 74 this.tooltip = options.tooltip;
75 75 this.config = options.config;
76 76
77 77 // create all attributed in constructor function
78 78 // even if null for V8 VM optimisation
79 79 this.input_prompt_number = null;
80 80 this.celltoolbar = null;
81 81 this.output_area = null;
82 82 this.last_msg_id = null;
83 83 this.completer = null;
84 84
85 85
86 86 var config = utils.mergeopt(CodeCell, this.config);
87 87 Cell.apply(this,[{
88 88 config: config,
89 89 keyboard_manager: options.keyboard_manager,
90 90 events: this.events}]);
91 91
92 92 // Attributes we want to override in this subclass.
93 93 this.cell_type = "code";
94 94
95 95 var that = this;
96 96 this.element.focusout(
97 97 function() { that.auto_highlight(); }
98 98 );
99 99 };
100 100
101 101 CodeCell.options_default = {
102 102 cm_config : {
103 103 extraKeys: {
104 104 "Tab" : "indentMore",
105 105 "Shift-Tab" : "indentLess",
106 106 "Backspace" : "delSpaceToPrevTabStop",
107 107 "Cmd-/" : "toggleComment",
108 108 "Ctrl-/" : "toggleComment"
109 109 },
110 110 mode: 'ipython',
111 111 theme: 'ipython',
112 112 matchBrackets: true
113 113 }
114 114 };
115 115
116 116 CodeCell.msg_cells = {};
117 117
118 118 CodeCell.prototype = new Cell();
119 119
120 120 /**
121 121 * @method auto_highlight
122 122 */
123 123 CodeCell.prototype.auto_highlight = function () {
124 124 this._auto_highlight(this.config.cell_magic_highlight);
125 125 };
126 126
127 127 /** @method create_element */
128 128 CodeCell.prototype.create_element = function () {
129 129 Cell.prototype.create_element.apply(this, arguments);
130 130
131 131 var cell = $('<div></div>').addClass('cell code_cell');
132 132 cell.attr('tabindex','2');
133 133
134 134 var input = $('<div></div>').addClass('input');
135 135 var prompt = $('<div/>').addClass('prompt input_prompt');
136 136 var inner_cell = $('<div/>').addClass('inner_cell');
137 137 this.celltoolbar = new celltoolbar.CellToolbar({
138 138 cell: this,
139 139 notebook: this.notebook});
140 140 inner_cell.append(this.celltoolbar.element);
141 141 var input_area = $('<div/>').addClass('input_area');
142 142 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
143 143 this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this))
144 144 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
145 145 inner_cell.append(input_area);
146 146 input.append(prompt).append(inner_cell);
147 147
148 148 var widget_area = $('<div/>')
149 149 .addClass('widget-area')
150 150 .hide();
151 151 this.widget_area = widget_area;
152 152 var widget_prompt = $('<div/>')
153 153 .addClass('prompt')
154 154 .appendTo(widget_area);
155 155 var widget_subarea = $('<div/>')
156 156 .addClass('widget-subarea')
157 157 .appendTo(widget_area);
158 158 this.widget_subarea = widget_subarea;
159 159 var widget_clear_buton = $('<button />')
160 160 .addClass('close')
161 161 .html('&times;')
162 162 .click(function() {
163 163 widget_area.slideUp('', function(){ widget_subarea.html(''); });
164 164 })
165 165 .appendTo(widget_prompt);
166 166
167 167 var output = $('<div></div>');
168 168 cell.append(input).append(widget_area).append(output);
169 169 this.element = cell;
170 170 this.output_area = new outputarea.OutputArea({
171 171 selector: output,
172 172 prompt_area: true,
173 173 events: this.events,
174 174 keyboard_manager: this.keyboard_manager});
175 175 this.completer = new completer.Completer(this, this.events);
176 176 };
177 177
178 178 /** @method bind_events */
179 179 CodeCell.prototype.bind_events = function () {
180 180 Cell.prototype.bind_events.apply(this);
181 181 var that = this;
182 182
183 183 this.element.focusout(
184 184 function() { that.auto_highlight(); }
185 185 );
186 186 };
187 187
188 188
189 189 /**
190 190 * This method gets called in CodeMirror's onKeyDown/onKeyPress
191 191 * handlers and is used to provide custom key handling. Its return
192 192 * value is used to determine if CodeMirror should ignore the event:
193 193 * true = ignore, false = don't ignore.
194 194 * @method handle_codemirror_keyevent
195 195 */
196 196 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
197 197
198 198 var that = this;
199 199 // whatever key is pressed, first, cancel the tooltip request before
200 200 // they are sent, and remove tooltip if any, except for tab again
201 201 var tooltip_closed = null;
202 202 if (event.type === 'keydown' && event.which != keycodes.tab ) {
203 203 tooltip_closed = this.tooltip.remove_and_cancel_tooltip();
204 204 }
205 205
206 206 var cur = editor.getCursor();
207 207 if (event.keyCode === keycodes.enter){
208 208 this.auto_highlight();
209 209 }
210 210
211 211 if (event.which === keycodes.down && event.type === 'keypress' && this.tooltip.time_before_tooltip >= 0) {
212 212 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
213 213 // browser and keyboard layout !
214 214 // Pressing '(' , request tooltip, don't forget to reappend it
215 215 // The second argument says to hide the tooltip if the docstring
216 216 // is actually empty
217 217 this.tooltip.pending(that, true);
218 218 } else if ( tooltip_closed && event.which === keycodes.esc && event.type === 'keydown') {
219 219 // If tooltip is active, cancel it. The call to
220 220 // remove_and_cancel_tooltip above doesn't pass, force=true.
221 221 // Because of this it won't actually close the tooltip
222 222 // if it is in sticky mode. Thus, we have to check again if it is open
223 223 // and close it with force=true.
224 224 if (!this.tooltip._hidden) {
225 225 this.tooltip.remove_and_cancel_tooltip(true);
226 226 }
227 227 // If we closed the tooltip, don't let CM or the global handlers
228 228 // handle this event.
229 229 event.codemirrorIgnore = true;
230 230 event.preventDefault();
231 231 return true;
232 232 } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
233 if (editor.somethingSelected()){
233 if (editor.somethingSelected() || editor.getSelections().length !== 1){
234 234 var anchor = editor.getCursor("anchor");
235 235 var head = editor.getCursor("head");
236 236 if( anchor.line != head.line){
237 237 return false;
238 238 }
239 239 }
240 240 this.tooltip.request(that);
241 241 event.codemirrorIgnore = true;
242 242 event.preventDefault();
243 243 return true;
244 244 } else if (event.keyCode === keycodes.tab && event.type == 'keydown') {
245 245 // Tab completion.
246 246 this.tooltip.remove_and_cancel_tooltip();
247 if (editor.somethingSelected()) {
247
248 // completion does not work on multicursor, it might be possible though in some cases
249 if (editor.somethingSelected() || editor.getSelections().length > 1) {
248 250 return false;
249 251 }
250 252 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
251 253 if (pre_cursor.trim() === "") {
252 254 // Don't autocomplete if the part of the line before the cursor
253 255 // is empty. In this case, let CodeMirror handle indentation.
254 256 return false;
255 257 } else {
256 258 event.codemirrorIgnore = true;
257 259 event.preventDefault();
258 260 this.completer.startCompletion();
259 261 return true;
260 262 }
261 263 }
262 264
263 265 // keyboard event wasn't one of those unique to code cells, let's see
264 266 // if it's one of the generic ones (i.e. check edit mode shortcuts)
265 267 return Cell.prototype.handle_codemirror_keyevent.apply(this, [editor, event]);
266 268 };
267 269
268 270 // Kernel related calls.
269 271
270 272 CodeCell.prototype.set_kernel = function (kernel) {
271 273 this.kernel = kernel;
272 274 };
273 275
274 276 /**
275 277 * Execute current code cell to the kernel
276 278 * @method execute
277 279 */
278 280 CodeCell.prototype.execute = function () {
279 281 this.output_area.clear_output();
280 282
281 283 // Clear widget area
282 284 this.widget_subarea.html('');
283 285 this.widget_subarea.height('');
284 286 this.widget_area.height('');
285 287 this.widget_area.hide();
286 288
287 289 this.set_input_prompt('*');
288 290 this.element.addClass("running");
289 291 if (this.last_msg_id) {
290 292 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
291 293 }
292 294 var callbacks = this.get_callbacks();
293 295
294 296 var old_msg_id = this.last_msg_id;
295 297 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true});
296 298 if (old_msg_id) {
297 299 delete CodeCell.msg_cells[old_msg_id];
298 300 }
299 301 CodeCell.msg_cells[this.last_msg_id] = this;
300 302 this.render();
301 303 };
302 304
303 305 /**
304 306 * Construct the default callbacks for
305 307 * @method get_callbacks
306 308 */
307 309 CodeCell.prototype.get_callbacks = function () {
308 310 return {
309 311 shell : {
310 312 reply : $.proxy(this._handle_execute_reply, this),
311 313 payload : {
312 314 set_next_input : $.proxy(this._handle_set_next_input, this),
313 315 page : $.proxy(this._open_with_pager, this)
314 316 }
315 317 },
316 318 iopub : {
317 319 output : $.proxy(this.output_area.handle_output, this.output_area),
318 320 clear_output : $.proxy(this.output_area.handle_clear_output, this.output_area),
319 321 },
320 322 input : $.proxy(this._handle_input_request, this)
321 323 };
322 324 };
323 325
324 326 CodeCell.prototype._open_with_pager = function (payload) {
325 327 this.events.trigger('open_with_text.Pager', payload);
326 328 };
327 329
328 330 /**
329 331 * @method _handle_execute_reply
330 332 * @private
331 333 */
332 334 CodeCell.prototype._handle_execute_reply = function (msg) {
333 335 this.set_input_prompt(msg.content.execution_count);
334 336 this.element.removeClass("running");
335 337 this.events.trigger('set_dirty.Notebook', {value: true});
336 338 };
337 339
338 340 /**
339 341 * @method _handle_set_next_input
340 342 * @private
341 343 */
342 344 CodeCell.prototype._handle_set_next_input = function (payload) {
343 345 var data = {'cell': this, 'text': payload.text};
344 346 this.events.trigger('set_next_input.Notebook', data);
345 347 };
346 348
347 349 /**
348 350 * @method _handle_input_request
349 351 * @private
350 352 */
351 353 CodeCell.prototype._handle_input_request = function (msg) {
352 354 this.output_area.append_raw_input(msg);
353 355 };
354 356
355 357
356 358 // Basic cell manipulation.
357 359
358 360 CodeCell.prototype.select = function () {
359 361 var cont = Cell.prototype.select.apply(this);
360 362 if (cont) {
361 363 this.code_mirror.refresh();
362 364 this.auto_highlight();
363 365 }
364 366 return cont;
365 367 };
366 368
367 369 CodeCell.prototype.render = function () {
368 370 var cont = Cell.prototype.render.apply(this);
369 371 // Always execute, even if we are already in the rendered state
370 372 return cont;
371 373 };
372 374
373 375 CodeCell.prototype.select_all = function () {
374 376 var start = {line: 0, ch: 0};
375 377 var nlines = this.code_mirror.lineCount();
376 378 var last_line = this.code_mirror.getLine(nlines-1);
377 379 var end = {line: nlines-1, ch: last_line.length};
378 380 this.code_mirror.setSelection(start, end);
379 381 };
380 382
381 383
382 384 CodeCell.prototype.collapse_output = function () {
383 385 this.collapsed = true;
384 386 this.output_area.collapse();
385 387 };
386 388
387 389
388 390 CodeCell.prototype.expand_output = function () {
389 391 this.collapsed = false;
390 392 this.output_area.expand();
391 393 this.output_area.unscroll_area();
392 394 };
393 395
394 396 CodeCell.prototype.scroll_output = function () {
395 397 this.output_area.expand();
396 398 this.output_area.scroll_if_long();
397 399 };
398 400
399 401 CodeCell.prototype.toggle_output = function () {
400 402 this.collapsed = Boolean(1 - this.collapsed);
401 403 this.output_area.toggle_output();
402 404 };
403 405
404 406 CodeCell.prototype.toggle_output_scroll = function () {
405 407 this.output_area.toggle_scroll();
406 408 };
407 409
408 410
409 411 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
410 412 var ns;
411 413 if (prompt_value === undefined) {
412 414 ns = "&nbsp;";
413 415 } else {
414 416 ns = encodeURIComponent(prompt_value);
415 417 }
416 418 return 'In&nbsp;[' + ns + ']:';
417 419 };
418 420
419 421 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
420 422 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
421 423 for(var i=1; i < lines_number; i++) {
422 424 html.push(['...:']);
423 425 }
424 426 return html.join('<br/>');
425 427 };
426 428
427 429 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
428 430
429 431
430 432 CodeCell.prototype.set_input_prompt = function (number) {
431 433 var nline = 1;
432 434 if (this.code_mirror !== undefined) {
433 435 nline = this.code_mirror.lineCount();
434 436 }
435 437 this.input_prompt_number = number;
436 438 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
437 439 // This HTML call is okay because the user contents are escaped.
438 440 this.element.find('div.input_prompt').html(prompt_html);
439 441 };
440 442
441 443
442 444 CodeCell.prototype.clear_input = function () {
443 445 this.code_mirror.setValue('');
444 446 };
445 447
446 448
447 449 CodeCell.prototype.get_text = function () {
448 450 return this.code_mirror.getValue();
449 451 };
450 452
451 453
452 454 CodeCell.prototype.set_text = function (code) {
453 455 return this.code_mirror.setValue(code);
454 456 };
455 457
456 458
457 459 CodeCell.prototype.clear_output = function (wait) {
458 460 this.output_area.clear_output(wait);
459 461 this.set_input_prompt();
460 462 };
461 463
462 464
463 465 // JSON serialization
464 466
465 467 CodeCell.prototype.fromJSON = function (data) {
466 468 Cell.prototype.fromJSON.apply(this, arguments);
467 469 if (data.cell_type === 'code') {
468 470 if (data.input !== undefined) {
469 471 this.set_text(data.input);
470 472 // make this value the starting point, so that we can only undo
471 473 // to this state, instead of a blank cell
472 474 this.code_mirror.clearHistory();
473 475 this.auto_highlight();
474 476 }
475 477 if (data.prompt_number !== undefined) {
476 478 this.set_input_prompt(data.prompt_number);
477 479 } else {
478 480 this.set_input_prompt();
479 481 }
480 482 this.output_area.trusted = data.metadata.trusted || false;
481 483 this.output_area.fromJSON(data.outputs);
482 484 if (data.collapsed !== undefined) {
483 485 if (data.collapsed) {
484 486 this.collapse_output();
485 487 } else {
486 488 this.expand_output();
487 489 }
488 490 }
489 491 }
490 492 };
491 493
492 494
493 495 CodeCell.prototype.toJSON = function () {
494 496 var data = Cell.prototype.toJSON.apply(this);
495 497 data.input = this.get_text();
496 498 // is finite protect against undefined and '*' value
497 499 if (isFinite(this.input_prompt_number)) {
498 500 data.prompt_number = this.input_prompt_number;
499 501 }
500 502 var outputs = this.output_area.toJSON();
501 503 data.outputs = outputs;
502 504 data.language = 'python';
503 505 data.metadata.trusted = this.output_area.trusted;
504 506 data.collapsed = this.output_area.collapsed;
505 507 return data;
506 508 };
507 509
508 510 /**
509 511 * handle cell level logic when a cell is unselected
510 512 * @method unselect
511 513 * @return is the action being taken
512 514 */
513 515 CodeCell.prototype.unselect = function () {
514 516 var cont = Cell.prototype.unselect.apply(this);
515 517 if (cont) {
516 518 // When a code cell is usnelected, make sure that the corresponding
517 519 // tooltip and completer to that cell is closed.
518 520 this.tooltip.remove_and_cancel_tooltip(true);
519 521 if (this.completer !== null) {
520 522 this.completer.close();
521 523 }
522 524 }
523 525 return cont;
524 526 };
525 527
526 528 // Backwards compatability.
527 529 IPython.CodeCell = CodeCell;
528 530
529 531 return {'CodeCell': CodeCell};
530 532 });
@@ -1,406 +1,406 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/keyboard',
9 9 'notebook/js/contexthint',
10 10 'codemirror/lib/codemirror',
11 11 ], function(IPython, $, utils, keyboard, CodeMirror) {
12 12 "use strict";
13 13
14 14 // easier key mapping
15 15 var keycodes = keyboard.keycodes;
16 16
17 17 var prepend_n_prc = function(str, n) {
18 18 for( var i =0 ; i< n ; i++){
19 19 str = '%'+str ;
20 20 }
21 21 return str;
22 22 };
23 23
24 24 var _existing_completion = function(item, completion_array){
25 25 for( var i=0; i < completion_array.length; i++) {
26 26 if (completion_array[i].trim().substr(-item.length) == item) {
27 27 return true;
28 28 }
29 29 }
30 30 return false;
31 31 };
32 32
33 33 // what is the common start of all completions
34 34 function shared_start(B, drop_prct) {
35 35 if (B.length == 1) {
36 36 return B[0];
37 37 }
38 38 var A = [];
39 39 var common;
40 40 var min_lead_prct = 10;
41 41 for (var i = 0; i < B.length; i++) {
42 42 var str = B[i].str;
43 43 var localmin = 0;
44 44 if(drop_prct === true){
45 45 while ( str.substr(0, 1) == '%') {
46 46 localmin = localmin+1;
47 47 str = str.substring(1);
48 48 }
49 49 }
50 50 min_lead_prct = Math.min(min_lead_prct, localmin);
51 51 A.push(str);
52 52 }
53 53
54 54 if (A.length > 1) {
55 55 var tem1, tem2, s;
56 56 A = A.slice(0).sort();
57 57 tem1 = A[0];
58 58 s = tem1.length;
59 59 tem2 = A.pop();
60 60 while (s && tem2.indexOf(tem1) == -1) {
61 61 tem1 = tem1.substring(0, --s);
62 62 }
63 63 if (tem1 === "" || tem2.indexOf(tem1) !== 0) {
64 64 return {
65 65 str:prepend_n_prc('', min_lead_prct),
66 66 type: "computed",
67 67 from: B[0].from,
68 68 to: B[0].to
69 69 };
70 70 }
71 71 return {
72 72 str: prepend_n_prc(tem1, min_lead_prct),
73 73 type: "computed",
74 74 from: B[0].from,
75 75 to: B[0].to
76 76 };
77 77 }
78 78 return null;
79 79 }
80 80
81 81
82 82 var Completer = function (cell, events) {
83 83 this.cell = cell;
84 84 this.editor = cell.code_mirror;
85 85 var that = this;
86 86 events.on('kernel_busy.Kernel', function () {
87 87 that.skip_kernel_completion = true;
88 88 });
89 89 events.on('kernel_idle.Kernel', function () {
90 90 that.skip_kernel_completion = false;
91 91 });
92 92 };
93 93
94 94 Completer.prototype.startCompletion = function () {
95 95 // call for a 'first' completion, that will set the editor and do some
96 96 // special behavior like autopicking if only one completion available.
97 if (this.editor.somethingSelected()) return;
97 if (this.editor.somethingSelected()|| this.editor.getSelections().length > 1) return;
98 98 this.done = false;
99 99 // use to get focus back on opera
100 100 this.carry_on_completion(true);
101 101 };
102 102
103 103
104 104 // easy access for julia to monkeypatch
105 105 //
106 106 Completer.reinvoke_re = /[%0-9a-z._/\\:~-]/i;
107 107
108 108 Completer.prototype.reinvoke= function(pre_cursor, block, cursor){
109 109 return Completer.reinvoke_re.test(pre_cursor);
110 110 };
111 111
112 112 /**
113 113 *
114 114 * pass true as parameter if this is the first invocation of the completer
115 115 * this will prevent the completer to dissmiss itself if it is not on a
116 116 * word boundary like pressing tab after a space, and make it autopick the
117 117 * only choice if there is only one which prevent from popping the UI. as
118 118 * well as fast-forwarding the typing if all completion have a common
119 119 * shared start
120 120 **/
121 121 Completer.prototype.carry_on_completion = function (first_invocation) {
122 122 // Pass true as parameter if you want the completer to autopick when
123 123 // only one completion. This function is automatically reinvoked at
124 124 // each keystroke with first_invocation = false
125 125 var cur = this.editor.getCursor();
126 126 var line = this.editor.getLine(cur.line);
127 127 var pre_cursor = this.editor.getRange({
128 128 line: cur.line,
129 129 ch: cur.ch - 1
130 130 }, cur);
131 131
132 132 // we need to check that we are still on a word boundary
133 133 // because while typing the completer is still reinvoking itself
134 134 // so dismiss if we are on a "bad" caracter
135 135 if (!this.reinvoke(pre_cursor) && !first_invocation) {
136 136 this.close();
137 137 return;
138 138 }
139 139
140 140 this.autopick = false;
141 141 if (first_invocation) {
142 142 this.autopick = true;
143 143 }
144 144
145 145 // We want a single cursor position.
146 if (this.editor.somethingSelected()) {
146 if (this.editor.somethingSelected()|| editor.getSelections().length > 1) {
147 147 return;
148 148 }
149 149
150 150 // one kernel completion came back, finish_completing will be called with the results
151 151 // we fork here and directly call finish completing if kernel is busy
152 152 var cursor_pos = utils.to_absolute_cursor_pos(this.editor, cur);
153 153 if (this.skip_kernel_completion) {
154 154 this.finish_completing({ content: {
155 155 matches: [],
156 156 cursor_start: cursor_pos,
157 157 cursor_end: cursor_pos,
158 158 }});
159 159 } else {
160 160 this.cell.kernel.complete(this.editor.getValue(), cursor_pos,
161 161 $.proxy(this.finish_completing, this)
162 162 );
163 163 }
164 164 };
165 165
166 166 Completer.prototype.finish_completing = function (msg) {
167 167 // let's build a function that wrap all that stuff into what is needed
168 168 // for the new completer:
169 169 var content = msg.content;
170 170 var start = content.cursor_start;
171 171 var end = content.cursor_end;
172 172 var matches = content.matches;
173 173
174 174 var cur = this.editor.getCursor();
175 175 if (end === null) {
176 176 // adapted message spec replies don't have cursor position info,
177 177 // interpret end=null as current position,
178 178 // and negative start relative to that
179 179 end = utils.to_absolute_cursor_pos(this.editor, cur);
180 180 if (start < 0) {
181 181 start = end + start;
182 182 }
183 183 }
184 184 var results = CodeMirror.contextHint(this.editor);
185 185 var filtered_results = [];
186 186 //remove results from context completion
187 187 //that are already in kernel completion
188 188 var i;
189 189 for (i=0; i < results.length; i++) {
190 190 if (!_existing_completion(results[i].str, matches)) {
191 191 filtered_results.push(results[i]);
192 192 }
193 193 }
194 194
195 195 // append the introspection result, in order, at at the beginning of
196 196 // the table and compute the replacement range from current cursor
197 197 // positon and matched_text length.
198 198 for (i = matches.length - 1; i >= 0; --i) {
199 199 filtered_results.unshift({
200 200 str: matches[i],
201 201 type: "introspection",
202 202 from: utils.from_absolute_cursor_pos(this.editor, start),
203 203 to: utils.from_absolute_cursor_pos(this.editor, end)
204 204 });
205 205 }
206 206
207 207 // one the 2 sources results have been merge, deal with it
208 208 this.raw_result = filtered_results;
209 209
210 210 // if empty result return
211 211 if (!this.raw_result || !this.raw_result.length) return;
212 212
213 213 // When there is only one completion, use it directly.
214 214 if (this.autopick && this.raw_result.length == 1) {
215 215 this.insert(this.raw_result[0]);
216 216 return;
217 217 }
218 218
219 219 if (this.raw_result.length == 1) {
220 220 // test if first and only completion totally matches
221 221 // what is typed, in this case dismiss
222 222 var str = this.raw_result[0].str;
223 223 var pre_cursor = this.editor.getRange({
224 224 line: cur.line,
225 225 ch: cur.ch - str.length
226 226 }, cur);
227 227 if (pre_cursor == str) {
228 228 this.close();
229 229 return;
230 230 }
231 231 }
232 232
233 233 if (!this.visible) {
234 234 this.complete = $('<div/>').addClass('completions');
235 235 this.complete.attr('id', 'complete');
236 236
237 237 // Currently webkit doesn't use the size attr correctly. See:
238 238 // https://code.google.com/p/chromium/issues/detail?id=4579
239 239 this.sel = $('<select/>')
240 240 .attr('tabindex', -1)
241 241 .attr('multiple', 'true');
242 242 this.complete.append(this.sel);
243 243 this.visible = true;
244 244 $('body').append(this.complete);
245 245
246 246 //build the container
247 247 var that = this;
248 248 this.sel.dblclick(function () {
249 249 that.pick();
250 250 });
251 251 this.sel.focus(function () {
252 252 that.editor.focus();
253 253 });
254 254 this._handle_keydown = function (cm, event) {
255 255 that.keydown(event);
256 256 };
257 257 this.editor.on('keydown', this._handle_keydown);
258 258 this._handle_keypress = function (cm, event) {
259 259 that.keypress(event);
260 260 };
261 261 this.editor.on('keypress', this._handle_keypress);
262 262 }
263 263 this.sel.attr('size', Math.min(10, this.raw_result.length));
264 264
265 265 // After everything is on the page, compute the postion.
266 266 // We put it above the code if it is too close to the bottom of the page.
267 267 var pos = this.editor.cursorCoords(
268 268 utils.from_absolute_cursor_pos(this.editor, start)
269 269 );
270 270 var left = pos.left-3;
271 271 var top;
272 272 var cheight = this.complete.height();
273 273 var wheight = $(window).height();
274 274 if (pos.bottom+cheight+5 > wheight) {
275 275 top = pos.top-cheight-4;
276 276 } else {
277 277 top = pos.bottom+1;
278 278 }
279 279 this.complete.css('left', left + 'px');
280 280 this.complete.css('top', top + 'px');
281 281
282 282 // Clear and fill the list.
283 283 this.sel.text('');
284 284 this.build_gui_list(this.raw_result);
285 285 return true;
286 286 };
287 287
288 288 Completer.prototype.insert = function (completion) {
289 289 this.editor.replaceRange(completion.str, completion.from, completion.to);
290 290 };
291 291
292 292 Completer.prototype.build_gui_list = function (completions) {
293 293 for (var i = 0; i < completions.length; ++i) {
294 294 var opt = $('<option/>').text(completions[i].str).addClass(completions[i].type);
295 295 this.sel.append(opt);
296 296 }
297 297 this.sel.children().first().attr('selected', 'true');
298 298 this.sel.scrollTop(0);
299 299 };
300 300
301 301 Completer.prototype.close = function () {
302 302 this.done = true;
303 303 $('#complete').remove();
304 304 this.editor.off('keydown', this._handle_keydown);
305 305 this.editor.off('keypress', this._handle_keypress);
306 306 this.visible = false;
307 307 };
308 308
309 309 Completer.prototype.pick = function () {
310 310 this.insert(this.raw_result[this.sel[0].selectedIndex]);
311 311 this.close();
312 312 };
313 313
314 314 Completer.prototype.keydown = function (event) {
315 315 var code = event.keyCode;
316 316 var that = this;
317 317
318 318 // Enter
319 319 if (code == keycodes.enter) {
320 320 event.codemirrorIgnore = true;
321 321 event._ipkmIgnore = true;
322 322 event.preventDefault();
323 323 this.pick();
324 324 // Escape or backspace
325 325 } else if (code == keycodes.esc || code == keycodes.backspace) {
326 326 event.codemirrorIgnore = true;
327 327 event._ipkmIgnore = true;
328 328 event.preventDefault();
329 329 this.close();
330 330 } else if (code == keycodes.tab) {
331 331 //all the fastforwarding operation,
332 332 //Check that shared start is not null which can append with prefixed completion
333 333 // like %pylab , pylab have no shred start, and ff will result in py<tab><tab>
334 334 // to erase py
335 335 var sh = shared_start(this.raw_result, true);
336 336 if (sh) {
337 337 this.insert(sh);
338 338 }
339 339 this.close();
340 340 //reinvoke self
341 341 setTimeout(function () {
342 342 that.carry_on_completion();
343 343 }, 50);
344 344 } else if (code == keycodes.up || code == keycodes.down) {
345 345 // need to do that to be able to move the arrow
346 346 // when on the first or last line ofo a code cell
347 347 event.codemirrorIgnore = true;
348 348 event._ipkmIgnore = true;
349 349 event.preventDefault();
350 350
351 351 var options = this.sel.find('option');
352 352 var index = this.sel[0].selectedIndex;
353 353 if (code == keycodes.up) {
354 354 index--;
355 355 }
356 356 if (code == keycodes.down) {
357 357 index++;
358 358 }
359 359 index = Math.min(Math.max(index, 0), options.length-1);
360 360 this.sel[0].selectedIndex = index;
361 361 } else if (code == keycodes.pageup || code == keycodes.pagedown) {
362 362 event._ipkmIgnore = true;
363 363
364 364 var options = this.sel.find('option');
365 365 var index = this.sel[0].selectedIndex;
366 366 if (code == keycodes.pageup) {
367 367 index -= 10; // As 10 is the hard coded size of the drop down menu
368 368 } else {
369 369 index += 10;
370 370 }
371 371 index = Math.min(Math.max(index, 0), options.length-1);
372 372 this.sel[0].selectedIndex = index;
373 373 } else if (code == keycodes.left || code == keycodes.right) {
374 374 this.close();
375 375 }
376 376 };
377 377
378 378 Completer.prototype.keypress = function (event) {
379 379 // FIXME: This is a band-aid.
380 380 // on keypress, trigger insertion of a single character.
381 381 // This simulates the old behavior of completion as you type,
382 382 // before events were disconnected and CodeMirror stopped
383 383 // receiving events while the completer is focused.
384 384
385 385 var that = this;
386 386 var code = event.keyCode;
387 387
388 388 // don't handle keypress if it's not a character (arrows on FF)
389 389 // or ENTER/TAB
390 390 if (event.charCode === 0 ||
391 391 code == keycodes.tab ||
392 392 code == keycodes.enter
393 393 ) return;
394 394
395 395 this.close();
396 396 this.editor.focus();
397 397 setTimeout(function () {
398 398 that.carry_on_completion();
399 399 }, 50);
400 400 };
401 401
402 402 // For backwards compatability.
403 403 IPython.Completer = Completer;
404 404
405 405 return {'Completer': Completer};
406 406 });
@@ -1,332 +1,332 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 // tooltip constructor
12 12 var Tooltip = function (events) {
13 13 var that = this;
14 14 this.events = events;
15 15 this.time_before_tooltip = 1200;
16 16
17 17 // handle to html
18 18 this.tooltip = $('#tooltip');
19 19 this._hidden = true;
20 20
21 21 // variable for consecutive call
22 22 this._old_cell = null;
23 23 this._old_request = null;
24 24 this._consecutive_counter = 0;
25 25
26 26 // 'sticky ?'
27 27 this._sticky = false;
28 28
29 29 // display tooltip if the docstring is empty?
30 30 this._hide_if_no_docstring = false;
31 31
32 32 // contain the button in the upper right corner
33 33 this.buttons = $('<div/>').addClass('tooltipbuttons');
34 34
35 35 // will contain the docstring
36 36 this.text = $('<div/>').addClass('tooltiptext').addClass('smalltooltip');
37 37
38 38 // build the buttons menu on the upper right
39 39 // expand the tooltip to see more
40 40 var expandlink = $('<a/>').attr('href', "#").addClass("ui-corner-all") //rounded corner
41 41 .attr('role', "button").attr('id', 'expanbutton').attr('title', 'Grow the tooltip vertically (press shift-tab twice)').click(function () {
42 42 that.expand();
43 43 }).append(
44 44 $('<span/>').text('Expand').addClass('ui-icon').addClass('ui-icon-plus'));
45 45
46 46 // open in pager
47 47 var morelink = $('<a/>').attr('href', "#").attr('role', "button").addClass('ui-button').attr('title', 'show the current docstring in pager (press shift-tab 4 times)');
48 48 var morespan = $('<span/>').text('Open in Pager').addClass('ui-icon').addClass('ui-icon-arrowstop-l-n');
49 49 morelink.append(morespan);
50 50 morelink.click(function () {
51 51 that.showInPager(that._old_cell);
52 52 });
53 53
54 54 // close the tooltip
55 55 var closelink = $('<a/>').attr('href', "#").attr('role', "button").addClass('ui-button');
56 56 var closespan = $('<span/>').text('Close').addClass('ui-icon').addClass('ui-icon-close');
57 57 closelink.append(closespan);
58 58 closelink.click(function () {
59 59 that.remove_and_cancel_tooltip(true);
60 60 });
61 61
62 62 this._clocklink = $('<a/>').attr('href', "#");
63 63 this._clocklink.attr('role', "button");
64 64 this._clocklink.addClass('ui-button');
65 65 this._clocklink.attr('title', 'Tootip is not dismissed while typing for 10 seconds');
66 66 var clockspan = $('<span/>').text('Close');
67 67 clockspan.addClass('ui-icon');
68 68 clockspan.addClass('ui-icon-clock');
69 69 this._clocklink.append(clockspan);
70 70 this._clocklink.click(function () {
71 71 that.cancel_stick();
72 72 });
73 73
74 74
75 75
76 76
77 77 //construct the tooltip
78 78 // add in the reverse order you want them to appear
79 79 this.buttons.append(closelink);
80 80 this.buttons.append(expandlink);
81 81 this.buttons.append(morelink);
82 82 this.buttons.append(this._clocklink);
83 83 this._clocklink.hide();
84 84
85 85
86 86 // we need a phony element to make the small arrow
87 87 // of the tooltip in css
88 88 // we will move the arrow later
89 89 this.arrow = $('<div/>').addClass('pretooltiparrow');
90 90 this.tooltip.append(this.buttons);
91 91 this.tooltip.append(this.arrow);
92 92 this.tooltip.append(this.text);
93 93
94 94 // function that will be called if you press tab 1, 2, 3... times in a row
95 95 this.tabs_functions = [function (cell, text, cursor) {
96 96 that._request_tooltip(cell, text, cursor);
97 97 }, function () {
98 98 that.expand();
99 99 }, function () {
100 100 that.stick();
101 101 }, function (cell) {
102 102 that.cancel_stick();
103 103 that.showInPager(cell);
104 104 }];
105 105 // call after all the tabs function above have bee call to clean their effects
106 106 // if necessary
107 107 this.reset_tabs_function = function (cell, text) {
108 108 this._old_cell = (cell) ? cell : null;
109 109 this._old_request = (text) ? text : null;
110 110 this._consecutive_counter = 0;
111 111 };
112 112 };
113 113
114 114 Tooltip.prototype.is_visible = function () {
115 115 return !this._hidden;
116 116 };
117 117
118 118 Tooltip.prototype.showInPager = function (cell) {
119 119 // reexecute last call in pager by appending ? to show back in pager
120 var that = this;
121 this.events.trigger('open_with_text.Pager', that._reply.content);
120 this.events.trigger('open_with_text.Pager', this._reply.content);
122 121 this.remove_and_cancel_tooltip();
123 122 };
124 123
125 124 // grow the tooltip verticaly
126 125 Tooltip.prototype.expand = function () {
127 126 this.text.removeClass('smalltooltip');
128 127 this.text.addClass('bigtooltip');
129 128 $('#expanbutton').hide('slow');
130 129 };
131 130
132 131 // deal with all the logic of hiding the tooltip
133 132 // and reset it's status
134 133 Tooltip.prototype._hide = function () {
135 134 this._hidden = true;
136 135 this.tooltip.fadeOut('fast');
137 136 $('#expanbutton').show('slow');
138 137 this.text.removeClass('bigtooltip');
139 138 this.text.addClass('smalltooltip');
140 139 // keep scroll top to be sure to always see the first line
141 140 this.text.scrollTop(0);
142 141 this.code_mirror = null;
143 142 };
144 143
145 144 // return true on successfully removing a visible tooltip; otherwise return
146 145 // false.
147 146 Tooltip.prototype.remove_and_cancel_tooltip = function (force) {
148 147 // note that we don't handle closing directly inside the calltip
149 148 // as in the completer, because it is not focusable, so won't
150 149 // get the event.
151 150 this.cancel_pending();
152 151 if (!this._hidden) {
153 152 if (force || !this._sticky) {
154 153 this.cancel_stick();
155 154 this._hide();
156 155 }
157 156 this.reset_tabs_function();
158 157 return true;
159 158 } else {
160 159 return false;
161 160 }
162 161 };
163 162
164 163 // cancel autocall done after '(' for example.
165 164 Tooltip.prototype.cancel_pending = function () {
166 165 if (this._tooltip_timeout !== null) {
167 166 clearTimeout(this._tooltip_timeout);
168 167 this._tooltip_timeout = null;
169 168 }
170 169 };
171 170
172 171 // will trigger tooltip after timeout
173 172 Tooltip.prototype.pending = function (cell, hide_if_no_docstring) {
174 173 var that = this;
175 174 this._tooltip_timeout = setTimeout(function () {
176 175 that.request(cell, hide_if_no_docstring);
177 176 }, that.time_before_tooltip);
178 177 };
179 178
180 179 // easy access for julia monkey patching.
181 180 Tooltip.last_token_re = /[a-z_][0-9a-z._]*$/gi;
182 181
183 182 Tooltip.prototype.extract_oir_token = function(line){
184 183 // use internally just to make the request to the kernel
185 184 // Feel free to shorten this logic if you are better
186 185 // than me in regEx
187 186 // basicaly you shoul be able to get xxx.xxx.xxx from
188 187 // something(range(10), kwarg=smth) ; xxx.xxx.xxx( firstarg, rand(234,23), kwarg1=2,
189 188 // remove everything between matchin bracket (need to iterate)
190 189 var matchBracket = /\([^\(\)]+\)/g;
191 190 var endBracket = /\([^\(]*$/g;
192 191 var oldline = line;
193 192
194 193 line = line.replace(matchBracket, "");
195 194 while (oldline != line) {
196 195 oldline = line;
197 196 line = line.replace(matchBracket, "");
198 197 }
199 198 // remove everything after last open bracket
200 199 line = line.replace(endBracket, "");
201 200 // reset the regex object
202 201 Tooltip.last_token_re.lastIndex = 0;
203 202 return Tooltip.last_token_re.exec(line);
204 203 };
205 204
206 205 Tooltip.prototype._request_tooltip = function (cell, text, cursor_pos) {
207 206 var callbacks = $.proxy(this._show, this);
208 207 var msg_id = cell.kernel.inspect(text, cursor_pos, callbacks);
209 208 };
210 209
211 // make an imediate completion request
210 // make an immediate completion request
212 211 Tooltip.prototype.request = function (cell, hide_if_no_docstring) {
213 212 // request(codecell)
214 213 // Deal with extracting the text from the cell and counting
215 214 // call in a row
216 215 this.cancel_pending();
217 216 var editor = cell.code_mirror;
218 217 var cursor = editor.getCursor();
219 218 var cursor_pos = utils.to_absolute_cursor_pos(editor, cursor);
220 219 var text = cell.get_text();
221 220
222 221 this._hide_if_no_docstring = hide_if_no_docstring;
223 222
224 223 if(editor.somethingSelected()){
224 // get only the most recent selection.
225 225 text = editor.getSelection();
226 226 }
227 227
228 // need a permanent handel to code_mirror for future auto recall
228 // need a permanent handle to code_mirror for future auto recall
229 229 this.code_mirror = editor;
230 230
231 231 // now we treat the different number of keypress
232 232 // first if same cell, same text, increment counter by 1
233 233 if (this._old_cell == cell && this._old_request == text && this._hidden === false) {
234 234 this._consecutive_counter++;
235 235 } else {
236 236 // else reset
237 237 this.cancel_stick();
238 238 this.reset_tabs_function (cell, text);
239 239 }
240 240
241 241 this.tabs_functions[this._consecutive_counter](cell, text, cursor_pos);
242 242
243 243 // then if we are at the end of list function, reset
244 244 if (this._consecutive_counter == this.tabs_functions.length) {
245 245 this.reset_tabs_function (cell, text, cursor);
246 246 }
247 247
248 248 return;
249 249 };
250 250
251 251 // cancel the option of having the tooltip to stick
252 252 Tooltip.prototype.cancel_stick = function () {
253 253 clearTimeout(this._stick_timeout);
254 254 this._stick_timeout = null;
255 255 this._clocklink.hide('slow');
256 256 this._sticky = false;
257 257 };
258 258
259 259 // put the tooltip in a sicky state for 10 seconds
260 260 // it won't be removed by remove_and_cancell() unless you called with
261 261 // the first parameter set to true.
262 262 // remove_and_cancell_tooltip(true)
263 263 Tooltip.prototype.stick = function (time) {
264 264 time = (time !== undefined) ? time : 10;
265 265 var that = this;
266 266 this._sticky = true;
267 267 this._clocklink.show('slow');
268 268 this._stick_timeout = setTimeout(function () {
269 269 that._sticky = false;
270 270 that._clocklink.hide('slow');
271 271 }, time * 1000);
272 272 };
273 273
274 274 // should be called with the kernel reply to actually show the tooltip
275 275 Tooltip.prototype._show = function (reply) {
276 276 // move the bubble if it is not hidden
277 277 // otherwise fade it
278 278 this._reply = reply;
279 279 var content = reply.content;
280 280 if (!content.found) {
281 281 // object not found, nothing to show
282 282 return;
283 283 }
284 284 this.name = content.name;
285 285
286 286 // do some math to have the tooltip arrow on more or less on left or right
287 287 // width of the editor
288 288 var w = $(this.code_mirror.getScrollerElement()).width();
289 289 // ofset of the editor
290 290 var o = $(this.code_mirror.getScrollerElement()).offset();
291 291
292 292 // whatever anchor/head order but arrow at mid x selection
293 293 var anchor = this.code_mirror.cursorCoords(false);
294 294 var head = this.code_mirror.cursorCoords(true);
295 295 var xinit = (head.left+anchor.left)/2;
296 296 var xinter = o.left + (xinit - o.left) / w * (w - 450);
297 297 var posarrowleft = xinit - xinter;
298 298
299 299 if (this._hidden === false) {
300 300 this.tooltip.animate({
301 301 'left': xinter - 30 + 'px',
302 302 'top': (head.bottom + 10) + 'px'
303 303 });
304 304 } else {
305 305 this.tooltip.css({
306 306 'left': xinter - 30 + 'px'
307 307 });
308 308 this.tooltip.css({
309 309 'top': (head.bottom + 10) + 'px'
310 310 });
311 311 }
312 312 this.arrow.animate({
313 313 'left': posarrowleft + 'px'
314 314 });
315 315
316 316 this._hidden = false;
317 317 this.tooltip.fadeIn('fast');
318 318 this.text.children().remove();
319 319
320 320 // This should support rich data types, but only text/plain for now
321 321 // Any HTML within the docstring is escaped by the fixConsole() method.
322 322 var pre = $('<pre/>').html(utils.fixConsole(content.data['text/plain']));
323 323 this.text.append(pre);
324 324 // keep scroll top to be sure to always see the first line
325 325 this.text.scrollTop(0);
326 326 };
327 327
328 // Backwards compatability.
328 // Backwards compatibility.
329 329 IPython.Tooltip = Tooltip;
330 330
331 331 return {'Tooltip': Tooltip};
332 332 });
General Comments 0
You need to be logged in to leave comments. Login now