@@ -14,6 +14,14 @@ type LineNumberClickEvent = Pick<
1414 | "defaultPrevented"
1515> ;
1616
17+ function toDocumentOffset (
18+ value : number | null | undefined ,
19+ fallback = 0 ,
20+ ) : number {
21+ const resolved = value != null ? Number ( value ) : fallback ;
22+ return Number . isFinite ( resolved ) ? resolved : fallback ;
23+ }
24+
1725/**
1826 * Resolve the selection range for a clicked document line.
1927 * Includes the trailing line break when one exists to mirror Ace's
@@ -24,18 +32,60 @@ export function getLineSelectionRange(
2432 line : LineInfo ,
2533) : { from : number ; to : number } | null {
2634 if ( ! line ) return null ;
27- const from = Math . max ( 0 , Number ( line . from ) || 0 ) ;
28- const to = Math . max ( from , Number ( line . to ) || from ) ;
35+ const from = Math . max ( 0 , toDocumentOffset ( line . from ) ) ;
36+ const to = Math . max ( from , toDocumentOffset ( line . to , from ) ) ;
2937 return {
3038 from,
3139 to : Math . min ( to + 1 , state . doc . length ) ,
3240 } ;
3341}
3442
43+ function getCurrentSelectionLineRange ( state : EditorView [ "state" ] ) : {
44+ from : number ;
45+ to : number ;
46+ } {
47+ const selection = state . selection . main ;
48+ const startLine = state . doc . lineAt ( selection . from ) ;
49+ const endPos = selection . empty
50+ ? selection . head
51+ : Math . max ( selection . to - 1 , selection . from ) ;
52+ const endLine = state . doc . lineAt ( endPos ) ;
53+ const startRange = getLineSelectionRange ( state , startLine ) ;
54+ const endRange = getLineSelectionRange ( state , endLine ) ;
55+
56+ return {
57+ from : startRange ?. from ?? selection . from ,
58+ to : endRange ?. to ?? selection . to ,
59+ } ;
60+ }
61+
62+ function createLineSelection ( range : {
63+ from : number ;
64+ to : number ;
65+ } ) : EditorSelection {
66+ return EditorSelection . single ( range . to , range . from ) ;
67+ }
68+
69+ function createExtendedLineSelection (
70+ state : EditorView [ "state" ] ,
71+ clickedRange : { from : number ; to : number } ,
72+ ) : EditorSelection {
73+ const currentRange = getCurrentSelectionLineRange ( state ) ;
74+ const from = Math . min ( currentRange . from , clickedRange . from ) ;
75+ const to = Math . max ( currentRange . to , clickedRange . to ) ;
76+
77+ if ( clickedRange . from <= currentRange . from ) {
78+ return EditorSelection . single ( to , from ) ;
79+ }
80+
81+ return EditorSelection . single ( from , to ) ;
82+ }
83+
3584/**
3685 * Select the clicked line from the line-number gutter.
37- * Ignores modified and non-primary clicks so it doesn't interfere with
38- * context menus or alternate selection gestures.
86+ * Shift-click extends the current selection by whole lines.
87+ * Other modified or non-primary clicks are ignored so they don't interfere
88+ * with context menus or alternate selection gestures.
3989 */
4090export function handleLineNumberClick (
4191 view : EditorView | null | undefined ,
@@ -44,7 +94,7 @@ export function handleLineNumberClick(
4494) : boolean {
4595 if ( ! view || ! event || event . defaultPrevented ) return false ;
4696 if ( ( event . button ?? 0 ) !== 0 ) return false ;
47- if ( event . shiftKey || event . altKey || event . ctrlKey || event . metaKey ) {
97+ if ( event . altKey || event . ctrlKey || event . metaKey ) {
4898 return false ;
4999 }
50100
@@ -53,8 +103,10 @@ export function handleLineNumberClick(
53103
54104 event . preventDefault ( ) ;
55105 view . dispatch ( {
56- selection : EditorSelection . single ( range . from , range . to ) ,
57- userEvent : "select.pointer" ,
106+ selection : event . shiftKey
107+ ? createExtendedLineSelection ( view . state , range )
108+ : createLineSelection ( range ) ,
109+ userEvent : event . shiftKey ? "select.extend.pointer" : "select.pointer" ,
58110 } ) ;
59111 view . focus ( ) ;
60112 return true ;
0 commit comments