diff --git a/rhwp-studio/src/engine/input-handler-keyboard.ts b/rhwp-studio/src/engine/input-handler-keyboard.ts index bfb1a4c44..b119b65d1 100644 --- a/rhwp-studio/src/engine/input-handler-keyboard.ts +++ b/rhwp-studio/src/engine/input-handler-keyboard.ts @@ -680,18 +680,26 @@ export function onKeyDown(this: any, e: KeyboardEvent): void { } // Alt 조합 단축키 처리 - if (e.altKey && this.dispatcher) { + if (e.altKey) { + // Alt+Backspace → 단어 단위 삭제 (macOS Option+Delete) + if (e.key === 'Backspace' && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + deleteWordBackward.call(this); + return; + } // Alt+V → Chord 대기 (보기 메뉴 단축키, 한컴 Alt+V,T 계승) if ((e.key === 'v' || e.key === 'V' || e.key === 'ㅍ') && !e.shiftKey && !e.ctrlKey) { e.preventDefault(); this._pendingChordV = true; return; } - const cmdId = matchShortcut(e, defaultShortcuts); - if (cmdId) { - e.preventDefault(); - this.dispatcher.dispatch(cmdId); - return; + if (this.dispatcher) { + const cmdId = matchShortcut(e, defaultShortcuts); + if (cmdId) { + e.preventDefault(); + this.dispatcher.dispatch(cmdId); + return; + } } } @@ -892,8 +900,17 @@ export function handleCtrlKey(this: any, e: KeyboardEvent): void { return; } - // 커맨드 시스템에 없는 직접 처리 (Ctrl+Home/End 등 커서 이동) + // 커맨드 시스템에 없는 직접 처리 (Ctrl+Home/End, Ctrl+Backspace 등) switch (e.key.toLowerCase()) { + case 'backspace': { + e.preventDefault(); + if (e.metaKey) { + deleteToLineStart.call(this); + } else { + deleteWordBackward.call(this); + } + break; + } case 'home': { e.preventDefault(); if (e.shiftKey) { @@ -930,6 +947,64 @@ export function handleSelectAll(this: any): void { this.updateCaret(); } +/** Cmd+Backspace (macOS): 현재 위치에서 줄 시작까지 삭제 */ +function deleteToLineStart(this: any): void { + if (this.cursor.hasSelection()) { + this.deleteSelection(); + return; + } + this.cursor.setAnchor(); + this.cursor.moveToLineStart(); + if (this.cursor.hasSelection()) { + this.deleteSelection(); + } +} + +/** Ctrl+Backspace (Win/Linux) / Alt+Backspace (macOS): 이전 단어 경계까지 삭제 */ +function deleteWordBackward(this: any): void { + if (this.cursor.hasSelection()) { + this.deleteSelection(); + return; + } + const pos = this.cursor.getPosition(); + const inCell = this.cursor.isInCell(); + const charOffset = inCell ? (pos.cellCharOffset ?? pos.charOffset) : pos.charOffset; + if (charOffset === 0) { + this.handleBackspace(pos, inCell); + return; + } + const text = getTextBeforeCursor(this.wasm, pos, inCell, charOffset); + const wordStart = findWordBoundaryInText(text); + if (wordStart < charOffset) { + this.cursor.setAnchor(); + const target = inCell + ? { ...pos, cellCharOffset: wordStart, charOffset: wordStart } + : { ...pos, charOffset: wordStart }; + this.cursor.moveTo(target); + this.deleteSelection(); + } +} + +function getTextBeforeCursor(wasm: WasmBridge, pos: any, inCell: boolean, offset: number): string { + if (inCell && pos.parentParaIndex != null && pos.controlIndex != null && pos.cellIndex != null && pos.cellParaIndex != null) { + try { + return wasm.getTextInCell(pos.sectionIndex, pos.parentParaIndex, pos.controlIndex, pos.cellIndex, pos.cellParaIndex, 0, offset); + } catch { /* fallback */ } + } + return wasm.getTextRange(pos.sectionIndex, pos.paragraphIndex, 0, offset); +} + +function findWordBoundaryInText(text: string): number { + let i = text.length - 1; + while (i >= 0 && isWordSeparator(text[i])) i--; + while (i >= 0 && !isWordSeparator(text[i])) i--; + return i + 1; +} + +function isWordSeparator(ch: string): boolean { + return /[\s .,;:!?'"()[\]{}<>\/\\|@#$%^&*~`+=\-_]/.test(ch); +} + export function onCopy(this: any, e: ClipboardEvent): void { if (!this.active) return;