Skip to content

Commit 3cc8737

Browse files
Improve emoji and mention matching (#24255)
Prioritize matches that start with the given text, then matches that contain the given text. I wanted to add a heart emoji on a pull request comment so I started writing `:`, `h`, `e`, `a`, `r` (at this point I still couldn't find the heart), `t`... The heart was not on the list, that's weird - it feels like I made a typo or a mistake. This fixes that. This also feels more like GitHub's emoji auto-complete. # Before ![image](https://user-images.githubusercontent.com/20454870/233630750-bd0a1b76-33d0-41d4-9218-a37b670c42b0.png) # After ![image](https://user-images.githubusercontent.com/20454870/233775128-05e67fc1-e092-4025-b6f7-1fd8e5f71e87.png) --------- Signed-off-by: Yarden Shoham <[email protected]> Co-authored-by: silverwind <[email protected]>
1 parent ce9c1dd commit 3cc8737

File tree

4 files changed

+103
-18
lines changed

4 files changed

+103
-18
lines changed

web_src/js/features/comp/ComboMarkdownEditor.js

+4-18
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import {attachTribute} from '../tribute.js';
55
import {hideElem, showElem, autosize} from '../../utils/dom.js';
66
import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js';
77
import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
8-
import {emojiKeys, emojiString} from '../emoji.js';
8+
import {emojiString} from '../emoji.js';
99
import {renderPreviewPanelContent} from '../repo-editor.js';
10+
import {matchEmoji, matchMention} from '../../utils/match.js';
1011

1112
let elementIdCounter = 0;
12-
const maxExpanderMatches = 6;
1313

1414
/**
1515
* validate if the given textarea is non-empty.
@@ -106,14 +106,7 @@ class ComboMarkdownEditor {
106106
const expander = this.container.querySelector('text-expander');
107107
expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => {
108108
if (key === ':') {
109-
const matches = [];
110-
const textLowerCase = text.toLowerCase();
111-
for (const name of emojiKeys) {
112-
if (name.toLowerCase().includes(textLowerCase)) {
113-
matches.push(name);
114-
if (matches.length >= maxExpanderMatches) break;
115-
}
116-
}
109+
const matches = matchEmoji(text);
117110
if (!matches.length) return provide({matched: false});
118111

119112
const ul = document.createElement('ul');
@@ -129,14 +122,7 @@ class ComboMarkdownEditor {
129122

130123
provide({matched: true, fragment: ul});
131124
} else if (key === '@') {
132-
const matches = [];
133-
const textLowerCase = text.toLowerCase();
134-
for (const obj of window.config.tributeValues) {
135-
if (obj.key.toLowerCase().includes(textLowerCase)) {
136-
matches.push(obj);
137-
if (matches.length >= maxExpanderMatches) break;
138-
}
139-
}
125+
const matches = matchMention(text);
140126
if (!matches.length) return provide({matched: false});
141127

142128
const ul = document.createElement('ul');

web_src/js/test/setup.js

+9
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,13 @@ window.config = {
33
pageData: {},
44
i18n: {},
55
appSubUrl: '',
6+
tributeValues: [
7+
{key: 'user1 User 1', value: 'user1', name: 'user1', fullname: 'User 1', avatar: 'https://avatar1.com'},
8+
{key: 'user2 User 2', value: 'user2', name: 'user2', fullname: 'User 2', avatar: 'https://avatar2.com'},
9+
{key: 'user3 User 3', value: 'user3', name: 'user3', fullname: 'User 3', avatar: 'https://avatar3.com'},
10+
{key: 'user4 User 4', value: 'user4', name: 'user4', fullname: 'User 4', avatar: 'https://avatar4.com'},
11+
{key: 'user5 User 5', value: 'user5', name: 'user5', fullname: 'User 5', avatar: 'https://avatar5.com'},
12+
{key: 'user6 User 6', value: 'user6', name: 'user6', fullname: 'User 6', avatar: 'https://avatar6.com'},
13+
{key: 'user7 User 7', value: 'user7', name: 'user7', fullname: 'User 7', avatar: 'https://avatar7.com'},
14+
],
615
};

web_src/js/utils/match.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import emojis from '../../../assets/emoji.json';
2+
3+
const maxMatches = 6;
4+
5+
function sortAndReduce(map) {
6+
const sortedMap = new Map([...map.entries()].sort((a, b) => a[1] - b[1]));
7+
return Array.from(sortedMap.keys()).slice(0, maxMatches);
8+
}
9+
10+
export function matchEmoji(queryText) {
11+
const query = queryText.toLowerCase().replaceAll('_', ' ');
12+
if (!query) return emojis.slice(0, maxMatches).map((e) => e.aliases[0]);
13+
14+
// results is a map of weights, lower is better
15+
const results = new Map();
16+
for (const {aliases} of emojis) {
17+
const mainAlias = aliases[0];
18+
for (const [aliasIndex, alias] of aliases.entries()) {
19+
const index = alias.replaceAll('_', ' ').indexOf(query);
20+
if (index === -1) continue;
21+
const existing = results.get(mainAlias);
22+
const rankedIndex = index + aliasIndex;
23+
results.set(mainAlias, existing ? existing - rankedIndex : rankedIndex);
24+
}
25+
}
26+
27+
return sortAndReduce(results);
28+
}
29+
30+
export function matchMention(queryText) {
31+
const query = queryText.toLowerCase();
32+
33+
// results is a map of weights, lower is better
34+
const results = new Map();
35+
for (const obj of window.config.tributeValues) {
36+
const index = obj.key.toLowerCase().indexOf(query);
37+
if (index === -1) continue;
38+
const existing = results.get(obj);
39+
results.set(obj, existing ? existing - index : index);
40+
}
41+
42+
return sortAndReduce(results);
43+
}

web_src/js/utils/match.test.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {test, expect} from 'vitest';
2+
import {matchEmoji, matchMention} from './match.js';
3+
4+
test('matchEmoji', () => {
5+
expect(matchEmoji('')).toEqual([
6+
'+1',
7+
'-1',
8+
'100',
9+
'1234',
10+
'1st_place_medal',
11+
'2nd_place_medal',
12+
]);
13+
14+
expect(matchEmoji('hea')).toEqual([
15+
'headphones',
16+
'headstone',
17+
'health_worker',
18+
'hear_no_evil',
19+
'heard_mcdonald_islands',
20+
'heart',
21+
]);
22+
23+
expect(matchEmoji('hear')).toEqual([
24+
'hear_no_evil',
25+
'heard_mcdonald_islands',
26+
'heart',
27+
'heart_decoration',
28+
'heart_eyes',
29+
'heart_eyes_cat',
30+
]);
31+
32+
expect(matchEmoji('poo')).toEqual([
33+
'poodle',
34+
'hankey',
35+
'spoon',
36+
'bowl_with_spoon',
37+
]);
38+
39+
expect(matchEmoji('1st_')).toEqual([
40+
'1st_place_medal',
41+
]);
42+
});
43+
44+
test('matchMention', () => {
45+
expect(matchMention('')).toEqual(window.config.tributeValues.slice(0, 6));
46+
expect(matchMention('user4')).toEqual([window.config.tributeValues[3]]);
47+
});

0 commit comments

Comments
 (0)