diff --git a/src/1p-filters.html b/src/1p-filters.html
index a6f6163dd..1712e36b4 100644
--- a/src/1p-filters.html
+++ b/src/1p-filters.html
@@ -6,7 +6,6 @@
uBlock — Your filters
-
@@ -32,10 +31,13 @@
-
-
+
-
+
+
+
+
+
diff --git a/src/asset-viewer.html b/src/asset-viewer.html
index fd69f8cd5..5c9c2bf3e 100644
--- a/src/asset-viewer.html
+++ b/src/asset-viewer.html
@@ -3,9 +3,10 @@
uBlock — Asset
-
-
-
+
+
+
+
+
-
-
+
-
+
+
+
+
+
diff --git a/src/css/codemirror.css b/src/css/codemirror.css
index 9f7aa7df7..80c709a25 100644
--- a/src/css/codemirror.css
+++ b/src/css/codemirror.css
@@ -1,6 +1,7 @@
.codeMirrorContainer {
font-size: 12px;
overflow: hidden;
+ position: relative;
}
.CodeMirror {
border: 1px solid #ddd;
@@ -12,3 +13,50 @@
.cm-staticext {color: #008;}
.cm-staticnet.cm-block {color: #800;}
.cm-staticnet.cm-allow {color: #004f00;}
+
+
+.cm-search-widget {
+ align-items: center;
+ background-color: #eee;
+ display: flex;
+ justify-content: center;
+ padding: 4px 8px;
+ /* position: absolute; */
+ right: 2em;
+ top: 0;
+ z-index: 1000;
+ }
+.cm-search-widget > span {
+ position: relative;
+ }
+.cm-search-widget .cm-search-widget-count {
+ align-items: center;
+ bottom: 0;
+ color: #888;
+ display: none;
+ margin-right: 4px;
+ position: absolute;
+ right: 0;
+ top: 0;
+ }
+.cm-search-widget[data-query] .cm-search-widget-count {
+ display: flex;
+ }
+.cm-search-widget .cm-search-widget-svg {
+ height: 1.2em;
+ padding: 4px;
+ width: 1.2em;
+ }
+.cm-search-widget .cm-search-widget-arrow {
+ padding-left: 2px;
+ padding-right: 2px;
+ width: 1.4em;
+ }
+.cm-search-widget .cm-search-widget-svg:hover {
+ background-color: #fff;
+ }
+.cm-search-widget svg {
+ height: 100%;
+ pointer-events: none;
+ width: 100%;
+ }
diff --git a/src/js/1p-filters.js b/src/js/1p-filters.js
index 939a7a951..8e709f955 100644
--- a/src/js/1p-filters.js
+++ b/src/js/1p-filters.js
@@ -35,6 +35,7 @@ var cachedUserFilters = '';
var cmEditor = new CodeMirror(
document.getElementById('userFilters'),
{
+ autofocus: true,
lineNumbers: true,
lineWrapping: true,
}
diff --git a/src/js/asset-viewer.js b/src/js/asset-viewer.js
index 33a1cf3dc..a10502867 100644
--- a/src/js/asset-viewer.js
+++ b/src/js/asset-viewer.js
@@ -45,6 +45,7 @@
var cmEditor = new CodeMirror(
document.getElementById('content'),
{
+ autofocus: true,
lineNumbers: true,
lineWrapping: true,
readOnly: true
diff --git a/src/js/codemirror/search.js b/src/js/codemirror/search.js
new file mode 100644
index 000000000..13f2fe7ab
--- /dev/null
+++ b/src/js/codemirror/search.js
@@ -0,0 +1,316 @@
+// 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 =
+ '';
+
+ 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);
+ } else if ( tcl.contains('cm-search-widget-close') ) {
+ clearSearch(cm, true);
+ cm.focus();
+ }
+ }
+
+ 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.posFrom = this.posTo = null;
+ 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('click', 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)
+ );
+ state.widget
+ .querySelector('.cm-search-widget-count > span:nth-of-type(2)')
+ .textContent = state.annotate.matches.length;
+ state.widget.setAttribute('data-query', state.queryText);
+ }
+ }
+
+ function findNext(cm, rev, callback) {
+ cm.operation(function() {
+ var state = getSearchState(cm);
+ var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
+ 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);
+ state.posFrom = cursor.from(); state.posTo = cursor.to();
+ 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);
+ state.posFrom = state.posTo = cm.getCursor();
+ 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;
+});
diff --git a/src/lib/codemirror/addon/mode/ubo-static-filtering.js b/src/js/codemirror/ubo-static-filtering.js
similarity index 100%
rename from src/lib/codemirror/addon/mode/ubo-static-filtering.js
rename to src/js/codemirror/ubo-static-filtering.js
diff --git a/src/lib/codemirror/addon/dialog/dialog.css b/src/lib/codemirror/addon/dialog/dialog.css
deleted file mode 100644
index 677c07838..000000000
--- a/src/lib/codemirror/addon/dialog/dialog.css
+++ /dev/null
@@ -1,32 +0,0 @@
-.CodeMirror-dialog {
- position: absolute;
- left: 0; right: 0;
- background: inherit;
- z-index: 15;
- padding: .1em .8em;
- overflow: hidden;
- color: inherit;
-}
-
-.CodeMirror-dialog-top {
- border-bottom: 1px solid #eee;
- top: 0;
-}
-
-.CodeMirror-dialog-bottom {
- border-top: 1px solid #eee;
- bottom: 0;
-}
-
-.CodeMirror-dialog input {
- border: none;
- outline: none;
- background: transparent;
- width: 20em;
- color: inherit;
- font-family: monospace;
-}
-
-.CodeMirror-dialog button {
- font-size: 70%;
-}
diff --git a/src/lib/codemirror/addon/dialog/dialog.js b/src/lib/codemirror/addon/dialog/dialog.js
deleted file mode 100644
index f10bb5bf1..000000000
--- a/src/lib/codemirror/addon/dialog/dialog.js
+++ /dev/null
@@ -1,157 +0,0 @@
-// CodeMirror, copyright (c) by Marijn Haverbeke and others
-// Distributed under an MIT license: http://codemirror.net/LICENSE
-
-// Open simple dialogs on top of an editor. Relies on dialog.css.
-
-(function(mod) {
- if (typeof exports == "object" && typeof module == "object") // CommonJS
- mod(require("../../lib/codemirror"));
- else if (typeof define == "function" && define.amd) // AMD
- define(["../../lib/codemirror"], mod);
- else // Plain browser env
- mod(CodeMirror);
-})(function(CodeMirror) {
- function dialogDiv(cm, template, bottom) {
- var wrap = cm.getWrapperElement();
- var dialog;
- dialog = wrap.appendChild(document.createElement("div"));
- if (bottom)
- dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom";
- else
- dialog.className = "CodeMirror-dialog CodeMirror-dialog-top";
-
- if (typeof template == "string") {
- dialog.innerHTML = template;
- } else { // Assuming it's a detached DOM element.
- dialog.appendChild(template);
- }
- return dialog;
- }
-
- function closeNotification(cm, newVal) {
- if (cm.state.currentNotificationClose)
- cm.state.currentNotificationClose();
- cm.state.currentNotificationClose = newVal;
- }
-
- CodeMirror.defineExtension("openDialog", function(template, callback, options) {
- if (!options) options = {};
-
- closeNotification(this, null);
-
- var dialog = dialogDiv(this, template, options.bottom);
- var closed = false, me = this;
- function close(newVal) {
- if (typeof newVal == 'string') {
- inp.value = newVal;
- } else {
- if (closed) return;
- closed = true;
- dialog.parentNode.removeChild(dialog);
- me.focus();
-
- if (options.onClose) options.onClose(dialog);
- }
- }
-
- var inp = dialog.getElementsByTagName("input")[0], button;
- if (inp) {
- inp.focus();
-
- if (options.value) {
- inp.value = options.value;
- if (options.selectValueOnOpen !== false) {
- inp.select();
- }
- }
-
- if (options.onInput)
- CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);});
- if (options.onKeyUp)
- CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);});
-
- CodeMirror.on(inp, "keydown", function(e) {
- if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; }
- if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) {
- inp.blur();
- CodeMirror.e_stop(e);
- close();
- }
- if (e.keyCode == 13) callback(inp.value, e);
- });
-
- if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close);
- } else if (button = dialog.getElementsByTagName("button")[0]) {
- CodeMirror.on(button, "click", function() {
- close();
- me.focus();
- });
-
- if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close);
-
- button.focus();
- }
- return close;
- });
-
- CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) {
- closeNotification(this, null);
- var dialog = dialogDiv(this, template, options && options.bottom);
- var buttons = dialog.getElementsByTagName("button");
- var closed = false, me = this, blurring = 1;
- function close() {
- if (closed) return;
- closed = true;
- dialog.parentNode.removeChild(dialog);
- me.focus();
- }
- buttons[0].focus();
- for (var i = 0; i < buttons.length; ++i) {
- var b = buttons[i];
- (function(callback) {
- CodeMirror.on(b, "click", function(e) {
- CodeMirror.e_preventDefault(e);
- close();
- if (callback) callback(me);
- });
- })(callbacks[i]);
- CodeMirror.on(b, "blur", function() {
- --blurring;
- setTimeout(function() { if (blurring <= 0) close(); }, 200);
- });
- CodeMirror.on(b, "focus", function() { ++blurring; });
- }
- });
-
- /*
- * openNotification
- * Opens a notification, that can be closed with an optional timer
- * (default 5000ms timer) and always closes on click.
- *
- * If a notification is opened while another is opened, it will close the
- * currently opened one and open the new one immediately.
- */
- CodeMirror.defineExtension("openNotification", function(template, options) {
- closeNotification(this, close);
- var dialog = dialogDiv(this, template, options && options.bottom);
- var closed = false, doneTimer;
- var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000;
-
- function close() {
- if (closed) return;
- closed = true;
- clearTimeout(doneTimer);
- dialog.parentNode.removeChild(dialog);
- }
-
- CodeMirror.on(dialog, 'click', function(e) {
- CodeMirror.e_preventDefault(e);
- close();
- });
-
- if (duration)
- doneTimer = setTimeout(close, duration);
-
- return close;
- });
-});
diff --git a/src/lib/codemirror/addon/display/panel.js b/src/lib/codemirror/addon/display/panel.js
new file mode 100644
index 000000000..f88d152b8
--- /dev/null
+++ b/src/lib/codemirror/addon/display/panel.js
@@ -0,0 +1,123 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ CodeMirror.defineExtension("addPanel", function(node, options) {
+ options = options || {};
+
+ if (!this.state.panels) initPanels(this);
+
+ var info = this.state.panels;
+ var wrapper = info.wrapper;
+ var cmWrapper = this.getWrapperElement();
+
+ if (options.after instanceof Panel && !options.after.cleared) {
+ wrapper.insertBefore(node, options.before.node.nextSibling);
+ } else if (options.before instanceof Panel && !options.before.cleared) {
+ wrapper.insertBefore(node, options.before.node);
+ } else if (options.replace instanceof Panel && !options.replace.cleared) {
+ wrapper.insertBefore(node, options.replace.node);
+ options.replace.clear();
+ } else if (options.position == "bottom") {
+ wrapper.appendChild(node);
+ } else if (options.position == "before-bottom") {
+ wrapper.insertBefore(node, cmWrapper.nextSibling);
+ } else if (options.position == "after-top") {
+ wrapper.insertBefore(node, cmWrapper);
+ } else {
+ wrapper.insertBefore(node, wrapper.firstChild);
+ }
+
+ var height = (options && options.height) || node.offsetHeight;
+ this._setSize(null, info.heightLeft -= height);
+ info.panels++;
+ if (options.stable && isAtTop(this, node))
+ this.scrollTo(null, this.getScrollInfo().top + height)
+
+ return new Panel(this, node, options, height);
+ });
+
+ function Panel(cm, node, options, height) {
+ this.cm = cm;
+ this.node = node;
+ this.options = options;
+ this.height = height;
+ this.cleared = false;
+ }
+
+ Panel.prototype.clear = function() {
+ if (this.cleared) return;
+ this.cleared = true;
+ var info = this.cm.state.panels;
+ this.cm._setSize(null, info.heightLeft += this.height);
+ if (this.options.stable && isAtTop(this.cm, this.node))
+ this.cm.scrollTo(null, this.cm.getScrollInfo().top - this.height)
+ info.wrapper.removeChild(this.node);
+ if (--info.panels == 0) removePanels(this.cm);
+ };
+
+ Panel.prototype.changed = function(height) {
+ var newHeight = height == null ? this.node.offsetHeight : height;
+ var info = this.cm.state.panels;
+ this.cm._setSize(null, info.heightLeft -= (newHeight - this.height));
+ this.height = newHeight;
+ };
+
+ function initPanels(cm) {
+ var wrap = cm.getWrapperElement();
+ var style = window.getComputedStyle ? window.getComputedStyle(wrap) : wrap.currentStyle;
+ var height = parseInt(style.height);
+ var info = cm.state.panels = {
+ setHeight: wrap.style.height,
+ heightLeft: height,
+ panels: 0,
+ wrapper: document.createElement("div")
+ };
+ wrap.parentNode.insertBefore(info.wrapper, wrap);
+ var hasFocus = cm.hasFocus();
+ info.wrapper.appendChild(wrap);
+ if (hasFocus) cm.focus();
+
+ cm._setSize = cm.setSize;
+ if (height != null) cm.setSize = function(width, newHeight) {
+ if (newHeight == null) return this._setSize(width, newHeight);
+ info.setHeight = newHeight;
+ if (typeof newHeight != "number") {
+ var px = /^(\d+\.?\d*)px$/.exec(newHeight);
+ if (px) {
+ newHeight = Number(px[1]);
+ } else {
+ info.wrapper.style.height = newHeight;
+ newHeight = info.wrapper.offsetHeight;
+ info.wrapper.style.height = "";
+ }
+ }
+ cm._setSize(width, info.heightLeft += (newHeight - height));
+ height = newHeight;
+ };
+ }
+
+ function removePanels(cm) {
+ var info = cm.state.panels;
+ cm.state.panels = null;
+
+ var wrap = cm.getWrapperElement();
+ info.wrapper.parentNode.replaceChild(wrap, info.wrapper);
+ wrap.style.height = info.setHeight;
+ cm.setSize = cm._setSize;
+ cm.setSize();
+ }
+
+ function isAtTop(cm, dom) {
+ for (var sibling = dom.nextSibling; sibling; sibling = sibling.nextSibling)
+ if (sibling == cm.getWrapperElement()) return true
+ return false
+ }
+});
diff --git a/src/lib/codemirror/addon/scroll/annotatescrollbar.js b/src/lib/codemirror/addon/scroll/annotatescrollbar.js
new file mode 100644
index 000000000..f2276fc79
--- /dev/null
+++ b/src/lib/codemirror/addon/scroll/annotatescrollbar.js
@@ -0,0 +1,122 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ CodeMirror.defineExtension("annotateScrollbar", function(options) {
+ if (typeof options == "string") options = {className: options};
+ return new Annotation(this, options);
+ });
+
+ CodeMirror.defineOption("scrollButtonHeight", 0);
+
+ function Annotation(cm, options) {
+ this.cm = cm;
+ this.options = options;
+ this.buttonHeight = options.scrollButtonHeight || cm.getOption("scrollButtonHeight");
+ this.annotations = [];
+ this.doRedraw = this.doUpdate = null;
+ this.div = cm.getWrapperElement().appendChild(document.createElement("div"));
+ this.div.style.cssText = "position: absolute; right: 0; top: 0; z-index: 7; pointer-events: none";
+ this.computeScale();
+
+ function scheduleRedraw(delay) {
+ clearTimeout(self.doRedraw);
+ self.doRedraw = setTimeout(function() { self.redraw(); }, delay);
+ }
+
+ var self = this;
+ cm.on("refresh", this.resizeHandler = function() {
+ clearTimeout(self.doUpdate);
+ self.doUpdate = setTimeout(function() {
+ if (self.computeScale()) scheduleRedraw(20);
+ }, 100);
+ });
+ cm.on("markerAdded", this.resizeHandler);
+ cm.on("markerCleared", this.resizeHandler);
+ if (options.listenForChanges !== false)
+ cm.on("change", this.changeHandler = function() {
+ scheduleRedraw(250);
+ });
+ }
+
+ Annotation.prototype.computeScale = function() {
+ var cm = this.cm;
+ var hScale = (cm.getWrapperElement().clientHeight - cm.display.barHeight - this.buttonHeight * 2) /
+ cm.getScrollerElement().scrollHeight
+ if (hScale != this.hScale) {
+ this.hScale = hScale;
+ return true;
+ }
+ };
+
+ Annotation.prototype.update = function(annotations) {
+ this.annotations = annotations;
+ this.redraw();
+ };
+
+ Annotation.prototype.redraw = function(compute) {
+ if (compute !== false) this.computeScale();
+ var cm = this.cm, hScale = this.hScale;
+
+ var frag = document.createDocumentFragment(), anns = this.annotations;
+
+ var wrapping = cm.getOption("lineWrapping");
+ var singleLineH = wrapping && cm.defaultTextHeight() * 1.5;
+ var curLine = null, curLineObj = null;
+ function getY(pos, top) {
+ if (curLine != pos.line) {
+ curLine = pos.line;
+ curLineObj = cm.getLineHandle(curLine);
+ }
+ if ((curLineObj.widgets && curLineObj.widgets.length) ||
+ (wrapping && curLineObj.height > singleLineH))
+ return cm.charCoords(pos, "local")[top ? "top" : "bottom"];
+ var topY = cm.heightAtLine(curLineObj, "local");
+ return topY + (top ? 0 : curLineObj.height);
+ }
+
+ var lastLine = cm.lastLine()
+ if (cm.display.barWidth) for (var i = 0, nextTop; i < anns.length; i++) {
+ var ann = anns[i];
+ if (ann.to.line > lastLine) continue;
+ var top = nextTop || getY(ann.from, true) * hScale;
+ var bottom = getY(ann.to, false) * hScale;
+ while (i < anns.length - 1) {
+ if (anns[i + 1].to.line > lastLine) break;
+ nextTop = getY(anns[i + 1].from, true) * hScale;
+ if (nextTop > bottom + .9) break;
+ ann = anns[++i];
+ bottom = getY(ann.to, false) * hScale;
+ }
+ if (bottom == top) continue;
+ var height = Math.max(bottom - top, 3);
+
+ var elt = frag.appendChild(document.createElement("div"));
+ elt.style.cssText = "position: absolute; right: 0px; width: " + Math.max(cm.display.barWidth - 1, 2) + "px; top: "
+ + (top + this.buttonHeight) + "px; height: " + height + "px";
+ elt.className = this.options.className;
+ if (ann.id) {
+ elt.setAttribute("annotation-id", ann.id);
+ }
+ }
+ this.div.textContent = "";
+ this.div.appendChild(frag);
+ };
+
+ Annotation.prototype.clear = function() {
+ this.cm.off("refresh", this.resizeHandler);
+ this.cm.off("markerAdded", this.resizeHandler);
+ this.cm.off("markerCleared", this.resizeHandler);
+ if (this.changeHandler) this.cm.off("change", this.changeHandler);
+ this.div.parentNode.removeChild(this.div);
+ };
+});
diff --git a/src/lib/codemirror/addon/search/matchesonscrollbar.css b/src/lib/codemirror/addon/search/matchesonscrollbar.css
new file mode 100644
index 000000000..77932cc90
--- /dev/null
+++ b/src/lib/codemirror/addon/search/matchesonscrollbar.css
@@ -0,0 +1,8 @@
+.CodeMirror-search-match {
+ background: gold;
+ border-top: 1px solid orange;
+ border-bottom: 1px solid orange;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ opacity: .5;
+}
diff --git a/src/lib/codemirror/addon/search/matchesonscrollbar.js b/src/lib/codemirror/addon/search/matchesonscrollbar.js
new file mode 100644
index 000000000..8d1922897
--- /dev/null
+++ b/src/lib/codemirror/addon/search/matchesonscrollbar.js
@@ -0,0 +1,97 @@
+// CodeMirror, copyright (c) by Marijn Haverbeke and others
+// Distributed under an MIT license: http://codemirror.net/LICENSE
+
+(function(mod) {
+ if (typeof exports == "object" && typeof module == "object") // CommonJS
+ mod(require("../../lib/codemirror"), require("./searchcursor"), require("../scroll/annotatescrollbar"));
+ else if (typeof define == "function" && define.amd) // AMD
+ define(["../../lib/codemirror", "./searchcursor", "../scroll/annotatescrollbar"], mod);
+ else // Plain browser env
+ mod(CodeMirror);
+})(function(CodeMirror) {
+ "use strict";
+
+ CodeMirror.defineExtension("showMatchesOnScrollbar", function(query, caseFold, options) {
+ if (typeof options == "string") options = {className: options};
+ if (!options) options = {};
+ return new SearchAnnotation(this, query, caseFold, options);
+ });
+
+ function SearchAnnotation(cm, query, caseFold, options) {
+ this.cm = cm;
+ this.options = options;
+ var annotateOptions = {listenForChanges: false};
+ for (var prop in options) annotateOptions[prop] = options[prop];
+ if (!annotateOptions.className) annotateOptions.className = "CodeMirror-search-match";
+ this.annotation = cm.annotateScrollbar(annotateOptions);
+ this.query = query;
+ this.caseFold = caseFold;
+ this.gap = {from: cm.firstLine(), to: cm.lastLine() + 1};
+ this.matches = [];
+ this.update = null;
+
+ this.findMatches();
+ this.annotation.update(this.matches);
+
+ var self = this;
+ cm.on("change", this.changeHandler = function(_cm, change) { self.onChange(change); });
+ }
+
+ var MAX_MATCHES = 1000;
+
+ SearchAnnotation.prototype.findMatches = function() {
+ if (!this.gap) return;
+ for (var i = 0; i < this.matches.length; i++) {
+ var match = this.matches[i];
+ if (match.from.line >= this.gap.to) break;
+ if (match.to.line >= this.gap.from) this.matches.splice(i--, 1);
+ }
+ var cursor = this.cm.getSearchCursor(this.query, CodeMirror.Pos(this.gap.from, 0), this.caseFold);
+ var maxMatches = this.options && this.options.maxMatches || MAX_MATCHES;
+ while (cursor.findNext()) {
+ var match = {from: cursor.from(), to: cursor.to()};
+ if (match.from.line >= this.gap.to) break;
+ this.matches.splice(i++, 0, match);
+ if (this.matches.length > maxMatches) break;
+ }
+ this.gap = null;
+ };
+
+ function offsetLine(line, changeStart, sizeChange) {
+ if (line <= changeStart) return line;
+ return Math.max(changeStart, line + sizeChange);
+ }
+
+ SearchAnnotation.prototype.onChange = function(change) {
+ var startLine = change.from.line;
+ var endLine = CodeMirror.changeEnd(change).line;
+ var sizeChange = endLine - change.to.line;
+ if (this.gap) {
+ this.gap.from = Math.min(offsetLine(this.gap.from, startLine, sizeChange), change.from.line);
+ this.gap.to = Math.max(offsetLine(this.gap.to, startLine, sizeChange), change.from.line);
+ } else {
+ this.gap = {from: change.from.line, to: endLine + 1};
+ }
+
+ if (sizeChange) for (var i = 0; i < this.matches.length; i++) {
+ var match = this.matches[i];
+ var newFrom = offsetLine(match.from.line, startLine, sizeChange);
+ if (newFrom != match.from.line) match.from = CodeMirror.Pos(newFrom, match.from.ch);
+ var newTo = offsetLine(match.to.line, startLine, sizeChange);
+ if (newTo != match.to.line) match.to = CodeMirror.Pos(newTo, match.to.ch);
+ }
+ clearTimeout(this.update);
+ var self = this;
+ this.update = setTimeout(function() { self.updateAfterChange(); }, 250);
+ };
+
+ SearchAnnotation.prototype.updateAfterChange = function() {
+ this.findMatches();
+ this.annotation.update(this.matches);
+ };
+
+ SearchAnnotation.prototype.clear = function() {
+ this.cm.off("change", this.changeHandler);
+ this.annotation.clear();
+ };
+});
diff --git a/src/lib/codemirror/addon/search/search.js b/src/lib/codemirror/addon/search/search.js
deleted file mode 100644
index 4059ccdd0..000000000
--- a/src/lib/codemirror/addon/search/search.js
+++ /dev/null
@@ -1,252 +0,0 @@
-// 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.
-
-(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) {
- "use strict";
-
- 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();
- }
- }};
- }
-
- function SearchState() {
- this.posFrom = this.posTo = this.lastQuery = this.query = null;
- this.overlay = null;
- }
-
- function getSearchState(cm) {
- return cm.state.search || (cm.state.search = new SearchState());
- }
-
- 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 persistentDialog(cm, text, deflt, onEnter, onKeyDown) {
- cm.openDialog(text, onEnter, {
- value: deflt,
- selectValueOnOpen: true,
- closeOnEnter: false,
- onClose: function() { clearSearch(cm); },
- onKeyDown: onKeyDown
- });
- }
-
- function dialog(cm, text, shortText, deflt, f) {
- if (cm.openDialog) cm.openDialog(text, f, {value: deflt, selectValueOnOpen: true});
- else f(prompt(shortText, deflt));
- }
-
- function confirmDialog(cm, text, shortText, fs) {
- if (cm.openConfirm) cm.openConfirm(text, fs);
- else if (confirm(shortText)) fs[0]();
- }
-
- 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;
- }
-
- var queryDialog =
- 'Search: (Use /re/ syntax for regexp search)';
-
- function startSearch(cm, state, query) {
- state.queryText = query;
- state.query = parseQuery(query);
- 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));
- }
- }
-
- function doSearch(cm, rev, persistent, immediate) {
- var state = getSearchState(cm);
- if (state.query) return findNext(cm, rev);
- var q = cm.getSelection() || state.lastQuery;
- if (q instanceof RegExp && q.source == "x^") q = null
- if (persistent && cm.openDialog) {
- var hiding = null
- var searchNext = function(query, event) {
- CodeMirror.e_stop(event);
- if (!query) return;
- if (query != state.queryText) {
- startSearch(cm, state, query);
- state.posFrom = state.posTo = cm.getCursor();
- }
- if (hiding) hiding.style.opacity = 1
- findNext(cm, event.shiftKey, function(_, to) {
- var dialog
- if (to.line < 3 && document.querySelector &&
- (dialog = cm.display.wrapper.querySelector(".CodeMirror-dialog")) &&
- dialog.getBoundingClientRect().bottom - 4 > cm.cursorCoords(to, "window").top)
- (hiding = dialog).style.opacity = .4
- })
- };
- persistentDialog(cm, queryDialog, q, searchNext, function(event, query) {
- var keyName = CodeMirror.keyName(event)
- var extra = cm.getOption('extraKeys'), cmd = (extra && extra[keyName]) || CodeMirror.keyMap[cm.getOption("keyMap")][keyName]
- if (cmd == "findNext" || cmd == "findPrev" ||
- cmd == "findPersistentNext" || cmd == "findPersistentPrev") {
- CodeMirror.e_stop(event);
- startSearch(cm, getSearchState(cm), query);
- cm.execCommand(cmd);
- } else if (cmd == "find" || cmd == "findPersistent") {
- CodeMirror.e_stop(event);
- searchNext(query, event);
- }
- });
- if (immediate && q) {
- startSearch(cm, state, q);
- findNext(cm, rev);
- }
- } else {
- dialog(cm, queryDialog, "Search for:", q, function(query) {
- if (query && !state.query) cm.operation(function() {
- startSearch(cm, state, query);
- state.posFrom = state.posTo = cm.getCursor();
- findNext(cm, rev);
- });
- });
- }
- }
-
- function findNext(cm, rev, callback) {cm.operation(function() {
- var state = getSearchState(cm);
- var cursor = getSearchCursor(cm, state.query, rev ? state.posFrom : state.posTo);
- 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);
- state.posFrom = cursor.from(); state.posTo = cursor.to();
- if (callback) callback(cursor.from(), cursor.to())
- });}
-
- function clearSearch(cm) {cm.operation(function() {
- var state = getSearchState(cm);
- state.lastQuery = state.query;
- if (!state.query) return;
- state.query = state.queryText = null;
- cm.removeOverlay(state.overlay);
- if (state.annotate) { state.annotate.clear(); state.annotate = null; }
- });}
-
- var replaceQueryDialog =
- ' (Use /re/ syntax for regexp search)';
- var replacementQueryDialog = 'With: ';
- var doReplaceConfirm = 'Replace? ';
-
- function replaceAll(cm, query, text) {
- cm.operation(function() {
- for (var cursor = getSearchCursor(cm, query); cursor.findNext();) {
- if (typeof query != "string") {
- var match = cm.getRange(cursor.from(), cursor.to()).match(query);
- cursor.replace(text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
- } else cursor.replace(text);
- }
- });
- }
-
- function replace(cm, all) {
- if (cm.getOption("readOnly")) return;
- var query = cm.getSelection() || getSearchState(cm).lastQuery;
- var dialogText = '' + (all ? 'Replace all:' : 'Replace:') + '';
- dialog(cm, dialogText + replaceQueryDialog, dialogText, query, function(query) {
- if (!query) return;
- query = parseQuery(query);
- dialog(cm, replacementQueryDialog, "Replace with:", "", function(text) {
- text = parseString(text)
- if (all) {
- replaceAll(cm, query, text)
- } else {
- clearSearch(cm);
- var cursor = getSearchCursor(cm, query, cm.getCursor("from"));
- var advance = function() {
- var start = cursor.from(), match;
- if (!(match = cursor.findNext())) {
- cursor = getSearchCursor(cm, query);
- if (!(match = cursor.findNext()) ||
- (start && cursor.from().line == start.line && cursor.from().ch == start.ch)) return;
- }
- cm.setSelection(cursor.from(), cursor.to());
- cm.scrollIntoView({from: cursor.from(), to: cursor.to()});
- confirmDialog(cm, doReplaceConfirm, "Replace?",
- [function() {doReplace(match);}, advance,
- function() {replaceAll(cm, query, text)}]);
- };
- var doReplace = function(match) {
- cursor.replace(typeof query == "string" ? text :
- text.replace(/\$(\d)/g, function(_, i) {return match[i];}));
- advance();
- };
- advance();
- }
- });
- });
- }
-
- CodeMirror.commands.find = function(cm) {clearSearch(cm); doSearch(cm);};
- CodeMirror.commands.findPersistent = function(cm) {clearSearch(cm); doSearch(cm, false, true);};
- CodeMirror.commands.findPersistentNext = function(cm) {doSearch(cm, false, true, true);};
- CodeMirror.commands.findPersistentPrev = function(cm) {doSearch(cm, true, true, true);};
- CodeMirror.commands.findNext = doSearch;
- CodeMirror.commands.findPrev = function(cm) {doSearch(cm, true);};
- CodeMirror.commands.clearSearch = clearSearch;
- CodeMirror.commands.replace = replace;
- CodeMirror.commands.replaceAll = function(cm) {replace(cm, true);};
-});