obsidian#EditorSuggest TypeScript Examples

The following examples show how to use obsidian#EditorSuggest. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: tagSuggest.ts    From obsidian-map-view with GNU General Public License v3.0 5 votes vote down vote up
export class TagSuggest extends EditorSuggest<SuggestInfo> {
    private app: App;

    constructor(app: App, settings: PluginSettings) {
        super(app);
        this.app = app;
    }

    onTrigger(
        cursor: EditorPosition,
        editor: Editor,
        file: TFile
    ): EditorSuggestTriggerInfo | null {
        const line = editor.getLine(cursor.line);
        // Start by verifying that the current line has an inline location.
        // If it doesn't, we don't wanna trigger the completion even if the user
        // starts typing 'tag:'
        const hasLocationMatch = matchInlineLocation(line);
        if (!hasLocationMatch || hasLocationMatch.length == 0) return null;
        const tagMatch = getTagUnderCursor(line, cursor.ch);
        if (tagMatch)
            return {
                start: { line: cursor.line, ch: tagMatch.index },
                end: {
                    line: cursor.line,
                    ch: tagMatch.index + tagMatch[0].length,
                },
                query: tagMatch[1],
            };
        return null;
    }

    getSuggestions(context: EditorSuggestContext): SuggestInfo[] {
		const noPound = (tagName: string) => {
			return tagName.startsWith('#') ? tagName.substring(1) : tagName;
		};
        const tagQuery = context.query ?? '';
        // Find all tags that include the query
        const matchingTags = utils
            .getAllTagNames(this.app)
			.map(value => noPound(value))
            .filter((value) =>
                value.toLowerCase().includes(tagQuery.toLowerCase())
            );
        return matchingTags.map((tagName) => {
            return {
                tagName: tagName,
                context: context,
            };
        });
    }

    renderSuggestion(value: SuggestInfo, el: HTMLElement) {
        el.setText(value.tagName);
    }

    selectSuggestion(value: SuggestInfo, evt: MouseEvent | KeyboardEvent) {
        const currentCursor = value.context.editor.getCursor();
        const linkResult = `tag:${value.tagName} `;
        value.context.editor.replaceRange(
            linkResult,
            value.context.start,
            value.context.end
        );
    }
}
Example #2
Source File: main.ts    From obsidian-emoji-shortcodes with MIT License 5 votes vote down vote up
class EmojiSuggester extends EditorSuggest<string> {
	plugin: EmojiShortcodesPlugin;

	constructor(plugin: EmojiShortcodesPlugin) {
		super(plugin.app);
		this.plugin = plugin;
	}

	onTrigger(cursor: EditorPosition, editor: Editor, _: TFile): EditorSuggestTriggerInfo | null {
		if (this.plugin.settings.suggester) {
			const sub = editor.getLine(cursor.line).substring(0, cursor.ch);
			const match = sub.match(/:\S+$/)?.first();
			if (match) {
				return {
					end: cursor,
					start: {
						ch: sub.lastIndexOf(match),
						line: cursor.line,
					},
					query: match,
				}
			}
		}
		return null;
	}

	getSuggestions(context: EditorSuggestContext): string[] {
		let emoji_query = context.query.replace(':', '');
		return Object.keys(emoji).filter(p => p.includes(emoji_query));
	}

	renderSuggestion(suggestion: string, el: HTMLElement): void {
		const outer = el.createDiv({ cls: "ES-suggester-container" });
		outer.createDiv({ cls: "ES-shortcode" }).setText(suggestion.replace(/:/g, ""));
		//@ts-expect-error
		outer.createDiv({ cls: "ES-emoji" }).setText(emoji[suggestion]);
	}

