mirror of https://github.com/gorhill/uBlock.git
Improve annotations for search operations in CodeMirror editor
Before this commit, CodeMirror's add-on for search occurrences was limited to find at most 1000 first occurrences, because of performance considerations. This commit removes this low limit by having the search occurrences done in a dedicated worker. The limit is now time-based, and highly unlikely to ever be hit under normal condition. With this change, all search occurrences are gathered, and as a result: - All occurrences are reported in the scrollbar instead of just the 1,000 first - The total count of all occurrences is now reported, instead of capping at "1000+". - The current occurrence rank at the cursor or selection position is now reported -- this was not possible to report this before. The number of occurrences is line-based, it's not useful to report finer-grained occurences in uBO.
This commit is contained in:
parent
90c7e79f4f
commit
23332400f5
|
@ -47,11 +47,11 @@
|
||||||
<script src="lib/codemirror/addon/fold/foldgutter.js"></script>
|
<script src="lib/codemirror/addon/fold/foldgutter.js"></script>
|
||||||
<script src="lib/codemirror/addon/hint/show-hint.js"></script>
|
<script src="lib/codemirror/addon/hint/show-hint.js"></script>
|
||||||
<script src="lib/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
<script src="lib/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
||||||
<script src="lib/codemirror/addon/search/matchesonscrollbar.js"></script>
|
|
||||||
<script src="lib/codemirror/addon/search/searchcursor.js"></script>
|
<script src="lib/codemirror/addon/search/searchcursor.js"></script>
|
||||||
<script src="lib/codemirror/addon/selection/active-line.js"></script>
|
<script src="lib/codemirror/addon/selection/active-line.js"></script>
|
||||||
|
|
||||||
<script src="js/codemirror/search.js"></script>
|
<script src="js/codemirror/search.js"></script>
|
||||||
|
<script src="js/codemirror/search-thread.js"></script>
|
||||||
<script src="js/codemirror/ubo-static-filtering.js"></script>
|
<script src="js/codemirror/ubo-static-filtering.js"></script>
|
||||||
|
|
||||||
<script src="js/fa-icons.js"></script>
|
<script src="js/fa-icons.js"></script>
|
||||||
|
|
|
@ -35,11 +35,11 @@ body {
|
||||||
<script src="lib/codemirror/addon/fold/foldcode.js"></script>
|
<script src="lib/codemirror/addon/fold/foldcode.js"></script>
|
||||||
<script src="lib/codemirror/addon/fold/foldgutter.js"></script>
|
<script src="lib/codemirror/addon/fold/foldgutter.js"></script>
|
||||||
<script src="lib/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
<script src="lib/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
||||||
<script src="lib/codemirror/addon/search/matchesonscrollbar.js"></script>
|
|
||||||
<script src="lib/codemirror/addon/search/searchcursor.js"></script>
|
<script src="lib/codemirror/addon/search/searchcursor.js"></script>
|
||||||
<script src="lib/codemirror/addon/selection/active-line.js"></script>
|
<script src="lib/codemirror/addon/selection/active-line.js"></script>
|
||||||
|
|
||||||
<script src="js/codemirror/search.js"></script>
|
<script src="js/codemirror/search.js"></script>
|
||||||
|
<script src="js/codemirror/search-thread.js"></script>
|
||||||
<script src="js/codemirror/ubo-static-filtering.js"></script>
|
<script src="js/codemirror/ubo-static-filtering.js"></script>
|
||||||
|
|
||||||
<script src="js/fa-icons.js"></script>
|
<script src="js/fa-icons.js"></script>
|
||||||
|
|
|
@ -83,8 +83,8 @@ div.CodeMirror span.CodeMirror-matchingbracket {
|
||||||
border: 1px solid gray;
|
border: 1px solid gray;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
max-width: 50vw;
|
min-width: 14em;
|
||||||
width: 16em;
|
width: 30vw;
|
||||||
}
|
}
|
||||||
.cm-search-widget-input > input {
|
.cm-search-widget-input > input {
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -93,12 +93,10 @@ div.CodeMirror span.CodeMirror-matchingbracket {
|
||||||
}
|
}
|
||||||
.cm-search-widget-input > .cm-search-widget-count {
|
.cm-search-widget-input > .cm-search-widget-count {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: #888;
|
|
||||||
display: none;
|
display: none;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
padding: 0 0.4em;
|
padding: 0 0.4em;
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
.cm-search-widget[data-query] .cm-search-widget-count {
|
.cm-search-widget[data-query] .cm-search-widget-count {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
|
@ -0,0 +1,196 @@
|
||||||
|
/*******************************************************************************
|
||||||
|
|
||||||
|
uBlock Origin - a browser extension to block requests.
|
||||||
|
Copyright (C) 2020-present Raymond Hill
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||||
|
|
||||||
|
Home: https://github.com/gorhill/uBlock
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
|
(( ) => {
|
||||||
|
// >>>>> start of local scope
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
|
// Worker context
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.WorkerGlobalScope instanceof Object &&
|
||||||
|
self instanceof self.WorkerGlobalScope
|
||||||
|
) {
|
||||||
|
let content = '';
|
||||||
|
|
||||||
|
const doSearch = function(details) {
|
||||||
|
const reEOLs = /\n\r|\r\n|\n|\r/g;
|
||||||
|
const t1 = Date.now() + 750;
|
||||||
|
|
||||||
|
let reSearch;
|
||||||
|
try {
|
||||||
|
reSearch = new RegExp(details.pattern, details.flags);
|
||||||
|
} catch(ex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = [];
|
||||||
|
const maxOffset = content.length;
|
||||||
|
let iLine = 0;
|
||||||
|
let iOffset = 0;
|
||||||
|
let size = 0;
|
||||||
|
while ( iOffset < maxOffset ) {
|
||||||
|
// Find next match
|
||||||
|
const match = reSearch.exec(content);
|
||||||
|
if ( match === null ) { break; }
|
||||||
|
// Find number of line breaks between last and current match.
|
||||||
|
reEOLs.lastIndex = 0;
|
||||||
|
const eols = content.slice(iOffset, match.index).match(reEOLs);
|
||||||
|
if ( Array.isArray(eols) ) {
|
||||||
|
iLine += eols.length;
|
||||||
|
}
|
||||||
|
// Store line
|
||||||
|
response.push(iLine);
|
||||||
|
size += 1;
|
||||||
|
// Find next line break.
|
||||||
|
reEOLs.lastIndex = reSearch.lastIndex;
|
||||||
|
const eol = reEOLs.exec(content);
|
||||||
|
iOffset = eol !== null
|
||||||
|
? reEOLs.lastIndex
|
||||||
|
: content.length;
|
||||||
|
reSearch.lastIndex = iOffset;
|
||||||
|
iLine += 1;
|
||||||
|
// Quit if this takes too long
|
||||||
|
if ( (size & 0x3FF) === 0 && Date.now() >= t1 ) { break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.onmessage = function(e) {
|
||||||
|
const msg = e.data;
|
||||||
|
|
||||||
|
switch ( msg.what ) {
|
||||||
|
case 'setHaystack':
|
||||||
|
content = msg.content;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'doSearch':
|
||||||
|
const response = doSearch(msg);
|
||||||
|
self.postMessage({ id: msg.id, response });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
|
// Main context
|
||||||
|
|
||||||
|
{
|
||||||
|
const workerTTL = 5 * 60 * 1000;
|
||||||
|
const pendingResponses = new Map();
|
||||||
|
|
||||||
|
let worker;
|
||||||
|
let workerTTLTimer;
|
||||||
|
let messageId = 1;
|
||||||
|
|
||||||
|
const onWorkerMessage = function(e) {
|
||||||
|
const msg = e.data;
|
||||||
|
const resolver = pendingResponses.get(msg.id);
|
||||||
|
if ( resolver === undefined ) { return; }
|
||||||
|
pendingResponses.delete(msg.id);
|
||||||
|
resolver(msg.response);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelPendingTasks = function() {
|
||||||
|
for ( const resolver of pendingResponses.values() ) {
|
||||||
|
resolver();
|
||||||
|
}
|
||||||
|
pendingResponses.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
const destroy = function() {
|
||||||
|
shutdown();
|
||||||
|
self.searchThread = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const shutdown = function() {
|
||||||
|
if ( workerTTLTimer !== undefined ) {
|
||||||
|
clearTimeout(workerTTLTimer);
|
||||||
|
workerTTLTimer = undefined;
|
||||||
|
}
|
||||||
|
if ( worker === undefined ) { return; }
|
||||||
|
worker.terminate();
|
||||||
|
worker.onmessage = undefined;
|
||||||
|
worker = undefined;
|
||||||
|
cancelPendingTasks();
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = function() {
|
||||||
|
if ( self.searchThread instanceof Object === false ) { return; }
|
||||||
|
if ( worker === undefined ) {
|
||||||
|
worker = new Worker('js/codemirror/search-thread.js');
|
||||||
|
worker.onmessage = onWorkerMessage;
|
||||||
|
}
|
||||||
|
if ( workerTTLTimer !== undefined ) {
|
||||||
|
clearTimeout(workerTTLTimer);
|
||||||
|
}
|
||||||
|
workerTTLTimer = vAPI.setTimeout(shutdown, workerTTL);
|
||||||
|
self.addEventListener('beforeunload', ( ) => {
|
||||||
|
destroy();
|
||||||
|
}, { once: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const setHaystack = function(content) {
|
||||||
|
init();
|
||||||
|
worker.postMessage({ what: 'setHaystack', content });
|
||||||
|
};
|
||||||
|
|
||||||
|
const search = function(query, overwrite = true) {
|
||||||
|
init();
|
||||||
|
if ( worker instanceof Object === false ) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
if ( overwrite ) {
|
||||||
|
cancelPendingTasks();
|
||||||
|
}
|
||||||
|
const id = messageId++;
|
||||||
|
worker.postMessage({
|
||||||
|
what: 'doSearch',
|
||||||
|
id,
|
||||||
|
pattern: query.source,
|
||||||
|
flags: query.flags,
|
||||||
|
isRE: query instanceof RegExp
|
||||||
|
});
|
||||||
|
return new Promise(resolve => {
|
||||||
|
pendingResponses.set(id, resolve);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
self.searchThread = { setHaystack, search, shutdown };
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
|
// <<<<< end of local scope
|
||||||
|
})();
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
|
void 0;
|
|
@ -3,8 +3,16 @@
|
||||||
// I added/removed and modified code in order to get a closer match to a
|
// 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
|
// browser's built-in find-in-page feature which are just enough for
|
||||||
// uBlock Origin.
|
// uBlock Origin.
|
||||||
|
//
|
||||||
|
// This file was originally wholly imported from:
|
||||||
|
// https://github.com/codemirror/CodeMirror/blob/3e1bb5fff682f8f6cbfaef0e56c61d62403d4798/addon/search/search.js
|
||||||
|
//
|
||||||
|
// And has been modified over time to better suit uBO's usage and coding style:
|
||||||
|
// https://github.com/gorhill/uBlock/commits/master/src/js/codemirror/search.js
|
||||||
|
//
|
||||||
|
// The original copyright notice is reproduced below:
|
||||||
|
|
||||||
|
// =====
|
||||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||||
|
|
||||||
|
@ -15,44 +23,39 @@
|
||||||
// Ctrl-G (or whatever is bound to findNext) press. You prevent a
|
// Ctrl-G (or whatever is bound to findNext) press. You prevent a
|
||||||
// replace by making sure the match is no longer selected when hitting
|
// replace by making sure the match is no longer selected when hitting
|
||||||
// Ctrl-G.
|
// Ctrl-G.
|
||||||
|
// =====
|
||||||
/* globals define, require, CodeMirror */
|
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
(function(mod) {
|
(function(CodeMirror) {
|
||||||
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) {
|
const searchOverlay = function(query, caseInsensitive) {
|
||||||
if (typeof query === "string")
|
if ( typeof query === 'string' )
|
||||||
query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseInsensitive ? "gi" : "g");
|
query = new RegExp(
|
||||||
else if (!query.global)
|
query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'),
|
||||||
query = new RegExp(query.source, query.ignoreCase ? "gi" : "g");
|
caseInsensitive ? 'gi' : 'g'
|
||||||
|
);
|
||||||
|
else if ( !query.global )
|
||||||
|
query = new RegExp(query.source, query.ignoreCase ? 'gi' : 'g');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token: function(stream) {
|
token: function(stream) {
|
||||||
query.lastIndex = stream.pos;
|
query.lastIndex = stream.pos;
|
||||||
var match = query.exec(stream.string);
|
const match = query.exec(stream.string);
|
||||||
if (match && match.index === stream.pos) {
|
if ( match && match.index === stream.pos ) {
|
||||||
stream.pos += match[0].length || 1;
|
stream.pos += match[0].length || 1;
|
||||||
return "searching";
|
return 'searching';
|
||||||
} else if (match) {
|
} else if ( match ) {
|
||||||
stream.pos = match.index;
|
stream.pos = match.index;
|
||||||
} else {
|
} else {
|
||||||
stream.skipToEnd();
|
stream.skipToEnd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
function searchWidgetKeydownHandler(cm, ev) {
|
const searchWidgetKeydownHandler = function(cm, ev) {
|
||||||
var keyName = CodeMirror.keyName(ev);
|
const keyName = CodeMirror.keyName(ev);
|
||||||
if ( !keyName ) { return; }
|
if ( !keyName ) { return; }
|
||||||
CodeMirror.lookupKey(
|
CodeMirror.lookupKey(
|
||||||
keyName,
|
keyName,
|
||||||
|
@ -64,9 +67,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function searchWidgetInputHandler(cm) {
|
const searchWidgetInputHandler = function(cm) {
|
||||||
let state = getSearchState(cm);
|
let state = getSearchState(cm);
|
||||||
if ( queryTextFromSearchWidget(cm) === state.queryText ) { return; }
|
if ( queryTextFromSearchWidget(cm) === state.queryText ) { return; }
|
||||||
if ( state.queryTimer !== null ) {
|
if ( state.queryTimer !== null ) {
|
||||||
|
@ -79,10 +82,10 @@
|
||||||
},
|
},
|
||||||
350
|
350
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
function searchWidgetClickHandler(cm, ev) {
|
const searchWidgetClickHandler = function(cm, ev) {
|
||||||
var tcl = ev.target.classList;
|
const tcl = ev.target.classList;
|
||||||
if ( tcl.contains('cm-search-widget-up') ) {
|
if ( tcl.contains('cm-search-widget-up') ) {
|
||||||
findNext(cm, -1);
|
findNext(cm, -1);
|
||||||
} else if ( tcl.contains('cm-search-widget-down') ) {
|
} else if ( tcl.contains('cm-search-widget-down') ) {
|
||||||
|
@ -93,27 +96,25 @@
|
||||||
} else {
|
} else {
|
||||||
ev.stopImmediatePropagation();
|
ev.stopImmediatePropagation();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function queryTextFromSearchWidget(cm) {
|
const queryTextFromSearchWidget = function(cm) {
|
||||||
return getSearchState(cm).widget.querySelector('input[type="search"]').value;
|
return getSearchState(cm).widget.querySelector('input[type="search"]').value;
|
||||||
}
|
};
|
||||||
|
|
||||||
function queryTextToSearchWidget(cm, q) {
|
const queryTextToSearchWidget = function(cm, q) {
|
||||||
var input = getSearchState(cm).widget.querySelector('input[type="search"]');
|
const input = getSearchState(cm).widget.querySelector('input[type="search"]');
|
||||||
if ( typeof q === 'string' && q !== input.value ) {
|
if ( typeof q === 'string' && q !== input.value ) {
|
||||||
input.value = q;
|
input.value = q;
|
||||||
}
|
}
|
||||||
input.setSelectionRange(0, input.value.length);
|
input.setSelectionRange(0, input.value.length);
|
||||||
input.focus();
|
input.focus();
|
||||||
}
|
};
|
||||||
|
|
||||||
function SearchState(cm) {
|
const SearchState = function(cm) {
|
||||||
this.query = null;
|
this.query = null;
|
||||||
this.overlay = null;
|
|
||||||
this.panel = null;
|
this.panel = null;
|
||||||
const widgetParent =
|
const widgetParent = document.querySelector('.cm-search-widget-template').cloneNode(true);
|
||||||
document.querySelector('.cm-search-widget-template').cloneNode(true);
|
|
||||||
this.widget = widgetParent.children[0];
|
this.widget = widgetParent.children[0];
|
||||||
this.widget.addEventListener('keydown', searchWidgetKeydownHandler.bind(null, cm));
|
this.widget.addEventListener('keydown', searchWidgetKeydownHandler.bind(null, cm));
|
||||||
this.widget.addEventListener('input', searchWidgetInputHandler.bind(null, cm));
|
this.widget.addEventListener('input', searchWidgetInputHandler.bind(null, cm));
|
||||||
|
@ -123,16 +124,29 @@
|
||||||
}
|
}
|
||||||
this.queryText = '';
|
this.queryText = '';
|
||||||
this.queryTimer = null;
|
this.queryTimer = null;
|
||||||
}
|
this.dirty = true;
|
||||||
|
this.lines = [];
|
||||||
|
cm.on('changes', (cm, changes) => {
|
||||||
|
for ( const change of changes ) {
|
||||||
|
if ( change.text.length !== 0 || change.removed !== 0 ) {
|
||||||
|
this.dirty = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cm.on('cursorActivity', cm => {
|
||||||
|
updateRank(cm);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// We want the search widget to behave as if the focus was on the
|
// We want the search widget to behave as if the focus was on the
|
||||||
// CodeMirror editor.
|
// CodeMirror editor.
|
||||||
|
|
||||||
const reSearchCommands = /^(?:find|findNext|findPrev|newlineAndIndent)$/;
|
const reSearchCommands = /^(?:find|findNext|findPrev|newlineAndIndent)$/;
|
||||||
|
|
||||||
function widgetCommandHandler(cm, command) {
|
const widgetCommandHandler = function(cm, command) {
|
||||||
if ( reSearchCommands.test(command) === false ) { return false; }
|
if ( reSearchCommands.test(command) === false ) { return false; }
|
||||||
var queryText = queryTextFromSearchWidget(cm);
|
const queryText = queryTextFromSearchWidget(cm);
|
||||||
if ( command === 'find' ) {
|
if ( command === 'find' ) {
|
||||||
queryTextToSearchWidget(cm);
|
queryTextToSearchWidget(cm);
|
||||||
return true;
|
return true;
|
||||||
|
@ -141,101 +155,202 @@
|
||||||
findNext(cm, command === 'findPrev' ? -1 : 1);
|
findNext(cm, command === 'findPrev' ? -1 : 1);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
function getSearchState(cm) {
|
const getSearchState = function(cm) {
|
||||||
return cm.state.search || (cm.state.search = new SearchState(cm));
|
return cm.state.search || (cm.state.search = new SearchState(cm));
|
||||||
}
|
};
|
||||||
|
|
||||||
function queryCaseInsensitive(query) {
|
const queryCaseInsensitive = function(query) {
|
||||||
return typeof query === "string" && query === query.toLowerCase();
|
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.
|
||||||
// Heuristic: if the query string is all lowercase, do a case insensitive search.
|
const getSearchCursor = function(cm, query, pos) {
|
||||||
return cm.getSearchCursor(
|
return cm.getSearchCursor(
|
||||||
query,
|
query,
|
||||||
pos,
|
pos,
|
||||||
{ caseFold: queryCaseInsensitive(query), multiline: false }
|
{ caseFold: queryCaseInsensitive(query), multiline: false }
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
// https://github.com/uBlockOrigin/uBlock-issues/issues/658
|
// https://github.com/uBlockOrigin/uBlock-issues/issues/658
|
||||||
// Modified to backslash-escape ONLY widely-used control characters.
|
// Modified to backslash-escape ONLY widely-used control characters.
|
||||||
function parseString(string) {
|
const parseString = function(string) {
|
||||||
return string.replace(/\\[nrt\\]/g, function(match) {
|
return string.replace(/\\[nrt\\]/g, match => {
|
||||||
if (match === "\\n") return "\n";
|
if ( match === '\\n' ) { return '\n'; }
|
||||||
if (match === "\\r") return "\r";
|
if ( match === '\\r' ) { return '\r'; }
|
||||||
if (match === '\\t') return '\t';
|
if ( match === '\\t' ) { return '\t'; }
|
||||||
if (match === '\\\\') return '\\';
|
if ( match === '\\\\' ) { return '\\'; }
|
||||||
return match;
|
return match;
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
// FIX: use all potential regex flags as is, and if this throws, treat
|
const reEscape = /[.*+\-?^${}()|[\]\\]/g;
|
||||||
// the query string as plain text.
|
|
||||||
function parseQuery(query) {
|
// Must always return a RegExp object.
|
||||||
let isRE = query.match(/^\/(.*)\/([a-z]*)$/);
|
//
|
||||||
if ( isRE ) {
|
// Assume case-sensitivity if there is at least one uppercase in plain
|
||||||
|
// query text.
|
||||||
|
const parseQuery = function(query) {
|
||||||
|
let flags = 'i';
|
||||||
|
let reParsed = query.match(/^\/(.+)\/([iu]*)$/);
|
||||||
|
if ( reParsed !== null ) {
|
||||||
try {
|
try {
|
||||||
query = new RegExp(isRE[1], isRE[2]);
|
const re = new RegExp(reParsed[1], reParsed[2]);
|
||||||
|
query = re.source;
|
||||||
|
flags = re.flags;
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
isRE = false;
|
reParsed = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ( isRE === false ) {
|
if ( reParsed === null ) {
|
||||||
query = parseString(query);
|
if ( /[A-Z]/.test(query) ) { flags = ''; }
|
||||||
|
query = parseString(query).replace(reEscape, '\\$&');
|
||||||
}
|
}
|
||||||
if ( typeof query === 'string' ? query === '' : query.test('') ) {
|
if ( typeof query === 'string' ? query === '' : query.test('') ) {
|
||||||
query = /x^/;
|
query = 'x^';
|
||||||
}
|
}
|
||||||
return query;
|
return new RegExp(query, 'gm' + flags);
|
||||||
}
|
};
|
||||||
|
|
||||||
function startSearch(cm, state) {
|
let intlNumberFormat;
|
||||||
|
|
||||||
|
const formatNumber = function(n) {
|
||||||
|
if ( intlNumberFormat === undefined ) {
|
||||||
|
intlNumberFormat = null;
|
||||||
|
if ( Intl.NumberFormat instanceof Function ) {
|
||||||
|
const intl = new Intl.NumberFormat(undefined, {
|
||||||
|
notation: 'compact',
|
||||||
|
maximumSignificantDigits: 3
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
intl.resolvedOptions instanceof Function &&
|
||||||
|
intl.resolvedOptions().hasOwnProperty('notation')
|
||||||
|
) {
|
||||||
|
intlNumberFormat = intl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n > 10000 && intlNumberFormat instanceof Object
|
||||||
|
? intlNumberFormat.format(n)
|
||||||
|
: n.toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCount = function(cm) {
|
||||||
|
const state = getSearchState(cm);
|
||||||
|
const count = state.lines.length;
|
||||||
|
const span = state.widget.querySelector(
|
||||||
|
'.cm-search-widget-count > span:nth-of-type(2)'
|
||||||
|
);
|
||||||
|
span.textContent = formatNumber(count);
|
||||||
|
span.title = count.toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRank = function(cm) {
|
||||||
|
const state = getSearchState(cm);
|
||||||
|
const lines = state.lines;
|
||||||
|
const current = cm.getCursor().line;
|
||||||
|
let l = 0;
|
||||||
|
let r = lines.length;
|
||||||
|
let i = -1;
|
||||||
|
while ( l < r ) {
|
||||||
|
i = l + r >>> 1;
|
||||||
|
const candidate = lines[i];
|
||||||
|
if ( current === candidate ) { break; }
|
||||||
|
if ( current < candidate ) {
|
||||||
|
r = i;
|
||||||
|
} else /* if ( current > candidate ) */ {
|
||||||
|
l = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let text = '';
|
||||||
|
if ( i !== -1 ) {
|
||||||
|
text = formatNumber(i + 1);
|
||||||
|
if ( lines[i] !== current ) {
|
||||||
|
text = '~' + text;
|
||||||
|
}
|
||||||
|
text = text + '\xA0/\xA0';
|
||||||
|
}
|
||||||
|
const span = state.widget.querySelector(
|
||||||
|
'.cm-search-widget-count > span:nth-of-type(1)'
|
||||||
|
);
|
||||||
|
span.textContent = text;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startSearch = function(cm, state) {
|
||||||
state.query = parseQuery(state.queryText);
|
state.query = parseQuery(state.queryText);
|
||||||
if ( state.overlay ) {
|
if ( state.overlay !== undefined ) {
|
||||||
cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
|
cm.removeOverlay(state.overlay, queryCaseInsensitive(state.query));
|
||||||
}
|
}
|
||||||
state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
|
state.overlay = searchOverlay(state.query, queryCaseInsensitive(state.query));
|
||||||
cm.addOverlay(state.overlay);
|
cm.addOverlay(state.overlay);
|
||||||
if ( cm.showMatchesOnScrollbar ) {
|
if ( state.dirty ) {
|
||||||
if ( state.annotate ) {
|
self.searchThread.setHaystack(cm.getValue());
|
||||||
state.annotate.clear();
|
state.dirty = false;
|
||||||
state.annotate = null;
|
|
||||||
}
|
|
||||||
state.annotate = cm.showMatchesOnScrollbar(
|
|
||||||
state.query,
|
|
||||||
queryCaseInsensitive(state.query),
|
|
||||||
{ multiline: false }
|
|
||||||
);
|
|
||||||
let 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);
|
|
||||||
// Ensure the caret is visible
|
|
||||||
let input = state.widget.querySelector('.cm-search-widget-input > input');
|
|
||||||
input.selectionStart = input.selectionStart;
|
|
||||||
}
|
}
|
||||||
}
|
self.searchThread.search(state.query).then(lines => {
|
||||||
|
if ( Array.isArray(lines) === false ) { return; }
|
||||||
|
state.lines = lines;
|
||||||
|
const count = lines.length;
|
||||||
|
updateRank(cm);
|
||||||
|
updateCount(cm);
|
||||||
|
if ( state.annotate !== undefined ) {
|
||||||
|
state.annotate.clear();
|
||||||
|
state.annotate = undefined;
|
||||||
|
}
|
||||||
|
if ( count === 0 ) { return; }
|
||||||
|
state.annotate = cm.annotateScrollbar('CodeMirror-search-match');
|
||||||
|
const annotations = [];
|
||||||
|
let lineBeg = -1;
|
||||||
|
let lineEnd = -1;
|
||||||
|
for ( const line of lines ) {
|
||||||
|
if ( lineBeg === -1 ) {
|
||||||
|
lineBeg = line;
|
||||||
|
lineEnd = line + 1;
|
||||||
|
continue;
|
||||||
|
} else if ( line === lineEnd ) {
|
||||||
|
lineEnd = line + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
annotations.push({
|
||||||
|
from: { line: lineBeg, ch: 0 },
|
||||||
|
to: { line: lineEnd, ch: 0 }
|
||||||
|
});
|
||||||
|
lineBeg = -1;
|
||||||
|
}
|
||||||
|
if ( lineBeg !== -1 ) {
|
||||||
|
annotations.push({
|
||||||
|
from: { line: lineBeg, ch: 0 },
|
||||||
|
to: { line: lineEnd, ch: 0 }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
state.annotate.update(annotations);
|
||||||
|
});
|
||||||
|
state.widget.setAttribute('data-query', state.queryText);
|
||||||
|
// Ensure the caret is visible
|
||||||
|
let input = state.widget.querySelector('.cm-search-widget-input > input');
|
||||||
|
input.selectionStart = input.selectionStart;
|
||||||
|
};
|
||||||
|
|
||||||
function findNext(cm, dir, callback) {
|
const findNext = function(cm, dir, callback) {
|
||||||
cm.operation(function() {
|
cm.operation(function() {
|
||||||
var state = getSearchState(cm);
|
const state = getSearchState(cm);
|
||||||
if ( !state.query ) { return; }
|
if ( !state.query ) { return; }
|
||||||
var cursor = getSearchCursor(
|
let cursor = getSearchCursor(
|
||||||
cm,
|
cm,
|
||||||
state.query,
|
state.query,
|
||||||
dir <= 0 ? cm.getCursor('from') : cm.getCursor('to')
|
dir <= 0 ? cm.getCursor('from') : cm.getCursor('to')
|
||||||
);
|
);
|
||||||
let previous = dir < 0;
|
const previous = dir < 0;
|
||||||
if (!cursor.find(previous)) {
|
if (!cursor.find(previous)) {
|
||||||
cursor = getSearchCursor(
|
cursor = getSearchCursor(
|
||||||
cm,
|
cm,
|
||||||
state.query,
|
state.query,
|
||||||
previous ? CodeMirror.Pos(cm.lastLine()) : CodeMirror.Pos(cm.firstLine(), 0)
|
previous
|
||||||
|
? CodeMirror.Pos(cm.lastLine())
|
||||||
|
: CodeMirror.Pos(cm.firstLine(), 0)
|
||||||
);
|
);
|
||||||
if (!cursor.find(previous)) return;
|
if (!cursor.find(previous)) return;
|
||||||
}
|
}
|
||||||
|
@ -243,21 +358,22 @@
|
||||||
cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20);
|
cm.scrollIntoView({from: cursor.from(), to: cursor.to()}, 20);
|
||||||
if (callback) callback(cursor.from(), cursor.to());
|
if (callback) callback(cursor.from(), cursor.to());
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
function clearSearch(cm, hard) {
|
const clearSearch = function(cm, hard) {
|
||||||
cm.operation(function() {
|
cm.operation(function() {
|
||||||
var state = getSearchState(cm);
|
const state = getSearchState(cm);
|
||||||
if ( state.query ) {
|
if ( state.query ) {
|
||||||
state.query = state.queryText = null;
|
state.query = state.queryText = null;
|
||||||
}
|
}
|
||||||
if ( state.overlay ) {
|
state.lines = [];
|
||||||
|
if ( state.overlay !== undefined ) {
|
||||||
cm.removeOverlay(state.overlay);
|
cm.removeOverlay(state.overlay);
|
||||||
state.overlay = null;
|
state.overlay = undefined;
|
||||||
}
|
}
|
||||||
if ( state.annotate ) {
|
if ( state.annotate ) {
|
||||||
state.annotate.clear();
|
state.annotate.clear();
|
||||||
state.annotate = null;
|
state.annotate = undefined;
|
||||||
}
|
}
|
||||||
state.widget.removeAttribute('data-query');
|
state.widget.removeAttribute('data-query');
|
||||||
if ( hard ) {
|
if ( hard ) {
|
||||||
|
@ -267,15 +383,15 @@
|
||||||
cm.state.search = null;
|
cm.state.search = null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
function findCommit(cm, dir) {
|
const findCommit = function(cm, dir) {
|
||||||
var state = getSearchState(cm);
|
const state = getSearchState(cm);
|
||||||
if ( state.queryTimer !== null ) {
|
if ( state.queryTimer !== null ) {
|
||||||
clearTimeout(state.queryTimer);
|
clearTimeout(state.queryTimer);
|
||||||
state.queryTimer = null;
|
state.queryTimer = null;
|
||||||
}
|
}
|
||||||
var queryText = queryTextFromSearchWidget(cm);
|
const queryText = queryTextFromSearchWidget(cm);
|
||||||
if ( queryText === state.queryText ) { return; }
|
if ( queryText === state.queryText ) { return; }
|
||||||
state.queryText = queryText;
|
state.queryText = queryText;
|
||||||
if ( state.queryText === '' ) {
|
if ( state.queryText === '' ) {
|
||||||
|
@ -286,12 +402,12 @@
|
||||||
findNext(cm, dir);
|
findNext(cm, dir);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
function findCommand(cm) {
|
const findCommand = function(cm) {
|
||||||
var queryText = cm.getSelection() || undefined;
|
let queryText = cm.getSelection() || undefined;
|
||||||
if ( !queryText ) {
|
if ( !queryText ) {
|
||||||
var word = cm.findWordAt(cm.getCursor());
|
const word = cm.findWordAt(cm.getCursor());
|
||||||
queryText = cm.getRange(word.anchor, word.head);
|
queryText = cm.getRange(word.anchor, word.head);
|
||||||
if ( /^\W|\W$/.test(queryText) ) {
|
if ( /^\W|\W$/.test(queryText) ) {
|
||||||
queryText = undefined;
|
queryText = undefined;
|
||||||
|
@ -300,17 +416,17 @@
|
||||||
}
|
}
|
||||||
queryTextToSearchWidget(cm, queryText);
|
queryTextToSearchWidget(cm, queryText);
|
||||||
findCommit(cm, 1);
|
findCommit(cm, 1);
|
||||||
}
|
};
|
||||||
|
|
||||||
function findNextCommand(cm) {
|
const findNextCommand = function(cm) {
|
||||||
var state = getSearchState(cm);
|
const state = getSearchState(cm);
|
||||||
if ( state.query ) { return findNext(cm, 1); }
|
if ( state.query ) { return findNext(cm, 1); }
|
||||||
}
|
};
|
||||||
|
|
||||||
function findPrevCommand(cm) {
|
const findPrevCommand = function(cm) {
|
||||||
var state = getSearchState(cm);
|
const state = getSearchState(cm);
|
||||||
if ( state.query ) { return findNext(cm, -1); }
|
if ( state.query ) { return findNext(cm, -1); }
|
||||||
}
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
const searchWidgetTemplate =
|
const searchWidgetTemplate =
|
||||||
|
@ -318,13 +434,13 @@
|
||||||
'<div class="cm-search-widget">' +
|
'<div class="cm-search-widget">' +
|
||||||
'<span class="fa-icon fa-icon-ro">search</span> ' +
|
'<span class="fa-icon fa-icon-ro">search</span> ' +
|
||||||
'<span class="cm-search-widget-input">' +
|
'<span class="cm-search-widget-input">' +
|
||||||
'<input type="search">' +
|
'<input type="search" spellcheck="false">' +
|
||||||
'<span class="cm-search-widget-count">' +
|
'<span class="cm-search-widget-count">' +
|
||||||
'<span><!-- future use --></span><span>0</span>' +
|
'<span></span><span>0</span>' +
|
||||||
'</span>' +
|
'</span>' +
|
||||||
'</span> ' +
|
'</span> ' +
|
||||||
'<span class="cm-search-widget-up cm-search-widget-button fa-icon">angle-up</span> ' +
|
'<span class="cm-search-widget-up cm-search-widget-button fa-icon">angle-up</span> ' +
|
||||||
'<span class="cm-search-widget-down cm-search-widget-button fa-icon fa-icon-vflipped">angle-up</span> ' +
|
'<span class="cm-search-widget-down cm-search-widget-button fa-icon fa-icon-vflipped">angle-up</span>' +
|
||||||
'<a class="fa-icon sourceURL" href>external-link</a>' +
|
'<a class="fa-icon sourceURL" href>external-link</a>' +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
|
@ -341,4 +457,4 @@
|
||||||
CodeMirror.defineInitHook(function(cm) {
|
CodeMirror.defineInitHook(function(cm) {
|
||||||
getSearchState(cm);
|
getSearchState(cm);
|
||||||
});
|
});
|
||||||
});
|
})(self.CodeMirror);
|
||||||
|
|
|
@ -457,7 +457,6 @@ if (
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
pendingResponses.set(id, resolve);
|
pendingResponses.set(id, resolve);
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// This tells the worker that filter lists may have changed.
|
// This tells the worker that filter lists may have changed.
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
|
||||||
// Distributed under an MIT license: https://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), {caseFold: this.caseFold, multiline: this.options.multiline});
|
|
||||||
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();
|
|
||||||
};
|
|
||||||
});
|
|
|
@ -41,12 +41,12 @@
|
||||||
<script src="lib/codemirror/lib/codemirror.js"></script>
|
<script src="lib/codemirror/lib/codemirror.js"></script>
|
||||||
<script src="lib/codemirror/addon/display/panel.js"></script>
|
<script src="lib/codemirror/addon/display/panel.js"></script>
|
||||||
<script src="lib/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
<script src="lib/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
||||||
<script src="lib/codemirror/addon/search/matchesonscrollbar.js"></script>
|
|
||||||
<script src="lib/codemirror/addon/search/searchcursor.js"></script>
|
<script src="lib/codemirror/addon/search/searchcursor.js"></script>
|
||||||
<script src="lib/codemirror/addon/selection/active-line.js"></script>
|
<script src="lib/codemirror/addon/selection/active-line.js"></script>
|
||||||
<script src="lib/punycode.js"></script>
|
<script src="lib/punycode.js"></script>
|
||||||
|
|
||||||
<script src="js/codemirror/search.js"></script>
|
<script src="js/codemirror/search.js"></script>
|
||||||
|
<script src="js/codemirror/search-thread.js"></script>
|
||||||
|
|
||||||
<script src="js/fa-icons.js"></script>
|
<script src="js/fa-icons.js"></script>
|
||||||
<script src="js/vapi.js"></script>
|
<script src="js/vapi.js"></script>
|
||||||
|
|
Loading…
Reference in New Issue