tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

search.js (12826B)


      1 // CodeMirror, copyright (c) by Marijn Haverbeke and others
      2 // Distributed under an MIT license: https://codemirror.net/LICENSE
      3 
      4 // Define search commands. Depends on dialog.js or another
      5 // implementation of the openDialog method.
      6 
      7 // Replace works a little oddly -- it will do the replace on the next
      8 // Ctrl-G (or whatever is bound to findNext) press. You prevent a
      9 // replace by making sure the match is no longer selected when hitting
     10 // Ctrl-G.
     11 
     12 (function(mod) {
     13  if (typeof exports == "object" && typeof module == "object") // CommonJS
     14    mod(require("resource://devtools/client/shared/sourceeditor/codemirror/lib/codemirror.js"), require("resource://devtools/client/shared/sourceeditor/codemirror/addon/search/searchcursor.js"), require("resource://devtools/client/shared/sourceeditor/codemirror/addon/dialog/dialog.js"));
     15  else if (typeof define == "function" && define.amd) // AMD
     16    define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod);
     17  else // Plain browser env
     18    mod(CodeMirror);
     19 })(function(CodeMirror) {
     20  "use strict";
     21 
     22  function searchOverlay(query, caseInsensitive) {
     23    if (typeof query == "string")
     24      query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g");
     25    else if (!query.global)
     26      query = new RegExp(query.source, query.ignoreCase ? "gi" : "g");
     27 
     28    return {token: function(stream) {
     29      query.lastIndex = stream.pos;
     30      var match = query.exec(stream.string);
     31      if (match && match.index == stream.pos) {
     32        stream.pos += match[0].length || 1;
     33        return "searching";
     34      } else if (match) {
     35        stream.pos = match.index;
     36      } else {
     37        stream.skipToEnd();
     38      }
     39    }};
     40  }
     41 
     42  function SearchState() {
     43    this.posFrom = this.posTo = this.lastQuery = this.query = null;
     44    this.overlay = null;
     45  }
     46 
     47  function getSearchState(cm) {
     48    return cm.state.search || (cm.state.search = new SearchState());
     49  }
     50 
     51  function queryCaseInsensitive(query) {
     52    return typeof query == "string" && query == query.toLowerCase();
     53  }
     54 
     55  function getSearchCursor(cm, query, pos) {
     56    // Heuristic: if the query string is all lowercase, do a case insensitive search.
     57    return cm.getSearchCursor(query, pos, {caseFold: queryCaseInsensitive(query), multiline: true});
     58  }
     59 
     60  function persistentDialog(cm, text, deflt, onEnter, onKeyDown) {
     61    cm.openDialog(text, onEnter, {
     62      value: deflt,
     63      selectValueOnOpen: true,
     64      closeOnEnter: false,
     65      onClose: function() { clearSearch(cm); },
     66      onKeyDown: onKeyDown
     67    });
     68  }
     69 
     70  function dialog(cm, text, shortText, deflt, f) {
     71    if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true});
     72    else f(prompt(shortText, deflt));
     73  }
     74 
     75  function confirmDialog(cm, text, shortText, fs) {
     76    if (cm.openConfirm) cm.openConfirm(text, fs);
     77    else if (confirm(shortText)) fs[0]();
     78  }
     79 
     80  function parseString(string) {
     81    return string.replace(/\\([nrt\\])/g, function(match, ch) {
     82      if (ch == "n") return "\n"
     83      if (ch == "r") return "\r"
     84      if (ch == "t") return "\t"
     85      if (ch == "\\") return "\\"
     86      return match
     87    })
     88  }
     89 
     90  function parseQuery(query) {
     91    var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
     92    if (isRE) {
     93      try { query = new RegExp(isRE[1], isRE[2].indexOf("i") == -1 ? "" : "i"); }
     94      catch(e) {} // Not a regular expression after all, do a string search
     95    } else {
     96      query = parseString(query)
     97    }
     98    if (typeof query == "string" ? query == "" : query.test(""))
     99      query = /x^/;
    100    return query;
    101  }
    102 
    103  function startSearch(cm, state, query) {
    104    state.queryText = query;
    105    state.query = parseQuery(query);
    106    cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
    107    state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
    108    cm.addOverlay(state.overlay);
    109    if (cm.showMatchesOnScrollbar) {
    110      if (state.annotate) { state.annotate.clear(); state.annotate = null; }
    111      state.annotate = cm.showMatchesOnScrollbar(state.query, queryCaseInsensitive(state.query));
    112    }
    113  }
    114 
    115  function doSearch(cm, rev, persistent, immediate) {
    116    // We used to only build this input the first time the search was triggered and
    117    // reuse it again on subsequent search.
    118    // Unfortunately, this doesn't play well with the `persistent` parameter;
    119    // new event listeners are added to the input each time `persistentDialog` is called,
    120    // which would make a single `Enter` key trigger multiple "findNext" actions, making
    121    // it look like the search would skip some results.
    122    const doc = cm.getWrapperElement().ownerDocument;
    123    const inp = doc.createElement("input");
    124 
    125    inp.type = "search";
    126    inp.classList.add("cm5-search-input");
    127    inp.placeholder = cm.l10n("findCmd.promptMessage");
    128    inp.addEventListener("focus", () => inp.select());
    129 
    130    const queryDialog = doc.createElement("div");
    131    queryDialog.classList.add("cm5-search-container");
    132    queryDialog.appendChild(inp);
    133 
    134    var state = getSearchState(cm);
    135    if (state.query) return findNext(cm, rev);
    136    var q = cm.getSelection() || state.lastQuery;
    137    if (q instanceof RegExp && q.source == "x^") q = null
    138    if (persistent && cm.openDialog) {
    139      var hiding = null
    140      var searchNext = function(query, event) {
    141        CodeMirror.e_stop(event);
    142        if (!query) return;
    143        if (query != state.queryText) {
    144          startSearch(cm, state, query);
    145          state.posFrom = state.posTo = cm.getCursor();
    146        }
    147        if (hiding) hiding.style.opacity = 1
    148        findNext(cm, event.shiftKey, function(_, to) {
    149          var dialog
    150          if (to.line < 3 && document.querySelector &&
    151              (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) &&
    152              dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top)
    153            (hiding = dialog).style.opacity = .4
    154        })
    155      };
    156      persistentDialog(cm, queryDialog, q, searchNext, function(event, query) {
    157        var keyName = CodeMirror.keyName(event)
    158        var extra = cm.getOption('extraKeys'), cmd = (extra && extra[keyName]) || CodeMirror.keyMap[cm.getOption("keyMap")][keyName]
    159        if (cmd == "findNext" || cmd == "findPrev" ||
    160          cmd == "findPersistentNext" || cmd == "findPersistentPrev") {
    161          CodeMirror.e_stop(event);
    162          startSearch(cm, getSearchState(cm), query);
    163          cm.execCommand(cmd);
    164        } else if (cmd == "find" || cmd == "findPersistent") {
    165          CodeMirror.e_stop(event);
    166          searchNext(query, event);
    167        }
    168      });
    169      if (immediate && q) {
    170        startSearch(cm, state, q);
    171        findNext(cm, rev);
    172      }
    173    } else {
    174      dialog(cm, queryDialog, "Search for:", q, function(query) {
    175        if (query && !state.query) cm.operation(function() {
    176          startSearch(cm, state, query);
    177          state.posFrom = state.posTo = cm.getCursor();
    178          findNext(cm, rev);
    179        });
    180      });
    181    }
    182  }
    183 
    184  function findNext(cm, rev, callback) {cm.operation(function() {
    185    var state = getSearchState(cm);
    186    var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
    187    if (!cursor.find(rev)) {
    188      cursor = getSearchCursor(cm, state.query, rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0));
    189      if (!cursor.find(rev)) return;
    190    }
    191    cm.setSelection(cursor.from(), cursor.to());
    192    cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20);
    193    state.posFrom = cursor.from(); state.posTo = cursor.to();
    194    if (callback) callback(cursor.from(), cursor.to())
    195  });}
    196 
    197  function clearSearch(cm) {cm.operation(function() {
    198    var state = getSearchState(cm);
    199    state.lastQuery = state.query;
    200    if (!state.query) return;
    201    state.query = state.queryText = null;
    202    cm.removeOverlay(state.overlay);
    203    if (state.annotate) { state.annotate.clear(); state.annotate = null; }
    204  });}
    205 
    206  function replaceAll(cm, query, text) {
    207    cm.operation(function() {
    208      for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {
    209        if (typeof query != "string") {
    210          var match = cm.getRange(cursor.from(), cursor.to()).match(query);
    211          cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
    212        } else cursor.replace(text);
    213      }
    214    });
    215  }
    216 
    217  function replace(cm, all) {
    218    if (cm.getOption("readOnly")) return;
    219    var query = cm.getSelection() || getSearchState(cm).lastQuery;
    220 
    221    let doc = cm.getWrapperElement().ownerDocument;
    222 
    223    // `searchLabel` is used as part of `replaceQueryFragment` and as a separate
    224    // argument by itself, so it should be cloned.
    225    let searchLabel = doc.createElement("span");
    226    searchLabel.classList.add("CodeMirror-search-label");
    227    searchLabel.textContent = all ? "Replace all:" : "Replace:";
    228 
    229    let replaceQueryFragment = doc.createDocumentFragment();
    230    replaceQueryFragment.appendChild(searchLabel.cloneNode(true));
    231 
    232    let searchField = doc.createElement("input");
    233    searchField.setAttribute("type", "text");
    234    searchField.classList.add("cm5-search-replace-input");
    235    replaceQueryFragment.appendChild(searchField);
    236 
    237    let searchHint = doc.createElement("span");
    238    searchHint.classList.add("cm5-search-replace-hint");
    239    searchHint.textContent = "(Use /re/ syntax for regexp search)";
    240    replaceQueryFragment.appendChild(searchHint);
    241 
    242    dialog(cm, replaceQueryFragment, searchLabel, query, function(query) {
    243      if (!query) return;
    244      query = parseQuery(query);
    245 
    246      let replacementQueryFragment = doc.createDocumentFragment();
    247 
    248      let replaceWithLabel = searchLabel.cloneNode(false);
    249      replaceWithLabel.textContent = "With:";
    250      replacementQueryFragment.appendChild(replaceWithLabel);
    251 
    252      let replaceField = doc.createElement("input");
    253      replaceField.setAttribute("type", "text");
    254      replaceField.classList.add("cm5-search-replace-input");
    255      replacementQueryFragment.appendChild(replaceField);
    256 
    257      dialog(cm, replacementQueryFragment, "Replace with:", "", function(text) {
    258        text = parseString(text)
    259        if (all) {
    260          replaceAll(cm, query, text)
    261        } else {
    262          clearSearch(cm);
    263          var cursor = getSearchCursor(cm, query, cm.getCursor("from"));
    264          var advance = function() {
    265            var start = cursor.from(), match;
    266            if (!(match = cursor.findNext())) {
    267              cursor = getSearchCursor(cm, query);
    268              if (!(match = cursor.findNext()) ||
    269                  (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
    270            }
    271            cm.setSelection(cursor.from(), cursor.to());
    272            cm.scrollIntoView({ from: cursor.from(), to: cursor.to() });
    273 
    274            let replaceConfirmFragment = doc.createDocumentFragment();
    275 
    276            let replaceConfirmLabel = searchLabel.cloneNode(false);
    277            replaceConfirmLabel.textContent = "Replace?";
    278            replaceConfirmFragment.appendChild(replaceConfirmLabel);
    279 
    280            let yesButton = doc.createElement("button");
    281            yesButton.textContent = "Yes";
    282            replaceConfirmFragment.appendChild(yesButton);
    283 
    284            let noButton = doc.createElement("button");
    285            noButton.textContent = "No";
    286            replaceConfirmFragment.appendChild(noButton);
    287 
    288            let allButton = doc.createElement("button");
    289            allButton.textContent = "All";
    290            replaceConfirmFragment.appendChild(allButton);
    291 
    292            let stopButton = doc.createElement("button");
    293            stopButton.textContent = "Stop";
    294            replaceConfirmFragment.appendChild(stopButton);
    295 
    296            confirmDialog(cm, replaceConfirmFragment, "Replace?",
    297                          [function() {doReplace(match);}, advance,
    298                           function() {replaceAll(cm, query, text)}]);
    299          };
    300          var doReplace = function(match) {
    301            cursor.replace(typeof query == "string" ? text :
    302                           text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
    303            advance();
    304          };
    305          advance();
    306        }
    307      });
    308    });
    309  }
    310 
    311  CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};
    312  CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);};
    313  CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);};
    314  CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);};
    315  CodeMirror.commands.findNext = doSearch;
    316  CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
    317  CodeMirror.commands.clearSearch = clearSearch;
    318  CodeMirror.commands.replace = replace;
    319  CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};
    320 });