Skip to content

Commit e663c4a

Browse files
authored
Fix issue suggestion bug (#33389)
Fix #33388
1 parent 2cc65e3 commit e663c4a

File tree

1 file changed

+51
-8
lines changed

1 file changed

+51
-8
lines changed

web_src/js/features/comp/TextExpander.ts

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,31 @@ import {getIssueColor, getIssueIcon} from '../issue.ts';
77
import {debounce} from 'perfect-debounce';
88
import type TextExpanderElement from '@github/text-expander-element';
99

10-
const debouncedSuggestIssues = debounce((key: string, text: string) => new Promise<{matched:boolean; fragment?: HTMLElement}>(async (resolve) => {
10+
type TextExpanderProvideResult = {
11+
matched: boolean,
12+
fragment?: HTMLElement,
13+
}
14+
15+
type TextExpanderChangeEvent = Event & {
16+
detail?: {
17+
key: string,
18+
text: string,
19+
provide: (result: TextExpanderProvideResult | Promise<TextExpanderProvideResult>) => void,
20+
}
21+
}
22+
23+
async function fetchIssueSuggestions(key: string, text: string): Promise<TextExpanderProvideResult> {
1124
const issuePathInfo = parseIssueHref(window.location.href);
1225
if (!issuePathInfo.ownerName) {
1326
const repoOwnerPathInfo = parseRepoOwnerPathInfo(window.location.pathname);
1427
issuePathInfo.ownerName = repoOwnerPathInfo.ownerName;
1528
issuePathInfo.repoName = repoOwnerPathInfo.repoName;
1629
// then no issuePathInfo.indexString here, it is only used to exclude the current issue when "matchIssue"
1730
}
18-
if (!issuePathInfo.ownerName) return resolve({matched: false});
31+
if (!issuePathInfo.ownerName) return {matched: false};
1932

2033
const matches = await matchIssue(issuePathInfo.ownerName, issuePathInfo.repoName, issuePathInfo.indexString, text);
21-
if (!matches.length) return resolve({matched: false});
34+
if (!matches.length) return {matched: false};
2235

2336
const ul = createElementFromAttrs('ul', {class: 'suggestions'});
2437
for (const issue of matches) {
@@ -30,11 +43,40 @@ const debouncedSuggestIssues = debounce((key: string, text: string) => new Promi
3043
);
3144
ul.append(li);
3245
}
33-
resolve({matched: true, fragment: ul});
34-
}), 100);
46+
return {matched: true, fragment: ul};
47+
}
3548

3649
export function initTextExpander(expander: TextExpanderElement) {
37-
expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}: Record<string, any>) => {
50+
if (!expander) return;
51+
52+
const textarea = expander.querySelector<HTMLTextAreaElement>('textarea');
53+
54+
// help to fix the text-expander "multiword+promise" bug: do not show the popup when there is no "#" before current line
55+
const shouldShowIssueSuggestions = () => {
56+
const posVal = textarea.value.substring(0, textarea.selectionStart);
57+
const lineStart = posVal.lastIndexOf('\n');
58+
const keyStart = posVal.lastIndexOf('#');
59+
return keyStart > lineStart;
60+
};
61+
62+
const debouncedIssueSuggestions = debounce(async (key: string, text: string): Promise<TextExpanderProvideResult> => {
63+
// https://github.com/github/text-expander-element/issues/71
64+
// Upstream bug: when using "multiword+promise", TextExpander will get wrong "key" position.
65+
// To reproduce, comment out the "shouldShowIssueSuggestions" check, use the "await sleep" below,
66+
// then use content "close #20\nclose #20\nclose #20" (3 lines), keep changing the last line `#20` part from the end (including removing the `#`)
67+
// There will be a JS error: Uncaught (in promise) IndexSizeError: Failed to execute 'setStart' on 'Range': The offset 28 is larger than the node's length (27).
68+
69+
// check the input before the request, to avoid emitting empty query to backend (still related to the upstream bug)
70+
if (!shouldShowIssueSuggestions()) return {matched: false};
71+
// await sleep(Math.random() * 1000); // help to reproduce the text-expander bug
72+
const ret = await fetchIssueSuggestions(key, text);
73+
// check the input again to avoid text-expander using incorrect position (upstream bug)
74+
if (!shouldShowIssueSuggestions()) return {matched: false};
75+
return ret;
76+
}, 300); // to match onInputDebounce delay
77+
78+
expander.addEventListener('text-expander-change', (e: TextExpanderChangeEvent) => {
79+
const {key, text, provide} = e.detail;
3880
if (key === ':') {
3981
const matches = matchEmoji(text);
4082
if (!matches.length) return provide({matched: false});
@@ -82,10 +124,11 @@ export function initTextExpander(expander: TextExpanderElement) {
82124

83125
provide({matched: true, fragment: ul});
84126
} else if (key === '#') {
85-
provide(debouncedSuggestIssues(key, text));
127+
provide(debouncedIssueSuggestions(key, text));
86128
}
87129
});
88-
expander?.addEventListener('text-expander-value', ({detail}: Record<string, any>) => {
130+
131+
expander.addEventListener('text-expander-value', ({detail}: Record<string, any>) => {
89132
if (detail?.item) {
90133
// add a space after @mentions and #issue as it's likely the user wants one
91134
const suffix = ['@', '#'].includes(detail.key) ? ' ' : '';

0 commit comments

Comments
 (0)