	selectSuggestion(suggestion: string): void {
		if(this.context) {
			(this.context.editor as Editor).replaceRange(this.plugin.settings.immediateReplace ? emoji[suggestion] : `${suggestion} `, this.context.start, this.context.end);
		}
	}
}
Example #3
Source File: suggest.ts    From obsidian-admonition with MIT License 5 votes vote down vote up
export class CalloutSuggest extends EditorSuggest<string> {
    constructor(public plugin: ObsidianAdmonition) {
        super(plugin.app);
    }
    getSuggestions(ctx: EditorSuggestContext) {
        return Object.keys(this.plugin.admonitions).filter((p) =>
            p.toLowerCase().contains(ctx.query.toLowerCase())
        );
    }
    renderSuggestion(text: string, el: HTMLElement) {
        el.createSpan({ text });
    }
    selectSuggestion(value: string, evt: MouseEvent | KeyboardEvent): void {
        if (!this.context) return;

        const line = this.context.editor
            .getLine(this.context.end.line)
            .slice(this.context.end.ch);
        const [_, exists] = line.match(/^(\] ?)/) ?? [];

        this.context.editor.replaceRange(
            `${value}] `,
            this.context.start,
            {
                ...this.context.end,
                ch:
                    this.context.start.ch +
                    this.context.query.length +
                    (exists?.length ?? 0)
            },
            "admonitions"
        );

        this.context.editor.setCursor(
            this.context.start.line,
            this.context.start.ch + value.length + 2
        );

        this.close();
    }
    onTrigger(
        cursor: EditorPosition,
        editor: Editor,
        file: TFile
    ): EditorSuggestTriggerInfo {
        const line = editor.getLine(cursor.line);
        //not inside the bracket
        if (/> \[!\w+\]/.test(line.slice(0, cursor.ch))) return null;
        if (!/> \[!\w*/.test(line)) return null;

        const match = line.match(/> \[!(\w*)\]?/);
        if (!match) return null;

        const [_, query] = match;

        if (
            !query ||
            Object.keys(this.plugin.admonitions).find(
                (p) => p.toLowerCase() == query.toLowerCase()
            )
        ) {
            return null;
        }
        const matchData = {
            end: cursor,
            start: {
                ch: match.index + 4,
                line: cursor.line
            },
            query
        };
        return matchData;
    }
}
Example #4
Source File: suggest.ts    From obsidian-admonition with MIT License 5 votes vote down vote up
export class AdmonitionSuggest extends EditorSuggest<string> {
    constructor(public plugin: ObsidianAdmonition) {
        super(plugin.app);
    }
    getSuggestions(ctx: EditorSuggestContext) {
        return Object.keys(this.plugin.admonitions).filter((p) =>
            p.toLowerCase().contains(ctx.query.toLowerCase())
        );
    }
    renderSuggestion(text: string, el: HTMLElement) {
        el.createSpan({ text });
    }
    selectSuggestion(value: string, evt: MouseEvent | KeyboardEvent): void {
        if (!this.context) return;

        this.context.editor.replaceRange(
            `${value}`,
            this.context.start,
            this.context.end,
            "admonitions"
        );

        this.close();
    }
    onTrigger(
        cursor: EditorPosition,
        editor: Editor,
        file: TFile
    ): EditorSuggestTriggerInfo {
        const line = editor.getLine(cursor.line);
        if (!/```ad-\w+/.test(line)) return null;
        const match = line.match(/```ad-(\w+)/);
        if (!match) return null;
        const [_, query] = match;

        if (
            !query ||
            Object.keys(this.plugin.admonitions).find(
                (p) => p.toLowerCase() == query.toLowerCase()
            )
        ) {
            return null;
        }

        const matchData = {
            end: cursor,
            start: {
                ch: match.index + 6,
                line: cursor.line
            },
            query
        };
        return matchData;
    }
}
Example #5
Source File: Autocomplete.ts    From Templater with GNU Affero General Public License v3.0 4 votes vote down vote up
export class Autocomplete extends EditorSuggest<TpSuggestDocumentation> {
    //private in_command = false;
    // https://regex101.com/r/ocmHzR/1
    private tp_keyword_regex =
        /tp\.(?<module>[a-z]*)?(?<fn_trigger>\.(?<fn>[a-z_]*)?)?$/;
    private documentation: Documentation;
    private latest_trigger_info: EditorSuggestTriggerInfo;
    private module_name: ModuleName | string;
    private function_trigger: boolean;
    private function_name: string;

