1+ import { EditorSelection } from "@codemirror/state" ;
12import selectionMenu from "lib/selectionMenu" ;
23
34const TAP_MAX_DELAY = 500 ;
@@ -126,9 +127,11 @@ class TouchSelectionMenuController {
126127 #view;
127128 #container;
128129 #getActiveFile;
130+ #isShiftSelectionActive;
129131 #stateSyncRaf = 0 ;
130132 #isScrolling = false ;
131133 #isPointerInteracting = false ;
134+ #shiftSelectionSession = null ;
132135 #menuActive = false ;
133136 #menuRequested = false ;
134137 #enabled = true ;
@@ -140,6 +143,8 @@ class TouchSelectionMenuController {
140143 this . #container =
141144 options . container || view . dom . closest ( ".editor-container" ) || view . dom ;
142145 this . #getActiveFile = options . getActiveFile || ( ( ) => null ) ;
146+ this . #isShiftSelectionActive =
147+ options . isShiftSelectionActive || ( ( ) => false ) ;
143148 this . $menu = document . createElement ( "menu" ) ;
144149 this . $menu . className = "cursor-menu" ;
145150 this . #bindEvents( ) ;
@@ -170,12 +175,14 @@ class TouchSelectionMenuController {
170175 this . #clearMenuShowTimer( ) ;
171176 cancelAnimationFrame ( this . #stateSyncRaf) ;
172177 this . #stateSyncRaf = 0 ;
178+ this . #shiftSelectionSession = null ;
173179 this . #hideMenu( true ) ;
174180 }
175181
176182 setEnabled ( enabled ) {
177183 this . #enabled = ! ! enabled ;
178184 if ( this . #enabled) return ;
185+ this . #shiftSelectionSession = null ;
179186 this . #menuRequested = false ;
180187 this . #isPointerInteracting = false ;
181188 this . #isScrolling = false ;
@@ -246,6 +253,7 @@ class TouchSelectionMenuController {
246253
247254 onSessionChanged ( ) {
248255 if ( ! this . #enabled) return ;
256+ this . #shiftSelectionSession = null ;
249257 this . #menuRequested = false ;
250258 this . #isPointerInteracting = false ;
251259 this . #isScrolling = false ;
@@ -265,19 +273,29 @@ class TouchSelectionMenuController {
265273 #onGlobalPointerDown = ( event ) => {
266274 const target = event . target ;
267275 if ( this . $menu . contains ( target ) ) return ;
268- if ( this . #isIgnoredPointerTarget( target ) ) return ;
276+ if ( this . #isIgnoredPointerTarget( target ) ) {
277+ this . #shiftSelectionSession = null ;
278+ return ;
279+ }
269280 if ( target instanceof Node && this . #view. dom . contains ( target ) ) {
281+ this . #captureShiftSelection( event ) ;
270282 this . #isPointerInteracting = true ;
271283 this . #clearMenuShowTimer( ) ;
272284 this . #hideMenu( ) ;
273285 return ;
274286 }
287+ this . #shiftSelectionSession = null ;
275288 this . #isPointerInteracting = false ;
276289 this . #menuRequested = false ;
277290 this . #hideMenu( ) ;
278291 } ;
279292
280- #onGlobalPointerUp = ( ) => {
293+ #onGlobalPointerUp = ( event ) => {
294+ if ( event . type === "pointerup" ) {
295+ this . #commitShiftSelection( event ) ;
296+ } else {
297+ this . #shiftSelectionSession = null ;
298+ }
281299 if ( ! this . #isPointerInteracting) return ;
282300 this . #isPointerInteracting = false ;
283301 if ( ! this . #enabled) return ;
@@ -291,6 +309,56 @@ class TouchSelectionMenuController {
291309 this . #hideMenu( ) ;
292310 } ;
293311
312+ #captureShiftSelection( event ) {
313+ if ( ! this . #canExtendSelection( event ) ) {
314+ this . #shiftSelectionSession = null ;
315+ return ;
316+ }
317+
318+ this . #shiftSelectionSession = {
319+ pointerId : event . pointerId ,
320+ anchor : this . #view. state . selection . main . anchor ,
321+ x : event . clientX ,
322+ y : event . clientY ,
323+ } ;
324+ }
325+
326+ #commitShiftSelection( event ) {
327+ const session = this . #shiftSelectionSession;
328+ this . #shiftSelectionSession = null ;
329+ if ( ! session ) return ;
330+ if ( ! this . #canExtendSelection( event ) ) return ;
331+ if ( event . pointerId !== session . pointerId ) return ;
332+ if (
333+ Math . hypot ( event . clientX - session . x , event . clientY - session . y ) >
334+ TAP_MAX_DISTANCE
335+ ) {
336+ return ;
337+ }
338+ const target = event . target ;
339+ if ( ! ( target instanceof Node ) || ! this . #view. dom . contains ( target ) ) return ;
340+ if ( this . #isIgnoredPointerTarget( target ) ) return ;
341+
342+ // Rely on pointer coordinates instead of click events so touch selection
343+ // keeps working when the browser/native path owns the actual tap.
344+ const head = this . #view. posAtCoords (
345+ { x : event . clientX , y : event . clientY } ,
346+ false ,
347+ ) ;
348+ this . #view. dispatch ( {
349+ selection : EditorSelection . range ( session . anchor , head ) ,
350+ userEvent : "select.extend" ,
351+ } ) ;
352+ event . preventDefault ( ) ;
353+ }
354+
355+ #canExtendSelection( event ) {
356+ if ( ! this . #enabled) return false ;
357+ if ( ! ( event . isTrusted && event . isPrimary ) ) return false ;
358+ if ( typeof event . button === "number" && event . button !== 0 ) return false ;
359+ return ! ! this . #isShiftSelectionActive( event ) ;
360+ }
361+
294362 #shouldShowMenu( ) {
295363 if ( this . #isScrolling || this . #isPointerInteracting || ! this . #view. hasFocus )
296364 return false ;
0 commit comments