@@ -7,18 +7,31 @@ import {getIssueColor, getIssueIcon} from '../issue.ts';
77import { debounce } from 'perfect-debounce' ;
88import 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
3649export 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