@@ -7,18 +7,31 @@ import {getIssueColor, getIssueIcon} from '../issue.ts';
7
7
import { debounce } from 'perfect-debounce' ;
8
8
import type TextExpanderElement from '@github/text-expander-element' ;
9
9
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 > {
11
24
const issuePathInfo = parseIssueHref ( window . location . href ) ;
12
25
if ( ! issuePathInfo . ownerName ) {
13
26
const repoOwnerPathInfo = parseRepoOwnerPathInfo ( window . location . pathname ) ;
14
27
issuePathInfo . ownerName = repoOwnerPathInfo . ownerName ;
15
28
issuePathInfo . repoName = repoOwnerPathInfo . repoName ;
16
29
// then no issuePathInfo.indexString here, it is only used to exclude the current issue when "matchIssue"
17
30
}
18
- if ( ! issuePathInfo . ownerName ) return resolve ( { matched : false } ) ;
31
+ if ( ! issuePathInfo . ownerName ) return { matched : false } ;
19
32
20
33
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 } ;
22
35
23
36
const ul = createElementFromAttrs ( 'ul' , { class : 'suggestions' } ) ;
24
37
for ( const issue of matches ) {
@@ -30,11 +43,40 @@ const debouncedSuggestIssues = debounce((key: string, text: string) => new Promi
30
43
) ;
31
44
ul . append ( li ) ;
32
45
}
33
- resolve ( { matched : true , fragment : ul } ) ;
34
- } ) , 100 ) ;
46
+ return { matched : true , fragment : ul } ;
47
+ }
35
48
36
49
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 ;
38
80
if ( key === ':' ) {
39
81
const matches = matchEmoji ( text ) ;
40
82
if ( ! matches . length ) return provide ( { matched : false } ) ;
@@ -82,10 +124,11 @@ export function initTextExpander(expander: TextExpanderElement) {
82
124
83
125
provide ( { matched : true , fragment : ul } ) ;
84
126
} else if ( key === '#' ) {
85
- provide ( debouncedSuggestIssues ( key , text ) ) ;
127
+ provide ( debouncedIssueSuggestions ( key , text ) ) ;
86
128
}
87
129
} ) ;
88
- expander ?. addEventListener ( 'text-expander-value' , ( { detail} : Record < string , any > ) => {
130
+
131
+ expander . addEventListener ( 'text-expander-value' , ( { detail} : Record < string , any > ) => {
89
132
if ( detail ?. item ) {
90
133
// add a space after @mentions and #issue as it's likely the user wants one
91
134
const suffix = [ '@' , '#' ] . includes ( detail . key ) ? ' ' : '' ;
0 commit comments