Drive "search-as-you-type" resulting in slow and cumbersome experience #2129

Implementation of asyncSearch and async search result display to avoid bad experience on large drive. Includes also search time display and debug
This commit is contained in:
Ludovic Dubost 2026-02-02 15:46:32 +01:00
parent 2f9f752a59
commit cc1c160233
4 changed files with 371 additions and 90 deletions

View File

@ -233,6 +233,7 @@
font-size: 30px;
margin: 15px;
height: 2.5rem;
position: relative;
input {
background: transparent;
color: @cp_drive-fg;
@ -262,6 +263,13 @@
.cp-app-drive-tree-search-icon, .cp-app-drive-search-cancel {
color: @cp_drive-fg;
}
.cp-app-drive-search-timing {
font-size: 0.5em;
position: absolute;
right: 60px;
top: 100%;
padding-top: 0.25rem;
}
}
.cp-app-drive-search-spinner {
display: inline-flex;
@ -271,7 +279,8 @@
svg {
height: 2rem;
width: 2rem;
margin-bottom: 1.5rem;
margin-bottom: 0.5rem;
margin-left: -1rem;
}
.cp-spinner svg {
animation: spin 10s linear infinite;
@ -297,7 +306,8 @@
}
.cp-app-drive-search-spinner svg {
width: 1.5rem;
margin-bottom: 1rem;
margin-bottom: 0.3rem;
margin-left: -0.75rem;
}
.cp-app-drive-search-noresult {
font-size: 22px;
@ -1146,4 +1156,3 @@
}
}

View File

