Skip to content

Commit 0ba0f3d

Browse files
samhoooodebanjum
andauthored
Show autocomplete suggestions for File Query Filters on Obsidian App (#1128)
- When you type in search modal, and matches the pattern `file:`, you should see list of all files in vault and non-vault - This list is filtered down as you type more letters ### Technical Details - Added file filter mode (`isFileFilterMode` state) to filter search results by specific files - Updated `getSuggestions()` function to search file from vault and non-vault via khoj backend. - Updated the selection behavior to handle both file selection and search result selection Closes #1025 --------- Co-authored-by: Debanjum <debanjum@gmail.com>
1 parent 60a2f6d commit 0ba0f3d

File tree

2 files changed

+109
-7
lines changed

2 files changed

+109
-7
lines changed

src/interface/obsidian/src/search_modal.ts

Lines changed: 104 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
1616
currentController: AbortController | null = null; // To cancel requests
1717
isLoading: boolean = false;
1818
loadingEl: HTMLElement;
19+
private isFileFilterMode: boolean = false;
20+
private fileSelected: string = "";
21+
private allFiles: Array<{path: string, inVault: boolean}> = [];
22+
private resultsTitle: HTMLDivElement;
1923

2024
constructor(app: App, setting: KhojSetting, find_similar_notes: boolean = false) {
2125
super(app);
@@ -85,6 +89,46 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
8589

8690
// Set Placeholder Text for Modal
8791
this.setPlaceholder('Search with Khoj...');
92+
93+
// Initialize allFiles with files in vault
94+
this.allFiles = this.app.vault.getFiles().map(file => ({
95+
path: file.path,
96+
inVault: true
97+
}));
98+
99+
// Update isFileFilterMode when input changes
100+
this.inputEl.addEventListener('input', () => {
101+
// Match file: at the end of input, with an optional unquoted partial path
102+
const fileFilterMatch = this.inputEl.value.match(/file:([^"\s]*)$/);
103+
if (fileFilterMatch) {
104+
// Enter file filter mode when we see an unquoted file: token
105+
this.isFileFilterMode = true;
106+
} else {
107+
// Exit file filter mode when input no longer ends with an unquoted file: token
108+
this.isFileFilterMode = false;
109+
this.fileSelected = "";
110+
}
111+
});
112+
113+
// Override selectSuggestion to prevent modal close during file filter selection
114+
const originalSelectSuggestion = this.selectSuggestion.bind(this);
115+
this.selectSuggestion = async (value: SearchResult & { inVault: boolean }, evt: MouseEvent | KeyboardEvent) => {
116+
if (this.isFileFilterMode) {
117+
// In file filter mode, handle selection without closing the modal
118+
await this.onChooseSuggestion(value, evt);
119+
} else {
120+
// For normal search results, use the original behavior
121+
originalSelectSuggestion(value, evt);
122+
}
123+
};
124+
125+
// Add title element
126+
this.resultsTitle = createDiv();
127+
this.resultsTitle.style.padding = "8px";
128+
this.resultsTitle.style.fontWeight = "bold";
129+
130+
// Insert title before results container
131+
this.resultContainerEl.parentElement?.insertBefore(this.resultsTitle, this.resultContainerEl);
88132
}
89133

90134
// Check if the file exists in the vault
@@ -99,7 +143,30 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
99143
}
100144

101145
async getSuggestions(query: string): Promise<SearchResult[]> {
102-
// Do not show loading if the query is empty
146+
// Check if we are in file filter mode and input matches file filter pattern
147+
const fileFilterMatch = query.match(/file:([^"\s]*)$/);
148+
if (this.isFileFilterMode && fileFilterMatch) {
149+
const partialPath = fileFilterMatch[1] || '';
150+
// Update title for file filter mode
151+
this.resultsTitle.setText("Select a file:");
152+
// Return filtered file suggestions
153+
return this.allFiles
154+
.filter(file => file.path.toLowerCase().includes(partialPath.toLowerCase().trim()))
155+
.map(file => ({
156+
entry: file.path,
157+
file: file.path,
158+
inVault: file.inVault
159+
}));
160+
}
161+
162+
// Update title for search results
163+
if (query.trim()) {
164+
this.resultsTitle.setText("Search results:");
165+
} else {
166+
this.resultsTitle.setText("");
167+
}
168+
169+
// If not in file filter mode, continue with normal search
103170
if (!query.trim()) {
104171
this.isLoading = false;
105172
this.updateLoadingState();
@@ -138,22 +205,29 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
138205

139206
const data = await response.json();
140207

141-
// Parse search results
208+
// Parse search results and update allFiles with any new non-vault files
142209
let results = data
143210
.filter((result: any) =>
144211
!this.find_similar_notes || !result.additional.file.endsWith(this.app.workspace.getActiveFile()?.path)
145212
)
146213
.map((result: any) => {
214+
const isInVault = this.isFileInVault(result.additional.file);
215+
216+
// Add new non-vault files to allFiles if they don't exist
217+
if (!this.allFiles.some(file => file.path === result.additional.file)) {
218+
this.allFiles.push({
219+
path: result.additional.file,
220+
inVault: isInVault
221+
});
222+
}
223+
147224
return {
148225
entry: result.entry,
149226
file: result.additional.file,
150-
inVault: this.isFileInVault(result.additional.file)
227+
inVault: isInVault
151228
} as SearchResult & { inVault: boolean };
152229
})
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-
});
230+
.sort((a: SearchResult & { inVault: boolean }, b: SearchResult & { inVault: boolean }) => Number(b.inVault) - Number(a.inVault));
157231

158232
this.query = query;
159233

@@ -203,6 +277,15 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
203277
}
204278

205279
async renderSuggestion(result: SearchResult & { inVault: boolean }, el: HTMLElement) {
280+
if (this.isFileFilterMode) {
281+
// Render file suggestions
282+
el.createEl("div", {
283+
text: result.entry,
284+
cls: "khoj-file-suggestion"
285+
});
286+
return;
287+
}
288+
206289
// Max number of lines to render
207290
let lines_to_render = 8;
208291

@@ -251,6 +334,20 @@ export class KhojSearchModal extends SuggestModal<SearchResult> {
251334
}
252335

253336
async onChooseSuggestion(result: SearchResult & { inVault: boolean }, _: MouseEvent | KeyboardEvent) {
337+
if (this.isFileFilterMode) {
338+
// When a file suggestion is selected, append it to the current input
339+
const currentValue = this.inputEl.value;
340+
const beforeFile = currentValue.substring(0, currentValue.lastIndexOf('file:'));
341+
this.inputEl.value = `${beforeFile}file:"${result.entry}"`;
342+
// Set fileSelected to the selected file
343+
this.fileSelected = result.entry;
344+
// Reset isFileFilterMode when a file is selected
345+
this.isFileFilterMode = false;
346+
// Trigger input event to refresh suggestions
347+
this.inputEl.dispatchEvent(new Event('input'));
348+
return;
349+
}
350+
254351
// Only open files that are in the vault
255352
if (!result.inVault) {
256353
new Notice("This file is not in your vault");

src/interface/obsidian/styles.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,11 @@ img.copy-icon {
13001300
}
13011301
}
13021302

1303+
.khoj-file-suggestion {
1304+
padding: 8px;
1305+
color: var(--text-muted);
1306+
}
1307+
13031308
.khoj-similar-message {
13041309
text-align: center;
13051310
padding: 20px;

0 commit comments

Comments
 (0)