@@ -9,72 +9,222 @@ import CodeEditTextView
9
9
import AppKit
10
10
11
11
extension TextViewController {
12
- /// Method called when CMD + / key sequence recognized, comments cursor's current line of code
13
- public func commandSlashCalled( ) {
14
- guard let cursorPosition = cursorPositions. first else {
15
- return
16
- }
17
- // Many languages require a character sequence at the beginning of the line to comment the line.
18
- // (ex. python #, C++ //)
19
- // If such a sequence exists, we will insert that sequence at the beginning of the line
20
- if !language. lineCommentString. isEmpty {
21
- toggleCharsAtBeginningOfLine ( chars: language. lineCommentString, lineNumber: cursorPosition. line)
22
- }
23
- // In other cases, languages require a character sequence at beginning and end of a line, aka a range comment
24
- // (Ex. HTML <!--line here -->)
25
- // We treat the line as a one-line range to comment it out using rangeCommentStrings on both sides of the line
26
- else {
27
- let ( openComment, closeComment) = language. rangeCommentStrings
28
- toggleCharsAtEndOfLine ( chars: closeComment, lineNumber: cursorPosition. line)
29
- toggleCharsAtBeginningOfLine ( chars: openComment, lineNumber: cursorPosition. line)
12
+ /// Method called when CMD + / key sequence is recognized.
13
+ /// Comments or uncomments the cursor's current line(s) of code.
14
+ public func handleCommandSlash( ) {
15
+ guard let cursorPosition = cursorPositions. first else { return }
16
+ // Set up a cache to avoid redundant computations.
17
+ // The cache stores line information (e.g., ranges), line contents,
18
+ // and other relevant data to improve efficiency.
19
+ var cache = CommentCache ( )
20
+ populateCommentCache ( for: cursorPosition. range, using: & cache)
21
+
22
+ // Begin an undo grouping to allow for a single undo operation for the entire comment toggle.
23
+ textView. undoManager? . beginUndoGrouping ( )
24
+ for lineInfo in cache. lineInfos {
25
+ if let lineInfo {
26
+ toggleComment ( lineInfo: lineInfo, cache: cache)
27
+ }
30
28
}
29
+
30
+ // End the undo grouping to complete the undo operation for the comment toggle.
31
+ textView. undoManager? . endUndoGrouping ( )
31
32
}
32
33
33
- /// Toggles comment string at the beginning of a specified line (lineNumber is 1-indexed)
34
- private func toggleCharsAtBeginningOfLine( chars: String , lineNumber: Int ) {
35
- guard let lineInfo = textView. layoutManager. textLineForIndex ( lineNumber - 1 ) ,
36
- let lineString = textView. textStorage. substring ( from: lineInfo. range) else {
34
+ // swiftlint:disable cyclomatic_complexity
35
+ /// Populates the comment cache with information about the lines within a specified range,
36
+ /// determining whether comment characters should be inserted or removed.
37
+ /// - Parameters:
38
+ /// - range: The range of text to process.
39
+ /// - commentCache: A cache object to store comment-related data, such as line information,
40
+ /// shift factors, and content.
41
+ func populateCommentCache( for range: NSRange , using commentCache: inout CommentCache ) {
42
+ // Determine the appropriate comment characters based on the language settings.
43
+ if language. lineCommentString. isEmpty {
44
+ commentCache. startCommentChars = language. rangeCommentStrings. 0
45
+ commentCache. endCommentChars = language. rangeCommentStrings. 1
46
+ } else {
47
+ commentCache. startCommentChars = language. lineCommentString
48
+ }
49
+
50
+ // Return early if no comment characters are available.
51
+ guard let startCommentChars = commentCache. startCommentChars else { return }
52
+
53
+ // Fetch the starting line's information and content.
54
+ guard let startLineInfo = textView. layoutManager. textLineForOffset ( range. location) ,
55
+ let startLineContent = textView. textStorage. substring ( from: startLineInfo. range) else {
37
56
return
38
57
}
39
- let firstNonWhiteSpaceCharIndex = lineString. firstIndex ( where: { !$0. isWhitespace} ) ?? lineString. startIndex
40
- let numWhitespaceChars = lineString. distance ( from: lineString. startIndex, to: firstNonWhiteSpaceCharIndex)
41
- let firstCharsInLine = lineString. suffix ( from: firstNonWhiteSpaceCharIndex) . prefix ( chars. count)
42
- // toggle comment off
43
- if firstCharsInLine == chars {
44
- textView. replaceCharacters (
45
- in: NSRange ( location: lineInfo. range. location + numWhitespaceChars, length: chars. count) ,
46
- with: " "
58
+
59
+ // Initialize cache with the first line's information.
60
+ commentCache. lineInfos = [ startLineInfo]
61
+ commentCache. lineStrings [ startLineInfo. index] = startLineContent
62
+ commentCache. shouldInsertCommentChars = !startLineContent
63
+ . trimmingCharacters ( in: . whitespacesAndNewlines) . starts ( with: startCommentChars)
64
+
65
+ // Retrieve information for the ending line. Proceed only if the ending line
66
+ // is different from the starting line, indicating that the user has selected more than one line.
67
+ guard let endLineInfo = textView. layoutManager. textLineForOffset ( range. upperBound) ,
68
+ endLineInfo. index != startLineInfo. index else { return }
69
+
70
+ // Check if comment characters need to be inserted for the ending line.
71
+ if let endLineContent = textView. textStorage. substring ( from: endLineInfo. range) {
72
+ // If comment characters need to be inserted, they should be added to every line within the range.
73
+ if !commentCache. shouldInsertCommentChars {
74
+ commentCache. shouldInsertCommentChars = !endLineContent
75
+ . trimmingCharacters ( in: . whitespacesAndNewlines) . starts ( with: startCommentChars)
76
+ }
77
+ commentCache. lineStrings [ endLineInfo. index] = endLineContent
78
+ }
79
+
80
+ // Process all lines between the start and end lines.
81
+ let intermediateLines = ( startLineInfo. index + 1 ) ..< endLineInfo. index
82
+ for (offset, lineIndex) in intermediateLines. enumerated ( ) {
83
+ guard let lineInfo = textView. layoutManager. textLineForIndex ( lineIndex) else { break }
84
+ // Cache the line content here since we'll need to access it anyway
85
+ // to append a comment at the end of the line.
86
+ if let lineContent = textView. textStorage. substring ( from: lineInfo. range) {
87
+ // Line content is accessed only when:
88
+ // - A line's comment is toggled off, or
89
+ // - Comment characters need to be appended to the end of the line.
90
+ if language. lineCommentString. isEmpty || !commentCache. shouldInsertCommentChars {
91
+ commentCache. lineStrings [ lineIndex] = lineContent
92
+ }
93
+
94
+ if !commentCache. shouldInsertCommentChars {
95
+ commentCache. shouldInsertCommentChars = !lineContent
96
+ . trimmingCharacters ( in: . whitespacesAndNewlines)
97
+ . starts ( with: startCommentChars)
98
+ }
99
+ }
100
+
101
+ // Cache line information and calculate the shift range factor.
102
+ commentCache. lineInfos. append ( lineInfo)
103
+ commentCache. shiftRangeFactors [ lineIndex] = calculateShiftRangeFactor (
104
+ startCount: startCommentChars. count,
105
+ endCount: commentCache. endCommentChars? . count,
106
+ lineCount: offset
47
107
)
108
+ }
109
+
110
+ // Cache the ending line's information and calculate its shift range factor.
111
+ commentCache. lineInfos. append ( endLineInfo)
112
+ commentCache. shiftRangeFactors [ endLineInfo. index] = calculateShiftRangeFactor (
113
+ startCount: startCommentChars. count,
114
+ endCount: commentCache. endCommentChars? . count,
115
+ lineCount: intermediateLines. count
116
+ )
117
+ }
118
+ // swiftlint:enable cyclomatic_complexity
119
+
120
+ /// Calculates the shift range factor based on the counts of start and
121
+ /// end comment characters and the number of intermediate lines.
122
+ ///
123
+ /// - Parameters:
124
+ /// - startCount: The number of characters in the start comment.
125
+ /// - endCount: An optional number of characters in the end comment. If `nil`, it is treated as 0.
126
+ /// - lineCount: The number of intermediate lines between the start and end comments.
127
+ ///
128
+ /// - Returns: The computed shift range factor as an `Int`.
129
+ func calculateShiftRangeFactor( startCount: Int , endCount: Int ? , lineCount: Int ) -> Int {
130
+ let effectiveEndCount = endCount ?? 0
131
+ return ( startCount + effectiveEndCount) * ( lineCount + 1 )
132
+ }
133
+ /// Toggles the presence of comment characters at the beginning and/or end
134
+ /// - Parameters:
135
+ /// - lineInfo: Contains information about the specific line, including its position and range.
136
+ /// - cache: A cache holding comment-related data such as the comment characters and line content.
137
+ private func toggleComment( lineInfo: TextLineStorage < TextLine > . TextLinePosition , cache: borrowing CommentCache ) {
138
+ if cache. endCommentChars != nil {
139
+ toggleCommentAtEndOfLine ( lineInfo: lineInfo, cache: cache)
140
+ toggleCommentAtBeginningOfLine ( lineInfo: lineInfo, cache: cache)
48
141
} else {
49
- // toggle comment on
50
- textView. replaceCharacters (
51
- in: NSRange ( location: lineInfo. range. location + numWhitespaceChars, length: 0 ) ,
52
- with: chars
53
- )
142
+ toggleCommentAtBeginningOfLine ( lineInfo: lineInfo, cache: cache)
54
143
}
55
144
}
56
145
57
- /// Toggles a specific string of characters at the end of a specified line. (lineNumber is 1-indexed)
58
- private func toggleCharsAtEndOfLine( chars: String , lineNumber: Int ) {
59
- guard let lineInfo = textView. layoutManager. textLineForIndex ( lineNumber - 1 ) , !lineInfo. range. isEmpty else {
146
+ /// Toggles the presence of comment characters at the beginning of a line in the text view.
147
+ /// - Parameters:
148
+ /// - lineInfo: Contains information about the specific line, including its position and range.
149
+ /// - cache: A cache holding comment-related data such as the comment characters and line content.
150
+ private func toggleCommentAtBeginningOfLine(
151
+ lineInfo: TextLineStorage < TextLine > . TextLinePosition ,
152
+ cache: borrowing CommentCache
153
+ ) {
154
+ // Ensure there are comment characters to toggle.
155
+ guard let startCommentChars = cache. startCommentChars else { return }
156
+
157
+ // Calculate the range shift based on cached factors, defaulting to 0 if unavailable.
158
+ let rangeShift = cache. shiftRangeFactors [ lineInfo. index] ?? 0
159
+
160
+ // If we need to insert comment characters at the beginning of the line.
161
+ if cache. shouldInsertCommentChars {
162
+ guard let adjustedRange = lineInfo. range. shifted ( by: rangeShift) else { return }
163
+ textView. replaceCharacters (
164
+ in: NSRange ( location: adjustedRange. location, length: 0 ) ,
165
+ with: startCommentChars
166
+ )
60
167
return
61
168
}
62
- let lineLastCharIndex = lineInfo. range. location + lineInfo. range. length - 1
63
- let closeCommentLength = chars. count
64
- let closeCommentRange = NSRange (
65
- location: lineLastCharIndex - closeCommentLength,
66
- length: closeCommentLength
169
+
170
+ // If we need to remove comment characters from the beginning of the line.
171
+ guard let adjustedRange = lineInfo. range. shifted ( by: - rangeShift) else { return }
172
+
173
+ // Retrieve the current line's string content from the cache or the text view's storage.
174
+ guard let lineContent =
175
+ cache. lineStrings [ lineInfo. index] ?? textView. textStorage. substring ( from: adjustedRange) else { return }
176
+
177
+ // Find the index of the first non-whitespace character.
178
+ let firstNonWhitespaceIndex = lineContent. firstIndex ( where: { !$0. isWhitespace } ) ?? lineContent. startIndex
179
+ let leadingWhitespaceCount = lineContent. distance ( from: lineContent. startIndex, to: firstNonWhitespaceIndex)
180
+
181
+ // Remove the comment characters from the beginning of the line.
182
+ textView. replaceCharacters (
183
+ in: NSRange ( location: adjustedRange. location + leadingWhitespaceCount, length: startCommentChars. count) ,
184
+ with: " "
67
185
)
68
- let lastCharsInLine = textView. textStorage. substring ( from: closeCommentRange)
69
- // toggle comment off
70
- if lastCharsInLine == chars {
71
- textView. replaceCharacters (
72
- in: NSRange ( location: lineLastCharIndex - closeCommentLength, length: closeCommentLength) ,
73
- with: " "
74
- )
186
+ }
187
+
188
+ /// Toggles the presence of comment characters at the end of a line in the text view.
189
+ /// - Parameters:
190
+ /// - lineInfo: Contains information about the specific line, including its position and range.
191
+ /// - cache: A cache holding comment-related data such as the comment characters and line content.
192
+ private func toggleCommentAtEndOfLine(
193
+ lineInfo: TextLineStorage < TextLine > . TextLinePosition ,
194
+ cache: borrowing CommentCache
195
+ ) {
196
+ // Ensure there are comment characters to toggle and the line is not empty.
197
+ guard let endingCommentChars = cache. endCommentChars else { return }
198
+ guard !lineInfo. range. isEmpty else { return }
199
+
200
+ // Calculate the range shift based on cached factors, defaulting to 0 if unavailable.
201
+ let rangeShift = cache. shiftRangeFactors [ lineInfo. index] ?? 0
202
+
203
+ // Shift the line range by `rangeShift` if inserting comment characters, or by `-rangeShift` if removing them.
204
+ guard let adjustedRange = lineInfo. range. shifted ( by: cache. shouldInsertCommentChars ? rangeShift : - rangeShift)
205
+ else { return }
206
+
207
+ // Retrieve the current line's string content from the cache or the text view's storage.
208
+ guard let lineContent =
209
+ cache. lineStrings [ lineInfo. index] ?? textView. textStorage. substring ( from: adjustedRange) else { return }
210
+
211
+ var endIndex = adjustedRange. upperBound
212
+
213
+ // If the last character is a newline, adjust the insertion point to before the newline.
214
+ if lineContent. last? . isNewline ?? false {
215
+ endIndex -= 1
216
+ }
217
+
218
+ if cache. shouldInsertCommentChars {
219
+ // Insert the comment characters at the calculated position.
220
+ textView. replaceCharacters ( in: NSRange ( location: endIndex, length: 0 ) , with: endingCommentChars)
75
221
} else {
76
- // toggle comment on
77
- textView. replaceCharacters ( in: NSRange ( location: lineLastCharIndex, length: 0 ) , with: chars)
222
+ // Remove the comment characters if they exist at the end of the line.
223
+ let commentRange = NSRange (
224
+ location: endIndex - endingCommentChars. count,
225
+ length: endingCommentChars. count
226
+ )
227
+ textView. replaceCharacters ( in: commentRange, with: " " )
78
228
}
79
229
}
80
230
}
0 commit comments