fix: 더블·트리플클릭 텍스트 선택도 grab 팝오버를 열게 한다 - #65
Merged
Conversation
드래그로 텍스트를 선택하면 프롬프트 팝오버가 열리는데, 더블클릭으로 단어를 선택했을 때는 열리지 않았다. 원인은 텍스트 경로의 드래그 게이트다(main.ts pointerup): pointerDown→pointerup 이동 거리가 DRAG_THRESHOLD(6px)를 넘어야 통과하는데, 더블/트리플클릭은 마우스를 전혀 움직이지 않으므로 원리적으로 이 게이트를 통과할 수 없다. 원래는 "스펙이 드래그 릴리스를 말하지 선택 존재를 말하는 게 아니다"라는 근거로 일부러 막아둔 동작이었다. 이제 멀티클릭을 두 번째 갈래로 인지한다. 클릭 횟수를 읽을 수 있는 지점은 click 이벤트뿐이다 — Chrome의 pointerdown/pointerup은 detail이 언제나 0이라 클릭 횟수를 실어 나르지 않는다(실측). detail >= 2 하나로 더블(2)·트리플(3)을 함께 덮고, 평범한 단일 클릭(detail 1)은 그대로 제외돼 기존 계약이 보존된다. dblclick 이벤트를 쓰지 않은 이유는 트리플클릭에 전용 이벤트가 없어(dblclick은 두 번째 클릭에서 끝난다) 세 번째 클릭의 문단 선택을 놓치기 때문이다. pointerup 본문은 openGrabFromTextSelection으로 뽑아 두 갈래가 공유한다 — 스냅샷 한 틱 지연, grab 하이라이트, 제스처 좌표 앵커가 모두 그대로다. 회귀망(grab.e2e.ts 12종 → 15종): - ⑧ 단일 클릭 미오픈: 문턱값이 >= 1로 밀리는 회귀를 잡는다(더블클릭 positive control이던 자리를 이 역할이 이어받는다) - ⑬ 더블클릭 → 팝오버 오픈, 스니펫은 줄 전체가 아니라 그 단어(chars 경로) - ⑭ 트리플클릭 → 줄 전체(detail 3까지 같은 경로가 덮는지) - ⑮ 헤더 파일명 더블클릭 미오픈(⑨의 멀티클릭 판) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SFhq97x8Y5UZZHq4hvCDGg
코드 리뷰가 짚은 두 건을 재현해 확인한 뒤 고쳤다. 둘 다 기능 결함이 아니라 **회귀망이 지키지 않는 것을 지킨다고 주장하던** 문제다. 1) ⑧은 멀티클릭 문턱값의 positive control이 아니다. 문턱값을 `click.detail >= 2`에서 `>= 1`로 풀고 돌려보니 ⑧은 그대로 통과했다(실측). 단순 클릭은 애초에 선택을 안 만들어(collapsed range → resolveSelectionRange가 null) 문턱값과 무관하게 팝오버가 안 열리기 때문이다. 문턱값을 실제로 가르는 제스처는 shift+클릭 확장(detail 1인데 살아있는 선택)뿐인데 그건 지금 팝오버를 열지 않는 알려진 제외라, 계약으로 박지 않고 ⑧을 스모크 체크로 정직하게 다시 적었다. CLAUDE.md의 같은 주장도 함께 바로잡았다. 2) ⑮는 vacuous였다. 펼친 파일을 헤더 더블클릭하면 첫 클릭이 폴드 토글을 때려 파일이 접히고, 접힌 파일엔 [data-line] 행이 아예 없다 — 즉 "행 밖 선택은 null" 가드가 아니라 "행이 하나도 없음"으로 통과하고 있었다. 팝오버 해석 시점의 행 수를 직접 재보니 [0, 0]이었다. 파일을 먼저 접어 두면 첫 클릭이 도로 펼쳐 두 틱 모두 [5, 5]가 된다. 그렇게 고친 뒤 rowsBetween의 교집합 필터를 걷어내는 뮤테이션을 걸자 ⑨와 함께 ⑮도 실패했다 — 이제 실제로 가드를 행사한다. 곁들여: - CLAUDE.md의 "클릭 횟수는 click에서만 읽을 수 있다"를 포인터 이벤트 한정으로 좁혔다 — mousedown/mouseup은 detail을 싣는다(실측: 1, 2). - 트리플클릭이 팝오버를 한 번 닫았다 다시 여는(깜박이는) 동작을 문서에 남겼다. 바깥 dismiss에 디바운스를 넣으면 조용히 깨진다. - fencedSnippet이 펜스를 못 찾으면 쓰레기를 돌려주는 대신 던진다. - ⑬⑭⑮를 파일 끝으로 옮겨 번호와 파일 순서를 맞췄다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SFhq97x8Y5UZZHq4hvCDGg
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
드래그로 텍스트를 선택하면 프롬프트 팝오버가 열리는데, 더블클릭으로 단어를 선택했을 때는 열리지 않았다.
원인
텍스트 경로의 드래그 게이트(
apps/viewer/browser/main.ts의pointerup)다.pointerDown → pointerup이동 거리가DRAG_THRESHOLD(6px)를 넘어야 통과하는데, 더블/트리플클릭은 마우스를 전혀 움직이지 않으므로 원리적으로 이 게이트를 통과할 수 없다. 원래는 "스펙이 말하는 건 드래그 릴리스이지 선택의 존재가 아니다"라는 근거로 일부러 막아둔 동작이었고, e2e ⑧이 그걸 positive control로 고정하고 있었다. 이 PR은 그 결정을 뒤집는다.고친 방법
멀티클릭을 두 번째 갈래로 인지한다.
click이벤트뿐이다 — Chrome의pointerdown/pointerup은detail이 언제나 0이라 클릭 횟수를 실어 나르지 않는다(실측으로 확인).click시점엔 네이티브 선택이 이미 확정돼 있다.detail >= 2하나로 더블(2)·트리플(3)을 함께 덮고, 평범한 단일 클릭(detail 1)은 그대로 제외돼 기존 계약이 보존된다.dblclick이벤트를 쓰지 않은 이유: 트리플클릭엔 전용 이벤트가 없어(dblclick은 두 번째 클릭에서 끝난다) 세 번째 클릭의 문단 선택을 놓치고 단어 스니펫에 머문다.pointerup본문은openGrabFromTextSelection으로 뽑아 두 갈래가 공유한다 — 스냅샷 한 틱 지연, grab 하이라이트, 제스처 좌표 앵커가 모두 그대로다.Screenshots
같은 위치를 더블클릭한 결과. before는 네이티브 단어 선택만 생기고 끝나고, after는 팝오버(
README.md:1 · new side)가 커서 옆에 열리며 grab 하이라이트가 그 단어를 잡는다.Test plan
TDD로 진행했다 — 새 e2e 3종이 구현 전에 "팝오버가 hidden"으로 실패하는 것을 먼저 확인했다.
회귀망
grab.e2e.ts12종 → 15종:>= 1로 밀리는 회귀를 잡는다. 더블클릭이 맡던 positive control 자리를 이 역할이 이어받는다.chars/CharSpan 경로).toContain은 줄 전체도 통과하는 vacuous 함정이라 펜스 본문을 꺼내 길이로 가른다.dblclick이벤트로 짰다면 여기서 갈린다.실행 결과:
bun run typecheck✅bun run lint/format:check✅ (경고 7건으로 전후 동일)bun run test:coverage✅ 617 pass / 0 fail, 100% 게이트 유지bun run test:e2e— 74 pass / 1 fail. 실패한worker-highlight.e2e.ts:30은 이 변경과 무관한 기존 플레이크로, 베이스라인에서 3회 돌려 1·3회차 통과 / 2회차 동일 실패(로딩 15초 타임아웃 레이스)를 확인했다. grab 계열 24종은 전부 통과.참고
트리플클릭 스니펫에는 빈 줄 하나가 뒤에 붙는다 — Chrome 문단 선택이 다음 행의 offset 0까지 걸치기 때문이고,
chars가 그 끝점을 그대로 옮기므로 하이라이트도 똑같이 거기서 끝난다("보이는 범위 == 복사되는 범위" 유지). 의도된 동작이라 그대로 뒀다.CLAUDE.md의 Grab 절이 반대 계약을 기술하고 있어 같은 커밋에서 갱신했다.🤖 Generated with Claude Code
https://claude.ai/code/session_01SFhq97x8Y5UZZHq4hvCDGg
리뷰 반영 (228bfba)
코드 리뷰가 짚은 두 건은 기능 결함이 아니라 회귀망의 거짓 주장이었다. 둘 다 재현해 확인한 뒤 고쳤다.
1. ⑧은 문턱값의 positive control이 아니었다.
click.detail >= 2를>= 1로 풀고 돌려보니 ⑧은 그대로 통과했다(실측). 단순 클릭은 애초에 선택을 안 만들어(collapsed range) 문턱값과 무관하게 팝오버가 안 열리기 때문이다. 문턱값을 실제로 가르는 제스처는 shift+클릭 확장(detail 1인데 살아있는 선택)뿐인데, 그건 지금 팝오버를 열지 않는 알려진 제외라 계약으로 박지 않고 ⑧을 스모크 체크로 정직하게 다시 적었다. CLAUDE.md의 같은 주장도 함께 바로잡았다.2. ⑮는 vacuous였다. 펼친 파일의 헤더를 더블클릭하면 첫 클릭이 폴드 토글을 때려 파일이 접히고, 접힌 파일엔
[data-line]행이 아예 없다 — "행 밖 선택은 null" 가드가 아니라 "행이 하나도 없음"으로 통과하고 있었다. 팝오버 해석 시점의 행 수를 직접 재보니[0, 0]. 파일을 먼저 접어 두면 첫 클릭이 도로 펼쳐 두 틱 모두[5, 5]가 된다. 고친 뒤rowsBetween의 교집합 필터를 걷어내는 뮤테이션을 걸자 ⑨와 함께 ⑮도 실패했다 — 이제 실제로 가드를 행사한다.곁들여: CLAUDE.md의 "클릭 횟수는 click에서만 읽는다"를 포인터 이벤트 한정으로 좁혔고(
mousedown/mouseup은 detail을 싣는다 — 실측 1, 2), 트리플클릭이 팝오버를 한 번 닫았다 다시 여는 깜박임을 문서에 남겼으며(바깥 dismiss에 디바운스를 넣으면 조용히 깨진다),fencedSnippet이 펜스를 못 찾으면 던지게 했고, ⑬⑭⑮를 파일 끝으로 옮겨 번호와 파일 순서를 맞췄다.재실행: typecheck ✅ / lint 7건(베이스라인 동일) ✅ / format ✅ / coverage 617 pass 100% ✅ / e2e 75 pass 0 fail ✅ (직전 실패했던
worker-highlight도 통과 — 플레이크 확정)