    constructor(private app: App, private plugin: TemplaterPlugin) {
        super(app);
        this.documentation = new Documentation(this.app);
    }

    onTrigger(
        cursor: EditorPosition,
        editor: Editor,
        file: TFile
    ): EditorSuggestTriggerInfo | null {
        const range = editor.getRange(
            { line: cursor.line, ch: 0 },
            { line: cursor.line, ch: cursor.ch }
        );
        const match = this.tp_keyword_regex.exec(range);
        if (!match) {
            return null;
        }

        let query: string;
        const module_name = match.groups["module"] || "";
        this.module_name = module_name;

        if (match.groups["fn_trigger"]) {
            if (module_name == "" || !is_module_name(module_name)) {
                return;
            }
            this.function_trigger = true;
            this.function_name = match.groups["fn"] || "";
            query = this.function_name;
        } else {
            this.function_trigger = false;
            query = this.module_name;
        }

        const trigger_info: EditorSuggestTriggerInfo = {
            start: { line: cursor.line, ch: cursor.ch - query.length },
            end: { line: cursor.line, ch: cursor.ch },
            query: query,
        };
        this.latest_trigger_info = trigger_info;
        return trigger_info;
    }

    getSuggestions(
        context: EditorSuggestContext
    ): Array<TpSuggestDocumentation> {
        let suggestions: Array<TpSuggestDocumentation>;
        if (this.module_name && this.function_trigger) {
            suggestions =
                this.documentation.get_all_functions_documentation(
                    this.module_name as ModuleName
                );
        } else {
            suggestions =
                this.documentation.get_all_modules_documentation();
        }
        if (!suggestions) {
            return [];
        }
        return suggestions.filter(s => s.name.startsWith(context.query));
    }

    renderSuggestion(value: TpSuggestDocumentation, el: HTMLElement): void {
        el.createEl("b", { text: value.name });
        el.createEl("br");
        if (this.function_trigger && is_function_documentation(value)) {
            el.createEl("code", { text: value.definition }); 
        }
        if (value.description) {
            el.createEl("div", { text: value.description });
        }
    }

    selectSuggestion(
        value: TpSuggestDocumentation,
        evt: MouseEvent | KeyboardEvent
    ): void {
        const active_view =
            this.app.workspace.getActiveViewOfType(MarkdownView);
        if (!active_view) {
            // TODO: Error msg
            return;
        }
        active_view.editor.replaceRange(
            value.name,
            this.latest_trigger_info.start,
            this.latest_trigger_info.end
        );
        if (this.latest_trigger_info.start.ch == this.latest_trigger_info.end.ch) {
            // Dirty hack to prevent the cursor being at the
            // beginning of the word after completion, 
            // Not sure what's the cause of this bug.
            const cursor_pos = this.latest_trigger_info.end;
            cursor_pos.ch += value.name.length;
            active_view.editor.setCursor(cursor_pos);
        }
    }
}
Example #6
Source File: date-suggest.ts    From nldates-obsidian with MIT License 4 votes vote down vote up
export default class DateSuggest extends EditorSuggest<IDateCompletion> {
  private plugin: NaturalLanguageDates;
  private app: App;

  constructor(app: App, plugin: NaturalLanguageDates) {
    super(app);
    this.app = app;
    this.plugin = plugin;

    // @ts-ignore
    this.scope.register(["Shift"], "Enter", (evt: KeyboardEvent) => {
      // @ts-ignore
      this.suggestions.useSelectedItem(evt);
      return false;
    });

    if (this.plugin.settings.autosuggestToggleLink) {
      this.setInstructions([{ command: "Shift", purpose: "Keep text as alias" }]);
    }
  }

  getSuggestions(context: EditorSuggestContext): IDateCompletion[] {
    const suggestions = this.getDateSuggestions(context);
    if (suggestions.length) {
      return suggestions;
    }

    // catch-all if there are no matches
    return [{ label: context.query }];
  }

