@@ -11,61 +11,100 @@ extension TextViewController {
1111 /// Moves the selected lines up by one line.
1212 public func moveLinesUp( ) {
1313 guard !cursorPositions. isEmpty else { return }
14+ guard let selection = textView. selectionManager. textSelections. first,
15+ let lineIndexes = getOverlappingLines ( for: selection. range) else { return }
16+ let firstIndex = lineIndexes. lowerBound
17+ guard firstIndex > 0 else { return }
1418
15- textView. undoManager? . beginUndoGrouping ( )
16-
17- textView. editSelections { textView, selection in
18- guard let lineIndexes = getOverlappingLines ( for: selection. range) else { return }
19- let lowerBound = lineIndexes. lowerBound
20- guard lowerBound > . zero,
21- let prevLineInfo = textView. layoutManager. textLineForIndex ( lowerBound - 1 ) ,
22- let prevString = textView. textStorage. substring ( from: prevLineInfo. range) ,
23- let lastSelectedString = textView. layoutManager. textLineForIndex ( lineIndexes. upperBound) else {
24- return
25- }
19+ guard let prevLine = textView. layoutManager. textLineForIndex ( firstIndex - 1 ) ,
20+ let firstSelectedLine = textView. layoutManager. textLineForIndex ( firstIndex) ,
21+ let lastSelectedLine = textView. layoutManager. textLineForIndex ( lineIndexes. upperBound) else {
22+ return
23+ }
2624
27- textView. insertString ( prevString, at: lastSelectedString. range. upperBound)
28- textView. replaceCharacters ( in: [ prevLineInfo. range] , with: String ( ) )
25+ // Combined range: previous line + selected lines
26+ let combinedRange = NSRange (
27+ location: prevLine. range. location,
28+ length: lastSelectedLine. range. upperBound - prevLine. range. location
29+ )
30+ guard let combinedText = textView. textStorage. substring ( from: combinedRange) else { return }
2931
30- let rangeToSelect = NSRange (
31- start: prevLineInfo. range. lowerBound,
32- end: lastSelectedString. range. location - prevLineInfo. range. length + lastSelectedString. range. length
33- )
32+ // Split into previous line text and selected lines text (UTF-16 safe)
33+ let selectedStart = firstSelectedLine. range. location - prevLine. range. location
34+ let nsCombined = combinedText as NSString
35+ let prevText = nsCombined. substring ( to: selectedStart)
36+ let selectedText = nsCombined. substring ( from: selectedStart)
3437
35- setCursorPositions ( [ CursorPosition ( range: rangeToSelect) ] , scrollToVisible: true )
38+ // Ensure both parts have proper newline handling
39+ let newText : String
40+ if selectedText. hasSuffix ( " \n " ) {
41+ newText = selectedText + prevText
42+ } else if prevText. hasSuffix ( " \n " ) {
43+ // Selected text is last line (no trailing \n), prev has \n
44+ newText = selectedText + " \n " + String( prevText. dropLast ( ) )
45+ } else {
46+ newText = selectedText + " \n " + prevText
3647 }
3748
49+ textView. undoManager? . beginUndoGrouping ( )
50+ textView. replaceCharacters ( in: combinedRange, with: newText)
51+
52+ // Place cursor at the start of the moved line
53+ setCursorPositions (
54+ [ CursorPosition ( range: NSRange ( location: prevLine. range. location, length: 0 ) ) ] ,
55+ scrollToVisible: true
56+ )
3857 textView. undoManager? . endUndoGrouping ( )
3958 }
4059
4160 /// Moves the selected lines down by one line.
4261 public func moveLinesDown( ) {
4362 guard !cursorPositions. isEmpty else { return }
63+ guard let selection = textView. selectionManager. textSelections. first,
64+ let lineIndexes = getOverlappingLines ( for: selection. range) else { return }
65+ let lastIndex = lineIndexes. upperBound
66+ guard lastIndex + 1 < textView. layoutManager. lineCount else { return }
4467
45- textView. undoManager? . beginUndoGrouping ( )
46-
47- textView. editSelections { textView, selection in
48- guard let lineIndexes = getOverlappingLines ( for: selection. range) else { return }
49- let totalLines = textView. layoutManager. lineCount
50- let upperBound = lineIndexes. upperBound
51- guard upperBound + 1 < totalLines,
52- let nextLineInfo = textView. layoutManager. textLineForIndex ( upperBound + 1 ) ,
53- let nextString = textView. textStorage. substring ( from: nextLineInfo. range) ,
54- let firstSelectedString = textView. layoutManager. textLineForIndex ( lineIndexes. lowerBound) else {
55- return
56- }
68+ guard let firstSelectedLine = textView. layoutManager. textLineForIndex ( lineIndexes. lowerBound) ,
69+ let lastSelectedLine = textView. layoutManager. textLineForIndex ( lastIndex) ,
70+ let nextLine = textView. layoutManager. textLineForIndex ( lastIndex + 1 ) ,
71+ nextLine. range. length > 0 else {
72+ return
73+ }
5774
58- textView. replaceCharacters ( in: [ nextLineInfo. range] , with: String ( ) )
59- textView. insertString ( nextString, at: firstSelectedString. range. lowerBound)
75+ // Combined range: selected lines + next line
76+ let combinedRange = NSRange (
77+ location: firstSelectedLine. range. location,
78+ length: nextLine. range. upperBound - firstSelectedLine. range. location
79+ )
80+ guard let combinedText = textView. textStorage. substring ( from: combinedRange) else { return }
6081
61- let rangeToSelect = NSRange (
62- start: firstSelectedString. range. location + nextLineInfo. range. length,
63- end: nextLineInfo. range. upperBound
64- )
82+ // Split into selected lines text and next line text (UTF-16 safe)
83+ let selectedLength = lastSelectedLine. range. upperBound - firstSelectedLine. range. location
84+ let nsCombined = combinedText as NSString
85+ let selectedText = nsCombined. substring ( to: selectedLength)
86+ let nextText = nsCombined. substring ( from: selectedLength)
6587
66- setCursorPositions ( [ CursorPosition ( range: rangeToSelect) ] , scrollToVisible: true )
88+ // Ensure both parts have proper newline handling
89+ let newText : String
90+ if nextText. hasSuffix ( " \n " ) {
91+ newText = nextText + selectedText
92+ } else if selectedText. hasSuffix ( " \n " ) {
93+ newText = nextText + " \n " + String( selectedText. dropLast ( ) )
94+ } else {
95+ newText = nextText + " \n " + selectedText
6796 }
6897
98+ textView. undoManager? . beginUndoGrouping ( )
99+ textView. replaceCharacters ( in: combinedRange, with: newText)
100+
101+ // Place cursor at the start of the moved line in its new position
102+ let nextLen = ( nextText as NSString ) . length + ( nextText. hasSuffix ( " \n " ) ? 0 : 1 )
103+ let newLocation = firstSelectedLine. range. location + nextLen
104+ setCursorPositions (
105+ [ CursorPosition ( range: NSRange ( location: newLocation, length: 0 ) ) ] ,
106+ scrollToVisible: true
107+ )
69108 textView. undoManager? . endUndoGrouping ( )
70109 }
71110}
0 commit comments