diff --git a/src/components/PlanEditor.vue b/src/components/PlanEditor.vue index 21b846d..230006f 100644 --- a/src/components/PlanEditor.vue +++ b/src/components/PlanEditor.vue @@ -17,6 +17,10 @@ const popup = ref({ left: 0, top: 0 }) // Suppress the popup for one keystroke after Escape, so it doesn't pop straight // back open while the author is still typing the word they just dismissed. let justEscaped = false +// Whether the author has moved the selection since the popup (re)opened. Tab +// always accepts; Enter only accepts once they've engaged with the menu — an +// untouched popup lets Enter do its normal job (a newline) rather than hijack it. +let engaged = false // Pixel metrics of the (monospace, non-wrapping) textarea, measured once. let metrics = { charWidth: 8, lineHeight: 20, padLeft: 16, padTop: 14 } @@ -58,6 +62,7 @@ function refresh() { if (!el) return completion.value = justEscaped ? null : getCompletions(el.value, el.selectionStart) selected.value = 0 + engaged = false if (completion.value) position() } @@ -99,6 +104,7 @@ function onKeydown(e: KeyboardEvent) { if (!ctx) return const move = (delta: number) => { e.preventDefault() + engaged = true selected.value = (selected.value + delta + ctx.items.length) % ctx.items.length } // Ctrl+N / Ctrl+P mirror ArrowDown / ArrowUp (readline-style navigation). @@ -113,11 +119,20 @@ function onKeydown(e: KeyboardEvent) { case "ArrowUp": move(-1) break - case "Enter": case "Tab": e.preventDefault() accept(selected.value) break + case "Enter": + // Only steal Enter once the author has navigated the menu; otherwise let + // it insert a newline (onInput → refresh reopens the popup if warranted). + if (engaged) { + e.preventDefault() + accept(selected.value) + } else { + completion.value = null + } + break case "Escape": e.preventDefault() completion.value = null