From ec98bac14663dc6c508d1b47d678d0376a416a27 Mon Sep 17 00:00:00 2001 From: Forge Date: Fri, 6 Mar 2026 14:36:48 +0800 Subject: [PATCH] fix: capture selection on toolbar button mousedown - definitive replace fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause (dual bugs): 1. SCOPE BUG: snapStart/snapEnd/snapElement/snapRange were local vars inside runAction() but showModal() is a sibling function — those snap vars were never in scope inside the Replace button handler. The condition always silently fell through, doing nothing. 2. TIMING BUG: Selection was only captured on document mouseup, which could race against focus changes when clicking the toolbar. Fix: - showModal() now accepts snapStart/snapEnd/snapElement/snapRange as explicit parameters — guaranteed in-scope, no closure ambiguity - Each toolbar button mousedown calls captureForButton() BEFORE any focus shift can occur (belt-and-suspenders, works even if e.preventDefault fails) - Added console.log debug lines to Replace handler and runAction snapshot - selectionLocked reset on Escape and overlay click (was missing) --- entrypoints/content.ts | 123 ++++++++++++++++++++++++++++++++++++++--- lexai-chrome-mv3.zip | Bin 74376 -> 74310 bytes 2 files changed, 115 insertions(+), 8 deletions(-) diff --git a/entrypoints/content.ts b/entrypoints/content.ts index 649fd9f..10b8216 100644 --- a/entrypoints/content.ts +++ b/entrypoints/content.ts @@ -60,6 +60,38 @@ export default defineContentScript({ return false; } + // ─── Capture selection on toolbar button mousedown ──────────────────────── + // Called from each toolbar button's mousedown — at this point focus has NOT + // shifted yet (even without e.preventDefault), so selection is guaranteed valid. + + function captureForButton() { + if (selectionLocked) return; + + const activeEl = document.activeElement; + if (activeEl instanceof HTMLTextAreaElement || activeEl instanceof HTMLInputElement) { + const start = activeEl.selectionStart ?? -1; + const end = activeEl.selectionEnd ?? -1; + if (start >= 0 && end > start) { + storedStart = start; + storedEnd = end; + storedElement = activeEl; + storedRange = null; + selectedText = activeEl.value.substring(start, end); + console.log('[LexAI captureForButton] textarea captured', { start, end, text: selectedText.substring(0, 40) }); + } + } else { + const sel = window.getSelection(); + if (sel && sel.rangeCount > 0 && sel.toString().trim().length > 0) { + storedRange = sel.getRangeAt(0).cloneRange(); + storedElement = null; + storedStart = -1; + storedEnd = -1; + selectedText = sel.toString().trim(); + console.log('[LexAI captureForButton] DOM range captured', { text: selectedText.substring(0, 40) }); + } + } + } + // ─── Replace using stored state ─────────────────────────────────────────── function replaceText(newText: string) { @@ -185,7 +217,10 @@ export default defineContentScript({ btn.style.borderColor = 'rgba(205,214,244,0.1)'; btn.style.transform = 'translateY(0)'; }); - btn.addEventListener('mousedown', (e) => e.preventDefault()); // don't lose selection + btn.addEventListener('mousedown', (e) => { + e.preventDefault(); // prevent focus shift away from textarea + captureForButton(); // re-capture selection while it's guaranteed valid + }); btn.addEventListener('click', (e) => { e.stopPropagation(); runAction(action); @@ -251,6 +286,24 @@ export default defineContentScript({ async function runAction(action: string) { if (!selectedText) return; const textToProcess = selectedText; + + // SNAPSHOT selection state RIGHT NOW before anything async happens. + // These are passed directly into showModal so the Replace handler has + // guaranteed access — no closure-scope ambiguity. + const snapStart = storedStart; + const snapEnd = storedEnd; + const snapElement = storedElement; + const snapRange = storedRange ? storedRange.cloneRange() : null; + + console.log('[LexAI runAction] snapshot captured', { + snapStart, + snapEnd, + snapElement: snapElement?.tagName, + snapElementId: snapElement?.id || snapElement?.name || '(no id)', + hasSnapRange: !!snapRange, + selectedText: textToProcess.substring(0, 50), + }); + selectionLocked = true; // lock stored selection — modal interaction won't reset it // Guard: extension may have been reloaded/updated @@ -285,9 +338,9 @@ export default defineContentScript({ if (response === null) return; // safeSendMessage already handled the error if (response?.error) { - showModal(`❌ ${response.error}`, null); + showModal(`❌ ${response.error}`, null, snapStart, snapEnd, snapElement, snapRange); } else { - showModal(response?.result ?? '(no result)', textToProcess); + showModal(response?.result ?? '(no result)', textToProcess, snapStart, snapEnd, snapElement, snapRange); } } catch (err) { hideToolbar(); @@ -295,14 +348,23 @@ export default defineContentScript({ String(err).includes('message channel closed')) { showErrorToast('LexAI was updated — please refresh this page.'); } else { - showModal(`❌ Error: ${String(err)}`, null); + showModal(`❌ Error: ${String(err)}`, null, snapStart, snapEnd, snapElement, snapRange); } } } // ─── Result Modal ───────────────────────────────────────────────────────── + // snapStart/snapEnd/snapElement/snapRange are passed explicitly from runAction + // so the Replace handler has guaranteed in-scope access to them. - function showModal(resultText: string, originalText: string | null) { + function showModal( + resultText: string, + originalText: string | null, + snapStart: number, + snapEnd: number, + snapElement: HTMLTextAreaElement | HTMLInputElement | null, + snapRange: Range | null, + ) { if (modal) modal.remove(); // Overlay @@ -362,6 +424,7 @@ export default defineContentScript({ lineHeight: '1', }); closeBtn.addEventListener('click', () => { + selectionLocked = false; overlay.remove(); modal!.remove(); modal = null; @@ -406,10 +469,52 @@ export default defineContentScript({ cursor: 'pointer', flex: '1', }); - replaceBtn.addEventListener('mousedown', (e) => e.preventDefault()); // keep stored selection intact + replaceBtn.addEventListener('mousedown', (e) => e.preventDefault()); replaceBtn.addEventListener('click', () => { - replaceText(resultText); - selectionLocked = false; // unlock after replace + // Debug log — confirms exactly what state we have at replace time + console.log('[LexAI Replace]', { + snapStart, + snapEnd, + snapElement: snapElement?.tagName ?? 'null', + snapElementValue: snapElement?.value?.substring(0, 50) ?? 'N/A', + hasSnapRange: !!snapRange, + resultText: resultText.substring(0, 50), + }); + + if (snapElement && snapStart >= 0 && snapEnd >= 0 && snapEnd > snapStart) { + // textarea / input path — use the snapshot indices directly + const before = snapElement.value.substring(0, snapStart); + const after = snapElement.value.substring(snapEnd); + console.log('[LexAI Replace] before:', JSON.stringify(before.substring(0, 30))); + console.log('[LexAI Replace] after:', JSON.stringify(after.substring(0, 30))); + snapElement.value = before + resultText + after; + snapElement.setSelectionRange(snapStart, snapStart + resultText.length); + snapElement.dispatchEvent(new Event('input', { bubbles: true })); + snapElement.dispatchEvent(new Event('change', { bubbles: true })); + snapElement.focus(); + } else if (snapRange) { + // contenteditable / DOM range path + console.log('[LexAI Replace] using DOM range'); + try { + snapRange.deleteContents(); + const textNode = document.createTextNode(resultText); + snapRange.insertNode(textNode); + const sel = window.getSelection(); + if (sel) { + sel.removeAllRanges(); + const newRange = document.createRange(); + newRange.setStartAfter(textNode); + newRange.collapse(true); + sel.addRange(newRange); + } + } catch (err) { + console.warn('[LexAI Replace] range replace failed:', err); + } + } else { + console.warn('[LexAI Replace] NO VALID SNAP STATE — snapStart:', snapStart, 'snapEnd:', snapEnd, 'snapElement:', snapElement, 'snapRange:', snapRange); + } + + selectionLocked = false; overlay.remove(); modal!.remove(); modal = null; @@ -460,6 +565,7 @@ export default defineContentScript({ // Close overlay on click overlay.addEventListener('click', () => { + selectionLocked = false; overlay.remove(); modal!.remove(); modal = null; @@ -542,6 +648,7 @@ export default defineContentScript({ if (e.key === 'Escape') { hideToolbar(); if (modal) { + selectionLocked = false; modal.remove(); modal = null; document.querySelectorAll('[data-lexai="true"]').forEach(el => el.remove()); diff --git a/lexai-chrome-mv3.zip b/lexai-chrome-mv3.zip index 274bb8727c42dc7a5cacfc62cdadce89bdf0c2ca..c11c002f916859443d491dbda975f91226cdaa36 100644 GIT binary patch delta 4887 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 Yk1{1C7WQ>(imSI92LiF9yB^|y0GfgfNB{r; delta 5007 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 Q5bgQhb6&5E>0+P%18GO^B>(^b