| 45 | } |
| 46 | |
| 47 | export class NLPSuggest extends AbstractInputSuggest< |
| 48 | TagSuggestion | ContextSuggestion | ProjectSuggestion | StatusSuggestion |
| 49 | > { |
| 50 | private plugin: TaskNotesPlugin; |
| 51 | private textarea: HTMLInputElement | HTMLTextAreaElement; |
| 52 | private currentTrigger: "@" | "#" | "+" | "status" | null = null; |
| 53 | // Store app reference explicitly to avoid relying on plugin.app in tests and runtime |
| 54 | private obsidianApp: App; |
| 55 | // Cache ProjectMetadataResolver to avoid recreating it for each suggestion |
| 56 | private projectMetadataResolver: ProjectMetadataResolver | null = null; |
| 57 | |
| 58 | constructor( |
| 59 | app: App, |
| 60 | textareaEl: HTMLInputElement | HTMLTextAreaElement, |
| 61 | plugin: TaskNotesPlugin |
| 62 | ) { |
| 63 | super(app, textareaEl as unknown as HTMLInputElement); |
| 64 | this.plugin = plugin; |
| 65 | this.textarea = textareaEl; |
| 66 | this.obsidianApp = app; |
| 67 | } |
| 68 | |
| 69 | private getCursorPosition(): number { |
| 70 | return this.textarea.selectionStart ?? this.textarea.value.length; |
| 71 | } |
| 72 | |
| 73 | /** |
| 74 | * Helper: Check if index is at a word boundary |
| 75 | */ |
| 76 | private isBoundary(textBeforeCursor: string, index: number): boolean { |
| 77 | if (index === -1) return false; |
| 78 | if (index === 0) return true; |
| 79 | const prev = textBeforeCursor[index - 1]; |
| 80 | return !/\w/.test(prev); |
| 81 | } |
| 82 | |
| 83 | /** |
| 84 | * Find the most recent valid trigger before cursor |
| 85 | */ |
| 86 | private findActiveTrigger(textBeforeCursor: string): { |
| 87 | trigger: "@" | "#" | "+" | "status" | null; |
| 88 | triggerIndex: number; |
| 89 | queryAfterTrigger: string; |
| 90 | } { |
| 91 | const lastAtIndex = textBeforeCursor.lastIndexOf("@"); |
| 92 | const lastHashIndex = textBeforeCursor.lastIndexOf("#"); |
| 93 | const lastPlusIndex = textBeforeCursor.lastIndexOf("+"); |
| 94 | const statusTrig = (this.plugin.settings.statusSuggestionTrigger || "").trim(); |
| 95 | const lastStatusIndex = statusTrig ? textBeforeCursor.lastIndexOf(statusTrig) : -1; |
| 96 | |
| 97 | // Determine most recent valid trigger by index |
| 98 | const candidates: Array<{ type: "@" | "#" | "+" | "status"; index: number }> = [ |
| 99 | { type: "@" as const, index: lastAtIndex }, |
| 100 | { type: "#" as const, index: lastHashIndex }, |
| 101 | { type: "+" as const, index: lastPlusIndex }, |
| 102 | { type: "status" as const, index: lastStatusIndex }, |
| 103 | ].filter((c) => this.isBoundary(textBeforeCursor, c.index)); |
| 104 |
nothing calls this directly
no outgoing calls
no test coverage detected