Skip to content

Commit dae56c8

Browse files
Toggle comment for current line via CMD + / (#241)
This change implements the functionality of <kbd>⌘</kbd> <kbd>/</kbd> for single line comments. It allows you to toggle between commented and uncommented for the line the cursor is currently on when you press <kbd>⌘</kbd> <kbd>/</kbd>. To do so, I implemented a `keyDown` event recognizer, which listens for when the relevant keys are pressed. If <kbd>⌘</kbd> <kbd>/</kbd> is pressed, it calls a method called `commandSlashCalled()`, which decides which toggle is supposed to happen depending on if the line is already commented or not. It also addresses the situation of special cases of languages like HTML, which need a comment at the beginning and end of the line (essentially a `rangeComment`) to comment a single line. ### Related Issues - #38 This PR accomplishes part of #38. I talked with some of the project leads (@FastestMolasses) and they said it makes sense to break #38 into a couple different issues (I.e. single-line vs highlighted chunks, etc). Single-line comment toggling is completed as of this PR but commenting highlighted code still needs to be completed. ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots <img width="626" alt="Screenshot 2024-04-19 at 5 44 24 PM" src="https://github.com/CodeEditApp/CodeEditSourceEditor/assets/143217945/1200a5c1-d8ea-48a4-9704-70db7aa23fc7"> --------- Co-authored-by: Abe <abe.malla8@gmail.com>
1 parent 0c741c2 commit dae56c8

File tree

3 files changed

+105
-1
lines changed

3 files changed

+105
-1
lines changed

Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift

+13
Original file line numberDiff line numberDiff line change
@@ -109,5 +109,18 @@ extension TextViewController {
109109
}
110110
}
111111
.store(in: &cancellables)
112+
113+
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
114+
guard self.view.window?.firstResponder == self.textView else { return event }
115+
let charactersIgnoringModifiers = event.charactersIgnoringModifiers
116+
let commandKey = NSEvent.ModifierFlags.command.rawValue
117+
let modifierFlags = event.modifierFlags.intersection(.deviceIndependentFlagsMask).rawValue
118+
if modifierFlags == commandKey && event.charactersIgnoringModifiers == "/" {
119+
self.commandSlashCalled()
120+
return nil
121+
} else {
122+
return event
123+
}
124+
}
112125
}
113126
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//
2+
// TextViewController+Shortcuts.swift
3+
// CodeEditSourceEditor
4+
//
5+
// Created by Sophia Hooley on 4/21/24.
6+
//
7+
8+
import CodeEditTextView
9+
import AppKit
10+
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+
print("There is no cursor \(#function)")
16+
return
17+
}
18+
// Many languages require a character sequence at the beginning of the line to comment the line.
19+
// (ex. python #, C++ //)
20+
// If such a sequence exists, we will insert that sequence at the beginning of the line
21+
if !language.lineCommentString.isEmpty {
22+
toggleCharsAtBeginningOfLine(chars: language.lineCommentString, lineNumber: cursorPosition.line)
23+
}
24+
// In other cases, languages require a character sequence at beginning and end of a line, aka a range comment
25+
// (Ex. HTML <!--line here -->)
26+
// We treat the line as a one-line range to comment it out using rangeCommentStrings on both sides of the line
27+
else {
28+
let (openComment, closeComment) = language.rangeCommentStrings
29+
toggleCharsAtEndOfLine(chars: closeComment, lineNumber: cursorPosition.line)
30+
toggleCharsAtBeginningOfLine(chars: openComment, lineNumber: cursorPosition.line)
31+
}
32+
}
33+
34+
/// Toggles comment string at the beginning of a specified line (lineNumber is 1-indexed)
35+
private func toggleCharsAtBeginningOfLine(chars: String, lineNumber: Int) {
36+
guard let lineInfo = textView.layoutManager.textLineForIndex(lineNumber - 1) else {
37+
print("There are no characters/lineInfo \(#function)")
38+
return
39+
}
40+
guard let lineString = textView.textStorage.substring(from: lineInfo.range) else {
41+
print("There are no characters/lineString \(#function)")
42+
return
43+
}
44+
let firstNonWhiteSpaceCharIndex = lineString.firstIndex(where: {!$0.isWhitespace}) ?? lineString.startIndex
45+
let numWhitespaceChars = lineString.distance(from: lineString.startIndex, to: firstNonWhiteSpaceCharIndex)
46+
let firstCharsInLine = lineString.suffix(from: firstNonWhiteSpaceCharIndex).prefix(chars.count)
47+
// toggle comment off
48+
if firstCharsInLine == chars {
49+
textView.replaceCharacters(in: NSRange(
50+
location: lineInfo.range.location + numWhitespaceChars,
51+
length: chars.count
52+
), with: "")
53+
}
54+
// toggle comment on
55+
else {
56+
textView.replaceCharacters(in: NSRange(
57+
location: lineInfo.range.location + numWhitespaceChars,
58+
length: 0
59+
), with: chars)
60+
}
61+
}
62+
63+
/// Toggles a specific string of characters at the end of a specified line. (lineNumber is 1-indexed)
64+
private func toggleCharsAtEndOfLine(chars: String, lineNumber: Int) {
65+
guard let lineInfo = textView.layoutManager.textLineForIndex(lineNumber - 1) else {
66+
print("There are no characters/lineInfo \(#function)")
67+
return
68+
}
69+
guard let lineString = textView.textStorage.substring(from: lineInfo.range) else {
70+
print("There are no characters/lineString \(#function)")
71+
return
72+
}
73+
let lineLastCharIndex = lineInfo.range.location + lineInfo.range.length - 1
74+
let closeCommentLength = chars.count
75+
let closeCommentRange = NSRange(
76+
location: lineLastCharIndex - closeCommentLength,
77+
length: closeCommentLength
78+
)
79+
let lastCharsInLine = textView.textStorage.substring(from: closeCommentRange)
80+
// toggle comment off
81+
if lastCharsInLine == chars {
82+
textView.replaceCharacters(in: NSRange(
83+
location: lineLastCharIndex - closeCommentLength,
84+
length: closeCommentLength
85+
), with: "")
86+
}
87+
// toggle comment on
88+
else {
89+
textView.replaceCharacters(in: NSRange(location: lineLastCharIndex, length: 0), with: chars)
90+
}
91+
}
92+
}

Sources/CodeEditSourceEditor/Controller/TextViewController.swift

-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import TextFormation
1616
///
1717
/// A view controller class for managing a source editor. Uses ``CodeEditTextView/TextView`` for input and rendering,
1818
/// tree-sitter for syntax highlighting, and TextFormation for live editing completions.
19-
///
2019
public class TextViewController: NSViewController {
2120
// swiftlint:disable:next line_length
2221
public static let cursorPositionUpdatedNotification: Notification.Name = .init("TextViewController.cursorPositionNotification")

0 commit comments

Comments
 (0)