PageSourceSearch

https://shiny-r.dev/EuropeMapApp3/shinyAce/shinyAce.js

js shiny-r.dev collected 2026-09-25 22:45:19 UTC 17,152 bytes, 476 lines download raw bytes

1(function () {
2  var lang = ace.require("ace/lib/lang");
3  var langTools = ace.require("ace/ext/language_tools");
4
5  var staticCompleter = {
6    getCompletions: function (editor, session, pos, prefix, callback) {
7      var comps = $('#' + editor.container.id).data('auto-complete-list');
8      if (comps) {
9        var words = [];
10        Object.keys(comps).forEach(function (key) {
11          var comps_key = comps[key];
12          if (!Array.isArray(comps[key])) {
13            comps_key = [comps_key];
14          }
15          words = words.concat(comps_key.map(function (d) {
16            return { name: d, value: d, meta: key };
17          }));
18        });
19        callback(null, words);
20      }
21    }
22  };
23  langTools.addCompleter(staticCompleter);
24
25  var rlangCompleter = {
26    identifierRegexps: [
27      /[a-zA-Z_0-9\.\:\-\u00A2-\uFFFF]/, 
28    ],
29    getCompletions: function(editor, session, pos, prefix, callback) {
30      var inputId = editor.container.id;
31      
32      // TODO: consider dropping onInputChange hook when completer is disabled for performance
33      Shiny.onInputChange(inputId + '_shinyAce_hint', {
34        // TODO: add an option to disable full document parsing for performance
35        document: session.getValue(),
36        linebuffer: session.getLine(pos.row),
37        cursorPosition: pos,
38        // nonce causes autocomplete event to trigger
39        // on R side even if Ctrl-Space is pressed twice
40        // with the same linebuffer and cursorPosition
41        nonce: Math.random()
42      });
43      
44      // store callback for dynamic completion
45      $('#' + inputId).data('autoCompleteCallback', callback);
46    }
47  };
48  langTools.addCompleter(rlangCompleter);
49
50  // behavior upon selection of an autocompletion
51  //   I was expecting this to be able to be defined with the rlangCompleter,
52  //   but it appears to only be able to be defined per-completion.
53  //   Upon a completion callback, completions are mapped over and assigned this 
54  //  function for insertion behavior for each completion entry
55  function r_insertMatch(editor, data) {
56    if (editor.completer.completions.filterText) {
57      var ranges = editor.selection.getAllRanges();
58      for (var i = 0, range; range = ranges[i]; i++) {
59        range.start.column -= editor.completer.completions.filterText.length;
60        editor.session.remove(range);
61      }
62    }
63    if (data.snippet) {
64      snippetManager.insertSnippet(editor, data.snippet);
65    } else {
66      // insert completion
67      var insertString = data.value || data;
68      editor.execCommand("insertstring", insertString);
69      
70      // automatically clobber existing code
71      var cursor = editor.getCursorPosition();
72      var remainder = editor.session.getLine(cursor.row).slice(cursor.column);
73      var re_match = remainder.match(/(^[a-zA-Z0-9._:]*)((?:\(\)?|\))?)/);
74      if (re_match && re_match[0].length) {
75        // remove word that we're clobbering
76        editor.getSession().getDocument().removeInLine(
77          cursor.row, cursor.column, cursor.column + re_match[1].length);
78          
79        // if function call, delete parens and navigate into existing call
80        if (insertString.endsWith("()") && re_match[2].length) {
81          editor.getSession().getDocument().removeInLine(
82            cursor.row, cursor.column - re_match[2].length, cursor.column);
83          if (re_match[2].length > 1) editor.navigateRight(1);
84        }
85        
86      } else if (insertString.endsWith("()")) {
87        // navigate backwards into ()'s for function completions
88        editor.navigateLeft(1);
89      }
90      
91    }
92  };
93  langTools.addCompleter(rlangCompleter);
94
95  function updateEditor(el, data) {
96    var editor = {};
97    if (typeof $(el).data('aceEditor') !== 'undefined')
98      editor = $(el).data('aceEditor');
99    else
100      editor = ace.edit(el);
101
102    if (data.hasOwnProperty('fontSize')) {
103      el.style.fontSize = data.fontSize + 'px';
104    }
105
106    if (data.hasOwnProperty('theme')) {
107      editor.setTheme("ace/theme/" + data.theme);
108    }
109
110    if (data.hasOwnProperty('mode')) {
111      editor.getSession().setMode('ace/mode/' + data.mode);
112    }
113
114    if (data.hasOwnProperty('value')) {
115      editor.setValue(data.value, -1);
116    }
117
118    if (data.hasOwnProperty("selectionId")) {
119      editor.getSelection().on("changeSelection", function () {
120        Shiny.onInputChange(el.id + "_" + data.selectionId, editor.getCopyText());
121      });
122    }
123
124    if (data.hasOwnProperty("cursorId")) {
125      editor.getSelection().on("changeCursor", function () {
126        Shiny.onInputChange(el.id + "_" + data.cursorId, editor.selection.getCursor());
127      });
128    }
129
130    if (data.hasOwnProperty("hotkeys")) {
131      Object.keys(data.hotkeys).forEach(function (key) {
132        editor.commands.addCommand({
133          name: key,
134          bindKey: data.hotkeys[key],
135          exec: function (editor) {
136            var selection = editor.session.getTextRange();
137            var range = editor.selection.getRange();
138            var imax = editor.session.getLength() - range.end.row;
139            var inputId = editor.container.id;
140            var shinyEvent = {
141              editorId: inputId,
142              selection: selection,
143              range: range,
144              randNum: Math.random()
145            };
146            Shiny.onInputChange(inputId + "_" + key, shinyEvent);
147          }, // exec end
148          readOnly: true // false if this command should not apply in readOnly mode
149        }); //editor.addCommand end
150      }); // forEach end
151    }
152
153    if (data.hasOwnProperty("code_hotkeys")) {
154      // data.code_hotkeys[0] should indicate the code type (e.g., "r", "python", etc.)
155      // in the future, this could load js code to "jump" through code of that type
156      Object.keys(data.code_hotkeys[1]).forEach(function (key) {
157        editor.commands.addCommand({
158          name: key,
159          bindKey: data.code_hotkeys[1][key],
160          exec: function (editor) {
161            var selection = editor.session.getTextRange();
162            var range = editor.selection.getRange();
163            var imax = editor.session.getLength() - range.end.row;
164            var inputId = editor.container.id;
165            var line = "";
166            if (selection === "") {
167              line = code_jump(editor, range, imax);
168            }
169            var shinyEvent = {
170              editorId: inputId,
171              selection: selection,
172              range: range,
173              line: line,
174              randNum: Math.random()
175            };
176            Shiny.onInputChange(inputId + "_" + key, shinyEvent);
177          }, // exec end
178          readOnly: true // false if this command should not apply in readOnly mode
179        }); //editor.addCommand end
180      }); // forEach end
181    }
182    
183    if (data.hasOwnProperty("debounce")) {
184      $(el).data("debounce", data.debounce);
185    }
186
187    if (data.hasOwnProperty("vimKeyBinding") && data.vimKeyBinding === true) {
188      editor.setKeyboardHandler("ace/keyboard/vim");
189    }
190
191    if (data.hasOwnProperty("showLineNumbers")) {
192      if (data.showLineNumbers === false) {
193        editor.renderer.setShowGutter(false);
194      } else if (data.showLineNumbers === true) {
195        editor.renderer.setShowGutter(true);
196      }
197    }
198
199    if (data.hasOwnProperty("highlightActiveLine") && data.highlightActiveLine === false) {
200      editor.setHighlightActiveLine(false);
201    }
202
203    if (data.hasOwnProperty('readOnly')) {
204      editor.setReadOnly(data.readOnly);
205    }
206
207    if (data.hasOwnProperty('wordWrap')) {
208      editor.getSession().setUseWrapMode(data.wordWrap);
209    }
210
211    if (data.hasOwnProperty("useSoftTabs")) {
212      editor.setOption("useSoftTabs", data.useSoftTabs);
213    }
214
215    if (data.hasOwnProperty("tabSize")) {
216      editor.setOption("tabSize", data.tabSize);
217    }
218
219    if (data.hasOwnProperty("showInvisibles")) {
220      editor.setOption("showInvisibles", data.showInvisibles);
221    }
222
223    if (data.hasOwnProperty("showPrintMargin")) {
224      editor.setOption("showPrintMargin", data.showPrintMargin);
225    }
226
227    if (data.hasOwnProperty('border')) {
228      var classes = ['acenormal', 'aceflash', 'acealert'];
229      $(el).removeClass(classes.join(' '));
230      $(el).addClass(data.border);
231    }
232    
233    if (data.annotations) {
234      editor.getSession().setAnnotations(data.annotations);
235    }
236    
237    if (data.docTooltip) {
238      // { docHTML: "", docText: "" }
239      if (data.docTooltip.docHTML || data.docTooltip.docText) {
240        if (!editor.completer.tooltipNode) {
241          if (editor.__tooltipTimerCall)
242            editor.__tooltipTimerCall.cancel();
243            
244          editor.__tooltipTimerCall = lang.delayedCall(
245            function() { 
246              if (this.completer.activated)
247                this.completer.showDocTooltip(data.docTooltip); 
248            }.bind(editor), 1000);
249            
250          editor.__tooltipTimerCall();
251        } else {
252          editor.completer.showDocTooltip(data.docTooltip);
253        }
254      }
255    }
256    
257    if (data.hasOwnProperty('autoComplete')) {
258      var value = data.autoComplete;
259      editor.setOption('enableLiveAutocompletion', value === 'live');
260      editor.setOption('enableBasicAutocompletion', value !== 'disabled');
261    }
262
263    if (data.hasOwnProperty('autoCompleters')) {
264      var completers = data.autoCompleters;
265      editor.completers = [];
266      if (completers) {
267        if (!Array.isArray(completers)) {
268          completers = [completers];
269        }
270        completers.forEach(function (completer) {
271          switch (completer) {
272            case 'snippet':
273              completer = langTools.snippetCompleter;
274              break;
275            case 'text':
276              completer = langTools.textCompleter;
277              break;
278            case 'keyword':
279              completer = langTools.keyWordCompleter;
280              break;
281            case 'static':
282              completer = staticCompleter;
283              break;
284            case 'rlang':
285              completer = rlangCompleter;
286              // add a keybind for "smart" tab - using tab for both indentation 
287              // and autocompletion triggering, specific to R object names
288              editor.commands.addCommand({
289                name: 'rlang_smartTab',
290                bindKey: { win: 'Tab', mac: 'TAB' },
291                multiSelectAction: 'forEach',
292                exec: function(editor) {
293                  var selection = editor.session.getTextRange();
294                  var range = editor.selection.getRange();
295                  var imax = editor.session.getLength() - range.end.row;
296                
297                  //// use regular indent whenever cursor is anything but standard
298                  if (selection !== '' || editor.inMultiSelectMode) {
299                    editor.indent();
300                    
301                  // otherwise see if autocompletion should be triggered
302                  } else {
303                    var linebuffer = editor.session.getLine(range.start.row).slice(0, range.start.column);
304                  
305                    // if at the start of an object name or function call, kick off autocompletion
306                    if (/[a-zA-Z._][a-zA-Z0-9._:]*$/.test(linebuffer) || /[a-zA-Z0-9._]\([^)]*$/.test(linebuffer)) {
307                      if (editor.completer) editor.completer.detach();
308                      editor.execCommand('startAutocomplete');
309                    
310                    // otherwise do an indentation
311                    } else {
312                      editor.indent();
313                    }
314                  }
315                }
316              });
317              break;
318          }
319          
320          // to each completer, add getDocTooltip callback to R
321          if (!completer.hasOwnProperty("getDocTooltip")) {
322            completer.getDocTooltip = function(item) {
323              Shiny.onInputChange(data.id + '_shinyAce_tooltipItem', item);
324            };
325          }
326          
327          editor.completers.push(completer);
328        });
329      }
330    }
331
332    if (data.hasOwnProperty('autoCompleteList')) {
333      $(el).data('auto-complete-list', data.autoCompleteList);
334    }
335
336    if (data.hasOwnProperty("setBehavioursEnabled") && data.setBehavioursEnabled === false) {
337      editor.setBehavioursEnabled(data.setBehavioursEnabled);
338    }
339    
340    // if (data.hasOwnProperty("codeCompletions")) {
341    //   var callback = $(el).data('autoCompleteCallback');
342    //   if (callback !== undefined) callback(null, data.codeCompletions);
343    // }
344
345    // this is a bit r-specific, perhaps there's a better way to only link it 
346    // to the rlang completer?
347    if (data.hasOwnProperty("codeCompletions")) {
348      var callback = $(el).data('autoCompleteCallback');
349      data.codeCompletions = data.codeCompletions.map(function(completion) {
350        if (typeof completion.completer == "string") {
351          switch (completion.completer) {
352            case 'rlang': 
353              completion.completer = {};
354              completion.completer.insertMatch = r_insertMatch;
355              break;
356          }
357        }
358        return completion;
359      });
360      
361      if (callback !== undefined) callback(null, data.codeCompletions);
362    }
363
364    if (data.hasOwnProperty("autoScrollEditorIntoView") && data.autoScrollEditorIntoView === true) {
365      editor.setOption("autoScrollEditorIntoView", true);
366      if (data.hasOwnProperty("maxLines")) {
367        editor.setOption("maxLines", data.maxLines);
368      }
369      if (data.hasOwnProperty("minLines")) {
370        editor.setOption("minLines", data.minLines);
371      }
372    }
373
374    if (data.hasOwnProperty('placeholder')) {
375      // adapted from https://stackoverflow.com/a/26700324/1974918
376      
377      function update() {
378        var shouldShow = !editor.session.getValue().length;
379        var node = editor.renderer.emptyMessageNode;
380        if (!shouldShow && node) {
381          editor.renderer.scroller.removeChild(editor.renderer.emptyMessageNode);
382          editor.renderer.emptyMessageNode = null;
383        } else if (shouldShow && !node) {
384          node = editor.renderer.emptyMessageNode = document.createElement("div");
385          node.textContent = data.placeholder;
386          node.className = "ace_emptyMessage";
387          node.style.padding = "0 15px";
388          node.style.position = "absolute";
389          node.style.zIndex = 9;
390          node.style.opacity = 0.5;
391          editor.renderer.scroller.appendChild(node);
392        }
393      }
394
395      editor.on("input", update);
396      setTimeout(update, 100);
397    }
398
399    if (typeof $(el).data('aceEditor') == 'undefined')
400      $(el).data("aceEditor", editor);
401
402  };
403
404  var shinyAceInputBinding = new Shiny.InputBinding();
405  $.extend(shinyAceInputBinding, {
406    find: function(scope) {
407      return $(scope).find(".shiny-ace");
408    },
409    initialize: function (el) {
410      var scriptData = document.querySelector("script[data-for='" + el.id + "'][type='application/json']");
411      if (scriptData) {
412        var data = JSON.parse(scriptData.textContent);
413        updateEditor(el, data);
414      }
415    },
416    getValue: function(el) {
417      return($(el).data('aceEditor').getValue());
418    },
419    setValue: function(el, value) {
420      //TODO
421    },
422    subscribe: function(el, callback) {
423      var editor = $(el).data('aceEditor');
424      
425      $(el).data('aceChangeCallback', function(e) {
426        callback(true);
427        
428        // always clear annotation at current line on edit
429        var cp = editor.getCursorPosition();
430        editor.getSession().setAnnotations(
431          editor.getSession().getAnnotations().filter(
432            a => !(a.row === cp.row && a.type === 'error')));
433        
434        // only trigger syntax parsing if idle for some time
435        if (editor.__annotationTimerCall)
436          editor.__annotationTimerCall.cancel();
437        
438        editor.__annotationTimerCall = lang.delayedCall(
439          function() { 
440            Shiny.onInputChange(
441              this.attr('id') + '_shinyAce_annotationTrigger', 
442              Math.random());
443          }.bind($(el)), 1000);
444        editor.__annotationTimerCall();
445      });
446            
447      $(el).data('aceEditor').getSession().addEventListener("change",
448        $(el).data('aceChangeCallback')
449      );
450    },
451    unsubscribe: function(el) {
452      $(el).data('aceEditor').getSession().removeEventListener("change", 
453        $(el).data('aceChangeCallback'));
454    }, 
455    getRatePolicy: function(el){
456      return ({policy: 'debounce', delay: $(el).data('debounce') || 1000 });
457    }
458  });
459
460  Shiny.inputBindings.register(shinyAceInputBinding);
461
462  Shiny.addCustomMessageHandler('shinyAce', function (data) {
463    var id = data.id;
464    var el = document.getElementById(data.id);
465    updateEditor(el, data);
466  });
467
468  // Allow toggle of the search-replace box in Ace
469  // see https://github.com/ajaxorg/ace/issues/3552
470  var toggle_search_replace = ace.require("ace/ext/searchbox").SearchBox.prototype.$searchBarKb.bindKey("Ctrl-f|Command-f|Ctrl-H|Command-Option-F", function (sb) {
471    var isReplace = sb.isReplace = !sb.isReplace;
472    sb.replaceBox.style.display = isReplace ? "" : "none";
473    sb[isReplace ? "replaceInput" : "searchInput"].focus();
474  });
475
476})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.