Skip to content

Commit 23ac435

Browse files
committed
fix
1 parent 6a516a0 commit 23ac435

File tree

1 file changed

+46
-8
lines changed

1 file changed

+46
-8
lines changed

web_src/js/features/comp/TextExpander.ts

Lines changed: 46 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,35 @@ 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+
const shouldShowIssueSuggestions = () => {
55+
const posVal = textarea.value.substring(0, textarea.selectionStart);
56+
const lineStart = posVal.lastIndexOf('\n');
57+
const keyStart = posVal.lastIndexOf('#');
58+
return keyStart > lineStart;
59+
};
60+
61+
const debouncedIssueSuggestions = debounce(async (key: string, text: string): Promise<TextExpanderProvideResult> => {
62+
// Upstream bug: when using "multiword", TextExpander will get wrong "key" position.
63+
// To reproduce, use the "await sleep" below,
64+
// then use content "close #20\nclose #20\close #20", keep changing the last line `#20` part from the end (including removing the `#`)
65+
// 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).
66+
if (!shouldShowIssueSuggestions()) return {matched: false};
67+
// await sleep(Math.random() * 1000); // help to reproduce the text-expander bug
68+
const ret = await fetchIssueSuggestions(key, text);
69+
if (!shouldShowIssueSuggestions()) return {matched: false};
70+
return ret;
71+
}, 300); // to match onInputDebounce delay
72+
73+
expander.addEventListener('text-expander-change', (e: TextExpanderChangeEvent) => {
74+
const {key, text, provide} = e.detail;
3875
if (key === ':') {
3976
const matches = matchEmoji(text);
4077
if (!matches.length) return provide({matched: false});
@@ -82,10 +119,11 @@ export function initTextExpander(expander: TextExpanderElement) {
82119

83120
provide({matched: true, fragment: ul});
84121
} else if (key === '#') {
85-
provide(debouncedSuggestIssues(key, text));
122+
provide(debouncedIssueSuggestions(key, text));
86123
}
87124
});
88-
expander?.addEventListener('text-expander-value', ({detail}: Record<string, any>) => {
125+
126+
expander.addEventListener('text-expander-value', ({detail}: Record<string, any>) => {
89127
if (detail?.item) {
90128
// add a space after @mentions and #issue as it's likely the user wants one
91129
const suffix = ['@', '#'].includes(detail.key) ? ' ' : '';

0 commit comments

Comments
 (0)