Change: modify match-highlighter plugin (#578)
* Change: modify match-highlighter plugin * Fix: boundary character should only be used when the query starts/ends with alphabet
This commit is contained in:
parent
4120907957
commit
e97a3ef269
|
@ -31,7 +31,6 @@
|
|||
<link href="vendor/codemirror/addon/search/matchesonscrollbar.css" rel="stylesheet">
|
||||
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
|
||||
<script src="vendor/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
||||
<script src="vendor/codemirror/addon/search/match-highlighter.js"></script>
|
||||
<script src="vendor/codemirror/addon/search/searchcursor.js"></script>
|
||||
|
||||
<script src="vendor/codemirror/addon/comment/comment.js"></script>
|
||||
|
@ -62,6 +61,8 @@
|
|||
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
|
||||
<script src="vendor-overwrites/colorpicker/colorview.js"></script>
|
||||
|
||||
<script src="vendor-overwrites/codemirror-addon/match-highlighter.js"></script>
|
||||
|
||||
<script src="js/promisify.js"></script>
|
||||
<script src="js/dom.js"></script>
|
||||
<script src="js/messaging.js"></script>
|
||||
|
@ -77,8 +78,6 @@
|
|||
<link href="edit/global-search.css" rel="stylesheet">
|
||||
<script src="edit/global-search.js"></script>
|
||||
|
||||
<script src="edit/match-highlighter-helper.js"></script>
|
||||
|
||||
<link href="edit/codemirror-default.css" rel="stylesheet">
|
||||
<script src="edit/codemirror-default.js"></script>
|
||||
|
||||
|
|
|
@ -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} :
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
})();
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user