@@ -23,9 +23,9 @@ import { getCoordsFromTextarea } from "roamjs-components/components/CursorMenu";
2323import { OnloadArgs } from "roamjs-components/types" ;
2424import getDiscourseNodes , { DiscourseNode } from "~/utils/getDiscourseNodes" ;
2525import getDiscourseNodeFormatExpression from "~/utils/getDiscourseNodeFormatExpression" ;
26- import { escapeCljString } from "~/utils/formatUtils" ;
2726import { Result } from "~/utils/types" ;
2827import { getSetting } from "~/utils/extensionSettings" ;
28+ import fuzzy from "fuzzy" ;
2929
3030type Props = {
3131 textarea : HTMLTextAreaElement ;
@@ -34,19 +34,32 @@ type Props = {
3434 triggerText : string ;
3535} ;
3636
37- const waitForBlock = (
38- uid : string ,
39- text : string ,
37+ const waitForBlock = ( {
38+ uid,
39+ text,
4040 retries = 0 ,
4141 maxRetries = 30 ,
42- ) : Promise < void > =>
42+ } : {
43+ uid : string ;
44+ text : string ;
45+ retries ?: number ;
46+ maxRetries ?: number ;
47+ } ) : Promise < void > =>
4348 getTextByBlockUid ( uid ) === text
4449 ? Promise . resolve ( )
4550 : retries >= maxRetries
4651 ? Promise . resolve ( )
4752 : new Promise ( ( resolve ) =>
4853 setTimeout (
49- ( ) => resolve ( waitForBlock ( uid , text , retries + 1 , maxRetries ) ) ,
54+ ( ) =>
55+ resolve (
56+ waitForBlock ( {
57+ uid,
58+ text,
59+ retries : retries + 1 ,
60+ maxRetries,
61+ } ) ,
62+ ) ,
5063 10 ,
5164 ) ,
5265 ) ;
@@ -63,6 +76,7 @@ const NodeSearchMenu = ({
6376 const [ discourseTypes , setDiscourseTypes ] = useState < DiscourseNode [ ] > ( [ ] ) ;
6477 const [ checkedTypes , setCheckedTypes ] = useState < Record < string , boolean > > ( { } ) ;
6578 const [ isLoading , setIsLoading ] = useState ( true ) ;
79+ const [ allNodes , setAllNodes ] = useState < Record < string , Result [ ] > > ( { } ) ;
6680 const [ searchResults , setSearchResults ] = useState < Record < string , Result [ ] > > (
6781 { } ,
6882 ) ;
@@ -89,10 +103,7 @@ const NodeSearchMenu = ({
89103 } , 300 ) ;
90104 } , [ ] ) ;
91105
92- const searchNodesForType = (
93- node : DiscourseNode ,
94- searchTerm : string ,
95- ) : Result [ ] => {
106+ const searchNodesForType = ( node : DiscourseNode ) : Result [ ] => {
96107 if ( ! node . format ) return [ ] ;
97108
98109 try {
@@ -102,19 +113,13 @@ const NodeSearchMenu = ({
102113 . replace ( / \\ / g, "\\\\" )
103114 . replace ( / " / g, '\\"' ) ;
104115
105- const searchCondition = searchTerm
106- ? `[(re-pattern "(?i).*${ escapeCljString ( searchTerm . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ) } .*") ?search-regex]
107- [(re-find ?search-regex ?node-title)]`
108- : "" ;
109-
110116 const query = `[
111117 :find
112118 (pull ?node [:block/string :node/title :block/uid])
113119 :where
114120 [(re-pattern "${ regexPattern } ") ?title-regex]
115121 [?node :node/title ?node-title]
116122 [(re-find ?title-regex ?node-title)]
117- ${ searchCondition }
118123 ]` ;
119124 const results = window . roamAlphaAPI . q ( query ) ;
120125
@@ -130,8 +135,22 @@ const NodeSearchMenu = ({
130135 }
131136 } ;
132137
138+ const filterNodesLocally = useCallback (
139+ ( nodes : Result [ ] , searchTerm : string ) : Result [ ] => {
140+ if ( ! searchTerm . trim ( ) ) return nodes ;
141+
142+ return fuzzy
143+ . filter ( searchTerm , nodes , {
144+ extract : ( node ) => node . text ,
145+ } )
146+ . map ( ( result ) => result . original )
147+ . filter ( ( node ) : node is Result => ! ! node ) ;
148+ } ,
149+ [ ] ,
150+ ) ;
151+
133152 useEffect ( ( ) => {
134- const fetchNodeTypes = async ( ) => {
153+ const fetchNodeTypes = ( ) => {
135154 setIsLoading ( true ) ;
136155
137156 const allNodeTypes = getDiscourseNodes ( ) . filter (
@@ -146,6 +165,12 @@ const NodeSearchMenu = ({
146165 } ) ;
147166 setCheckedTypes ( initialCheckedTypes ) ;
148167
168+ const allNodesCache : Record < string , Result [ ] > = { } ;
169+ allNodeTypes . forEach ( ( type ) => {
170+ allNodesCache [ type . type ] = searchNodesForType ( type ) ;
171+ } ) ;
172+ setAllNodes ( allNodesCache ) ;
173+
149174 const initialSearchResults = Object . fromEntries (
150175 allNodeTypes . map ( ( type ) => [ type . type , [ ] ] ) ,
151176 ) ;
@@ -158,22 +183,25 @@ const NodeSearchMenu = ({
158183 } , [ ] ) ;
159184
160185 useEffect ( ( ) => {
161- if ( isLoading ) return ;
186+ if ( isLoading || Object . keys ( allNodes ) . length === 0 ) return ;
162187
163188 const newResults : Record < string , Result [ ] > = { } ;
164189
165190 discourseTypes . forEach ( ( type ) => {
166- newResults [ type . type ] = searchNodesForType ( type , searchTerm ) ;
191+ const cachedNodes = allNodes [ type . type ] || [ ] ;
192+ newResults [ type . type ] = filterNodesLocally ( cachedNodes , searchTerm ) ;
167193 } ) ;
168194
169195 setSearchResults ( newResults ) ;
170- } , [ searchTerm , isLoading , discourseTypes ] ) ;
196+ } , [ searchTerm , isLoading , allNodes , discourseTypes , filterNodesLocally ] ) ;
171197
172198 const menuRef = useRef < HTMLUListElement > ( null ) ;
173199 const { [ "block-uid" ] : blockUid , [ "window-id" ] : windowId } = useMemo (
174200 ( ) =>
175201 window . roamAlphaAPI . ui . getFocusedBlock ( ) || {
202+ // eslint-disable-next-line @typescript-eslint/naming-convention
176203 "block-uid" : "" ,
204+ // eslint-disable-next-line @typescript-eslint/naming-convention
177205 "window-id" : "" ,
178206 } ,
179207 [ ] ,
@@ -204,7 +232,7 @@ const NodeSearchMenu = ({
204232
205233 const onSelect = useCallback (
206234 ( item : Result ) => {
207- void waitForBlock ( blockUid , textarea . value ) . then ( ( ) => {
235+ void waitForBlock ( { uid : blockUid , text : textarea . value } ) . then ( ( ) => {
208236 onClose ( ) ;
209237
210238 setTimeout ( ( ) => {
@@ -215,13 +243,15 @@ const NodeSearchMenu = ({
215243 const pageRef = `[[${ item . text } ]]` ;
216244
217245 const newText = `${ prefix } ${ pageRef } ${ suffix } ` ;
218- updateBlock ( { uid : blockUid , text : newText } ) . then ( ( ) => {
246+ void updateBlock ( { uid : blockUid , text : newText } ) . then ( ( ) => {
219247 const newCursorPosition = triggerPosition + pageRef . length ;
220248
221249 if ( window . roamAlphaAPI . ui . setBlockFocusAndSelection ) {
222- window . roamAlphaAPI . ui . setBlockFocusAndSelection ( {
250+ void window . roamAlphaAPI . ui . setBlockFocusAndSelection ( {
223251 location : {
252+ // eslint-disable-next-line @typescript-eslint/naming-convention
224253 "block-uid" : blockUid ,
254+ // eslint-disable-next-line @typescript-eslint/naming-convention
225255 "window-id" : windowId ,
226256 } ,
227257 selection : { start : newCursorPosition } ,
@@ -230,14 +260,9 @@ const NodeSearchMenu = ({
230260 setTimeout ( ( ) => {
231261 const textareaElements = document . querySelectorAll ( "textarea" ) ;
232262 for ( const el of textareaElements ) {
233- if (
234- getUids ( el as HTMLTextAreaElement ) . blockUid === blockUid
235- ) {
236- ( el as HTMLTextAreaElement ) . focus ( ) ;
237- ( el as HTMLTextAreaElement ) . setSelectionRange (
238- newCursorPosition ,
239- newCursorPosition ,
240- ) ;
263+ if ( getUids ( el ) . blockUid === blockUid ) {
264+ el . focus ( ) ;
265+ el . setSelectionRange ( newCursorPosition , newCursorPosition ) ;
241266 break ;
242267 }
243268 }
@@ -553,11 +578,13 @@ export const renderDiscourseNodeSearchMenu = (props: Props) => {
553578 parent . style . top = `${ coords . top } px` ;
554579 props . textarea . parentElement ?. insertBefore ( parent , props . textarea ) ;
555580
581+ // eslint-disable-next-line react/no-deprecated
556582 ReactDOM . render (
557583 < NodeSearchMenu
558584 { ...props }
559585 onClose = { ( ) => {
560586 props . onClose ( ) ;
587+ // eslint-disable-next-line react/no-deprecated
561588 ReactDOM . unmountComponentAtNode ( parent ) ;
562589 parent . remove ( ) ;
563590 } }
0 commit comments