mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-11 17:41:03 +01:00
323 lines
11 KiB
JavaScript
323 lines
11 KiB
JavaScript
// The following code is heavily based on the standard CodeMirror
|
|
// search addon found at: https://codemirror.net/addon/search/search.js
|
|
// I added/removed and modified code in order to get a closer match to a
|
|
// browser's built-in find-in-page feature which are just enough for
|
|
// uBlock Origin.
|
|
|
|
|
|
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
|
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
|
|
|
// Define search commands. Depends on dialog.js or another
|
|
// implementation of the openDialog method.
|
|
|
|
// Replace works a little oddly -- it will do the replace on the next
|
|
// Ctrl-G (or whatever is bound to findNext) press. You prevent a
|
|
// replace by making sure the match is no longer selected when hitting
|
|
// Ctrl-G.
|
|
|
|
/* globals define, require, CodeMirror */
|
|
|
|
'use strict';
|
|
|
|
(function(mod) {
|
|
if (typeof exports === "object" && typeof module === "object") // CommonJS
|
|
mod(require("../../lib/codemirror"), require("./searchcursor"), require("../dialog/dialog"));
|
|
else if (typeof define === "function" && define.amd) // AMD
|
|
define(["../../lib/codemirror", "./searchcursor", "../dialog/dialog"], mod);
|
|
else // Plain browser env
|
|
mod(CodeMirror);
|
|
})(function(CodeMirror) {
|
|
|
|
function searchOverlay(query, caseInsensitive) {
|
|
if (typeof query === "string")
|
|
query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g");
|
|
else if (!query.global)
|
|
query = new RegExp(query.source, query.ignoreCase ? "gi" : "g");
|
|
|
|
return {
|
|
token: function(stream) {
|
|
query.lastIndex = stream.pos;
|
|
var match = query.exec(stream.string);
|
|
if (match && match.index === stream.pos) {
|
|
stream.pos += match[0].length || 1;
|
|
return "searching";
|
|
} else if (match) {
|
|
stream.pos = match.index;
|
|
} else {
|
|
stream.skipToEnd();
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
var searchWidgetHtml =
|
|
'<div class="cm-search-widget">' +
|
|
'<span class="fa"></span> ' +
|
|
'<span>' +
|
|
'<input type="text" size="32">' +
|
|
'<span class="cm-search-widget-count">' +
|
|
'<span><!-- future use --></span><span>0</span>' +
|
|
'</span>' +
|
|
'</span> ' +
|
|
'<span class="cm-search-widget-up cm-search-widget-button fa"></span> ' +
|
|
'<span class="cm-search-widget-down cm-search-widget-button fa"></span> ' +
|
|
'</div>';
|
|
|
|
function searchWidgetKeydownHandler(cm, ev) {
|
|
var keyName = CodeMirror.keyName(ev);
|
|
if ( !keyName ) { return; }
|
|
CodeMirror.lookupKey(
|
|
keyName,
|
|
cm.getOption('keyMap'),
|
|
function(command) {
|
|
if ( widgetCommandHandler(cm, command) ) {
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
function searchWidgetTimerHandler(cm) {
|
|
var state = getSearchState(cm);
|
|
state.queryTimer = null;
|
|
findCommit(cm);
|
|
}
|
|
|
|
function searchWidgetInputHandler(cm) {
|
|
var state = getSearchState(cm);
|
|
if ( queryTextFromSearchWidget(cm) !== state.queryText ) {
|
|
if ( state.queryTimer !== null ) {
|
|
clearTimeout(state.queryTimer);
|
|
}
|
|
state.queryTimer = setTimeout(
|
|
searchWidgetTimerHandler.bind(null, cm),
|
|
350
|
|
);
|
|
}
|
|
}
|
|
|
|
function searchWidgetClickHandler(cm, ev) {
|
|
var tcl = ev.target.classList;
|
|
if ( tcl.contains('cm-search-widget-up') ) {
|
|
findNext(cm, true);
|
|
} else if ( tcl.contains('cm-search-widget-down') ) {
|
|
findNext(cm, false);
|
|
}
|
|
if ( ev.target.localName !== 'input' ) {
|
|
ev.preventDefault();
|
|
} else {
|
|
ev.stopImmediatePropagation();
|
|
}
|
|
}
|
|
|
|
function queryTextFromSearchWidget(cm) {
|
|
return getSearchState(cm).widget.querySelector('input[type="text"]').value;
|
|
}
|
|
|
|
function queryTextToSearchWidget(cm, q) {
|
|
var input = getSearchState(cm).widget.querySelector('input[type="text"]');
|
|
if ( typeof q === 'string' && q !== input.value ) {
|
|
input.value = q;
|
|
}
|
|
input.setSelectionRange(0, input.value.length);
|
|
input.focus();
|
|
}
|
|
|
|
function SearchState(cm) {
|
|
this.query = null;
|
|
this.overlay = null;
|
|
this.panel = null;
|
|
this.widget = null;
|
|
var domParser = new DOMParser();
|
|
var doc = domParser.parseFromString(searchWidgetHtml, 'text/html');
|
|
this.widget = document.adoptNode(doc.body.firstElementChild);
|
|
this.widget.addEventListener('keydown', searchWidgetKeydownHandler.bind(null, cm));
|
|
this.widget.addEventListener('input', searchWidgetInputHandler.bind(null, cm));
|
|
this.widget.addEventListener('mousedown', searchWidgetClickHandler.bind(null, cm));
|
|
if ( typeof cm.addPanel === 'function' ) {
|
|
this.panel = cm.addPanel(this.widget);
|
|
}
|
|
this.queryText = '';
|
|
this.queryTimer = null;
|
|
}
|
|
|
|
// We want the search widget to behave as if the focus was on the
|
|
// CodeMirror editor.
|
|
|
|
var reSearchCommands = /^(?:find|findNext|findPrev|newlineAndIndent)$/;
|
|
|
|
function widgetCommandHandler(cm, command) {
|
|
if ( reSearchCommands.test(command) === false ) { return false; }
|
|
var queryText = queryTextFromSearchWidget(cm);
|
|
if ( command === 'find' ) {
|
|
queryTextToSearchWidget(cm);
|
|
return true;
|
|
}
|
|
if ( queryText.length !== 0 ) {
|
|
findNext(cm, command === 'findPrev');
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function getSearchState(cm) {
|
|
return cm.state.search || (cm.state.search = new SearchState(cm));
|
|
}
|
|
|
|
function queryCaseInsensitive(query) {
|
|
return typeof query === "string" && query === query.toLowerCase();
|
|
}
|
|
|
|
function getSearchCursor(cm, query, pos) {
|
|
// Heuristic: if the query string is all lowercase, do a case insensitive search.
|
|
return cm.getSearchCursor(
|
|
query,
|
|
pos,
|
|
{caseFold: queryCaseInsensitive(query), multiline: true}
|
|
);
|
|
}
|
|
|
|
function parseString(string) {
|
|
return string.replace(/\\(.)/g, function(_, ch) {
|
|
if (ch === "n") return "\n";
|
|
if (ch === "r") return "\r";
|
|
return ch;
|
|
});
|
|
}
|
|
|
|
function parseQuery(query) {
|
|
var isRE = query.match(/^\/(.*)\/([a-z]*)$/);
|
|
if (isRE) {
|
|
try { query = new RegExp(isRE[1], isRE[2].indexOf("i") === -1 ? "" : "i"); }
|
|
catch(e) {} // Not a regular expression after all, do a string search
|
|
} else {
|
|
query = parseString(query);
|
|
}
|
|
if (typeof query === "string" ? query === "" : query.test(""))
|
|
query = /x^/;
|
|
return query;
|
|
}
|
|
|
|
function startSearch(cm, state) {
|
|
state.query = parseQuery(state.queryText);
|
|
if ( state.overlay ) {
|
|
cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
|
|
}
|
|
state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
|
|
cm.addOverlay(state.overlay);
|
|
if ( cm.showMatchesOnScrollbar ) {
|
|
if ( state.annotate ) {
|
|
state.annotate.clear();
|
|
state.annotate = null;
|
|
}
|
|
state.annotate = cm.showMatchesOnScrollbar(
|
|
state.query,
|
|
queryCaseInsensitive(state.query)
|
|
);
|
|
var count = state.annotate.matches.length;
|
|
state.widget
|
|
.querySelector('.cm-search-widget-count > span:nth-of-type(2)')
|
|
.textContent = count > 1000 ? '1000+' : count;
|
|
state.widget.setAttribute('data-query', state.queryText);
|
|
}
|
|
}
|
|
|
|
function findNext(cm, rev, callback) {
|
|
cm.operation(function() {
|
|
var state = getSearchState(cm);
|
|
if ( !state.query ) { return; }
|
|
var cursor = getSearchCursor(
|
|
cm,
|
|
state.query,
|
|
rev ? cm.getCursor('from') : cm.getCursor('to')
|
|
);
|
|
if (!cursor.find(rev)) {
|
|
cursor = getSearchCursor(
|
|
cm,
|
|
state.query,
|
|
rev ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0)
|
|
);
|
|
if (!cursor.find(rev)) return;
|
|
}
|
|
cm.setSelection(cursor.from(), cursor.to());
|
|
cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20);
|
|
if (callback) callback(cursor.from(), cursor.to());
|
|
});
|
|
}
|
|
|
|
function clearSearch(cm, hard) {
|
|
cm.operation(function() {
|
|
var state = getSearchState(cm);
|
|
if ( state.query ) {
|
|
state.query = state.queryText = null;
|
|
}
|
|
if ( state.overlay ) {
|
|
cm.removeOverlay(state.overlay);
|
|
state.overlay = null;
|
|
}
|
|
if ( state.annotate ) {
|
|
state.annotate.clear();
|
|
state.annotate = null;
|
|
}
|
|
state.widget.removeAttribute('data-query');
|
|
if ( hard ) {
|
|
state.panel.clear();
|
|
state.panel = null;
|
|
state.widget = null;
|
|
cm.state.search = null;
|
|
}
|
|
});
|
|
}
|
|
|
|
function findCommit(cm) {
|
|
var state = getSearchState(cm);
|
|
if ( state.queryTimer !== null ) {
|
|
clearTimeout(state.queryTimer);
|
|
state.queryTimer = null;
|
|
}
|
|
var queryText = queryTextFromSearchWidget(cm);
|
|
if ( queryText === state.queryText ) { return; }
|
|
state.queryText = queryText;
|
|
if ( state.queryText === '' ) {
|
|
clearSearch(cm);
|
|
} else {
|
|
cm.operation(function() {
|
|
startSearch(cm, state);
|
|
findNext(cm, false);
|
|
});
|
|
}
|
|
}
|
|
|
|
function findCommand(cm) {
|
|
var queryText = cm.getSelection() || undefined;
|
|
if ( !queryText ) {
|
|
var word = cm.findWordAt(cm.getCursor());
|
|
queryText = cm.getRange(word.anchor, word.head);
|
|
if ( /^\W|\W$/.test(queryText) ) {
|
|
queryText = undefined;
|
|
}
|
|
cm.setCursor(word.anchor);
|
|
}
|
|
queryTextToSearchWidget(cm, queryText);
|
|
findCommit(cm);
|
|
}
|
|
|
|
function findNextCommand(cm) {
|
|
var state = getSearchState(cm);
|
|
if ( state.query ) { return findNext(cm, false); }
|
|
}
|
|
|
|
function findPrevCommand(cm) {
|
|
var state = getSearchState(cm);
|
|
if ( state.query ) { return findNext(cm, true); }
|
|
}
|
|
|
|
CodeMirror.commands.find = findCommand;
|
|
CodeMirror.commands.findNext = findNextCommand;
|
|
CodeMirror.commands.findPrev = findPrevCommand;
|
|
|
|
CodeMirror.defineInitHook(function(cm) {
|
|
getSearchState(cm);
|
|
});
|
|
});
|