##// END OF EJS Templates
Merge pull request #7889 from Carreau/after-one-year...
Matthias Bussonnier -
r20597:c552c824 merge
parent child Browse files
Show More
@@ -1,887 +1,888
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 'codemirror/lib/codemirror',
8 8 'moment',
9 9 // silently upgrades CodeMirror
10 10 'codemirror/mode/meta',
11 11 ], function(IPython, $, CodeMirror, moment){
12 12 "use strict";
13 13
14 14 var load_extensions = function () {
15 15 // load one or more IPython notebook extensions with requirejs
16 16
17 17 var extensions = [];
18 18 var extension_names = arguments;
19 19 for (var i = 0; i < extension_names.length; i++) {
20 20 extensions.push("nbextensions/" + arguments[i]);
21 21 }
22 22
23 23 require(extensions,
24 24 function () {
25 25 for (var i = 0; i < arguments.length; i++) {
26 26 var ext = arguments[i];
27 27 var ext_name = extension_names[i];
28 28 // success callback
29 29 console.log("Loaded extension: " + ext_name);
30 30 if (ext && ext.load_ipython_extension !== undefined) {
31 31 ext.load_ipython_extension();
32 32 }
33 33 }
34 34 },
35 35 function (err) {
36 36 // failure callback
37 37 console.log("Failed to load extension(s):", err.requireModules, err);
38 38 }
39 39 );
40 40 };
41 41
42 42 IPython.load_extensions = load_extensions;
43 43
44 44 /**
45 45 * Wait for a config section to load, and then load the extensions specified
46 46 * in a 'load_extensions' key inside it.
47 47 */
48 48 function load_extensions_from_config(section) {
49 49 section.loaded.then(function() {
50 50 if (section.data.load_extensions) {
51 51 var nbextension_paths = Object.getOwnPropertyNames(
52 52 section.data.load_extensions);
53 53 load_extensions.apply(this, nbextension_paths);
54 54 }
55 55 });
56 56 }
57 57
58 58 //============================================================================
59 59 // Cross-browser RegEx Split
60 60 //============================================================================
61 61
62 62 // This code has been MODIFIED from the code licensed below to not replace the
63 63 // default browser split. The license is reproduced here.
64 64
65 65 // see http://blog.stevenlevithan.com/archives/cross-browser-split for more info:
66 66 /*!
67 67 * Cross-Browser Split 1.1.1
68 68 * Copyright 2007-2012 Steven Levithan <stevenlevithan.com>
69 69 * Available under the MIT License
70 70 * ECMAScript compliant, uniform cross-browser split method
71 71 */
72 72
73 73 /**
74 74 * Splits a string into an array of strings using a regex or string
75 75 * separator. Matches of the separator are not included in the result array.
76 76 * However, if `separator` is a regex that contains capturing groups,
77 77 * backreferences are spliced into the result each time `separator` is
78 78 * matched. Fixes browser bugs compared to the native
79 79 * `String.prototype.split` and can be used reliably cross-browser.
80 80 * @param {String} str String to split.
81 81 * @param {RegExp} separator Regex to use for separating
82 82 * the string.
83 83 * @param {Number} [limit] Maximum number of items to include in the result
84 84 * array.
85 85 * @returns {Array} Array of substrings.
86 86 * @example
87 87 *
88 88 * // Basic use
89 89 * regex_split('a b c d', ' ');
90 90 * // -> ['a', 'b', 'c', 'd']
91 91 *
92 92 * // With limit
93 93 * regex_split('a b c d', ' ', 2);
94 94 * // -> ['a', 'b']
95 95 *
96 96 * // Backreferences in result array
97 97 * regex_split('..word1 word2..', /([a-z]+)(\d+)/i);
98 98 * // -> ['..', 'word', '1', ' ', 'word', '2', '..']
99 99 */
100 100 var regex_split = function (str, separator, limit) {
101 101 var output = [],
102 102 flags = (separator.ignoreCase ? "i" : "") +
103 103 (separator.multiline ? "m" : "") +
104 104 (separator.extended ? "x" : "") + // Proposed for ES6
105 105 (separator.sticky ? "y" : ""), // Firefox 3+
106 106 lastLastIndex = 0,
107 107 separator2, match, lastIndex, lastLength;
108 108 // Make `global` and avoid `lastIndex` issues by working with a copy
109 109 separator = new RegExp(separator.source, flags + "g");
110 110
111 111 var compliantExecNpcg = typeof(/()??/.exec("")[1]) === "undefined";
112 112 if (!compliantExecNpcg) {
113 113 // Doesn't need flags gy, but they don't hurt
114 114 separator2 = new RegExp("^" + separator.source + "$(?!\\s)", flags);
115 115 }
116 116 /* Values for `limit`, per the spec:
117 117 * If undefined: 4294967295 // Math.pow(2, 32) - 1
118 118 * If 0, Infinity, or NaN: 0
119 119 * If positive number: limit = Math.floor(limit); if (limit > 4294967295) limit -= 4294967296;
120 120 * If negative number: 4294967296 - Math.floor(Math.abs(limit))
121 121 * If other: Type-convert, then use the above rules
122 122 */
123 123 limit = typeof(limit) === "undefined" ?
124 124 -1 >>> 0 : // Math.pow(2, 32) - 1
125 125 limit >>> 0; // ToUint32(limit)
126 126 for (match = separator.exec(str); match; match = separator.exec(str)) {
127 127 // `separator.lastIndex` is not reliable cross-browser
128 128 lastIndex = match.index + match[0].length;
129 129 if (lastIndex > lastLastIndex) {
130 130 output.push(str.slice(lastLastIndex, match.index));
131 131 // Fix browsers whose `exec` methods don't consistently return `undefined` for
132 132 // nonparticipating capturing groups
133 133 if (!compliantExecNpcg && match.length > 1) {
134 134 match[0].replace(separator2, function () {
135 135 for (var i = 1; i < arguments.length - 2; i++) {
136 136 if (typeof(arguments[i]) === "undefined") {
137 137 match[i] = undefined;
138 138 }
139 139 }
140 140 });
141 141 }
142 142 if (match.length > 1 && match.index < str.length) {
143 143 Array.prototype.push.apply(output, match.slice(1));
144 144 }
145 145 lastLength = match[0].length;
146 146 lastLastIndex = lastIndex;
147 147 if (output.length >= limit) {
148 148 break;
149 149 }
150 150 }
151 151 if (separator.lastIndex === match.index) {
152 152 separator.lastIndex++; // Avoid an infinite loop
153 153 }
154 154 }
155 155 if (lastLastIndex === str.length) {
156 156 if (lastLength || !separator.test("")) {
157 157 output.push("");
158 158 }
159 159 } else {
160 160 output.push(str.slice(lastLastIndex));
161 161 }
162 162 return output.length > limit ? output.slice(0, limit) : output;
163 163 };
164 164
165 165 //============================================================================
166 166 // End contributed Cross-browser RegEx Split
167 167 //============================================================================
168 168
169 169
170 170 var uuid = function () {
171 171 /**
172 172 * http://www.ietf.org/rfc/rfc4122.txt
173 173 */
174 174 var s = [];
175 175 var hexDigits = "0123456789ABCDEF";
176 176 for (var i = 0; i < 32; i++) {
177 177 s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
178 178 }
179 179 s[12] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
180 180 s[16] = hexDigits.substr((s[16] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
181 181
182 182 var uuid = s.join("");
183 183 return uuid;
184 184 };
185 185
186 186
187 187 //Fix raw text to parse correctly in crazy XML
188 188 function xmlencode(string) {
189 189 return string.replace(/\&/g,'&'+'amp;')
190 190 .replace(/</g,'&'+'lt;')
191 191 .replace(/>/g,'&'+'gt;')
192 192 .replace(/\'/g,'&'+'apos;')
193 193 .replace(/\"/g,'&'+'quot;')
194 194 .replace(/`/g,'&'+'#96;');
195 195 }
196 196
197 197
198 198 //Map from terminal commands to CSS classes
199 199 var ansi_colormap = {
200 200 "01":"ansibold",
201 201
202 202 "30":"ansiblack",
203 203 "31":"ansired",
204 204 "32":"ansigreen",
205 205 "33":"ansiyellow",
206 206 "34":"ansiblue",
207 207 "35":"ansipurple",
208 208 "36":"ansicyan",
209 209 "37":"ansigray",
210 210
211 211 "40":"ansibgblack",
212 212 "41":"ansibgred",
213 213 "42":"ansibggreen",
214 214 "43":"ansibgyellow",
215 215 "44":"ansibgblue",
216 216 "45":"ansibgpurple",
217 217 "46":"ansibgcyan",
218 218 "47":"ansibggray"
219 219 };
220 220
221 221 function _process_numbers(attrs, numbers) {
222 222 // process ansi escapes
223 223 var n = numbers.shift();
224 224 if (ansi_colormap[n]) {
225 225 if ( ! attrs["class"] ) {
226 226 attrs["class"] = ansi_colormap[n];
227 227 } else {
228 228 attrs["class"] += " " + ansi_colormap[n];
229 229 }
230 230 } else if (n == "38" || n == "48") {
231 231 // VT100 256 color or 24 bit RGB
232 232 if (numbers.length < 2) {
233 233 console.log("Not enough fields for VT100 color", numbers);
234 234 return;
235 235 }
236 236
237 237 var index_or_rgb = numbers.shift();
238 238 var r,g,b;
239 239 if (index_or_rgb == "5") {
240 240 // 256 color
241 241 var idx = parseInt(numbers.shift(), 10);
242 242 if (idx < 16) {
243 243 // indexed ANSI
244 244 // ignore bright / non-bright distinction
245 245 idx = idx % 8;
246 246 var ansiclass = ansi_colormap[n[0] + (idx % 8).toString()];
247 247 if ( ! attrs["class"] ) {
248 248 attrs["class"] = ansiclass;
249 249 } else {
250 250 attrs["class"] += " " + ansiclass;
251 251 }
252 252 return;
253 253 } else if (idx < 232) {
254 254 // 216 color 6x6x6 RGB
255 255 idx = idx - 16;
256 256 b = idx % 6;
257 257 g = Math.floor(idx / 6) % 6;
258 258 r = Math.floor(idx / 36) % 6;
259 259 // convert to rgb
260 260 r = (r * 51);
261 261 g = (g * 51);
262 262 b = (b * 51);
263 263 } else {
264 264 // grayscale
265 265 idx = idx - 231;
266 266 // it's 1-24 and should *not* include black or white,
267 267 // so a 26 point scale
268 268 r = g = b = Math.floor(idx * 256 / 26);
269 269 }
270 270 } else if (index_or_rgb == "2") {
271 271 // Simple 24 bit RGB
272 272 if (numbers.length > 3) {
273 273 console.log("Not enough fields for RGB", numbers);
274 274 return;
275 275 }
276 276 r = numbers.shift();
277 277 g = numbers.shift();
278 278 b = numbers.shift();
279 279 } else {
280 280 console.log("unrecognized control", numbers);
281 281 return;
282 282 }
283 283 if (r !== undefined) {
284 284 // apply the rgb color
285 285 var line;
286 286 if (n == "38") {
287 287 line = "color: ";
288 288 } else {
289 289 line = "background-color: ";
290 290 }
291 291 line = line + "rgb(" + r + "," + g + "," + b + ");";
292 292 if ( !attrs.style ) {
293 293 attrs.style = line;
294 294 } else {
295 295 attrs.style += " " + line;
296 296 }
297 297 }
298 298 }
299 299 }
300 300
301 301 function ansispan(str) {
302 302 // ansispan function adapted from github.com/mmalecki/ansispan (MIT License)
303 303 // regular ansi escapes (using the table above)
304 304 var is_open = false;
305 305 return str.replace(/\033\[(0?[01]|22|39)?([;\d]+)?m/g, function(match, prefix, pattern) {
306 306 if (!pattern) {
307 307 // [(01|22|39|)m close spans
308 308 if (is_open) {
309 309 is_open = false;
310 310 return "</span>";
311 311 } else {
312 312 return "";
313 313 }
314 314 } else {
315 315 is_open = true;
316 316
317 317 // consume sequence of color escapes
318 318 var numbers = pattern.match(/\d+/g);
319 319 var attrs = {};
320 320 while (numbers.length > 0) {
321 321 _process_numbers(attrs, numbers);
322 322 }
323 323
324 324 var span = "<span ";
325 325 Object.keys(attrs).map(function (attr) {
326 326 span = span + " " + attr + '="' + attrs[attr] + '"';
327 327 });
328 328 return span + ">";
329 329 }
330 330 });
331 331 }
332 332
333 333 // Transform ANSI color escape codes into HTML <span> tags with css
334 334 // classes listed in the above ansi_colormap object. The actual color used
335 335 // are set in the css file.
336 336 function fixConsole(txt) {
337 337 txt = xmlencode(txt);
338 338
339 339 // Strip all ANSI codes that are not color related. Matches
340 340 // all ANSI codes that do not end with "m".
341 341 var ignored_re = /(?=(\033\[[\d;=]*[a-ln-zA-Z]{1}))\1(?!m)/g;
342 342 txt = txt.replace(ignored_re, "");
343 343
344 344 // color ansi codes
345 345 txt = ansispan(txt);
346 346 return txt;
347 347 }
348 348
349 349 // Remove chunks that should be overridden by the effect of
350 350 // carriage return characters
351 351 function fixCarriageReturn(txt) {
352 352 var tmp = txt;
353 353 do {
354 354 txt = tmp;
355 355 tmp = txt.replace(/\r+\n/gm, '\n'); // \r followed by \n --> newline
356 356 tmp = tmp.replace(/^.*\r+/gm, ''); // Other \r --> clear line
357 357 } while (tmp.length < txt.length);
358 358 return txt;
359 359 }
360 360
361 361 // Locate any URLs and convert them to a anchor tag
362 362 function autoLinkUrls(txt) {
363 363 return txt.replace(/(^|\s)(https?|ftp)(:[^'">\s]+)/gi,
364 364 "$1<a target=\"_blank\" href=\"$2$3\">$2$3</a>");
365 365 }
366 366
367 367 var points_to_pixels = function (points) {
368 368 /**
369 369 * A reasonably good way of converting between points and pixels.
370 370 */
371 371 var test = $('<div style="display: none; width: 10000pt; padding:0; border:0;"></div>');
372 372 $('body').append(test);
373 373 var pixel_per_point = test.width()/10000;
374 374 test.remove();
375 375 return Math.floor(points*pixel_per_point);
376 376 };
377 377
378 378 var always_new = function (constructor) {
379 379 /**
380 380 * wrapper around contructor to avoid requiring `var a = new constructor()`
381 381 * useful for passing constructors as callbacks,
382 382 * not for programmer laziness.
383 383 * from http://programmers.stackexchange.com/questions/118798
384 384 */
385 385 return function () {
386 386 var obj = Object.create(constructor.prototype);
387 387 constructor.apply(obj, arguments);
388 388 return obj;
389 389 };
390 390 };
391 391
392 392 var url_path_join = function () {
393 393 /**
394 394 * join a sequence of url components with '/'
395 395 */
396 396 var url = '';
397 397 for (var i = 0; i < arguments.length; i++) {
398 398 if (arguments[i] === '') {
399 399 continue;
400 400 }
401 401 if (url.length > 0 && url[url.length-1] != '/') {
402 402 url = url + '/' + arguments[i];
403 403 } else {
404 404 url = url + arguments[i];
405 405 }
406 406 }
407 407 url = url.replace(/\/\/+/, '/');
408 408 return url;
409 409 };
410 410
411 411 var url_path_split = function (path) {
412 412 /**
413 413 * Like os.path.split for URLs.
414 414 * Always returns two strings, the directory path and the base filename
415 415 */
416 416
417 417 var idx = path.lastIndexOf('/');
418 418 if (idx === -1) {
419 419 return ['', path];
420 420 } else {
421 421 return [ path.slice(0, idx), path.slice(idx + 1) ];
422 422 }
423 423 };
424 424
425 425 var parse_url = function (url) {
426 426 /**
427 427 * an `a` element with an href allows attr-access to the parsed segments of a URL
428 428 * a = parse_url("http://localhost:8888/path/name#hash")
429 429 * a.protocol = "http:"
430 430 * a.host = "localhost:8888"
431 431 * a.hostname = "localhost"
432 432 * a.port = 8888
433 433 * a.pathname = "/path/name"
434 434 * a.hash = "#hash"
435 435 */
436 436 var a = document.createElement("a");
437 437 a.href = url;
438 438 return a;
439 439 };
440 440
441 441 var encode_uri_components = function (uri) {
442 442 /**
443 443 * encode just the components of a multi-segment uri,
444 444 * leaving '/' separators
445 445 */
446 446 return uri.split('/').map(encodeURIComponent).join('/');
447 447 };
448 448
449 449 var url_join_encode = function () {
450 450 /**
451 451 * join a sequence of url components with '/',
452 452 * encoding each component with encodeURIComponent
453 453 */
454 454 return encode_uri_components(url_path_join.apply(null, arguments));
455 455 };
456 456
457 457
458 458 var splitext = function (filename) {
459 459 /**
460 460 * mimic Python os.path.splitext
461 461 * Returns ['base', '.ext']
462 462 */
463 463 var idx = filename.lastIndexOf('.');
464 464 if (idx > 0) {
465 465 return [filename.slice(0, idx), filename.slice(idx)];
466 466 } else {
467 467 return [filename, ''];
468 468 }
469 469 };
470 470
471 471
472 472 var escape_html = function (text) {
473 473 /**
474 474 * escape text to HTML
475 475 */
476 476 return $("<div/>").text(text).html();
477 477 };
478 478
479 479
480 480 var get_body_data = function(key) {
481 481 /**
482 482 * get a url-encoded item from body.data and decode it
483 483 * we should never have any encoded URLs anywhere else in code
484 484 * until we are building an actual request
485 485 */
486 486 var val = $('body').data(key);
487 487 if (!val)
488 488 return val;
489 489 return decodeURIComponent(val);
490 490 };
491 491
492 492 var to_absolute_cursor_pos = function (cm, cursor) {
493 493 /**
494 494 * get the absolute cursor position from CodeMirror's col, ch
495 495 */
496 496 if (!cursor) {
497 497 cursor = cm.getCursor();
498 498 }
499 499 var cursor_pos = cursor.ch;
500 500 for (var i = 0; i < cursor.line; i++) {
501 501 cursor_pos += cm.getLine(i).length + 1;
502 502 }
503 503 return cursor_pos;
504 504 };
505 505
506 506 var from_absolute_cursor_pos = function (cm, cursor_pos) {
507 507 /**
508 508 * turn absolute cursor postion into CodeMirror col, ch cursor
509 509 */
510 var i, line;
510 var i, line, next_line;
511 511 var offset = 0;
512 for (i = 0, line=cm.getLine(i); line !== undefined; i++, line=cm.getLine(i)) {
513 if (offset + line.length < cursor_pos) {
514 offset += line.length + 1;
512 for (i = 0, next_line=cm.getLine(i); next_line !== undefined; i++, next_line=cm.getLine(i)) {
513 line = next_line;
514 if (offset + next_line.length < cursor_pos) {
515 offset += next_line.length + 1;
515 516 } else {
516 517 return {
517 518 line : i,
518 519 ch : cursor_pos - offset,
519 520 };
520 521 }
521 522 }
522 523 // reached end, return endpoint
523 524 return {
524 ch : line.length - 1,
525 525 line : i - 1,
526 ch : line.length - 1,
526 527 };
527 528 };
528 529
529 530 // http://stackoverflow.com/questions/2400935/browser-detection-in-javascript
530 531 var browser = (function() {
531 532 if (typeof navigator === 'undefined') {
532 533 // navigator undefined in node
533 534 return 'None';
534 535 }
535 536 var N= navigator.appName, ua= navigator.userAgent, tem;
536 537 var M= ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
537 538 if (M && (tem= ua.match(/version\/([\.\d]+)/i)) !== null) M[2]= tem[1];
538 539 M= M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
539 540 return M;
540 541 })();
541 542
542 543 // http://stackoverflow.com/questions/11219582/how-to-detect-my-browser-version-and-operating-system-using-javascript
543 544 var platform = (function () {
544 545 if (typeof navigator === 'undefined') {
545 546 // navigator undefined in node
546 547 return 'None';
547 548 }
548 549 var OSName="None";
549 550 if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
550 551 if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
551 552 if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
552 553 if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
553 554 return OSName;
554 555 })();
555 556
556 557 var get_url_param = function (name) {
557 558 // get a URL parameter. I cannot believe we actually need this.
558 559 // Based on http://stackoverflow.com/a/25359264/938949
559 560 var match = new RegExp('[?&]' + name + '=([^&]*)').exec(window.location.search);
560 561 if (match){
561 562 return decodeURIComponent(match[1] || '');
562 563 }
563 564 };
564 565
565 566 var is_or_has = function (a, b) {
566 567 /**
567 568 * Is b a child of a or a itself?
568 569 */
569 570 return a.has(b).length !==0 || a.is(b);
570 571 };
571 572
572 573 var is_focused = function (e) {
573 574 /**
574 575 * Is element e, or one of its children focused?
575 576 */
576 577 e = $(e);
577 578 var target = $(document.activeElement);
578 579 if (target.length > 0) {
579 580 if (is_or_has(e, target)) {
580 581 return true;
581 582 } else {
582 583 return false;
583 584 }
584 585 } else {
585 586 return false;
586 587 }
587 588 };
588 589
589 590 var mergeopt = function(_class, options, overwrite){
590 591 options = options || {};
591 592 overwrite = overwrite || {};
592 593 return $.extend(true, {}, _class.options_default, options, overwrite);
593 594 };
594 595
595 596 var ajax_error_msg = function (jqXHR) {
596 597 /**
597 598 * Return a JSON error message if there is one,
598 599 * otherwise the basic HTTP status text.
599 600 */
600 601 if (jqXHR.responseJSON && jqXHR.responseJSON.traceback) {
601 602 return jqXHR.responseJSON.traceback;
602 603 } else if (jqXHR.responseJSON && jqXHR.responseJSON.message) {
603 604 return jqXHR.responseJSON.message;
604 605 } else {
605 606 return jqXHR.statusText;
606 607 }
607 608 };
608 609 var log_ajax_error = function (jqXHR, status, error) {
609 610 /**
610 611 * log ajax failures with informative messages
611 612 */
612 613 var msg = "API request failed (" + jqXHR.status + "): ";
613 614 console.log(jqXHR);
614 615 msg += ajax_error_msg(jqXHR);
615 616 console.log(msg);
616 617 };
617 618
618 619 var requireCodeMirrorMode = function (mode, callback, errback) {
619 620 /**
620 621 * find a predefined mode or detect from CM metadata then
621 622 * require and callback with the resolveable mode string: mime or
622 623 * custom name
623 624 */
624 625
625 626 var modename = (typeof mode == "string") ? mode :
626 627 mode.mode || mode.name;
627 628
628 629 // simplest, cheapest check by mode name: mode may also have config
629 630 if (CodeMirror.modes.hasOwnProperty(modename)) {
630 631 // return the full mode object, if it has a name
631 632 callback(mode.name ? mode : modename);
632 633 return;
633 634 }
634 635
635 636 // *somehow* get back a CM.modeInfo-like object that has .mode and
636 637 // .mime
637 638 var info = (mode && mode.mode && mode.mime && mode) ||
638 639 CodeMirror.findModeByName(modename) ||
639 640 CodeMirror.findModeByExtension(modename.split(".").slice(-1)) ||
640 641 CodeMirror.findModeByMIME(modename) ||
641 642 {mode: modename, mime: modename};
642 643
643 644 require([
644 645 // might want to use CodeMirror.modeURL here
645 646 ['codemirror/mode', info.mode, info.mode].join('/'),
646 647 ], function() {
647 648 // return the original mode, as from a kernelspec on first load
648 649 // or the mimetype, as for most highlighting
649 650 callback(mode.name ? mode : info.mime);
650 651 }, errback
651 652 );
652 653 };
653 654
654 655 /** Error type for wrapped XHR errors. */
655 656 var XHR_ERROR = 'XhrError';
656 657
657 658 /**
658 659 * Wraps an AJAX error as an Error object.
659 660 */
660 661 var wrap_ajax_error = function (jqXHR, status, error) {
661 662 var wrapped_error = new Error(ajax_error_msg(jqXHR));
662 663 wrapped_error.name = XHR_ERROR;
663 664 // provide xhr response
664 665 wrapped_error.xhr = jqXHR;
665 666 wrapped_error.xhr_status = status;
666 667 wrapped_error.xhr_error = error;
667 668 return wrapped_error;
668 669 };
669 670
670 671 var promising_ajax = function(url, settings) {
671 672 /**
672 673 * Like $.ajax, but returning an ES6 promise. success and error settings
673 674 * will be ignored.
674 675 */
675 676 settings = settings || {};
676 677 return new Promise(function(resolve, reject) {
677 678 settings.success = function(data, status, jqXHR) {
678 679 resolve(data);
679 680 };
680 681 settings.error = function(jqXHR, status, error) {
681 682 log_ajax_error(jqXHR, status, error);
682 683 reject(wrap_ajax_error(jqXHR, status, error));
683 684 };
684 685 $.ajax(url, settings);
685 686 });
686 687 };
687 688
688 689 var WrappedError = function(message, error){
689 690 /**
690 691 * Wrappable Error class
691 692 *
692 693 * The Error class doesn't actually act on `this`. Instead it always
693 694 * returns a new instance of Error. Here we capture that instance so we
694 695 * can apply it's properties to `this`.
695 696 */
696 697 var tmp = Error.apply(this, [message]);
697 698
698 699 // Copy the properties of the error over to this.
699 700 var properties = Object.getOwnPropertyNames(tmp);
700 701 for (var i = 0; i < properties.length; i++) {
701 702 this[properties[i]] = tmp[properties[i]];
702 703 }
703 704
704 705 // Keep a stack of the original error messages.
705 706 if (error instanceof WrappedError) {
706 707 this.error_stack = error.error_stack;
707 708 } else {
708 709 this.error_stack = [error];
709 710 }
710 711 this.error_stack.push(tmp);
711 712
712 713 return this;
713 714 };
714 715
715 716 WrappedError.prototype = Object.create(Error.prototype, {});
716 717
717 718
718 719 var load_class = function(class_name, module_name, registry) {
719 720 /**
720 721 * Tries to load a class
721 722 *
722 723 * Tries to load a class from a module using require.js, if a module
723 724 * is specified, otherwise tries to load a class from the global
724 725 * registry, if the global registry is provided.
725 726 */
726 727 return new Promise(function(resolve, reject) {
727 728
728 729 // Try loading the view module using require.js
729 730 if (module_name) {
730 731 require([module_name], function(module) {
731 732 if (module[class_name] === undefined) {
732 733 reject(new Error('Class '+class_name+' not found in module '+module_name));
733 734 } else {
734 735 resolve(module[class_name]);
735 736 }
736 737 }, reject);
737 738 } else {
738 739 if (registry && registry[class_name]) {
739 740 resolve(registry[class_name]);
740 741 } else {
741 742 reject(new Error('Class '+class_name+' not found in registry '));
742 743 }
743 744 }
744 745 });
745 746 };
746 747
747 748 var resolve_promises_dict = function(d) {
748 749 /**
749 750 * Resolve a promiseful dictionary.
750 751 * Returns a single Promise.
751 752 */
752 753 var keys = Object.keys(d);
753 754 var values = [];
754 755 keys.forEach(function(key) {
755 756 values.push(d[key]);
756 757 });
757 758 return Promise.all(values).then(function(v) {
758 759 d = {};
759 760 for(var i=0; i<keys.length; i++) {
760 761 d[keys[i]] = v[i];
761 762 }
762 763 return d;
763 764 });
764 765 };
765 766
766 767 var reject = function(message, log) {
767 768 /**
768 769 * Creates a wrappable Promise rejection function.
769 770 *
770 771 * Creates a function that returns a Promise.reject with a new WrappedError
771 772 * that has the provided message and wraps the original error that
772 773 * caused the promise to reject.
773 774 */
774 775 return function(error) {
775 776 var wrapped_error = new WrappedError(message, error);
776 777 if (log) console.error(wrapped_error);
777 778 return Promise.reject(wrapped_error);
778 779 };
779 780 };
780 781
781 782 var typeset = function(element, text) {
782 783 /**
783 784 * Apply MathJax rendering to an element, and optionally set its text
784 785 *
785 786 * If MathJax is not available, make no changes.
786 787 *
787 788 * Returns the output any number of typeset elements, or undefined if
788 789 * MathJax was not available.
789 790 *
790 791 * Parameters
791 792 * ----------
792 793 * element: Node, NodeList, or jQuery selection
793 794 * text: option string
794 795 */
795 796 var $el = element.jquery ? element : $(element);
796 797 if(arguments.length > 1){
797 798 $el.text(text);
798 799 }
799 800 if(!window.MathJax){
800 801 return;
801 802 }
802 803 return $el.map(function(){
803 804 // MathJax takes a DOM node: $.map makes `this` the context
804 805 return MathJax.Hub.Queue(["Typeset", MathJax.Hub, this]);
805 806 });
806 807 };
807 808
808 809 var time = {};
809 810 time.milliseconds = {};
810 811 time.milliseconds.s = 1000;
811 812 time.milliseconds.m = 60 * time.milliseconds.s;
812 813 time.milliseconds.h = 60 * time.milliseconds.m;
813 814 time.milliseconds.d = 24 * time.milliseconds.h;
814 815
815 816 time.thresholds = {
816 817 // moment.js thresholds in milliseconds
817 818 s: moment.relativeTimeThreshold('s') * time.milliseconds.s,
818 819 m: moment.relativeTimeThreshold('m') * time.milliseconds.m,
819 820 h: moment.relativeTimeThreshold('h') * time.milliseconds.h,
820 821 d: moment.relativeTimeThreshold('d') * time.milliseconds.d,
821 822 };
822 823
823 824 time.timeout_from_dt = function (dt) {
824 825 /** compute a timeout based on dt
825 826
826 827 input and output both in milliseconds
827 828
828 829 use moment's relative time thresholds:
829 830
830 831 - 10 seconds if in 'seconds ago' territory
831 832 - 1 minute if in 'minutes ago'
832 833 - 1 hour otherwise
833 834 */
834 835 if (dt < time.thresholds.s) {
835 836 return 10 * time.milliseconds.s;
836 837 } else if (dt < time.thresholds.m) {
837 838 return time.milliseconds.m;
838 839 } else {
839 840 return time.milliseconds.h;
840 841 }
841 842 };
842 843
843 844 var utils = {
844 845 load_extensions: load_extensions,
845 846 load_extensions_from_config: load_extensions_from_config,
846 847 regex_split : regex_split,
847 848 uuid : uuid,
848 849 fixConsole : fixConsole,
849 850 fixCarriageReturn : fixCarriageReturn,
850 851 autoLinkUrls : autoLinkUrls,
851 852 points_to_pixels : points_to_pixels,
852 853 get_body_data : get_body_data,
853 854 parse_url : parse_url,
854 855 url_path_split : url_path_split,
855 856 url_path_join : url_path_join,
856 857 url_join_encode : url_join_encode,
857 858 encode_uri_components : encode_uri_components,
858 859 splitext : splitext,
859 860 escape_html : escape_html,
860 861 always_new : always_new,
861 862 to_absolute_cursor_pos : to_absolute_cursor_pos,
862 863 from_absolute_cursor_pos : from_absolute_cursor_pos,
863 864 browser : browser,
864 865 platform: platform,
865 866 get_url_param: get_url_param,
866 867 is_or_has : is_or_has,
867 868 is_focused : is_focused,
868 869 mergeopt: mergeopt,
869 870 ajax_error_msg : ajax_error_msg,
870 871 log_ajax_error : log_ajax_error,
871 872 requireCodeMirrorMode : requireCodeMirrorMode,
872 873 XHR_ERROR : XHR_ERROR,
873 874 wrap_ajax_error : wrap_ajax_error,
874 875 promising_ajax : promising_ajax,
875 876 WrappedError: WrappedError,
876 877 load_class: load_class,
877 878 resolve_promises_dict: resolve_promises_dict,
878 879 reject: reject,
879 880 typeset: typeset,
880 881 time: time,
881 882 };
882 883
883 884 // Backwards compatability.
884 885 IPython.utils = utils;
885 886
886 887 return utils;
887 888 });
@@ -1,418 +1,416
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 /**
96 96 * call for a 'first' completion, that will set the editor and do some
97 97 * special behavior like autopicking if only one completion available.
98 98 */
99 99 if (this.editor.somethingSelected()|| this.editor.getSelections().length > 1) return;
100 100 this.done = false;
101 101 // use to get focus back on opera
102 102 this.carry_on_completion(true);
103 103 };
104 104
105 105
106 106 // easy access for julia to monkeypatch
107 107 //
108 108 Completer.reinvoke_re = /[%0-9a-z._/\\:~-]/i;
109 109
110 110 Completer.prototype.reinvoke= function(pre_cursor, block, cursor){
111 111 return Completer.reinvoke_re.test(pre_cursor);
112 112 };
113 113
114 114 /**
115 115 *
116 116 * pass true as parameter if this is the first invocation of the completer
117 117 * this will prevent the completer to dissmiss itself if it is not on a
118 118 * word boundary like pressing tab after a space, and make it autopick the
119 119 * only choice if there is only one which prevent from popping the UI. as
120 120 * well as fast-forwarding the typing if all completion have a common
121 121 * shared start
122 122 **/
123 123 Completer.prototype.carry_on_completion = function (first_invocation) {
124 124 /**
125 125 * Pass true as parameter if you want the completer to autopick when
126 126 * only one completion. This function is automatically reinvoked at
127 127 * each keystroke with first_invocation = false
128 128 */
129 129 var cur = this.editor.getCursor();
130 130 var line = this.editor.getLine(cur.line);
131 131 var pre_cursor = this.editor.getRange({
132 132 line: cur.line,
133 133 ch: cur.ch - 1
134 134 }, cur);
135 135
136 136 // we need to check that we are still on a word boundary
137 137 // because while typing the completer is still reinvoking itself
138 138 // so dismiss if we are on a "bad" caracter
139 139 if (!this.reinvoke(pre_cursor) && !first_invocation) {
140 140 this.close();
141 141 return;
142 142 }
143 143
144 144 this.autopick = false;
145 145 if (first_invocation) {
146 146 this.autopick = true;
147 147 }
148 148
149 149 // We want a single cursor position.
150 150 if (this.editor.somethingSelected()|| this.editor.getSelections().length > 1) {
151 151 return;
152 152 }
153 153
154 154 // one kernel completion came back, finish_completing will be called with the results
155 155 // we fork here and directly call finish completing if kernel is busy
156 156 var cursor_pos = utils.to_absolute_cursor_pos(this.editor, cur);
157 157 if (this.skip_kernel_completion) {
158 158 this.finish_completing({ content: {
159 159 matches: [],
160 160 cursor_start: cursor_pos,
161 161 cursor_end: cursor_pos,
162 162 }});
163 163 } else {
164 164 this.cell.kernel.complete(this.editor.getValue(), cursor_pos,
165 165 $.proxy(this.finish_completing, this)
166 166 );
167 167 }
168 168 };
169 169
170 170 Completer.prototype.finish_completing = function (msg) {
171 171 /**
172 172 * let's build a function that wrap all that stuff into what is needed
173 173 * for the new completer:
174 174 */
175 175 var content = msg.content;
176 176 var start = content.cursor_start;
177 177 var end = content.cursor_end;
178 178 var matches = content.matches;
179 179
180 180 var cur = this.editor.getCursor();
181 181 if (end === null) {
182 182 // adapted message spec replies don't have cursor position info,
183 183 // interpret end=null as current position,
184 184 // and negative start relative to that
185 185 end = utils.to_absolute_cursor_pos(this.editor, cur);
186 186 if (start === null) {
187 187 start = end;
188 188 } else if (start < 0) {
189 189 start = end + start;
190 190 }
191 191 }
192 192 var results = CodeMirror.contextHint(this.editor);
193 193 var filtered_results = [];
194 194 //remove results from context completion
195 195 //that are already in kernel completion
196 196 var i;
197 197 for (i=0; i < results.length; i++) {
198 198 if (!_existing_completion(results[i].str, matches)) {
199 199 filtered_results.push(results[i]);
200 200 }
201 201 }
202 202
203 203 // append the introspection result, in order, at at the beginning of
204 204 // the table and compute the replacement range from current cursor
205 205 // positon and matched_text length.
206 206 var from = utils.from_absolute_cursor_pos(this.editor, start);
207 207 var to = utils.from_absolute_cursor_pos(this.editor, end);
208 208 for (i = matches.length - 1; i >= 0; --i) {
209 209 filtered_results.unshift({
210 210 str: matches[i],
211 211 type: "introspection",
212 212 from: from,
213 213 to: to
214 214 });
215 215 }
216 216
217 217 // one the 2 sources results have been merge, deal with it
218 218 this.raw_result = filtered_results;
219 219
220 220 // if empty result return
221 221 if (!this.raw_result || !this.raw_result.length) return;
222 222
223 223 // When there is only one completion, use it directly.
224 224 if (this.autopick && this.raw_result.length == 1) {
225 225 this.insert(this.raw_result[0]);
226 226 return;
227 227 }
228 228
229 229 if (this.raw_result.length == 1) {
230 230 // test if first and only completion totally matches
231 231 // what is typed, in this case dismiss
232 232 var str = this.raw_result[0].str;
233 233 var pre_cursor = this.editor.getRange({
234 234 line: cur.line,
235 235 ch: cur.ch - str.length
236 236 }, cur);
237 237 if (pre_cursor == str) {
238 238 this.close();
239 239 return;
240 240 }
241 241 }
242 242
243 243 if (!this.visible) {
244 244 this.complete = $('<div/>').addClass('completions');
245 245 this.complete.attr('id', 'complete');
246 246
247 247 // Currently webkit doesn't use the size attr correctly. See:
248 248 // https://code.google.com/p/chromium/issues/detail?id=4579
249 249 this.sel = $('<select/>')
250 250 .attr('tabindex', -1)
251 251 .attr('multiple', 'true');
252 252 this.complete.append(this.sel);
253 253 this.visible = true;
254 254 $('body').append(this.complete);
255 255
256 256 //build the container
257 257 var that = this;
258 258 this.sel.dblclick(function () {
259 259 that.pick();
260 260 });
261 261 this.sel.focus(function () {
262 262 that.editor.focus();
263 263 });
264 264 this._handle_keydown = function (cm, event) {
265 265 that.keydown(event);
266 266 };
267 267 this.editor.on('keydown', this._handle_keydown);
268 268 this._handle_keypress = function (cm, event) {
269 269 that.keypress(event);
270 270 };
271 271 this.editor.on('keypress', this._handle_keypress);
272 272 }
273 273 this.sel.attr('size', Math.min(10, this.raw_result.length));
274 274
275 275 // After everything is on the page, compute the postion.
276 276 // We put it above the code if it is too close to the bottom of the page.
277 277 var pos = this.editor.cursorCoords(
278 278 utils.from_absolute_cursor_pos(this.editor, start)
279 279 );
280 280 var left = pos.left-3;
281 281 var top;
282 282 var cheight = this.complete.height();
283 283 var wheight = $(window).height();
284 284 if (pos.bottom+cheight+5 > wheight) {
285 285 top = pos.top-cheight-4;
286 286 } else {
287 287 top = pos.bottom+1;
288 288 }
289 289 this.complete.css('left', left + 'px');
290 290 this.complete.css('top', top + 'px');
291 291
292 292 // Clear and fill the list.
293 293 this.sel.text('');
294 294 this.build_gui_list(this.raw_result);
295 295 return true;
296 296 };
297 297
298 298 Completer.prototype.insert = function (completion) {
299 299 this.editor.replaceRange(completion.str, completion.from, completion.to);
300 300 };
301 301
302 302 Completer.prototype.build_gui_list = function (completions) {
303 303 for (var i = 0; i < completions.length; ++i) {
304 304 var opt = $('<option/>').text(completions[i].str).addClass(completions[i].type);
305 305 this.sel.append(opt);
306 306 }
307 307 this.sel.children().first().attr('selected', 'true');
308 308 this.sel.scrollTop(0);
309 309 };
310 310
311 311 Completer.prototype.close = function () {
312 312 this.done = true;
313 313 $('#complete').remove();
314 314 this.editor.off('keydown', this._handle_keydown);
315 315 this.editor.off('keypress', this._handle_keypress);
316 316 this.visible = false;
317 317 };
318 318
319 319 Completer.prototype.pick = function () {
320 320 this.insert(this.raw_result[this.sel[0].selectedIndex]);
321 321 this.close();
322 322 };
323 323
324 324 Completer.prototype.keydown = function (event) {
325 325 var code = event.keyCode;
326 var that = this;
327 326
328 327 // Enter
328 var options;
329 var index;
329 330 if (code == keycodes.enter) {
330 331 event.codemirrorIgnore = true;
331 332 event._ipkmIgnore = true;
332 333 event.preventDefault();
333 334 this.pick();
334 335 // Escape or backspace
335 336 } else if (code == keycodes.esc || code == keycodes.backspace) {
336 337 event.codemirrorIgnore = true;
337 338 event._ipkmIgnore = true;
338 339 event.preventDefault();
339 340 this.close();
340 341 } else if (code == keycodes.tab) {
341 342 //all the fastforwarding operation,
342 343 //Check that shared start is not null which can append with prefixed completion
343 344 // like %pylab , pylab have no shred start, and ff will result in py<tab><tab>
344 345 // to erase py
345 346 var sh = shared_start(this.raw_result, true);
346 if (sh) {
347 if (sh.str !== '') {
347 348 this.insert(sh);
348 349 }
349 350 this.close();
350 //reinvoke self
351 setTimeout(function () {
352 that.carry_on_completion();
353 }, 50);
351 this.carry_on_completion();
354 352 } else if (code == keycodes.up || code == keycodes.down) {
355 353 // need to do that to be able to move the arrow
356 354 // when on the first or last line ofo a code cell
357 355 event.codemirrorIgnore = true;
358 356 event._ipkmIgnore = true;
359 357 event.preventDefault();
360 358
361 var options = this.sel.find('option');
362 var index = this.sel[0].selectedIndex;
359 options = this.sel.find('option');
360 index = this.sel[0].selectedIndex;
363 361 if (code == keycodes.up) {
364 362 index--;
365 363 }
366 364 if (code == keycodes.down) {
367 365 index++;
368 366 }
369 367 index = Math.min(Math.max(index, 0), options.length-1);
370 368 this.sel[0].selectedIndex = index;
371 369 } else if (code == keycodes.pageup || code == keycodes.pagedown) {
372 370 event._ipkmIgnore = true;
373 371
374 var options = this.sel.find('option');
375 var index = this.sel[0].selectedIndex;
372 options = this.sel.find('option');
373 index = this.sel[0].selectedIndex;
376 374 if (code == keycodes.pageup) {
377 375 index -= 10; // As 10 is the hard coded size of the drop down menu
378 376 } else {
379 377 index += 10;
380 378 }
381 379 index = Math.min(Math.max(index, 0), options.length-1);
382 380 this.sel[0].selectedIndex = index;
383 381 } else if (code == keycodes.left || code == keycodes.right) {
384 382 this.close();
385 383 }
386 384 };
387 385
388 386 Completer.prototype.keypress = function (event) {
389 387 /**
390 388 * FIXME: This is a band-aid.
391 389 * on keypress, trigger insertion of a single character.
392 390 * This simulates the old behavior of completion as you type,
393 391 * before events were disconnected and CodeMirror stopped
394 392 * receiving events while the completer is focused.
395 393 */
396 394
397 395 var that = this;
398 396 var code = event.keyCode;
399 397
400 398 // don't handle keypress if it's not a character (arrows on FF)
401 399 // or ENTER/TAB
402 400 if (event.charCode === 0 ||
403 401 code == keycodes.tab ||
404 402 code == keycodes.enter
405 403 ) return;
406 404
407 405 this.close();
408 406 this.editor.focus();
409 407 setTimeout(function () {
410 408 that.carry_on_completion();
411 409 }, 50);
412 410 };
413 411
414 412 // For backwards compatability.
415 413 IPython.Completer = Completer;
416 414
417 415 return {'Completer': Completer};
418 416 });
General Comments 0
You need to be logged in to leave comments. Login now