  getDateSuggestions(context: EditorSuggestContext): IDateCompletion[] {
    if (context.query.match(/^time/)) {
      return ["now", "+15 minutes", "+1 hour", "-15 minutes", "-1 hour"]
        .map((val) => ({ label: `time:${val}` }))
        .filter((item) => item.label.toLowerCase().startsWith(context.query));
    }
    if (context.query.match(/(next|last|this)/i)) {
      const reference = context.query.match(/(next|last|this)/i)[1];
      return [
        "week",
        "month",
        "year",
        "Sunday",
        "Monday",
        "Tuesday",
        "Wednesday",
        "Thursday",
        "Friday",
        "Saturday",
      ]
        .map((val) => ({ label: `${reference} ${val}` }))
        .filter((items) => items.label.toLowerCase().startsWith(context.query));
    }

    const relativeDate =
      context.query.match(/^in ([+-]?\d+)/i) || context.query.match(/^([+-]?\d+)/i);
    if (relativeDate) {
      const timeDelta = relativeDate[1];
      return [
        { label: `in ${timeDelta} minutes` },
        { label: `in ${timeDelta} hours` },
        { label: `in ${timeDelta} days` },
        { label: `in ${timeDelta} weeks` },
        { label: `in ${timeDelta} months` },
        { label: `${timeDelta} days ago` },
        { label: `${timeDelta} weeks ago` },
        { label: `${timeDelta} months ago` },
      ].filter((items) => items.label.toLowerCase().startsWith(context.query));
    }

    return [{ label: "Today" }, { label: "Yesterday" }, { label: "Tomorrow" }].filter(
      (items) => items.label.toLowerCase().startsWith(context.query)
    );
  }

  renderSuggestion(suggestion: IDateCompletion, el: HTMLElement): void {
    el.setText(suggestion.label);
  }

  selectSuggestion(suggestion: IDateCompletion, event: KeyboardEvent | MouseEvent): void {
    const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);
    if (!activeView) {
      return;
    }

    const includeAlias = event.shiftKey;
    let dateStr = "";
    let makeIntoLink = this.plugin.settings.autosuggestToggleLink;

    if (suggestion.label.startsWith("time:")) {
      const timePart = suggestion.label.substring(5);
      dateStr = this.plugin.parseTime(timePart).formattedString;
      makeIntoLink = false;
    } else {
      dateStr = this.plugin.parseDate(suggestion.label).formattedString;
    }

    if (makeIntoLink) {
      dateStr = generateMarkdownLink(
        this.app,
        dateStr,
        includeAlias ? suggestion.label : undefined
      );
    }

    activeView.editor.replaceRange(dateStr, this.context.start, this.context.end);
  }

  onTrigger(
    cursor: EditorPosition,
    editor: Editor,
    file: TFile
  ): EditorSuggestTriggerInfo {
    if (!this.plugin.settings.isAutosuggestEnabled) {
      return null;
    }

    const triggerPhrase = this.plugin.settings.autocompleteTriggerPhrase;
    const startPos = this.context?.start || {
      line: cursor.line,
      ch: cursor.ch - triggerPhrase.length,
    };

    if (!editor.getRange(startPos, cursor).startsWith(triggerPhrase)) {
      return null;
    }

    const precedingChar = editor.getRange(
      {
        line: startPos.line,
        ch: startPos.ch - 1,
      },
      startPos
    );

    // Short-circuit if `@` as a part of a word (e.g. part of an email address)
    if (precedingChar && /[`a-zA-Z0-9]/.test(precedingChar)) {
      return null;
    }

    return {
      start: startPos,
      end: cursor,
      query: editor.getRange(startPos, cursor).substring(triggerPhrase.length),
    };
  }
}
Example #7
Source File: locationSuggest.ts    From obsidian-map-view with GNU General Public License v3.0 4 votes vote down vote up
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}'`);
        }
    }
}