mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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:
parent
2cb783ea7b
commit
b6f5718ad0
@ -11,6 +11,7 @@ import {localeCompare, nativeCompare, sortedIndex} from 'app/common/gutil';
|
|||||||
import {DomContents} from 'grainjs';
|
import {DomContents} from 'grainjs';
|
||||||
import escapeRegExp = require("lodash/escapeRegExp");
|
import escapeRegExp = require("lodash/escapeRegExp");
|
||||||
import deburr = require("lodash/deburr");
|
import deburr = require("lodash/deburr");
|
||||||
|
import split = require("lodash/split");
|
||||||
|
|
||||||
export interface ACItem {
|
export interface ACItem {
|
||||||
// This should be a trimmed lowercase version of the item's text. It may be an accessor.
|
// 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) {
|
for (let i = 0; i < textParts.length; i += 2) {
|
||||||
const word = textParts[i];
|
const word = textParts[i];
|
||||||
const separator = textParts[i + 1] || '';
|
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) {
|
if (prefixLen === 0) {
|
||||||
outputs[outputs.length - 1] += word + separator;
|
outputs[outputs.length - 1] += word + separator;
|
||||||
} else {
|
} 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;
|
return outputs;
|
||||||
|
@ -281,9 +281,18 @@ describe('ACIndex', function() {
|
|||||||
|
|
||||||
it('should highlight multi-byte unicode', function() {
|
it('should highlight multi-byte unicode', function() {
|
||||||
const acIndex = new ACIndexImpl(['Lorem ipsum 𝌆 dolor sit ameͨ͆t.', "mañana", "Москва"].map(makeItem), 3);
|
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)),
|
assert.deepEqual(acResult.items.map(i => acResult.highlightFunc(i.text)),
|
||||||
[["", "Моск", "ва"], ["", "mañ", "ana"], ["Lorem ipsum 𝌆 dolor sit ", "am", "eͨ͆t."]]);
|
[["", "Моск", "ва"], ["", "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() {
|
it('should match a brute-force scoring implementation', function() {
|
||||||
|
@ -456,32 +456,34 @@ export async function rightClick(cell: WebElement) {
|
|||||||
* grid view or field name for detail view.
|
* grid view or field name for detail view.
|
||||||
*/
|
*/
|
||||||
export async function getCursorPosition() {
|
export async function getCursorPosition() {
|
||||||
const section = await driver.findWait('.active_section', 4000);
|
return await retryOnStale(async () => {
|
||||||
const cursor = await section.findWait('.active_cursor', 1000);
|
const section = await driver.findWait('.active_section', 4000);
|
||||||
// Query assuming the cursor is in a GridView and a DetailView, then use whichever query data
|
const cursor = await section.findWait('.active_cursor', 1000);
|
||||||
// works out.
|
// Query assuming the cursor is in a GridView and a DetailView, then use whichever query data
|
||||||
const [colIndex, rowIndex, rowNum, colName] = await Promise.all([
|
// works out.
|
||||||
catchNoSuchElem(() => cursor.findClosest('.field').index()),
|
const [colIndex, rowIndex, rowNum, colName] = await Promise.all([
|
||||||
catchNoSuchElem(() => cursor.findClosest('.gridview_row').index()),
|
catchNoSuchElem(() => cursor.findClosest('.field').index()),
|
||||||
catchNoSuchElem(() => cursor.findClosest('.g_record_detail').find('.detail_row_num').getText()),
|
catchNoSuchElem(() => cursor.findClosest('.gridview_row').index()),
|
||||||
catchNoSuchElem(() => cursor.findClosest('.g_record_detail_el')
|
catchNoSuchElem(() => cursor.findClosest('.g_record_detail').find('.detail_row_num').getText()),
|
||||||
.find('.g_record_detail_label').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.
|
if (rowNum && colName) {
|
||||||
return {rowNum: parseInt(rowNum, 10), col: colName};
|
// This must be a detail view, and we just got the info we need.
|
||||||
} else {
|
return {rowNum: parseInt(rowNum, 10), col: colName};
|
||||||
// We might be on a single card record
|
} else {
|
||||||
const counter = await section.findAll(".grist-single-record__menu__count");
|
// We might be on a single card record
|
||||||
if (counter.length) {
|
const counter = await section.findAll(".grist-single-record__menu__count");
|
||||||
const cardRow = (await counter[0].getText()).split(' OF ')[0];
|
if (counter.length) {
|
||||||
return { rowNum : parseInt(cardRow), col: colName };
|
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.
|
* Type keys in the currently active cell, then hit Enter to save, and wait for the server.
|
||||||
|
Loading…
Reference in New Issue
Block a user