@ -1656,6 +1656,35 @@ const factory = (UserObject, Util, Hash,
});
return ret;
};
var searchAsync = function (Env, value, cb, opts) {
cb = Util.once(cb || function () {});
var ret = [];
var userObjects = _getUserObjects(Env);
var index = 0;
var next = function () {
if (index >= userObjects.length) { return void cb(ret); }
var uo = userObjects[index++];
var fPath = _getUserObjectPath(Env, uo);
var handleResults = function (results) {
if (!results || !results.length) { return void next(); }
if (fPath) {
results.forEach(function (r) {
r.inSharedFolder = true;
r.paths.forEach(function (p) {
Array.prototype.unshift.apply(p, fPath);
});
});
}
Array.prototype.push.apply(ret, results);
next();
};
if (uo.searchAsync) {
return void uo.searchAsync(value, handleResults, opts);
}
handleResults(uo.search(value));
};
next();
};
var getRecentPads = function (Env) {
var files = [];
@ -1799,6 +1828,7 @@ const factory = (UserObject, Util, Hash,
isStaticFile: callWithEnv(isStaticFile),
getFiles: callWithEnv(getFiles),
search: callWithEnv(search),
searchAsync: callWithEnv(searchAsync),
getRecentPads: callWithEnv(getRecentPads),
getOwnedPads: callWithEnv(getOwnedPads),
getTagsList: callWithEnv(getTagsList),

View File

@ -782,6 +782,109 @@ const factory = (Util, Hash,
return ret;
};
exp.searchAsync = function (value, cb, opts) {
cb = Util.once(cb || function () {});
if (typeof(value) !== "string") { return void cb([]); }
value = value.trim();
var res = [];
var allFilesList = files[FILES_DATA];
var allSFList = files[SHARED_FOLDERS];
var lValue = value.toLowerCase();
var tags;
if (/^#/.test(lValue)) {
tags = [lValue.slice(1).trim()];
}
var containsSearchedTag = function (T) {
if (!tags) { return false; }
if (!T.length) { return false; }
T = T.map(function (t) { return t.toLowerCase(); });
return tags.some(function (tag) {
return T.some(function (t) {
return t === tag;
});
});
};
var ids = getFiles([FILES_DATA, SHARED_FOLDERS]);
var index = 0;
var batchSize = opts && opts.batchSize ? opts.batchSize : 250;
var processFilesBatch = function () {
console.log("SEARCH: " + new Date().toISOString() + " searchAsync files batch start");
var end = Math.min(index + batchSize, ids.length);
for (; index < end; index++) {
var id = ids[index];
var data = allFilesList[id] || allSFList[id];
if (!data) { continue; }
if (Array.isArray(data.tags) && containsSearchedTag(data.tags)) {
res.push(id);
continue;
}
var title = data.title || data.lastTitle;
if ((title && title.toLowerCase().indexOf(lValue) !== -1) ||
(data.filename && data.filename.toLowerCase().indexOf(lValue) !== -1)) {
res.push(id);
}
}
if (index < ids.length) {
return void setTimeout(processFilesBatch, 0);
}
var href = Hash.getRelativeHref(value);
if (href) {
var hrefId = getIdFromHref(href);
if (hrefId) { res.push(hrefId); }
}
res = Util.deduplicateString(res);
var ret = [];
res.forEach(function (l) {
ret.push({
id: l,
paths: findFile(l),
data: exp.getFileData(l)
});
});
var resFolders = [];
var stack = [{ folder: files[ROOT], path: [ROOT] }];
var folderBatchSize = opts && opts.folderBatchSize ? opts.folderBatchSize : 50;
var processFoldersBatch = function () {
console.log("SEARCH: " + new Date().toISOString() + " searchAsync folders batch start");
var processed = 0;
while (stack.length && processed < folderBatchSize) {
var current = stack.pop();
var folder = current.folder;
var path = current.path;
for (var key in folder) {
if (isFolder(folder[key]) && !isSharedFolder(folder[key])) {
if (key.toLowerCase().indexOf(lValue) !== -1) {
resFolders.push({
id: null,
paths: [path.concat(key)],
data: {
title: key
}
});
}
stack.push({ folder: folder[key], path: path.concat(key) });
}
}
processed++;
}
if (stack.length) {
return void setTimeout(processFoldersBatch, 0);
}
resFolders = resFolders.sort(function (a, b) {
return a.data.title.toLowerCase() > b.data.title.toLowerCase();
});
ret = resFolders.concat(ret);
cb(ret);
};
processFoldersBatch();
};
processFilesBatch();
};
exp.getRecentPads = function () {
var allFiles = files[FILES_DATA];
var sorted = Object.keys(allFiles).filter(function (a) { return allFiles[a]; })

View File

@ -3931,6 +3931,9 @@ define([
APP.Search = {};
var displaySearch = function ($list, value) {
var search = APP.Search;
var logSearch = function (message) {
console.log("SEARCH: " + new Date().toISOString() + " " + message);
};
var $div = $('<div>', {'id': 'cp-app-drive-search', 'class': 'cp-unselectable'});
$searchIcon.clone().appendTo($div);
@ -3945,56 +3948,56 @@ define([
draggable: false,
tabindex: 1,
}).keyup(function (e) {
if (searching) {
e.preventDefault();
e.stopPropagation();
return;
}
logSearch("new text detected");
var currentValue = $input.val().trim();
if (search.to) { window.clearTimeout(search.to); }
if (e.which === 13) {
spinner.spin();
var newLocation = [SEARCH, $input.val()];
search.cursor = $input[0].selectionStart;
if (!manager.comparePath(newLocation, currentPath.slice())) {
searching = true;
APP.displayDirectory(newLocation);
}
logSearch("search launched via enter");
renderSearchResults($input.val());
return;
}
if (e.which === 27) {
$input.val('');
logSearch("text field updated (escape clear)");
search.cursor = 0;
searching = true;
APP.displayDirectory([SEARCH]);
renderSearchResults('');
return;
}
if (currentValue === "") {
search.cursor = 0;
APP.displayDirectory([SEARCH]);
renderSearchResults('');
return;
}
if (currentValue.length < 2) { return; } // Don't autosearch 1 character
search.inputVersion = (search.inputVersion || 0) + 1;
var inputVersion = search.inputVersion;
search.to = window.setTimeout(function () {
var newLocation = [SEARCH, $input.val()];
if (inputVersion !== search.inputVersion) { return; }
search.cursor = $input[0].selectionStart;
if (currentValue === search.value) { return; }
if (!manager.comparePath(newLocation, currentPath.slice())) {
searching = true;
APP.displayDirectory(newLocation);
}
logSearch("search launched via debounce");
renderSearchResults($input.val());
}, 500);
}).on('click mousedown mouseup', function (e) {
e.stopPropagation();
}).on('focus', function () {
logSearch("input focus");
}).on('blur', function () {
logSearch("input blur");
}).val(value || '').appendTo($div);
logSearch("text field updated (initial value)");
search.value = typeof(value) === "string" ? value : '';
$input[0].selectionStart = search.cursor || 0;
$input[0].selectionEnd = search.cursor || 0;
var cancel = Icons.get('close', {class: 'cp-app-drive-search-cancel',title:Messages.cancel}); // not visible
cancel.addEventListener('click', function () {
$input.val('');
logSearch("text field updated (cancel click)");
search.cursor = 0;
APP.displayDirectory([SEARCH]);
});
@ -4002,90 +4005,226 @@ define([
$div.append($spinnerContainer);
$list.append($div);
var $timing = $('<div>', {
'class': 'cp-app-drive-search-timing'
}).appendTo($div);
setTimeout(function () {
$input.focus();
});
if (typeof(value) === "string" && value.trim()) {
spinner.spin();
} else {
searching = false;
return;
}
var clearSearchResults = function () {
$list.children().not('#cp-app-drive-search').remove();
};
setTimeout(function () {
//$list.closest('#cp-app-drive-content-folder').addClass('cp-app-drive-content-list');
var filesList = manager.search(value);
if (!filesList.length) {
$list.append(h('div.cp-app-drive-search-noresult', Messages.fm_noResult));
var renderSearchResults = function (nextValue) {
logSearch("launching search for value " + nextValue);
if (searching==true) {
logSearch("search already in progress");
setTimeout(function () {
logSearch("retrying search for value " + nextValue);
renderSearchResults(nextValue);
}, 1000);
return;
}
var searchValue = typeof(nextValue) === "string" ? nextValue : '';
search.value = searchValue;
clearSearchResults();
$timing.text('');
if (!searchValue.trim()) {
spinner.hide();
searching = false;
logSearch("finished search (empty query)");
return;
}
var sortable = {};
var sortableFolders = [];
filesList.forEach(function (r) {
// if r.id === null, then it's a folder, not a file
r.paths.forEach(function (path) {
if (!r.inSharedFolder &&
APP.hideDuplicateOwned && manager.isDuplicateOwned(path)) { return; }
var _path = path.slice();
var key = path.pop();
var root = manager.find(path);
var obj = {
path: path,
_path: _path,
key: key,
root: root,
data: r.data
};
if (manager.isFolder(root[key])) {
sortableFolders.push(obj);
var searchStartedAt = Date.now();
spinner.spin();
searching = true;
search.displayVersion = (search.displayVersion || 0) + 1;
var displayVersion = search.displayVersion;
setTimeout(function () {
if (displayVersion !== search.displayVersion) {
spinner.hide();
searching = false;
return;
}
var latestValue = $input.val();
if (latestValue !== searchValue) {
spinner.hide();
searching = false;
return;
}
//$list.closest('#cp-app-drive-content-folder').addClass('cp-app-drive-content-list');
logSearch("search execution scheduled for value " + searchValue);
setTimeout(function () {
if (displayVersion !== search.displayVersion) {
spinner.hide();
searching = false;
return;
}
sortable[root[key]] = obj;
});
});
var _folders = sortElements(true, [ROOT], sortableFolders, null, !getSortFolderDesc(), true);
var sortableKeys = Object.keys(sortable).map(Number);
var _files = sortElements(false, [ROOT], sortableKeys, APP.store[SORT_FILE_BY], !getSortFileDesc(), true);
if ($input.val() !== searchValue) {
spinner.hide();
searching = false;
return;
}
logSearch("search execution started for value " + searchValue);
var searchExecStartedAt = Date.now();
var handleSearchResults = function (filesList) {
if (displayVersion !== search.displayVersion) {
spinner.hide();
searching = false;
return;
}
if ($input.val() !== searchValue) {
spinner.hide();
searching = false;
return;
}
var searchExecElapsed = Date.now() - searchExecStartedAt;
logSearch("search execution done in " + searchExecElapsed + "ms");
logSearch("starting render search results");
var sortStartedAt = Date.now();
if (!filesList.length) {
$list.append(h('div.cp-app-drive-search-noresult', Messages.fm_noResult));
spinner.hide();
searching = false;
var totalElapsed = Date.now() - searchStartedAt;
logSearch("sort time 0ms (no results)");
logSearch("render time 0ms (no results)");
logSearch("total time " + totalElapsed + "ms");
$timing.text(totalElapsed + 'ms');
logSearch("finished render (no results)");
return;
}
var sortable = {};
var sortableFolders = [];
filesList.forEach(function (r) {
// if r.id === null, then it's a folder, not a file
r.paths.forEach(function (path) {
if (!r.inSharedFolder &&
APP.hideDuplicateOwned && manager.isDuplicateOwned(path)) { return; }
var _path = path.slice();
var key = path.pop();
var root = manager.find(path);
var obj = {
path: path,
_path: _path,
key: key,
root: root,
data: r.data
};
if (manager.isFolder(root[key])) {
sortableFolders.push(obj);
return;
}
sortable[root[key]] = obj;
});
});
var _folders = sortElements(true, [ROOT], sortableFolders, null, !getSortFolderDesc(), true);
var sortableKeys = Object.keys(sortable).map(Number);
var _files = sortElements(false, [ROOT], sortableKeys, APP.store[SORT_FILE_BY], !getSortFileDesc(), true);
var sortElapsed = Date.now() - sortStartedAt;
var addEl = function (obj, folder) {
var $element = createElement(obj.path, obj.key, obj.root, folder);
$element.addClass('cp-app-drive-element-notrash cp-app-drive-search-result');
$element.off('contextmenu');
$element.contextmenu(openContextMenu('default'));
$element.data('context', 'default');
if (folder) {
$element.find('.cp-app-drive-element-list').css({
visibility: 'hidden'
}).text('');
}
if (manager.isPathIn(obj._path, ['hrefArray'])) {
obj._path.pop();
obj._path.push(obj.data.title);
}
var $path = $('<span>', {
'class': 'cp-app-drive-search-path'
}).appendTo($element.find('.cp-app-drive-element-name'));
createTitle($path, obj._path);
var addEl = function (obj, folder) {
var $element = createElement(obj.path, obj.key, obj.root, folder);
$element.addClass('cp-app-drive-element-notrash cp-app-drive-search-result');
$element.off('contextmenu');
$element.contextmenu(openContextMenu('default'));
$element.data('context', 'default');
if (folder) {
$element.find('.cp-app-drive-element-list').css({
visibility: 'hidden'
}).text('');
}
if (manager.isPathIn(obj._path, ['hrefArray'])) {
obj._path.pop();
obj._path.push(obj.data.title);
}
var $path = $('<span>', {
'class': 'cp-app-drive-search-path'
}).appendTo($element.find('.cp-app-drive-element-name'));
createTitle($path, obj._path);
$list.append($element);
};
if (_folders.length) { getFolderListHeader(true, true).appendTo($list); }
_folders.forEach(function (el) {
var obj = el;
addEl(obj, true);
$list.append($element);
};
var renderStartedAt = Date.now();
var appendQueue = [];
if (_folders.length) {
appendQueue.push(function () {
getFolderListHeader(true, true).appendTo($list);
});
_folders.forEach(function (el) {
var obj = el;
appendQueue.push(function () {
addEl(obj, true);
});
});
}
if (_files.length) {
appendQueue.push(function () {
getFileListHeader(true).appendTo($list);
});
_files.forEach(function (el) {
var obj = sortable[el];
appendQueue.push(function () {
addEl(obj, false);
});
});
}
var appendIndex = 0;
var appendBatch = function () {
var batchStartedAt = Date.now();
if (displayVersion !== search.displayVersion) {
spinner.hide();
searching = false;
return;
}
if ($input.val() !== searchValue) {
spinner.hide();
searching = false;
return;
}
var end = Math.min(appendIndex + 30, appendQueue.length);
for (; appendIndex < end; appendIndex++) {
appendQueue[appendIndex]();
}
var batchElapsed = Date.now() - batchStartedAt;
logSearch("render batch time " + batchElapsed + "ms (" + appendIndex + "/" + appendQueue.length + ")");
if (appendIndex < appendQueue.length) {
window.requestAnimationFrame(appendBatch);
return;
}
var renderElapsed = Date.now() - renderStartedAt;
setTimeout(collapseDrivePath);
spinner.hide();
searching = false;
var totalElapsed = Date.now() - searchStartedAt;
logSearch("sort time " + sortElapsed + "ms");
logSearch("render time " + renderElapsed + "ms");
logSearch("total time " + totalElapsed + "ms");
$timing.text(totalElapsed + 'ms');
logSearch("finished render (results displayed)");
};
appendBatch();
};
if (manager.searchAsync) {
manager.searchAsync(searchValue, handleSearchResults);
} else {
handleSearchResults(manager.search(searchValue));
}
}, 0);
});
if (_files.length) { getFileListHeader(true).appendTo($list); }
_files.forEach(function (el) {
var obj = sortable[el];
addEl(obj, false);
});
setTimeout(collapseDrivePath);
spinner.hide();
};
if (typeof(value) === "string") {
renderSearchResults(value);
} else {
searching = false;
});
}
};
var displayRecent = function ($list, typeFilter) {