@@ -9,72 +9,222 @@ import CodeEditTextView
99import AppKit
1010
1111extension 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+ }
3028 }
29+
30+ // End the undo grouping to complete the undo operation for the comment toggle.
31+ textView. undoManager? . endUndoGrouping ( )
3132 }
3233
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 {
3756 return
3857 }
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
47107 )
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)
48141 } 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)
54143 }
55144 }
56145
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+ )
60167 return
61168 }
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: " "
67185 )
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)
75221 } 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: " " )
78228 }
79229 }
80230}
0 commit comments