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">
|
<link href="vendor/codemirror/addon/search/matchesonscrollbar.css" rel="stylesheet">
|
||||||
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
|
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
|
||||||
<script src="vendor/codemirror/addon/scroll/annotatescrollbar.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/search/searchcursor.js"></script>
|
||||||
|
|
||||||
<script src="vendor/codemirror/addon/comment/comment.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/colorpicker.js"></script>
|
||||||
<script src="vendor-overwrites/colorpicker/colorview.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/promisify.js"></script>
|
||||||
<script src="js/dom.js"></script>
|
<script src="js/dom.js"></script>
|
||||||
<script src="js/messaging.js"></script>
|
<script src="js/messaging.js"></script>
|
||||||
|
@ -77,8 +78,6 @@
|
||||||
<link href="edit/global-search.css" rel="stylesheet">
|
<link href="edit/global-search.css" rel="stylesheet">
|
||||||
<script src="edit/global-search.js"></script>
|
<script src="edit/global-search.js"></script>
|
||||||
|
|
||||||
<script src="edit/match-highlighter-helper.js"></script>
|
|
||||||
|
|
||||||
<link href="edit/codemirror-default.css" rel="stylesheet">
|
<link href="edit/codemirror-default.css" rel="stylesheet">
|
||||||
<script src="edit/codemirror-default.js"></script>
|
<script src="edit/codemirror-default.js"></script>
|
||||||
|
|
||||||
|
|
|
@ -32,12 +32,14 @@ const cmFactory = (() => {
|
||||||
if (value === 'token') {
|
if (value === 'token') {
|
||||||
cm.setOption('highlightSelectionMatches', {
|
cm.setOption('highlightSelectionMatches', {
|
||||||
showToken: /[#.\-\w]/,
|
showToken: /[#.\-\w]/,
|
||||||
annotateScrollbar: true
|
annotateScrollbar: true,
|
||||||
|
onUpdate: updateMatchHighlightCount
|
||||||
});
|
});
|
||||||
} else if (value === 'selection') {
|
} else if (value === 'selection') {
|
||||||
cm.setOption('highlightSelectionMatches', {
|
cm.setOption('highlightSelectionMatches', {
|
||||||
showToken: false,
|
showToken: false,
|
||||||
annotateScrollbar: true
|
annotateScrollbar: true,
|
||||||
|
onUpdate: updateMatchHighlightCount
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
cm.setOption('highlightSelectionMatches', null);
|
cm.setOption('highlightSelectionMatches', null);
|
||||||
|
@ -80,6 +82,10 @@ const cmFactory = (() => {
|
||||||
});
|
});
|
||||||
return {create, destroy, setOption};
|
return {create, destroy, setOption};
|
||||||
|
|
||||||
|
function updateMatchHighlightCount(cm, state) {
|
||||||
|
cm.display.wrapper.dataset.matchHighlightCount = state.matchesonscroll.matches.length;
|
||||||
|
}
|
||||||
|
|
||||||
function configureMouseFn(cm, repeat) {
|
function configureMouseFn(cm, repeat) {
|
||||||
return repeat === 'double' ?
|
return repeat === 'double' ?
|
||||||
{unit: selectTokenOnDoubleclick} :
|
{unit: selectTokenOnDoubleclick} :
|
||||||
|
|
|
@ -364,14 +364,14 @@ input:invalid {
|
||||||
.resize-grip-enabled .CodeMirror-scrollbar-filler {
|
.resize-grip-enabled .CodeMirror-scrollbar-filler {
|
||||||
bottom: 7px; /* make space for resize-grip */
|
bottom: 7px; /* make space for resize-grip */
|
||||||
}
|
}
|
||||||
body[data-match-highlight="token"] .cm-matchhighlight-approved .cm-matchhighlight,
|
.cm-matchhighlight,
|
||||||
body[data-match-highlight="token"] .CodeMirror-selection-highlight-scrollbar {
|
.CodeMirror-selection-highlight-scrollbar {
|
||||||
animation: fadein-match-highlighter 1s cubic-bezier(.97,.01,.42,.98);
|
animation: fadein-match-highlighter 1s cubic-bezier(.97,.01,.42,.98);
|
||||||
animation-fill-mode: both;
|
animation-fill-mode: both;
|
||||||
}
|
}
|
||||||
body[data-match-highlight="selection"] .cm-matchhighlight-approved .cm-matchhighlight,
|
[data-match-highlight-count="1"] .cm-matchhighlight,
|
||||||
body[data-match-highlight="selection"] .CodeMirror-selection-highlight-scrollbar {
|
[data-match-highlight-count="1"] .CodeMirror-selection-highlight-scrollbar {
|
||||||
background-color: rgba(1, 151, 193, 0.1);
|
animation: none;
|
||||||
}
|
}
|
||||||
@-webkit-keyframes highlight {
|
@-webkit-keyframes highlight {
|
||||||
from {
|
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,
|
wordsOnly: false,
|
||||||
annotateScrollbar: false,
|
annotateScrollbar: false,
|
||||||
showToken: false,
|
showToken: false,
|
||||||
trim: true
|
trim: true,
|
||||||
|
onUpdate: () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function State(options) {
|
function State(options) {
|
||||||
|
@ -46,6 +47,7 @@
|
||||||
this.overlay = this.timeout = null;
|
this.overlay = this.timeout = null;
|
||||||
this.matchesonscroll = null;
|
this.matchesonscroll = null;
|
||||||
this.active = false;
|
this.active = false;
|
||||||
|
this.query = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
|
CodeMirror.defineOption("highlightSelectionMatches", false, function(cm, val, old) {
|
||||||
|
@ -88,12 +90,24 @@
|
||||||
|
|
||||||
function addOverlay(cm, query, hasBoundary, style) {
|
function addOverlay(cm, query, hasBoundary, style) {
|
||||||
var state = cm.state.matchHighlighter;
|
var state = cm.state.matchHighlighter;
|
||||||
|
if (state.query === query) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeOverlay(cm);
|
||||||
|
state.query = query;
|
||||||
cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
|
cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
|
||||||
if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
|
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,
|
state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
|
||||||
{className: "CodeMirror-selection-highlight-scrollbar"});
|
{className: "CodeMirror-selection-highlight-scrollbar"});
|
||||||
}
|
}
|
||||||
|
state.options.onUpdate(cm, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeOverlay(cm) {
|
function removeOverlay(cm) {
|
||||||
|
@ -106,19 +120,22 @@
|
||||||
state.matchesonscroll = null;
|
state.matchesonscroll = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
state.query = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightMatches(cm) {
|
function highlightMatches(cm) {
|
||||||
cm.operation(function() {
|
cm.operation(function() {
|
||||||
var state = cm.state.matchHighlighter;
|
var state = cm.state.matchHighlighter;
|
||||||
removeOverlay(cm);
|
|
||||||
if (!cm.somethingSelected() && state.options.showToken) {
|
if (!cm.somethingSelected() && state.options.showToken) {
|
||||||
var re = state.options.showToken === true ? /[\w$]/ : 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;
|
var cur = cm.getCursor(), line = cm.getLine(cur.line), start = cur.ch, end = start;
|
||||||
while (start && re.test(line.charAt(start - 1))) --start;
|
while (start && re.test(line.charAt(start - 1))) --start;
|
||||||
while (end < line.length && re.test(line.charAt(end))) ++end;
|
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);
|
addOverlay(cm, line.slice(start, end), re, state.options.style);
|
||||||
|
} else {
|
||||||
|
removeOverlay(cm);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var from = cm.getCursor("from"), to = cm.getCursor("to");
|
var from = cm.getCursor("from"), to = cm.getCursor("to");
|
||||||
|
@ -126,8 +143,11 @@
|
||||||
if (state.options.wordsOnly && !isWord(cm, from, to)) return;
|
if (state.options.wordsOnly && !isWord(cm, from, to)) return;
|
||||||
var selection = cm.getRange(from, to)
|
var selection = cm.getRange(from, to)
|
||||||
if (state.options.trim) selection = selection.replace(/^\s+|\s+$/g, "")
|
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);
|
addOverlay(cm, selection, false, state.options.style);
|
||||||
|
} else {
|
||||||
|
removeOverlay(cm);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user