diff --git a/src/setting/settings.ts b/src/setting/settings.ts index fbc946e..4e2f6f9 100644 --- a/src/setting/settings.ts +++ b/src/setting/settings.ts @@ -47,6 +47,7 @@ export interface Settings { // key customization hotkeys: { select: Hotkey[]; + "select with custom alias": Hotkey[]; up: Hotkey[]; down: Hotkey[]; "select 1st": Hotkey[]; @@ -156,6 +157,7 @@ export const DEFAULT_SETTINGS: Settings = { // key customization hotkeys: { select: [{ modifiers: [], key: "Enter" }], + "select with custom alias": [], up: [{ modifiers: [], key: "ArrowUp" }], down: [{ modifiers: [], key: "ArrowDown" }], "select 1st": [], diff --git a/src/ui/AutoCompleteSuggest.ts b/src/ui/AutoCompleteSuggest.ts index 3558909..dd0b055 100644 --- a/src/ui/AutoCompleteSuggest.ts +++ b/src/ui/AutoCompleteSuggest.ts @@ -109,6 +109,8 @@ export class AutoCompleteSuggest activeLeafChangeRef: EventRef; metadataCacheChangeRef: EventRef; + spareEditorSuggestContext: EditorSuggestContext | null = null; + private constructor(app: App, statusBar: ProviderStatusBar) { super(app); this.appHelper = new AppHelper(app); @@ -503,6 +505,20 @@ export class AutoCompleteSuggest ["select 7th", (evt) => commands.select(this, evt, 6)], ["select 8th", (evt) => commands.select(this, evt, 7)], ["select 9th", (evt) => commands.select(this, evt, 8)], + [ + "select with custom alias", + (evt) => { + // For restore the context of suggestions to insert editor after submitted input + this.spareEditorSuggestContext = this.context; + + commands.selectWithCustomAlias(this, evt).then((item) => { + if (item) { + this.selectSuggestion(item); + } + }); + return false; + }, + ], ["open", (_) => commands.open(this)], ["completion", (_) => commands.completion(this)], ["insert as text", (evt) => commands.insertAsText(this, evt)], @@ -973,9 +989,15 @@ export class AutoCompleteSuggest } } - constructInternalLinkText(word: InternalLinkWord): string { + constructInternalLinkText( + word: InternalLinkWord, + forceWithAlias: boolean, + ): string { // With aliases - if (this.settings.suggestInternalLinkWithAlias && word.aliasMeta) { + if ( + (this.settings.suggestInternalLinkWithAlias || forceWithAlias) && + word.aliasMeta + ) { const { link } = this.appHelper.optimizeMarkdownLinkText( word.aliasMeta.origin, )!; @@ -1023,14 +1045,21 @@ export class AutoCompleteSuggest : `[${displayed}](${encodeSpace(link)}.md)`; } - selectSuggestion(word: Word, _evt: MouseEvent | KeyboardEvent): void { - if (!this.context) { + selectSuggestion(word: Word): void { + let forceWithAlias = false; + let context = this.context; + if (!context) { + context = this.spareEditorSuggestContext; + this.spareEditorSuggestContext = null; + forceWithAlias = true; + } + if (!context) { return; } let insertedText = word.value; if (word.type === "internalLink") { - insertedText = this.constructInternalLinkText(word); + insertedText = this.constructInternalLinkText(word, forceWithAlias); } if (word.type === "frontMatter") { @@ -1063,14 +1092,14 @@ export class AutoCompleteSuggest } } - const editor = this.context.editor; + const editor = context.editor; editor.replaceRange( insertedText, { - ...this.context.start, + ...context.start, ch: this.contextStartCh + word.offset!, }, - this.context.end, + context.end, ); if (positionToMove !== -1) { @@ -1084,12 +1113,7 @@ export class AutoCompleteSuggest } // The workaround of strange behavior for that cursor doesn't move after completion only if it doesn't input any word. - if ( - this.appHelper.equalsAsEditorPosition( - this.context.start, - this.context.end, - ) - ) { + if (this.appHelper.equalsAsEditorPosition(context.start, context.end)) { editor.setCursor( editor.offsetToPos( editor.posToOffset(editor.getCursor()) + insertedText.length, diff --git a/src/ui/component/InputDialog.ts b/src/ui/component/InputDialog.ts new file mode 100644 index 0000000..747fcc8 --- /dev/null +++ b/src/ui/component/InputDialog.ts @@ -0,0 +1,68 @@ +import { Modal } from "obsidian"; + +export class InputDialog extends Modal { + inputEl!: HTMLInputElement; + promise!: Promise; + submitted = false; + + constructor( + public args: { + title: string; + placeholder?: string; + defaultValue?: string; + }, + ) { + super(app); + } + + onOpen(): void { + this.titleEl.setText(this.args.title); + + this.inputEl = this.contentEl.createEl("input", { + type: "text", + placeholder: this.args.placeholder ?? "", + cls: "carnelian-input-dialog-input", + value: this.args.defaultValue, + }); + } + + /** + * This function returns + * - Promise if submitted not empty string + * - Promise<""> if submitted empty string + * - Promise if canceled + */ + open(args?: { initialSelect: boolean }): Promise { + super.open(); + + this.promise = new Promise((resolve) => { + const listener = (ev: KeyboardEvent) => { + if (ev.isComposing) { + return; + } + if (ev.code === "Enter") { + ev.preventDefault(); + resolve(this.inputEl.value); + this.submitted = true; + this.close(); + } + }; + + this.inputEl.addEventListener("keydown", listener); + + this.onClose = () => { + super.onClose(); + this.inputEl.removeEventListener("keydown", listener); + if (!this.submitted) { + resolve(null); + } + }; + + if (args?.initialSelect) { + this.inputEl.select(); + } + }); + + return this.promise; + } +} diff --git a/src/ui/popup-commands.ts b/src/ui/popup-commands.ts index f0030c6..ecbba72 100644 --- a/src/ui/popup-commands.ts +++ b/src/ui/popup-commands.ts @@ -1,6 +1,8 @@ import type { AutoCompleteSuggest } from "./AutoCompleteSuggest"; import { Notice } from "obsidian"; import { excludeEmoji, findCommonPrefix } from "../util/strings"; +import { InputDialog } from "./component/InputDialog"; +import type { InternalLinkWord } from "src/model/Word"; export type CommandReturnType = boolean | undefined; @@ -27,6 +29,43 @@ export function select( } } +export async function selectWithCustomAlias( + popup: AutoCompleteSuggest, + evt: KeyboardEvent, +): Promise { + if (!popup.context || evt.isComposing) { + return null; + } + + if (popup.selectionLock) { + popup.close(); + return null; + } + + const item = popup.suggestions.values[popup.suggestions.selectedItem]; + if (item.type !== "internalLink") { + return null; + } + + const input = await new InputDialog({ + title: "Type custom alias", + defaultValue: item.value, + }).open({ initialSelect: true }); + if (!input) { + return null; + } + + if (item.value === input) { + return item; + } + + item.aliasMeta = { + origin: item.aliasMeta?.origin ?? item.value, + }; + item.value = input; + return item; +} + export function insertAsText( popup: AutoCompleteSuggest, evt: KeyboardEvent,