(core) Fixing ACIndex highlightMatches functions

Summary:
Highlighting wasn't working correctly for the new
normalized search for autocomplate widgets.

Test Plan: Existing tests

Reviewers: alexmojaki

Reviewed By: alexmojaki

Differential Revision: https://phab.getgrist.com/D3602
This commit is contained in:
Jarosław Sadziński 2022-08-26 08:05:05 +02:00
parent 2cb783ea7b
commit b6f5718ad0
3 changed files with 55 additions and 28 deletions

View File

@ -11,6 +11,7 @@ import {localeCompare, nativeCompare, sortedIndex} from 'app/common/gutil';
import {DomContents} from 'grainjs';
import escapeRegExp = require("lodash/escapeRegExp");
import deburr = require("lodash/deburr");
import split = require("lodash/split");
export interface ACItem {
// This should be a trimmed lowercase version of the item's text. It may be an accessor.
@ -243,11 +244,17 @@ function highlightMatches(searchWords: string[], text: string): string[] {
for (let i = 0; i < textParts.length; i += 2) {
const word = textParts[i];
const separator = textParts[i + 1] || '';
const prefixLen = findLongestPrefixLen(word.toLowerCase(), searchWords);
// deburr (remove diacritics) was used to produce searchWords, so `word` needs to match that.
const prefixLen = findLongestPrefixLen(deburr(word).toLowerCase(), searchWords);
if (prefixLen === 0) {
outputs[outputs.length - 1] += word + separator;
} else {
outputs.push(word.slice(0, prefixLen), word.slice(prefixLen) + separator);
// Split into unicode 'characters' that keep diacritics combined
const chars = split(word, '');
outputs.push(
chars.slice(0, prefixLen).join(''),
chars.slice(prefixLen).join('') + separator
);
}
}
return outputs;

View File

@ -281,9 +281,18 @@ describe('ACIndex', function() {
it('should highlight multi-byte unicode', function() {
const acIndex = new ACIndexImpl(['Lorem ipsum 𝌆 dolor sit ameͨ͆t.', "mañana", "Москва"].map(makeItem), 3);
const acResult: ACResults<TestACItem> = acIndex.search("mañ моск am");
let acResult: ACResults<TestACItem> = acIndex.search("mañ моск am");
assert.deepEqual(acResult.items.map(i => acResult.highlightFunc(i.text)),
[["", "Моск", "ва"], ["", "mañ", "ana"], ["Lorem ipsum 𝌆 dolor sit ", "am", "eͨ͆t."]]);
const original = "ameͨ͆";
assert.equal(original.length, 5);
for (let end = 3; end <= original.length; end++) {
const text = original.slice(0, end); // i.e. test: ame, ameͨ, ameͨ͆ (hard to see the difference in some editors)
acResult = acIndex.search(text);
assert.deepEqual(acResult.items.map(i => acResult.highlightFunc(i.text))[0],
["Lorem ipsum 𝌆 dolor sit ", original, "t."]);
}
});
it('should match a brute-force scoring implementation', function() {

View File

@ -456,32 +456,34 @@ export async function rightClick(cell: WebElement) {
* grid view or field name for detail view.
*/
export async function getCursorPosition() {
const section = await driver.findWait('.active_section', 4000);
const cursor = await section.findWait('.active_cursor', 1000);
// Query assuming the cursor is in a GridView and a DetailView, then use whichever query data
// works out.
const [colIndex, rowIndex, rowNum, colName] = await Promise.all([
catchNoSuchElem(() => cursor.findClosest('.field').index()),
catchNoSuchElem(() => cursor.findClosest('.gridview_row').index()),
catchNoSuchElem(() => cursor.findClosest('.g_record_detail').find('.detail_row_num').getText()),
catchNoSuchElem(() => cursor.findClosest('.g_record_detail_el')
.find('.g_record_detail_label').getText())
]);
if (rowNum && colName) {
// This must be a detail view, and we just got the info we need.
return {rowNum: parseInt(rowNum, 10), col: colName};
} else {
// We might be on a single card record
const counter = await section.findAll(".grist-single-record__menu__count");
if (counter.length) {
const cardRow = (await counter[0].getText()).split(' OF ')[0];
return { rowNum : parseInt(cardRow), col: colName };
return await retryOnStale(async () => {
const section = await driver.findWait('.active_section', 4000);
const cursor = await section.findWait('.active_cursor', 1000);
// Query assuming the cursor is in a GridView and a DetailView, then use whichever query data
// works out.
const [colIndex, rowIndex, rowNum, colName] = await Promise.all([
catchNoSuchElem(() => cursor.findClosest('.field').index()),
catchNoSuchElem(() => cursor.findClosest('.gridview_row').index()),
catchNoSuchElem(() => cursor.findClosest('.g_record_detail').find('.detail_row_num').getText()),
catchNoSuchElem(() => cursor.findClosest('.g_record_detail_el')
.find('.g_record_detail_label').getText())
]);
if (rowNum && colName) {
// This must be a detail view, and we just got the info we need.
return {rowNum: parseInt(rowNum, 10), col: colName};
} else {
// We might be on a single card record
const counter = await section.findAll(".grist-single-record__menu__count");
if (counter.length) {
const cardRow = (await counter[0].getText()).split(' OF ')[0];
return { rowNum : parseInt(cardRow), col: colName };
}
// Otherwise, it's a grid view, and we need to use indices to look up the info.
const gridRows = await section.findAll('.gridview_data_row_num');
const gridRowNum = await gridRows[rowIndex].getText();
return { rowNum: parseInt(gridRowNum, 10), col: colIndex };
}
// Otherwise, it's a grid view, and we need to use indices to look up the info.
const gridRows = await section.findAll('.gridview_data_row_num');
const gridRowNum = await gridRows[rowIndex].getText();
return { rowNum: parseInt(gridRowNum, 10), col: colIndex };
}
});
}
/**
@ -496,6 +498,15 @@ async function catchNoSuchElem(query: () => any) {
}
}
async function retryOnStale<T>(query: () => Promise<T>): Promise<T> {
try {
return await query();
} catch (err) {
if (err instanceof error.StaleElementReferenceError) { return await query(); }
throw err;
}
}
/**
* Type keys in the currently active cell, then hit Enter to save, and wait for the server.