@@ -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,52 +232,61 @@ const NodeSearchMenu = ({
204232
205233 const onSelect = useCallback (
206234 ( item : Result ) => {
207- void waitForBlock ( blockUid , textarea . value ) . then ( ( ) => {
208- onClose ( ) ;
209-
210- setTimeout ( ( ) => {
211- const originalText = getTextByBlockUid ( blockUid ) ;
212-
213- const prefix = originalText . substring ( 0 , triggerPosition ) ;
214- const suffix = originalText . substring ( textarea . selectionStart ) ;
215- const pageRef = `[[${ item . text } ]]` ;
216-
217- const newText = `${ prefix } ${ pageRef } ${ suffix } ` ;
218- void updateBlock ( { uid : blockUid , text : newText } ) . then ( ( ) => {
219- const newCursorPosition = triggerPosition + pageRef . length ;
220-
221- if ( window . roamAlphaAPI . ui . setBlockFocusAndSelection ) {
222- void window . roamAlphaAPI . ui . setBlockFocusAndSelection ( {
223- location : {
224- "block-uid" : blockUid ,
225- "window-id" : windowId ,
226- } ,
227- selection : { start : newCursorPosition } ,
228- } ) ;
229- } else {
230- setTimeout ( ( ) => {
231- const textareaElements = document . querySelectorAll ( "textarea" ) ;
232- 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- ) ;
241- break ;
242- }
243- }
244- } , 50 ) ;
245- }
246- } ) ;
247- posthog . capture ( "Discourse Node: Selected from Search Menu" , {
248- id : item . id ,
249- text : item . text ,
250- } ) ;
251- } , 10 ) ;
252- } ) ;
235+ if ( ! blockUid ) {
236+ onClose ( ) ;
237+ return ;
238+ }
239+ void waitForBlock ( { uid : blockUid , text : textarea . value } )
240+ . then ( ( ) => {
241+ onClose ( ) ;
242+
243+ setTimeout ( ( ) => {
244+ const originalText = getTextByBlockUid ( blockUid ) ;
245+
246+ const prefix = originalText . substring ( 0 , triggerPosition ) ;
247+ const suffix = originalText . substring ( textarea . selectionStart ) ;
248+ const pageRef = `[[${ item . text } ]]` ;
249+
250+ const newText = `${ prefix } ${ pageRef } ${ suffix } ` ;
251+ void updateBlock ( { uid : blockUid , text : newText } ) . then ( ( ) => {
252+ const newCursorPosition = triggerPosition + pageRef . length ;
253+
254+ if ( window . roamAlphaAPI . ui . setBlockFocusAndSelection ) {
255+ void window . roamAlphaAPI . ui . setBlockFocusAndSelection ( {
256+ location : {
257+ // eslint-disable-next-line @typescript-eslint/naming-convention
258+ "block-uid" : blockUid ,
259+ // eslint-disable-next-line @typescript-eslint/naming-convention
260+ "window-id" : windowId ,
261+ } ,
262+ selection : { start : newCursorPosition } ,
263+ } ) ;
264+ } else {
265+ setTimeout ( ( ) => {
266+ const textareaElements =
267+ document . querySelectorAll ( "textarea" ) ;
268+ for ( const el of textareaElements ) {
269+ if ( getUids ( el ) . blockUid === blockUid ) {
270+ el . focus ( ) ;
271+ el . setSelectionRange (
272+ newCursorPosition ,
273+ newCursorPosition ,
274+ ) ;
275+ break ;
276+ }
277+ }
278+ } , 50 ) ;
279+ }
280+ } ) ;
281+ posthog . capture ( "Discourse Node: Selected from Search Menu" , {
282+ id : item . id ,
283+ text : item . text ,
284+ } ) ;
285+ } , 10 ) ;
286+ } )
287+ . catch ( ( error ) => {
288+ console . error ( "Error waiting for block:" , error ) ;
289+ } ) ;
253290 } ,
254291 [ blockUid , onClose , textarea , triggerPosition , windowId ] ,
255292 ) ;
@@ -553,11 +590,13 @@ export const renderDiscourseNodeSearchMenu = (props: Props) => {
553590 parent . style . top = `${ coords . top } px` ;
554591 props . textarea . parentElement ?. insertBefore ( parent , props . textarea ) ;
555592
593+ // eslint-disable-next-line react/no-deprecated
556594 ReactDOM . render (
557595 < NodeSearchMenu
558596 { ...props }
559597 onClose = { ( ) => {
560598 props . onClose ( ) ;
599+ // eslint-disable-next-line react/no-deprecated
561600 ReactDOM . unmountComponentAtNode ( parent ) ;
562601 parent . remove ( ) ;
563602 } }
0 commit comments