import { App, Editor, Notice, EditorSuggest, EditorPosition, TFile, EditorSuggestTriggerInfo, EditorSuggestContext, } from 'obsidian'; import { GeoSearcher } from 'src/geosearch'; import * as utils from 'src/utils'; import { PluginSettings } from 'src/settings'; import { GeoSearchResult } from 'src/geosearch'; class SuggestInfo extends GeoSearchResult { context: EditorSuggestContext; } export class LocationSuggest extends EditorSuggest<SuggestInfo> { private cursorInsideGeolinkFinder = /\[(.*?)\]\(geo:.*?\)/g; private lastSearchTime = 0; private delayInMs = 250; private settings: PluginSettings; private searcher: GeoSearcher; constructor(app: App, settings: PluginSettings) { super(app); this.settings = settings; this.searcher = new GeoSearcher(app, settings); } onTrigger( cursor: EditorPosition, editor: Editor, file: TFile ): EditorSuggestTriggerInfo | null { const currentLink = this.getGeolinkOfCursor(cursor, editor); if (currentLink) return { start: { line: cursor.line, ch: currentLink.index }, end: { line: cursor.line, ch: currentLink.linkEnd }, query: currentLink.name, }; return null; } async getSuggestions( context: EditorSuggestContext ): Promise<SuggestInfo[]> { if (context.query.length < 2) return []; return await this.getSearchResultsWithDelay(context); } renderSuggestion(value: SuggestInfo, el: HTMLElement) { el.setText(value.name); } selectSuggestion(value: SuggestInfo, evt: MouseEvent | KeyboardEvent) { // Replace the link under the cursor with the retrieved location. // We call getGeolinkOfCursor again instead of using the original context because it's possible that // the user continued to type text after the suggestion was made const currentCursor = value.context.editor.getCursor(); const linkOfCursor = this.getGeolinkOfCursor( currentCursor, value.context.editor ); const finalResult = `[${value.context.query}](geo:${value.location.lat},${value.location.lng})`; value.context.editor.replaceRange( finalResult, { line: currentCursor.line, ch: linkOfCursor.index }, { line: currentCursor.line, ch: linkOfCursor.linkEnd } ); if (utils.verifyOrAddFrontMatter(value.context.editor, 'locations', '')) new Notice( "The note's front matter was updated to denote locations are present" ); } getGeolinkOfCursor(cursor: EditorPosition, editor: Editor) { const results = editor .getLine(cursor.line) .matchAll(this.cursorInsideGeolinkFinder); if (!results) return null; for (let result of results) { const linkName = result[1]; if ( cursor.ch >= result.index && cursor.ch < result.index + linkName.length + 2 ) return { index: result.index, name: linkName, linkEnd: result.index + result[0].length, }; } return null; } async getSearchResultsWithDelay( context: EditorSuggestContext ): Promise<SuggestInfo[] | null> { const timestamp = Date.now(); this.lastSearchTime = timestamp; const Sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); await Sleep(this.delayInMs); if (this.lastSearchTime != timestamp) { // Search is canceled by a newer search return null; } // After the sleep our search is still the last -- so the user stopped and we can go on const searchResults = await this.searcher.search(context.query); let suggestions: SuggestInfo[] = []; for (const result of searchResults) suggestions.push({ ...result, context: context, }); return suggestions; } async selectionToLink(editor: Editor) { const selection = editor.getSelection(); const results = await this.searcher.search(selection); if (results && results.length > 0) { const firstResult = results[0]; const location = firstResult.location; editor.replaceSelection( `[${selection}](geo:${location.lat},${location.lng})` ); new Notice(firstResult.name, 10 * 1000); if (utils.verifyOrAddFrontMatter(editor, 'locations', '')) new Notice( "The note's front matter was updated to denote locations are present" ); } else { new Notice(`No location found for the term '${selection}'`); } } }