1
- import { App , SuggestModal , request , MarkdownRenderer , Instruction , Platform } from 'obsidian' ;
1
+ import { App , SuggestModal , request , MarkdownRenderer , Instruction , Platform , Notice } from 'obsidian' ;
2
2
import { KhojSetting } from 'src/settings' ;
3
3
import { supportedBinaryFileTypes , createNoteAndCloseModal , getFileFromPath , getLinkToEntry , supportedImageFilesTypes } from 'src/utils' ;
4
4
@@ -13,6 +13,9 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
13
13
find_similar_notes : boolean ;
14
14
query : string = "" ;
15
15
app : App ;
16
+ currentController : AbortController | null = null ; // To cancel requests
17
+ isLoading : boolean = false ;
18
+ loadingEl : HTMLElement ;
16
19
17
20
constructor ( app : App , setting : KhojSetting , find_similar_notes : boolean = false ) {
18
21
super ( app ) ;
@@ -23,6 +26,24 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
23
26
// Hide input element in Similar Notes mode
24
27
this . inputEl . hidden = this . find_similar_notes ;
25
28
29
+ // Create loading element
30
+ this . loadingEl = createDiv ( { cls : "search-loading" } ) ;
31
+ const spinnerEl = this . loadingEl . createDiv ( { cls : "search-loading-spinner" } ) ;
32
+
33
+ this . loadingEl . style . position = "absolute" ;
34
+ this . loadingEl . style . top = "50%" ;
35
+ this . loadingEl . style . left = "50%" ;
36
+ this . loadingEl . style . transform = "translate(-50%, -50%)" ;
37
+ this . loadingEl . style . zIndex = "1000" ;
38
+ this . loadingEl . style . display = "none" ;
39
+
40
+ // Add the element to the modal
41
+ this . modalEl . appendChild ( this . loadingEl ) ;
42
+
43
+ // Customize empty state message
44
+ // @ts -ignore - Access to private property to customize the message
45
+ this . emptyStateText = "" ;
46
+
26
47
// Register Modal Keybindings to Rerank Results
27
48
this . scope . register ( [ 'Mod' ] , 'Enter' , async ( ) => {
28
49
// Re-rank when explicitly triggered by user
@@ -66,6 +87,101 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
66
87
this . setPlaceholder ( 'Search with Khoj...' ) ;
67
88
}
68
89
90
+ // Check if the file exists in the vault
91
+ private isFileInVault ( filePath : string ) : boolean {
92
+ // Normalize the path to handle different separators
93
+ const normalizedPath = filePath . replace ( / \\ / g, '/' ) ;
94
+
95
+ // Check if the file exists in the vault
96
+ return this . app . vault . getFiles ( ) . some ( file =>
97
+ file . path === normalizedPath
98
+ ) ;
99
+ }
100
+
101
+ async getSuggestions ( query : string ) : Promise < SearchResult [ ] > {
102
+ // Do not show loading if the query is empty
103
+ if ( ! query . trim ( ) ) {
104
+ this . isLoading = false ;
105
+ this . updateLoadingState ( ) ;
106
+ return [ ] ;
107
+ }
108
+
109
+ // Show loading state
110
+ this . isLoading = true ;
111
+ this . updateLoadingState ( ) ;
112
+
113
+ // Cancel previous request if it exists
114
+ if ( this . currentController ) {
115
+ this . currentController . abort ( ) ;
116
+ }
117
+
118
+ try {
119
+ // Create a new controller for this request
120
+ this . currentController = new AbortController ( ) ;
121
+
122
+ // Setup Query Khoj backend for search results
123
+ let encodedQuery = encodeURIComponent ( query ) ;
124
+ let searchUrl = `${ this . setting . khojUrl } /api/search?q=${ encodedQuery } &n=${ this . setting . resultsCount } &r=${ this . rerank } &client=obsidian` ;
125
+ let headers = {
126
+ 'Authorization' : `Bearer ${ this . setting . khojApiKey } ` ,
127
+ }
128
+
129
+ // Get search results from Khoj backend
130
+ const response = await fetch ( searchUrl , {
131
+ headers : headers ,
132
+ signal : this . currentController . signal
133
+ } ) ;
134
+
135
+ if ( ! response . ok ) {
136
+ throw new Error ( `HTTP error! status: ${ response . status } ` ) ;
137
+ }
138
+
139
+ const data = await response . json ( ) ;
140
+
141
+ // Parse search results
142
+ let results = data
143
+ . filter ( ( result : any ) =>
144
+ ! this . find_similar_notes || ! result . additional . file . endsWith ( this . app . workspace . getActiveFile ( ) ?. path )
145
+ )
146
+ . map ( ( result : any ) => {
147
+ return {
148
+ entry : result . entry ,
149
+ file : result . additional . file ,
150
+ inVault : this . isFileInVault ( result . additional . file )
151
+ } as SearchResult & { inVault : boolean } ;
152
+ } )
153
+ . sort ( ( a : SearchResult & { inVault : boolean } , b : SearchResult & { inVault : boolean } ) => {
154
+ if ( a . inVault === b . inVault ) return 0 ;
155
+ return a . inVault ? - 1 : 1 ;
156
+ } ) ;
157
+
158
+ this . query = query ;
159
+
160
+ // Hide loading state only on successful completion
161
+ this . isLoading = false ;
162
+ this . updateLoadingState ( ) ;
163
+
164
+ return results ;
165
+ } catch ( error ) {
166
+ // Ignore cancellation errors and keep loading state
167
+ if ( error . name === 'AbortError' ) {
168
+ // When cancelling, we don't want to render anything
169
+ return undefined as any ;
170
+ }
171
+
172
+ // For other errors, hide loading state
173
+ console . error ( 'Search error:' , error ) ;
174
+ this . isLoading = false ;
175
+ this . updateLoadingState ( ) ;
176
+ return [ ] ;
177
+ }
178
+ }
179
+
180
+ private updateLoadingState ( ) {
181
+ // Show or hide loading element
182
+ this . loadingEl . style . display = this . isLoading ? "block" : "none" ;
183
+ }
184
+
69
185
async onOpen ( ) {
70
186
if ( this . find_similar_notes ) {
71
187
// If markdown file is currently active
@@ -86,39 +202,33 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
86
202
}
87
203
}
88
204
89
- async getSuggestions ( query : string ) : Promise < SearchResult [ ] > {
90
- // Setup Query Khoj backend for search results
91
- let encodedQuery = encodeURIComponent ( query ) ;
92
- let searchUrl = `${ this . setting . khojUrl } /api/search?q=${ encodedQuery } &n=${ this . setting . resultsCount } &r=${ this . rerank } &client=obsidian` ;
93
- let headers = { 'Authorization' : `Bearer ${ this . setting . khojApiKey } ` }
94
-
95
- // Get search results from Khoj backend
96
- let response = await request ( { url : `${ searchUrl } ` , headers : headers } ) ;
97
-
98
- // Parse search results
99
- let results = JSON . parse ( response )
100
- . filter ( ( result : any ) => ! this . find_similar_notes || ! result . additional . file . endsWith ( this . app . workspace . getActiveFile ( ) ?. path ) )
101
- . map ( ( result : any ) => { return { entry : result . entry , file : result . additional . file } as SearchResult ; } ) ;
102
-
103
- this . query = query ;
104
- return results ;
105
- }
106
-
107
- async renderSuggestion ( result : SearchResult , el : HTMLElement ) {
205
+ async renderSuggestion ( result : SearchResult & { inVault : boolean } , el : HTMLElement ) {
108
206
// Max number of lines to render
109
207
let lines_to_render = 8 ;
110
208
111
209
// Extract filename of result
112
210
let os_path_separator = result . file . includes ( '\\' ) ? '\\' : '/' ;
113
211
let filename = result . file . split ( os_path_separator ) . pop ( ) ;
114
212
115
- // Show filename of each search result for context
116
- el . createEl ( "div" , { cls : 'khoj-result-file' } ) . setText ( filename ?? "" ) ;
213
+ // Show filename of each search result for context with appropriate color
214
+ const fileEl = el . createEl ( "div" , {
215
+ cls : `khoj-result-file ${ result . inVault ? 'in-vault' : 'not-in-vault' } `
216
+ } ) ;
217
+ fileEl . setText ( filename ?? "" ) ;
218
+
219
+ // Add a visual indication for files not in vault
220
+ if ( ! result . inVault ) {
221
+ fileEl . createSpan ( {
222
+ text : " (not in vault)" ,
223
+ cls : "khoj-result-file-status"
224
+ } ) ;
225
+ }
226
+
117
227
let result_el = el . createEl ( "div" , { cls : 'khoj-result-entry' } )
118
228
119
229
let resultToRender = "" ;
120
230
let fileExtension = filename ?. split ( "." ) . pop ( ) ?? "" ;
121
- if ( supportedImageFilesTypes . includes ( fileExtension ) && filename ) {
231
+ if ( supportedImageFilesTypes . includes ( fileExtension ) && filename && result . inVault ) {
122
232
let linkToEntry : string = filename ;
123
233
let imageFiles = this . app . vault . getFiles ( ) . filter ( file => supportedImageFilesTypes . includes ( fileExtension ) ) ;
124
234
// Find vault file of chosen search result
@@ -140,7 +250,13 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
140
250
MarkdownRenderer . renderMarkdown ( resultToRender , result_el , result . file , null ) ;
141
251
}
142
252
143
- async onChooseSuggestion ( result : SearchResult , _ : MouseEvent | KeyboardEvent ) {
253
+ async onChooseSuggestion ( result : SearchResult & { inVault : boolean } , _ : MouseEvent | KeyboardEvent ) {
254
+ // Only open files that are in the vault
255
+ if ( ! result . inVault ) {
256
+ new Notice ( "This file is not in your vault" ) ;
257
+ return ;
258
+ }
259
+
144
260
// Get all markdown, pdf and image files in vault
145
261
const mdFiles = this . app . vault . getMarkdownFiles ( ) ;
146
262
const binaryFiles = this . app . vault . getFiles ( ) . filter ( file => supportedBinaryFileTypes . includes ( file . extension ) ) ;
0 commit comments