Skip to content

Commit

Permalink
Improve annotations for search operations in CodeMirror editor
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gorhill committed Aug 2, 2020
1 parent 90c7e79 commit 2333240
Show file tree
Hide file tree
Showing 8 changed files with 440 additions and 228 deletions.
2 changes: 1 addition & 1 deletion src/1p-filters.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@
<script src="lib/codemirror/addon/fold/foldgutter.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/search/matchesonscrollbar.js"></script>
<script src="lib/codemirror/addon/search/searchcursor.js"></script>
<script src="lib/codemirror/addon/selection/active-line.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/fa-icons.js"></script>
Expand Down
2 changes: 1 addition & 1 deletion src/asset-viewer.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@
<script src="lib/codemirror/addon/fold/foldcode.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/search/matchesonscrollbar.js"></script>
<script src="lib/codemirror/addon/search/searchcursor.js"></script>
<script src="lib/codemirror/addon/selection/active-line.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/fa-icons.js"></script>
Expand Down
6 changes: 2 additions & 4 deletions src/css/codemirror.css
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ div.CodeMirror span.CodeMirror-matchingbracket {
border: 1px solid gray;
border-radius: 3px;
display: inline-flex;
max-width: 50vw;
width: 16em;
min-width: 14em;
width: 30vw;
}
.cm-search-widget-input > input {
border: 0;
Expand All @@ -93,12 +93,10 @@ div.CodeMirror span.CodeMirror-matchingbracket {
}
.cm-search-widget-input > .cm-search-widget-count {
align-items: center;
color: #888;
display: none;
flex-grow: 0;
font-size: 80%;
padding: 0 0.4em;
pointer-events: none;
}
.cm-search-widget[data-query] .cm-search-widget-count {
display: inline-flex;
Expand Down
196 changes: 196 additions & 0 deletions src/js/codemirror/search-thread.js
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 2333240

Please sign in to comment.