diff --git a/edit.html b/edit.html
index c064d89f..da439c2b 100644
--- a/edit.html
+++ b/edit.html
@@ -31,7 +31,6 @@
-
@@ -62,6 +61,8 @@
+
+
@@ -77,8 +78,6 @@
-
-
diff --git a/edit/codemirror-factory.js b/edit/codemirror-factory.js
index 68996bb9..887e6c08 100644
--- a/edit/codemirror-factory.js
+++ b/edit/codemirror-factory.js
@@ -32,12 +32,14 @@ const cmFactory = (() => {
if (value === 'token') {
cm.setOption('highlightSelectionMatches', {
showToken: /[#.\-\w]/,
- annotateScrollbar: true
+ annotateScrollbar: true,
+ onUpdate: updateMatchHighlightCount
});
} else if (value === 'selection') {
cm.setOption('highlightSelectionMatches', {
showToken: false,
- annotateScrollbar: true
+ annotateScrollbar: true,
+ onUpdate: updateMatchHighlightCount
});
} else {
cm.setOption('highlightSelectionMatches', null);
@@ -80,6 +82,10 @@ const cmFactory = (() => {
});
return {create, destroy, setOption};
+ function updateMatchHighlightCount(cm, state) {
+ cm.display.wrapper.dataset.matchHighlightCount = state.matchesonscroll.matches.length;
+ }
+
function configureMouseFn(cm, repeat) {
return repeat === 'double' ?
{unit: selectTokenOnDoubleclick} :
diff --git a/edit/edit.css b/edit/edit.css
index 907cac06..22d9a110 100644
--- a/edit/edit.css
+++ b/edit/edit.css
@@ -364,14 +364,14 @@ input:invalid {
.resize-grip-enabled .CodeMirror-scrollbar-filler {
bottom: 7px; /* make space for resize-grip */
}
-body[data-match-highlight="token"] .cm-matchhighlight-approved .cm-matchhighlight,
-body[data-match-highlight="token"] .CodeMirror-selection-highlight-scrollbar {
+.cm-matchhighlight,
+.CodeMirror-selection-highlight-scrollbar {
animation: fadein-match-highlighter 1s cubic-bezier(.97,.01,.42,.98);
animation-fill-mode: both;
}
-body[data-match-highlight="selection"] .cm-matchhighlight-approved .cm-matchhighlight,
-body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar {
- background-color: rgba(1, 151, 193, 0.1);
+[data-match-highlight-count="1"] .cm-matchhighlight,
+[data-match-highlight-count="1"] .CodeMirror-selection-highlight-scrollbar {
+ animation: none;
}
@-webkit-keyframes highlight {
from {
diff --git a/edit/match-highlighter-helper.js b/edit/match-highlighter-helper.js
deleted file mode 100644
index 8396a7ed..00000000
--- a/edit/match-highlighter-helper.js
+++ /dev/null
@@ -1,223 +0,0 @@
-/* global CodeMirror prefs */
-'use strict';
-
-(() => {
- /*
- The original match-highlighter addon always recreates the highlight overlay
- even if the token under cursor hasn't changed, which is terribly ineffective
- (the entire view is re-rendered) and makes our animated token highlight effect
- restart on every cursor movement.
-
- Invocation sequence of our hooks:
-
- 1. removeOverlayForHighlighter()
- The original addon removes the overlay unconditionally
- so this hook saves the state if the token hasn't changed.
-
- 2. addOverlayForHighlighter()
- Restores the saved state instead of creating a new overlay,
- installs a hook to count occurrences.
-
- 3. matchesOnScrollbar()
- Saves the query regexp passed from the original addon in our helper object,
- and in case removeOverlayForHighlighter() decided to keep the overlay
- only rewrites the regexp without invoking the original constructor.
- */
-
- const HL_APPROVED = 'cm-matchhighlight-approved';
- const SEARCH_MATCH_TOKEN_NAME = 'searching';
-
- const originalAddOverlay = CodeMirror.prototype.addOverlay;
- const originalRemoveOverlay = CodeMirror.prototype.removeOverlay;
- const originalMatchesOnScrollbar = CodeMirror.prototype.showMatchesOnScrollbar;
- const originalSetOption = CodeMirror.prototype.setOption;
- let originalGetOption;
-
- CodeMirror.prototype.addOverlay = addOverlay;
- CodeMirror.prototype.removeOverlay = removeOverlay;
- CodeMirror.prototype.showMatchesOnScrollbar = matchesOnScrollbar;
- CodeMirror.prototype.setOption = setOption;
-
- let enabled = Boolean(prefs.get('editor.matchHighlight'));
-
- return;
-
- function setOption(option, value) {
- enabled = option === 'highlightSelectionMatches' ? value : enabled;
- return originalSetOption.apply(this, arguments);
- }
-
- function shouldIntercept(overlay) {
- const hlState = this.state.matchHighlighter || {};
- return overlay === hlState.overlay && (hlState.options || {}).showToken;
- }
-
- function addOverlay() {
- return enabled && shouldIntercept.apply(this, arguments) &&
- addOverlayForHighlighter.apply(this, arguments) ||
- originalAddOverlay.apply(this, arguments);
- }
-
- function removeOverlay() {
- return enabled && shouldIntercept.apply(this, arguments) &&
- removeOverlayForHighlighter.apply(this, arguments) ||
- originalRemoveOverlay.apply(this, arguments);
- }
-
- function addOverlayForHighlighter(overlay) {
- const state = this.state.matchHighlighter || {};
- const helper = state.highlightHelper = state.highlightHelper || {};
-
- helper.rewriteScrollbarQuery = true;
-
- // since we're here the original addon decided there's something to highlight,
- // so we cancel removeOverlayIfExpired() scheduled in our removeOverlay hook
- clearTimeout(helper.hookTimer);
-
- // the original addon just removed its overlays, which was intercepted by removeOverlayForHighlighter,
- // which decided to restore it and saved the previous overlays in our helper object,
- // so here we are now, restoring them
- if (helper.skipMatchesOnScrollbar) {
- state.matchesonscroll = helper.matchesonscroll;
- state.overlay = helper.overlay;
- return true;
- }
-
- // hook the newly created overlay's token() to count the occurrences
- if (overlay.token !== tokenHook) {
- overlay.highlightHelper = {
- token: overlay.token,
- occurrences: 0,
- };
- overlay.token = tokenHook;
- }
-
- // speed up rendering of scrollbar marks 4 times: we don't need ultimate precision there
- // so for the duration of this event loop cycle we spoof the "lineWrapping" option
- // and restore it in the next event loop cycle
- if (this.options.lineWrapping && CodeMirror.prototype.getOption !== spoofLineWrappingOption) {
- originalGetOption = CodeMirror.prototype.getOption;
- CodeMirror.prototype.getOption = spoofLineWrappingOption;
- setTimeout(() => (CodeMirror.prototype.getOption = originalGetOption));
- }
- }
-
- function spoofLineWrappingOption(option) {
- return option !== 'lineWrapping' && originalGetOption.apply(this, arguments);
- }
-
- function tokenHook(stream) {
- // we don't highlight a single match in case 'editor.matchHighlight' option is 'token'
- // so this hook counts the occurrences and toggles HL_APPROVED class on CM's wrapper element
- const style = this.highlightHelper.token.call(this, stream);
- if (style !== 'matchhighlight') {
- return style;
- }
-
- const tokens = stream.lineOracle.baseTokens;
- const tokenIndex = tokens.indexOf(stream.pos, 1);
- if (tokenIndex > 0) {
- const tokenStart = tokenIndex > 2 ? tokens[tokenIndex - 2] : 0;
- const token = tokenStart === stream.start && tokens[tokenIndex + 1];
- const index = token && token.indexOf(SEARCH_MATCH_TOKEN_NAME);
- if (token && index >= 0 &&
- (token[index - 1] || ' ') === ' ' &&
- (token[index + SEARCH_MATCH_TOKEN_NAME.length] || ' ') === ' ') {
- return;
- }
- }
-
- const num = ++this.highlightHelper.occurrences;
- if (num === 1) {
- stream.lineOracle.doc.cm.display.wrapper.classList.remove(HL_APPROVED);
- } else if (num === 2) {
- stream.lineOracle.doc.cm.display.wrapper.classList.add(HL_APPROVED);
- }
- return style;
- }
-
- function removeOverlayForHighlighter() {
- const state = this.state.matchHighlighter || {};
- const helper = state.highlightHelper;
- const {query, originalToken} = helper || state.matchesonscroll || {};
- // no current query means nothing to preserve => remove the overlay
- if (!query || !originalToken) {
- return;
- }
- const sel = this.getSelection();
- // current query differs from the selected text => remove the overlay
- if (sel && sel.toLowerCase() !== originalToken.toLowerCase()) {
- helper.query = helper.originalToken = sel;
- return;
- }
- // if token under cursor has changed => remove the overlay
- if (!sel) {
- const {line, ch} = this.getCursor();
- const queryLen = originalToken.length;
- const start = Math.max(0, ch - queryLen + 1);
- const end = ch + queryLen;
- const string = this.getLine(line);
- const area = string.slice(start, end);
- const i = area.indexOf(query);
- const startInArea = i < 0 ? NaN : i;
- if (isNaN(startInArea) || start + startInArea > ch ||
- state.options.showToken.test(string[start + startInArea - 1] || '') ||
- state.options.showToken.test(string[start + startInArea + queryLen] || '')) {
- // pass the displayed instance back to the original code to remove it
- state.matchesonscroll = state.matchesonscroll || helper && helper.matchesonscroll;
- return;
- }
- }
- // since the same token is under cursor we prevent the highlighter from rerunning
- // by saving current overlays in a helper object so that it's restored in addOverlayForHighlighter()
- state.highlightHelper = {
- overlay: state.overlay,
- matchesonscroll: state.matchesonscroll || (helper || {}).matchesonscroll,
- // instruct our matchesOnScrollbar hook to preserve current scrollbar annotations
- skipMatchesOnScrollbar: true,
- // in case the original addon won't highlight anything we need to actually remove the overlays
- // by setting a timer that runs in the next event loop cycle and can be canceled in this cycle
- hookTimer: setTimeout(removeOverlayIfExpired, 0, this, state),
- originalToken,
- query,
- };
- // fool the original addon so it won't invoke state.matchesonscroll.clear()
- state.matchesonscroll = null;
- return true;
- }
-
- function removeOverlayIfExpired(self, state) {
- const {overlay, matchesonscroll} = state.highlightHelper || {};
- if (overlay) {
- originalRemoveOverlay.call(self, overlay);
- }
- if (matchesonscroll) {
- matchesonscroll.clear();
- }
- state.highlightHelper = null;
- }
-
- function matchesOnScrollbar(query, ...args) {
- if (!enabled) {
- return originalMatchesOnScrollbar.call(this, query, ...args);
- }
- const state = this.state.matchHighlighter;
- const helper = state.highlightHelper = state.highlightHelper || {};
- // rewrite the \btoken\b regexp so it matches .token and #token and --token
- if (helper.rewriteScrollbarQuery && /^\\b.*?\\b$/.test(query.source)) {
- helper.rewriteScrollbarQuery = undefined;
- helper.originalToken = query.source.slice(2, -2);
- const notToken = '(?!' + state.options.showToken.source + ').';
- query = new RegExp(`(^|${notToken})` + helper.originalToken + `(${notToken}|$)`);
- }
- // save the query for future use in removeOverlayForHighlighter
- helper.query = query;
- // if removeOverlayForHighlighter() decided to keep the overlay
- if (helper.skipMatchesOnScrollbar) {
- helper.skipMatchesOnScrollbar = undefined;
- return;
- } else {
- return originalMatchesOnScrollbar.call(this, query, ...args);
- }
- }
-})();
diff --git a/vendor/codemirror/addon/search/match-highlighter.js b/vendor-overwrites/codemirror-addon/match-highlighter.js
similarity index 89%
rename from vendor/codemirror/addon/search/match-highlighter.js
rename to vendor-overwrites/codemirror-addon/match-highlighter.js
index b344ac79..cf2a53b0 100644
--- a/vendor/codemirror/addon/search/match-highlighter.js
+++ b/vendor-overwrites/codemirror-addon/match-highlighter.js
@@ -36,7 +36,8 @@
wordsOnly: false,
annotateScrollbar: false,
showToken: false,
- trim: true
+ trim: true,
+ onUpdate: () => {}
}
function State(options) {
@@ -46,6 +47,7 @@
this.overlay = this.timeout = null;
this.matchesonscroll = null;
this.active = false;
+ this.query = null;
}
CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
@@ -88,12 +90,24 @@
function addOverlay(cm, query, hasBoundary, style) {
var state = cm.state.matchHighlighter;
+ if (state.query === query) {
+ return;
+ }
+ removeOverlay(cm);
+ state.query = query;
cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
- var searchFor = hasBoundary ? new RegExp("\\b" + query.replace(/[\\\[.+*?(){|^$]/g, "\\$&") + "\\b") : query;
+ var searchFor = hasBoundary ?
+ new RegExp(
+ (/[a-z]/i.test(query[0]) ? "\\b" : "") +
+ query.replace(/[\\\[.+*?(){|^$]/g, "\\$&") +
+ (/[a-z]/i.test(query[query.length - 1]) ? "\\b" : ""),
+ "m"
+ ) : query;
state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
{className: "CodeMirror-selection-highlight-scrollbar"});
}
+ state.options.onUpdate(cm, state);
}
function removeOverlay(cm) {
@@ -106,19 +120,22 @@
state.matchesonscroll = null;
}
}
+ state.query = null;
}
function highlightMatches(cm) {
cm.operation(function() {
var state = cm.state.matchHighlighter;
- removeOverlay(cm);
if (!cm.somethingSelected() && state.options.showToken) {
var re = state.options.showToken === true ? /[\w$]/ : state.options.showToken;
var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
while (start && re.test(line.charAt(start - 1))) --start;
while (end < line.length && re.test(line.charAt(end))) ++end;
- if (start < end)
+ if (start < end) {
addOverlay(cm, line.slice(start, end), re, state.options.style);
+ } else {
+ removeOverlay(cm);
+ }
return;
}
var from = cm.getCursor("from"), to = cm.getCursor("to");
@@ -126,8 +143,11 @@
if (state.options.wordsOnly && !isWord(cm, from, to)) return;
var selection = cm.getRange(from, to)
if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "")
- if (selection.length >= state.options.minChars)
+ if (selection.length >= state.options.minChars) {
addOverlay(cm, selection, false, state.options.style);
+ } else {
+ removeOverlay(cm);
+ }
});
}