not. We can't do that if we arrived directly on #search from outside. */
canGoBackToHideSearch: false,
+ /* Autocompletion in the input field is whitelisted only for character
+ input (so not deletion, cut, or anything else). This is flipped in the
+ onkeypress event and reset after each oninput event. */
+ autocompleteNextInputEvent: false,
+
init: function(buffer, maxResults) {
let view = new DataView(buffer);
toUtf8: function(string) { return unescape(encodeURIComponent(string)); },
fromUtf8: function(string) { return decodeURIComponent(escape(string)); },
+ autocompletedCharsToString: function(chars) {
+ /* Strip incomplete UTF-8 chars from the autocompletion end */
+ for(let i = chars.length - 1; i >= 0; --i) {
+ let c = chars[i];
+
+ /* We're safe, finish */
+ if(
+ /* ASCII value at the end */
+ (c < 128 && i + 1 == chars.length) ||
+
+ /* Full two-byte character at the end */
+ ((c & 0xe0) == 0xc0 && i + 2 == chars.length) ||
+
+ /* Full three-byte character at the end */
+ ((c & 0xf0) == 0xe0 && i + 3 == chars.length) ||
+
+ /* Full four-byte character at the end */
+ ((c & 0xf8) == 0xf0 && i + 4 == chars.length)
+ ) break;
+
+ /* Continuing UTF-8 character, go further back */
+ if((c & 0xc0) == 0x80) continue;
+
+ /* Otherwise the character is not complete, drop it from the end */
+ chars.length = i;
+ break;
+ }
+
+ /* Convert the autocompleted UTF-8 sequence to a string */
+ let suggestedTabAutocompletionString = '';
+ for(let i = 0; i != chars.length; ++i)
+ suggestedTabAutocompletionString += String.fromCharCode(chars[i]);
+ return this.fromUtf8(suggestedTabAutocompletionString);
+ },
+
/* Returns the values in UTF-8, but input is in whatever shitty 16bit
encoding JS has */
search: function(searchString) {
if(link)
link.href = link.dataset.searchEngine.replace('{query}', encodeURIComponent(searchString));
}
- return [];
+ return [[], ''];
}
/* Otherwise gather the results */
+ let suggestedTabAutocompletionChars = [];
let results = [];
let leaves = [[this.searchStack[this.searchStack.length - 1], 0]];
while(leaves.length) {
results.push(this.gatherResult(index, suffixLength, 0xffffff)); /* should be enough haha */
/* 'nuff said. */
- if(results.length >= this.maxResults) return results;
+ if(results.length >= this.maxResults)
+ return [results, this.autocompletedCharsToString(suggestedTabAutocompletionChars)];
}
/* Dig deeper */
/* Append to the queue */
leaves.push([offsetBarrier & 0x007fffff, suffixLength + 1]);
+
+ /* We don't have anything yet and this is the only path
+ forward, add the char to suggested Tab autocompletion. Can't
+ extract it from the leftmost 8 bits of offsetBarrier because
+ that would make it negative, have to load as Uint8 instead.
+ Also can't use String.fromCharCode(), because later doing
+ str.charCodeAt() would give me back UTF-16 values, which is
+ absolutely unwanted when all I want is check for truncated
+ UTF-8. */
+ if(!results.length && leaves.length == 1 && childCount == 1)
+ suggestedTabAutocompletionChars.push(this.trie.getUint8(childOffset + j*4 + 3));
}
}
- return results;
+ return [results, this.autocompletedCharsToString(suggestedTabAutocompletionChars)];
},
gatherResult: function(index, suffixLength, maxUrlPrefix) {
return this.escape(name).replace(/[:=]/g, '‎$&').replace(/(\)|>|&|\/)/g, '‎$&‎');
},
- renderResults: /* istanbul ignore next */ function(value, results) {
+ renderResults: /* istanbul ignore next */ function(value, resultsSuggestedTabAutocompletion) {
/* Normalize the value and encode as UTF-8 so the slicing works
properly */
value = this.toUtf8(value.trim());
document.getElementById('search-help').style.display = 'none';
- if(results.length) {
+ /* Results found */
+ if(resultsSuggestedTabAutocompletion[0].length) {
+ let results = resultsSuggestedTabAutocompletion[0];
+
document.getElementById('search-results').style.display = 'block';
document.getElementById('search-notfound').style.display = 'none';
document.getElementById('search-results').innerHTML = this.fromUtf8(list);
document.getElementById('search-current').scrollIntoView(true);
+ /* Append the suggested tab autocompletion, if any, and if the user
+ didn't just delete it */
+ let searchInput = document.getElementById('search-input');
+ if(this.autocompleteNextInputEvent && resultsSuggestedTabAutocompletion[1].length && searchInput.selectionEnd == searchInput.value.length) {
+ let suggestedTabAutocompletion = this.fromUtf8(resultsSuggestedTabAutocompletion[1]);
+
+ let lengthBefore = searchInput.value.length;
+ searchInput.value += suggestedTabAutocompletion;
+ searchInput.setSelectionRange(lengthBefore, searchInput.value.length);
+ }
+
+ /* Nothing found */
} else {
document.getElementById('search-results').style.display = 'none';
document.getElementById('search-notfound').style.display = 'block';
/* Don't allow things to be selected just by motionless mouse cursor
suddenly appearing over a search result */
this.mouseMovedSinceLastRender = false;
+
+ /* Reset autocompletion, if it was allowed. It'll get whitelisted next
+ time a character gets inserted. */
+ this.autocompleteNextInputEvent = false;
},
- searchAndRender: function(value) {
+ searchAndRender: /* istanbul ignore next */ function(value) {
let prev = performance.now();
let results = this.search(value);
let after = performance.now();
this.renderResults(value, results);
if(value.trim().length) {
document.getElementById('search-symbolcount').innerHTML =
- results.length + (results.length >= this.maxResults ? '+' : '') + " results (" + Math.round((after - prev)*10)/10 + " ms)";
+ results[0].length + (results.length >= this.maxResults ? '+' : '') + " results (" + Math.round((after - prev)*10)/10 + " ms)";
} else
document.getElementById('search-symbolcount').innerHTML =
this.symbolCount + " symbols (" + Math.round(this.dataSize/102.4)/10 + " kB)";
document.getElementById('search-input').focus();
return false; /* so T doesn't get entered into the box */
+ /* Fill in the autocompleted selection */
+ } else if(event.key == 'Tab' && !event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) {
+ /* But only if the input has selection at the end */
+ let input = document.getElementById('search-input');
+ if(input.selectionEnd == input.value.length && input.selectionStart != input.selectionEnd)
+ input.setSelectionRange(input.value.length, input.value.length);
+
/* Select next item */
- } else if(event.key == 'ArrowDown' || (event.key == 'Tab' && !event.shiftKey)) {
+ } else if(event.key == 'ArrowDown') {
let current = document.getElementById('search-current');
if(current) {
let next = current.nextSibling;
return false; /* so the keypress doesn't affect input cursor */
/* Select prev item */
- } else if(event.key == 'ArrowUp' || (event.key == 'Tab' && event.shiftKey)) {
+ } else if(event.key == 'ArrowUp') {
let current = document.getElementById('search-current');
if(current) {
let prev = current.previousSibling;
document.body.style.overflow = 'auto';
document.body.style.paddingRight = '0';
return false; /* so the form doesn't get sent */
+
+ /* Looks like the user is inserting some text (and not cutting,
+ copying or whatever), allow autocompletion for the new
+ character. The oninput event resets this back to false, so this
+ basically whitelists only keyboard input, including Shift-key
+ and special chars using right Alt (or equivalent on Mac), but
+ excluding Ctrl-key, which is usually not for text input. In the
+ worst case the autocompletion won't be allowed ever, which is
+ much more acceptable behavior than having no ability to disable
+ it and annoying the users. See also this WONTFIX Android bug:
+ https://bugs.chromium.org/p/chromium/issues/detail?id=118639 */
+ } else if(event.key != 'Backspace' && event.key != 'Delete' && !event.metaKey && (!event.ctrlKey || event.altKey)) {
+ Search.autocompleteNextInputEvent = true;
+ /* Otherwise reset the flag, because when the user would press e.g.
+ the 'a' key and then e.g. ArrowRight (which doesn't trigger
+ oninput), a Backspace after would still result in
+ autocompleteNextInputEvent, because nothing reset it back. */
+ } else {
+ Search.autocompleteNextInputEvent = false;
}
/* Search hidden */
let buffer = fs.readFileSync(path.join(__dirname, "js-test-data/empty.bin"));
assert.ok(Search.init(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)));
assert.equal(Search.symbolCount, 0);
- assert.deepEqual(Search.search(''), []);
+ assert.deepEqual(Search.search(''), [[], '']);
}
/* Search */
assert.equal(Search.maxResults, 100);
/* Blow up */
- let resultsForM = [
+ let resultsForM = [[
{ name: 'Math',
url: 'namespaceMath.html',
flags: 16,
{ name: 'Math::Range::min() const',
url: 'classMath_1_1Range.html#min',
flags: 109,
- suffixLength: 10 }];
+ suffixLength: 10 }], ''];
assert.deepEqual(Search.search('m'), resultsForM);
/* Add more characters */
- assert.deepEqual(Search.search('min'), [
+ assert.deepEqual(Search.search('min'), [[
{ name: 'Math::min(int, int)',
url: 'namespaceMath.html#min',
flags: 105,
{ name: 'Math::Range::min() const',
url: 'classMath_1_1Range.html#min',
flags: 109,
- suffixLength: 8 }]);
+ suffixLength: 8 }], '()']);
/* Go back, get the same thing */
assert.deepEqual(Search.search('m'), resultsForM);
/* Search for something else */
- let resultsForVec = [
+ let resultsForVec = [[
{ name: 'Math::Vector',
url: 'classMath_1_1Vector.html',
flags: 40|2, /* Deprecated */
- suffixLength: 3 }];
+ suffixLength: 3 }], 'tor'];
assert.deepEqual(Search.search('vec'), resultsForVec);
/* Uppercase things and spaces */
assert.deepEqual(Search.search(' Vec '), resultsForVec);
/* Not found */
- assert.deepEqual(Search.search('pizza'), []);
+ assert.deepEqual(Search.search('pizza'), [[], '']);
/* UTF-8 decoding */
- assert.deepEqual(Search.search('su'), [
+ assert.deepEqual(Search.search('su'), [[
{ name: Search.toUtf8('Page » Subpage'),
url: 'subpage.html',
flags: 192,
- suffixLength: 5 }]);
+ suffixLength: 5 }], 'bpage']);
/* Alias */
- assert.deepEqual(Search.search('r'), [
+ assert.deepEqual(Search.search('r'), [[
{ name: 'Rectangle::Rect()',
alias: 'Math::Range',
url: 'classMath_1_1Range.html',
alias: 'Math::Range',
url: 'classMath_1_1Range.html',
flags: 40,
- suffixLength: 8 }]);
+ suffixLength: 8 }], '']);
}
/* Search, limiting the results to 3 */
assert.equal(Search.dataSize, 638);
assert.equal(Search.symbolCount, 7);
assert.equal(Search.maxResults, 3);
- assert.deepEqual(Search.search('m'), [
+ assert.deepEqual(Search.search('m'), [[
{ name: 'Math',
url: 'namespaceMath.html',
flags: 16,
{ name: 'Math::Vector::min() const',
url: 'classMath_1_1Vector.html#min',
flags: 105,
- suffixLength: 10 }]);
+ suffixLength: 10 }], '']);
}
/* Search loaded from a base85-encoded file should work properly */
assert.equal(Search.dataSize, 640); /* some padding on the end, that's okay */
assert.equal(Search.symbolCount, 7);
assert.equal(Search.maxResults, 100);
- assert.deepEqual(Search.search('min'), [
+ assert.deepEqual(Search.search('min'), [[
{ name: 'Math::min(int, int)',
url: 'namespaceMath.html#min',
flags: 105,
{ name: 'Math::Range::min() const',
url: 'classMath_1_1Range.html#min',
flags: 109,
- suffixLength: 8 }]);
+ suffixLength: 8 }], '()']);
}
/* Search, Unicode */
assert.ok(Search.init(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)));
assert.equal(Search.dataSize, 124);
assert.equal(Search.symbolCount, 2);
- assert.deepEqual(Search.search('h'), [
+ /* Both "Hýždě" and "Hárá" have common autocompletion to "h\xA1", which is
+ not valid UTF-8, so it has to get truncated */
+ assert.deepEqual(Search.search('h'), [[
{ name: Search.toUtf8('Hárá'),
url: '#b',
flags: 192,
{ name: Search.toUtf8('Hýždě'),
url: '#a',
flags: 192,
- suffixLength: 7 }]);
- assert.deepEqual(Search.search('hý'), [
+ suffixLength: 7 }], '']);
+ /* These autocompletions are valid UTF-8, so nothing gets truncated */
+ assert.deepEqual(Search.search('hý'), [[
{ name: Search.toUtf8('Hýždě'),
url: '#a',
flags: 192,
- suffixLength: 5 }]);
- assert.deepEqual(Search.search('há'), [
+ suffixLength: 5 }], 'ždě']);
+ assert.deepEqual(Search.search('há'), [[
{ name: Search.toUtf8('Hárá'),
url: '#b',
flags: 192,
- suffixLength: 3 }]);
+ suffixLength: 3 }], 'rá']);
}
/* Properly combine heavily nested URLs */
assert.ok(Search.init(buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength)));
assert.equal(Search.dataSize, 295);
assert.equal(Search.symbolCount, 4);
- assert.deepEqual(Search.search('geo'), [
+ assert.deepEqual(Search.search('geo'), [[
{ name: 'Magnum::Math::Geometry',
url: 'namespaceMagnum_1_1Math_1_1Geometry.html',
flags: 24,
- suffixLength: 5 }]);
+ suffixLength: 5 }], 'metry']);
- assert.deepEqual(Search.search('ra'), [
+ assert.deepEqual(Search.search('ra'), [[
{ name: 'Magnum::Math::Range',
url: 'classMagnum_1_1Math_1_1Range.html',
flags: 40,
- suffixLength: 3 }]);
+ suffixLength: 3 }], 'nge']);
}
/* Not testing Search.download() because the xmlhttprequest npm package is *crap* */