diff --git a/main.js b/main.js index a262063..1c3f4b5 100644 --- a/main.js +++ b/main.js @@ -35,6 +35,9 @@ var ChatView = class extends import_obsidian.ItemView { this.pendingContext = []; this.explicitContext = []; this.isLoading = false; + // Mention autocomplete state + this.mentionSelectedIdx = 0; + this.mentionMatches = []; this.plugin = plugin; this.renderComponent = new import_obsidian.Component(); } @@ -93,6 +96,8 @@ var ChatView = class extends import_obsidian.ItemView { this.contextPreviewEl.style.display = "none"; const inputArea = chatArea.createDiv("vc-input-area"); const inputWrapper = inputArea.createDiv("vc-input-wrapper"); + this.mentionDropdownEl = inputWrapper.createDiv("vc-mention-dropdown"); + this.mentionDropdownEl.style.display = "none"; this.inputEl = inputWrapper.createEl("textarea", { cls: "vc-input", attr: { placeholder: "Frage stellen\u2026 (@ f\xFCr Notiz einf\xFCgen)" } @@ -106,6 +111,28 @@ var ChatView = class extends import_obsidian.ItemView { this.sendBtn.setText("Senden"); this.sendBtn.onclick = () => this.handleSend(); this.inputEl.addEventListener("keydown", (e) => { + if (this.mentionDropdownEl.style.display !== "none") { + if (e.key === "ArrowDown") { + e.preventDefault(); + this.moveMentionSelection(1); + return; + } + if (e.key === "ArrowUp") { + e.preventDefault(); + this.moveMentionSelection(-1); + return; + } + if (e.key === "Enter" || e.key === "Tab") { + e.preventDefault(); + this.confirmMentionSelection(); + return; + } + if (e.key === "Escape") { + e.preventDefault(); + this.hideMentionDropdown(); + return; + } + } if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); this.handleSend(); @@ -418,8 +445,71 @@ ${content}`; this.statusEl.style.display = text ? "block" : "none"; } handleInputChange() { + var _a; this.inputEl.style.height = "auto"; this.inputEl.style.height = Math.min(this.inputEl.scrollHeight, 200) + "px"; + const cursor = (_a = this.inputEl.selectionStart) != null ? _a : 0; + const textBefore = this.inputEl.value.slice(0, cursor); + const match = textBefore.match(/@([^@\n[\]]{2,})$/); + if (match) { + this.updateMentionDropdown(match[1]); + } else { + this.hideMentionDropdown(); + } + } + updateMentionDropdown(query) { + const lower = query.toLowerCase(); + this.mentionMatches = this.app.vault.getMarkdownFiles().map((f) => f.basename).filter((name) => name.toLowerCase().includes(lower)).slice(0, 8); + if (this.mentionMatches.length === 0) { + this.hideMentionDropdown(); + return; + } + this.mentionSelectedIdx = 0; + this.renderMentionDropdown(); + this.mentionDropdownEl.style.display = "block"; + } + renderMentionDropdown() { + this.mentionDropdownEl.empty(); + this.mentionMatches.forEach((name, i) => { + const item = this.mentionDropdownEl.createDiv( + i === this.mentionSelectedIdx ? "vc-mention-item vc-mention-item--active" : "vc-mention-item" + ); + item.setText(name); + item.addEventListener("mousedown", (e) => { + e.preventDefault(); + this.insertMention(name); + }); + }); + } + moveMentionSelection(dir) { + this.mentionSelectedIdx = (this.mentionSelectedIdx + dir + this.mentionMatches.length) % this.mentionMatches.length; + this.renderMentionDropdown(); + } + confirmMentionSelection() { + const name = this.mentionMatches[this.mentionSelectedIdx]; + if (name) + this.insertMention(name); + } + insertMention(basename) { + var _a; + const cursor = (_a = this.inputEl.selectionStart) != null ? _a : 0; + const text = this.inputEl.value; + const textBefore = text.slice(0, cursor); + const match = textBefore.match(/@([^@\n[\]]{2,})$/); + if (!match) + return; + const start = cursor - match[0].length; + const replacement = `[[${basename}]]`; + this.inputEl.value = text.slice(0, start) + replacement + text.slice(cursor); + const newCursor = start + replacement.length; + this.inputEl.setSelectionRange(newCursor, newCursor); + this.hideMentionDropdown(); + } + hideMentionDropdown() { + this.mentionDropdownEl.style.display = "none"; + this.mentionDropdownEl.empty(); + this.mentionMatches = []; + this.mentionSelectedIdx = 0; } // ─── Persistence ───────────────────────────────────────────────────────── loadThreads() { diff --git a/src/ChatView.ts b/src/ChatView.ts index e9b4fae..628b604 100644 --- a/src/ChatView.ts +++ b/src/ChatView.ts @@ -37,6 +37,11 @@ export class ChatView extends ItemView { private contextPreviewEl!: HTMLElement; private sendBtn!: HTMLButtonElement; private statusEl!: HTMLElement; + private mentionDropdownEl!: HTMLElement; + + // Mention autocomplete state + private mentionSelectedIdx = 0; + private mentionMatches: string[] = []; constructor(leaf: WorkspaceLeaf, plugin: MemexChatPlugin) { super(leaf); @@ -123,6 +128,8 @@ export class ChatView extends ItemView { const inputArea = chatArea.createDiv("vc-input-area"); const inputWrapper = inputArea.createDiv("vc-input-wrapper"); + this.mentionDropdownEl = inputWrapper.createDiv("vc-mention-dropdown"); + this.mentionDropdownEl.style.display = "none"; this.inputEl = inputWrapper.createEl("textarea", { cls: "vc-input", attr: { placeholder: "Frage stellen… (@ für Notiz einfügen)" }, @@ -141,6 +148,28 @@ export class ChatView extends ItemView { // Key bindings this.inputEl.addEventListener("keydown", (e) => { + if (this.mentionDropdownEl.style.display !== "none") { + if (e.key === "ArrowDown") { + e.preventDefault(); + this.moveMentionSelection(1); + return; + } + if (e.key === "ArrowUp") { + e.preventDefault(); + this.moveMentionSelection(-1); + return; + } + if (e.key === "Enter" || e.key === "Tab") { + e.preventDefault(); + this.confirmMentionSelection(); + return; + } + if (e.key === "Escape") { + e.preventDefault(); + this.hideMentionDropdown(); + return; + } + } if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); this.handleSend(); @@ -514,6 +543,80 @@ export class ChatView extends ItemView { // Auto-resize textarea this.inputEl.style.height = "auto"; this.inputEl.style.height = Math.min(this.inputEl.scrollHeight, 200) + "px"; + + // @mention autocomplete + const cursor = this.inputEl.selectionStart ?? 0; + const textBefore = this.inputEl.value.slice(0, cursor); + const match = textBefore.match(/@([^@\n[\]]{2,})$/); + if (match) { + this.updateMentionDropdown(match[1]); + } else { + this.hideMentionDropdown(); + } + } + + private updateMentionDropdown(query: string): void { + const lower = query.toLowerCase(); + this.mentionMatches = this.app.vault + .getMarkdownFiles() + .map((f) => f.basename) + .filter((name) => name.toLowerCase().includes(lower)) + .slice(0, 8); + + if (this.mentionMatches.length === 0) { + this.hideMentionDropdown(); + return; + } + + this.mentionSelectedIdx = 0; + this.renderMentionDropdown(); + this.mentionDropdownEl.style.display = "block"; + } + + private renderMentionDropdown(): void { + this.mentionDropdownEl.empty(); + this.mentionMatches.forEach((name, i) => { + const item = this.mentionDropdownEl.createDiv( + i === this.mentionSelectedIdx ? "vc-mention-item vc-mention-item--active" : "vc-mention-item" + ); + item.setText(name); + item.addEventListener("mousedown", (e) => { + e.preventDefault(); + this.insertMention(name); + }); + }); + } + + private moveMentionSelection(dir: 1 | -1): void { + this.mentionSelectedIdx = + (this.mentionSelectedIdx + dir + this.mentionMatches.length) % this.mentionMatches.length; + this.renderMentionDropdown(); + } + + private confirmMentionSelection(): void { + const name = this.mentionMatches[this.mentionSelectedIdx]; + if (name) this.insertMention(name); + } + + private insertMention(basename: string): void { + const cursor = this.inputEl.selectionStart ?? 0; + const text = this.inputEl.value; + const textBefore = text.slice(0, cursor); + const match = textBefore.match(/@([^@\n[\]]{2,})$/); + if (!match) return; + const start = cursor - match[0].length; + const replacement = `[[${basename}]]`; + this.inputEl.value = text.slice(0, start) + replacement + text.slice(cursor); + const newCursor = start + replacement.length; + this.inputEl.setSelectionRange(newCursor, newCursor); + this.hideMentionDropdown(); + } + + private hideMentionDropdown(): void { + this.mentionDropdownEl.style.display = "none"; + this.mentionDropdownEl.empty(); + this.mentionMatches = []; + this.mentionSelectedIdx = 0; } // ─── Persistence ───────────────────────────────────────────────────────── diff --git a/styles.css b/styles.css index f13e3e0..886cefd 100644 --- a/styles.css +++ b/styles.css @@ -400,9 +400,39 @@ } .vc-input-wrapper { + position: relative; margin-bottom: 6px; } +.vc-mention-dropdown { + position: absolute; + bottom: calc(100% + 4px); + left: 0; + right: 0; + background: var(--background-primary); + border: 1px solid var(--background-modifier-border); + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); + overflow: hidden; + z-index: 100; +} + +.vc-mention-item { + padding: 7px 12px; + font-size: 13px; + cursor: pointer; + color: var(--text-normal); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.vc-mention-item:hover, +.vc-mention-item--active { + background: var(--background-modifier-hover); + color: var(--text-accent); +} + .vc-input { width: 100%; resize: none;