##// END OF EJS Templates
implement the completer in a separate class...
Matthias BUSSONNIER -
Show More
@@ -0,0 +1,233 b''
1 // function completer.
2 //
3 // completer should be a class that take an editor instance, and a list of
4 // function to call to get the list of completion.
5 //
6 // the function that send back the list of completion should received the
7 // editor handle as sole argument, and should return a json object with the
8 // following structure
9
10 // {list: clist, # list of n string containing the completions
11 // type : rp, # list of n string containingtype/ origin of the completion
12 // # (will be set as the class of the <option> to be able to style
13 // # them according to the origin of the completion)
14 // from: {line: cur.line, ch: token.start},
15 // to: {line: cur.line, ch: token.end}
16 // };
17 //
18
19 var IPython = (function(IPython ) {
20 // that will prevent us froom missspelling
21 "use strict";
22
23 // easyier key mapping
24 var key = { tab:9,
25 esc:27,
26 backspace:8,
27 space:32,
28 shift:16,
29 enter:13,
30 upArrow:38, // check with keyDown..
31 downArrow :40 // check with keyUp
32 };
33
34 // what is the common start of all completions
35 function sharedStart(B){
36 if(B.length == 1){return B[0]}
37 var A = new Array()
38 for(i=0; i< B.length; i++)
39 {
40 A.push(B[i].str);
41 }
42 if(A.length > 1 ){
43 var tem1, tem2, s, A = A.slice(0).sort();
44 tem1 = A[0];
45 s = tem1.length;
46 tem2 = A.pop();
47 while(s && tem2.indexOf(tem1) == -1){
48 tem1 = tem1.substring(0, --s);
49 }
50 return { str : tem1,
51 type : "computed",
52 from : B[0].from,
53 to : B[0].to
54 };
55 }
56 return null;
57 }
58
59 // user to nsert the given completion
60 var Completer = function(getHints) {
61
62 this.hintfunc = getHints;
63 // if last caractere before cursor is not in this, we stop completing
64 this.reg = /[A-Za-z.]/;
65 }
66
67 Completer.prototype.startCompletionFor = function(ed)
68 {
69 // call for a 'first' completion, that will set the editor and do some
70 // special behaviour like autopicking if only one completion availlable
71 //
72 this.editor = ed;
73 if (this.editor.somethingSelected()) return;
74 this.done = false;
75 // use to get focus back on opera
76 this.carryOnCompletion(true);
77 }
78
79 Completer.prototype.carryOnCompletion = function(ff)
80 {
81 // pass true as parameter if you want the commpleter to autopick
82 // when only one completion
83 // as this function is auto;atically reinvoked at each keystroke with
84 // ff = false
85 var cur = this.editor.getCursor();
86 var pre_cursor = this.editor.getRange({line:cur.line,ch:cur.ch-1},cur);
87
88 // we nned to check that we are still on a word boundary
89 // because while typing the completer is still reinvoking itself
90 if(!this.reg.test(pre_cursor)){ this.close(); return;}
91
92 this.autopick = false;
93 if( ff != 'undefined' && ff==true)
94 {
95 this.autopick=true;
96 }
97 // We want a single cursor position.
98 if (this.editor.somethingSelected()) return;
99
100 // there we will need to gather the results for all the function (and merge them ?)
101 // lets assume for now only one source
102 //
103 var that = this;
104 this.hintfunc(this.editor,function(result){that._resume_completion(result)});
105 }
106 Completer.prototype._resume_completion = function(results)
107 {
108 this.raw_result = results;
109
110 // if empty result return
111 if (!this.raw_result || !this.raw_result.length) return;
112
113
114
115 // When there is only one completion, use it directly.
116 if (this.autopick == true && this.raw_result.length == 1)
117 {
118 this.insert(this.raw_result[0]);
119 return true;
120 }
121
122 if (this.raw_result.length == 1)
123 {
124 // test if first and only completion totally matches
125 // what is typed, in this case dismiss
126 var str = this.raw_result[0].str
127 var cur = this.editor.getCursor();
128 var pre_cursor = this.editor.getRange({line:cur.line,ch:cur.ch-str.length},cur);
129 if(pre_cursor == str){
130 this.close();
131 return ;
132 }
133 }
134
135 this.complete = $('<div/>').addClass('completions');
136 this.complete.attr('id','complete');
137
138 this.sel = $('<select/>').attr('multiple','true');
139 var pos = this.editor.cursorCoords();
140
141 // TODO: I propose to remove enough horizontal pixel
142 // to align the text later
143 this.complete.css('left',pos.x+'px');
144 this.complete.css('top',pos.yBot+'px');
145 this.complete.append(this.sel);
146
147 $('body').append(this.complete);
148 //build the container
149 var that = this;
150 this.sel.dblclick(function(){that.pick()});
151 this.sel.blur(this.close);
152 this.sel.keydown(function(event){that.keydown(event)});
153
154 this.build_gui_list(this.raw_result);
155 var that=this;
156 //CodeMirror.connect(that.sel, "dblclick", function(){that.pick()});
157
158 this.sel.focus();
159 // Opera sometimes ignores focusing a freshly created node
160 if (window.opera) setTimeout(function(){if (!this.done) this.sel.focus();}, 100);
161 // why do we return true ?
162 return true;
163 }
164
165 Completer.prototype.insert = function(completion) {
166 this.editor.replaceRange(completion.str, completion.from, completion.to);
167 }
168
169 Completer.prototype.build_gui_list = function(completions){
170 // Need to clear the all list
171 for (var i = 0; i < completions.length; ++i) {
172 var opt = $('<option/>')
173 .text(completions[i].str)
174 .addClass(completions[i].type);
175 this.sel.append(opt);
176 }
177 this.sel.children().first().attr('selected','true');
178
179 //sel.size = Math.min(10, completions.length);
180 // Hack to hide the scrollbar.
181 //if (completions.length <= 10)
182 //this.complete.style.width = (this.sel.clientWidth - 1) + "px";
183 }
184
185 Completer.prototype.close = function() {
186 if (this.done) return;
187 this.done = true;
188 $('.completions').remove();
189 }
190
191 Completer.prototype.pick = function(){
192 this.insert(this.raw_result[this.sel[0].selectedIndex]);
193 this.close();
194 var that = this;
195 setTimeout(function(){that.editor.focus();}, 50);
196 }
197
198
199 Completer.prototype.keydown = function(event) {
200 var code = event.keyCode;
201 // Enter
202 if (code == key.enter) {CodeMirror.e_stop(event); this.pick();}
203 // Escape or backspace
204 else if (code == key.esc ) {CodeMirror.e_stop(event); this.close(); this.editor.focus();}
205 else if (code == key.space || code == key.backspace) {this.close(); this.editor.focus();}
206 else if (code == key.tab){
207 //all the fastforwarding operation,
208 this.insert(sharedStart(this.raw_result));
209 this.close();
210 CodeMirror.e_stop(event);
211 this.editor.focus();
212 //reinvoke self
213 var that = this;
214 setTimeout(function(){that.carryOnCompletion();}, 50);
215 }
216 else if (code == key.upArrow || code == key.downArrow) {
217 // need to do that to be able to move the arrow
218 // when on the first or last line ofo a code cell
219 event.stopPropagation();
220 }
221 else if (code != key.upArrow && code != key.downArrow) {
222 this.close(); this.editor.focus();
223 //we give focus to the editor immediately and call sell in 50 ms
224 var that = this;
225 setTimeout(function(){that.carryOnCompletion();}, 50);
226 }
227 }
228
229
230 IPython.Completer = Completer;
231
232 return IPython;
233 }(IPython));
@@ -0,0 +1,89 b''
1 // highly adapted for codemiror jshint
2
3 (function () {
4 function forEach(arr, f) {
5 for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]);
6 }
7
8 function arrayContains(arr, item) {
9 if (!Array.prototype.indexOf) {
10 var i = arr.length;
11 while (i--) {
12 if (arr[i] === item) {
13 return true;
14 }
15 }
16 return false;
17 }
18 return arr.indexOf(item) != -1;
19 }
20
21 CodeMirror.contextHint = function(editor) {
22 // Find the token at the cursor
23 var cur = editor.getCursor(), token = editor.getTokenAt(cur), tprop = token;
24 // If it's not a 'word-style' token, ignore the token.
25 // If it is a property, find out what it is a property of.
26
27 var list = new Array();
28 var clist = getCompletions(token,editor) ;
29 for( var i = 0 ; i < clist.length ; i++)
30 {
31 list.push(
32 {
33 str : clist[i],
34 type : "context",
35 from : {line: cur.line, ch: token.start},
36 to : {line: cur.line, ch: token.end}
37 }
38 )
39
40 }
41 return list;
42 }
43
44 // find all 'words' of current cell
45 function getAllTokens(editor)
46 {
47 var found = [];
48 // get all text remove and split it before dot and at space
49 // keep the dot for completing token that also start with dot
50 var candidates = editor.getValue()
51 .replace(/[. ]/g,"\n")
52 .split('\n');
53 // append to arry if not already (the function)
54 function maybeAdd(str) {
55 if (!arrayContains(found, str)) found.push(str);
56 }
57
58 // append to arry if not already
59 // (here we do it )
60 for( c in candidates )
61 {
62 if(candidates[c].length >= 1){
63 maybeAdd(candidates[c]);}
64 }
65 return found;
66
67 }
68
69 function getCompletions(token,editor)
70 {
71 var candidates = getAllTokens(editor);
72 // filter all token that have a common start (but nox exactly) the lenght of the current token
73 var prependchar ='';
74 if(token.string.indexOf('.') == 0)
75 {
76 prependchar = '.'
77 }
78 var lambda = function(x){
79 x = prependchar+x;
80 return (x.indexOf(token.string)==0 && x != token.string)};
81 var filterd = candidates.filter(lambda);
82 for( var i in filterd)
83 {
84 // take care of reappending '.' at the beginning
85 filterd[i] = prependchar+filterd[i];
86 }
87 return filterd;
88 }
89 })();
@@ -273,6 +273,13 b' div.text_cell_render {'
273 font-family: monospace;
273 font-family: monospace;
274 }
274 }
275
275
276 option.context {
277 background-color: #DEF7FF;
278 }
279 option.introspection {
280 background-color: #EBF4EB;
281 }
282
276 @-moz-keyframes fadeIn {
283 @-moz-keyframes fadeIn {
277 from {opacity:0;}
284 from {opacity:0;}
278 to {opacity:1;}
285 to {opacity:1;}
@@ -10,7 +10,6 b''
10 //============================================================================
10 //============================================================================
11
11
12 var IPython = (function (IPython) {
12 var IPython = (function (IPython) {
13
14 var utils = IPython.utils;
13 var utils = IPython.utils;
15
14
16 var CodeCell = function (notebook) {
15 var CodeCell = function (notebook) {
@@ -23,6 +22,8 b' var IPython = (function (IPython) {'
23 this.tooltip_timeout = null;
22 this.tooltip_timeout = null;
24 this.clear_out_timeout = null;
23 this.clear_out_timeout = null;
25 IPython.Cell.apply(this, arguments);
24 IPython.Cell.apply(this, arguments);
25 var that = this;
26 this.ccc = new IPython.Completer(function(ed, callback){that.requestCompletion(ed, callback)});
26 };
27 };
27
28
28
29
@@ -40,8 +41,11 b' var IPython = (function (IPython) {'
40 mode: 'python',
41 mode: 'python',
41 theme: 'ipython',
42 theme: 'ipython',
42 readOnly: this.read_only,
43 readOnly: this.read_only,
43 onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this)
44 onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this),
44 });
45 });
46 var that = this;
47 ccm = this.code_mirror;
48 ccc = this.ccc;
45 input.append(input_area);
49 input.append(input_area);
46 var output = $('<div></div>').addClass('output vbox');
50 var output = $('<div></div>').addClass('output vbox');
47 cell.append(input).append(output);
51 cell.append(input).append(output);
@@ -129,13 +133,9 b' var IPython = (function (IPython) {'
129 // Prevent CodeMirror from handling the tab.
133 // Prevent CodeMirror from handling the tab.
130 return true;
134 return true;
131 } else {
135 } else {
132 pre_cursor.trim();
133 // Autocomplete the current line.
134 event.stop();
136 event.stop();
135 var line = editor.getLine(cur.line);
137 this.ccc.startCompletionFor(this.code_mirror);
136 this.is_completing = true;
138
137 this.completion_cursor = cur;
138 IPython.notebook.complete_cell(this, line, cur.ch);
139 return true;
139 return true;
140 };
140 };
141 } else if (event.keyCode === 8 && event.type == 'keydown') {
141 } else if (event.keyCode === 8 && event.type == 'keydown') {
@@ -263,285 +263,39 b' var IPython = (function (IPython) {'
263 };
263 };
264
264
265 // As you type completer
265 // As you type completer
266 CodeCell.prototype.finish_completing = function (matched_text, matches) {
266 CodeCell.prototype.requestCompletion= function(ed,callback)
267 if(matched_text[0]=='%'){
267 {
268 completing_from_magic = true;
268 this._compcallback = callback;
269 completing_to_magic = false;
269 this._editor = ed;
270 } else {
270 var cur = ed.getCursor();
271 completing_from_magic = false;
271 var pre_cursor = this.code_mirror.getRange({line:cur.line,ch:0},cur);
272 completing_to_magic = false;
272 pre_cursor.trim();
273 }
273 // Autocomplete the current line.
274 //return if not completing or nothing to complete
274 var line = this.code_mirror.getLine(cur.line);
275 if (!this.is_completing || matches.length === 0) {return;}
275 this.is_completing = true;
276
276 this.completion_cursor = cur;
277 // for later readability
277 IPython.notebook.complete_cell(this, line, cur.ch);
278 var key = { tab:9,
278 }
279 esc:27,
280 backspace:8,
281 space:32,
282 shift:16,
283 enter:13,
284 // _ is 95
285 isCompSymbol : function (code)
286 {
287 return (code > 64 && code <= 90)
288 || (code >= 97 && code <= 122)
289 || (code == 95)
290 },
291 dismissAndAppend : function (code)
292 {
293 chararr = '()[]+-/\\. ,=*'.split("");
294 codearr = chararr.map(function(x){return x.charCodeAt(0)});
295 return jQuery.inArray(code, codearr) != -1;
296 }
297
298 }
299
300 // smart completion, sort kwarg ending with '='
301 var newm = new Array();
302 if(this.notebook.smart_completer)
303 {
304 kwargs = new Array();
305 other = new Array();
306 for(var i = 0 ; i<matches.length ; ++i){
307 if(matches[i].substr(-1) === '='){
308 kwargs.push(matches[i]);
309 }else{other.push(matches[i]);}
310 }
311 newm = kwargs.concat(other);
312 matches = newm;
313 }
314 // end sort kwargs
315
316 // give common prefix of a array of string
317 function sharedStart(A){
318 shared='';
319 if(A.length == 1){shared=A[0]}
320 if(A.length > 1 ){
321 var tem1, tem2, s, A = A.slice(0).sort();
322 tem1 = A[0];
323 s = tem1.length;
324 tem2 = A.pop();
325 while(s && tem2.indexOf(tem1) == -1){
326 tem1 = tem1.substring(0, --s);
327 }
328 shared = tem1;
329 }
330 if (shared[0] == '%' && !completing_from_magic)
331 {
332 shared = shared.substr(1);
333 return [shared, true];
334 } else {
335 return [shared, false];
336 }
337 }
338
339
340 //try to check if the user is typing tab at least twice after a word
341 // and completion is "done"
342 fallback_on_tooltip_after = 2
343 if(matches.length == 1 && matched_text === matches[0])
344 {
345 if(this.npressed >fallback_on_tooltip_after && this.prevmatch==matched_text)
346 {
347 this.request_tooltip_after_time(matched_text+'(',0);
348 return;
349 }
350 this.prevmatch = matched_text
351 this.npressed = this.npressed+1;
352 }
353 else
354 {
355 this.prevmatch = "";
356 this.npressed = 0;
357 }
358 // end fallback on tooltip
359 //==================================
360 // Real completion logic start here
361 var that = this;
362 var cur = this.completion_cursor;
363 var done = false;
364
365 // call to dismmiss the completer
366 var close = function () {
367 if (done) return;
368 done = true;
369 if (complete != undefined)
370 {complete.remove();}
371 that.is_completing = false;
372 that.completion_cursor = null;
373 };
374
375 // update codemirror with the typed text
376 prev = matched_text
377 var update = function (inserted_text, event) {
378 that.code_mirror.replaceRange(
379 inserted_text,
380 {line: cur.line, ch: (cur.ch-matched_text.length)},
381 {line: cur.line, ch: (cur.ch+prev.length-matched_text.length)}
382 );
383 prev = inserted_text
384 if(event != null){
385 event.stopPropagation();
386 event.preventDefault();
387 }
388 };
389 // insert the given text and exit the completer
390 var insert = function (selected_text, event) {
391 update(selected_text)
392 close();
393 setTimeout(function(){that.code_mirror.focus();}, 50);
394 };
395
396 // insert the curent highlited selection and exit
397 var pick = function () {
398 insert(select.val()[0],null);
399 };
400
401
279
402 // Define function to clear the completer, refill it with the new
280 CodeCell.prototype.finish_completing = function (matched_text, matches) {
403 // matches, update the pseuso typing field. autopick insert match if
281 // let's build a function that wrap all that stuff into what is needed for the
404 // only one left, in no matches (anymore) dismiss itself by pasting
282 // new completer:
405 // what the user have typed until then
283 //
406 var complete_with = function(matches,typed_text,autopick,event)
284 var cur = this._editor.getCursor();
285 res = CodeMirror.contextHint(this._editor);
286 for( i=0; i< matches.length ; i++)
407 {
287 {
408 // If autopick an only one match, past.
288 res.push(
409 // Used to 'pick' when pressing tab
410 var prefix = '';
411 if(completing_to_magic && !completing_from_magic)
412 {
413 prefix='%';
414 }
415 if (matches.length < 1) {
416 insert(prefix+typed_text,event);
417 if(event != null){
418 event.stopPropagation();
419 event.preventDefault();
420 }
421 } else if (autopick && matches.length == 1) {
422 insert(matches[0],event);
423 if(event != null){
424 event.stopPropagation();
425 event.preventDefault();
426 }
427 return;
428 }
429 //clear the previous completion if any
430 update(prefix+typed_text,event);
431 complete.children().children().remove();
432 $('#asyoutype').html("<b>"+prefix+matched_text+"</b>"+typed_text.substr(matched_text.length));
433 select = $('#asyoutypeselect');
434 for (var i = 0; i<matches.length; ++i) {
435 select.append($('<option/>').html(matches[i]));
436 }
437 select.children().first().attr('selected','true');
438 }
439
440 // create html for completer
441 var complete = $('<div/>').addClass('completions');
442 complete.attr('id','complete');
443 complete.append($('<p/>').attr('id', 'asyoutype').html('<b>fixed part</b>user part'));//pseudo input field
444
445 var select = $('<select/>').attr('multiple','true');
446 select.attr('id', 'asyoutypeselect')
447 select.attr('size',Math.min(10,matches.length));
448 var pos = this.code_mirror.cursorCoords();
449
450 // TODO: I propose to remove enough horizontal pixel
451 // to align the text later
452 complete.css('left',pos.x+'px');
453 complete.css('top',pos.yBot+'px');
454 complete.append(select);
455
456 $('body').append(complete);
457
458 // So a first actual completion. see if all the completion start wit
459 // the same letter and complete if necessary
460 ff = sharedStart(matches)
461 fastForward = ff[0];
462 completing_to_magic = ff[1];
463 typed_characters = fastForward.substr(matched_text.length);
464 complete_with(matches,matched_text+typed_characters,true,null);
465 filterd = matches;
466 // Give focus to select, and make it filter the match as the user type
467 // by filtering the previous matches. Called by .keypress and .keydown
468 var downandpress = function (event,press_or_down) {
469 var code = event.which;
470 var autopick = false; // auto 'pick' if only one match
471 if (press_or_down === 0){
472 press = true; down = false; //Are we called from keypress or keydown
473 } else if (press_or_down == 1){
474 press = false; down = true;
475 }
476 if (code === key.shift) {
477 // nothing on Shift
478 return;
479 }
480 if (key.dismissAndAppend(code) && press) {
481 var newchar = String.fromCharCode(code);
482 typed_characters = typed_characters+newchar;
483 insert(matched_text+typed_characters,event);
484 return
485 }
486 if (code === key.enter) {
487 // Pressing ENTER will cause a pick
488 event.stopPropagation();
489 event.preventDefault();
490 pick();
491 } else if (code === 38 || code === 40) {
492 // We don't want the document keydown handler to handle UP/DOWN,
493 // but we want the default action.
494 event.stopPropagation();
495 } else if ( (code == key.backspace)||(code == key.tab && down) || press || key.isCompSymbol(code)){
496 if( key.isCompSymbol(code) && press)
497 {
289 {
498 var newchar = String.fromCharCode(code);
290 str : matches[i],
499 typed_characters = typed_characters+newchar;
291 type : "introspection",
500 } else if (code == key.tab) {
292 from : {line: cur.line, ch: cur.ch-matched_text.length},
501 ff = sharedStart(matches)
293 to : {line: cur.line, ch: cur.ch}
502 fastForward = ff[0];
503 completing_to_magic = ff[1];
504 ffsub = fastForward.substr(matched_text.length+typed_characters.length);
505 typed_characters = typed_characters+ffsub;
506 autopick = true;
507 } else if (code == key.backspace && down) {
508 // cancel if user have erase everything, otherwise decrease
509 // what we filter with
510 event.preventDefault();
511 if (typed_characters.length <= 0)
512 {
513 insert(matched_text,event)
514 return
515 }
516 typed_characters = typed_characters.substr(0,typed_characters.length-1);
517 } else if (press && code != key.backspace && code != key.tab && code != 0){
518 insert(matched_text+typed_characters,event);
519 return
520 } else {
521 return
522 }
294 }
523 re = new RegExp("^"+"\%?"+matched_text+typed_characters,"");
295 )
524 filterd = matches.filter(function(x){return re.test(x)});
525 ff = sharedStart(filterd);
526 completing_to_magic = ff[1];
527 complete_with(filterd,matched_text+typed_characters,autopick,event);
528 } else if (code == key.esc) {
529 // dismiss the completer and go back to before invoking it
530 insert(matched_text,event);
531 } else if (press) { // abort only on .keypress or esc
532 }
533 }
296 }
534 select.keydown(function (event) {
297 this._compcallback(res);
535 downandpress(event,1)
298
536 });
537 select.keypress(function (event) {
538 downandpress(event,0)
539 });
540 // Double click also causes a pick.
541 // and bind the last actions.
542 select.dblclick(pick);
543 select.blur(close);
544 select.focus();
545 };
299 };
546
300
547
301
@@ -1,5 +1,4 b''
1 {% extends page.html %}
1 {% extends page.html %}
2
3 {% block stylesheet %}
2 {% block stylesheet %}
4
3
5 {% if mathjax_url %}
4 {% if mathjax_url %}
@@ -220,6 +219,7 b' data-notebook-id={{notebook_id}}'
220 <script src="{{ static_url("js/initmathjax.js") }}" type="text/javascript" charset="utf-8"></script>
219 <script src="{{ static_url("js/initmathjax.js") }}" type="text/javascript" charset="utf-8"></script>
221 <script src="{{ static_url("js/cell.js") }}" type="text/javascript" charset="utf-8"></script>
220 <script src="{{ static_url("js/cell.js") }}" type="text/javascript" charset="utf-8"></script>
222 <script src="{{ static_url("js/codecell.js") }}" type="text/javascript" charset="utf-8"></script>
221 <script src="{{ static_url("js/codecell.js") }}" type="text/javascript" charset="utf-8"></script>
222 <script src="{{ static_url("js/completer.js") }}" type="text/javascript" charset="utf-8"></script>
223 <script src="{{ static_url("js/textcell.js") }}" type="text/javascript" charset="utf-8"></script>
223 <script src="{{ static_url("js/textcell.js") }}" type="text/javascript" charset="utf-8"></script>
224 <script src="{{ static_url("js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
224 <script src="{{ static_url("js/kernel.js") }}" type="text/javascript" charset="utf-8"></script>
225 <script src="{{ static_url("js/savewidget.js") }}" type="text/javascript" charset="utf-8"></script>
225 <script src="{{ static_url("js/savewidget.js") }}" type="text/javascript" charset="utf-8"></script>
@@ -231,5 +231,8 b' data-notebook-id={{notebook_id}}'
231 <script src="{{ static_url("js/notificationwidget.js") }}" type="text/javascript" charset="utf-8"></script>
231 <script src="{{ static_url("js/notificationwidget.js") }}" type="text/javascript" charset="utf-8"></script>
232 <script src="{{ static_url("js/notebookmain.js") }}" type="text/javascript" charset="utf-8"></script>
232 <script src="{{ static_url("js/notebookmain.js") }}" type="text/javascript" charset="utf-8"></script>
233
233
234 <script src="{{ static_url("js/context-hint.js") }} charset="utf-8"></script>
235 <script src="{{ static_url("codemirror/lib/util/simple-hint.js") }} charset="utf-8"></script>
236
234 {% end %}
237 {% end %}
235
238
General Comments 0
You need to be logged in to leave comments. Login now