From dae56c8f37489166a2b8f29677f3bb10cb291bf9 Mon Sep 17 00:00:00 2001
From: Sophia Hooley <143217945+Sophiahooley@users.noreply.github.com>
Date: Thu, 25 Apr 2024 11:45:58 -0500
Subject: [PATCH] Toggle comment for current line via CMD + / (#241)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This change implements the functionality of ⌘ /
for single line comments. It allows you to toggle between commented and
uncommented for the line the cursor is currently on when you press
⌘ /.
To do so, I implemented a `keyDown` event recognizer, which listens for
when the relevant keys are pressed. If ⌘ / 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
---------
Co-authored-by: Abe
---
.../TextViewController+LoadView.swift | 13 +++
.../TextViewController+Shortcuts.swift | 92 +++++++++++++++++++
.../Controller/TextViewController.swift | 1 -
3 files changed, 105 insertions(+), 1 deletion(-)
create mode 100644 Sources/CodeEditSourceEditor/Controller/TextViewController+Shortcuts.swift
diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift
index 598359647..9750bd1eb 100644
--- a/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift
+++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift
@@ -109,5 +109,18 @@ extension TextViewController {
}
}
.store(in: &cancellables)
+
+ NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
+ guard self.view.window?.firstResponder == self.textView else { return event }
+ let charactersIgnoringModifiers = event.charactersIgnoringModifiers
+ let commandKey = NSEvent.ModifierFlags.command.rawValue
+ let modifierFlags = event.modifierFlags.intersection(.deviceIndependentFlagsMask).rawValue
+ if modifierFlags == commandKey && event.charactersIgnoringModifiers == "/" {
+ self.commandSlashCalled()
+ return nil
+ } else {
+ return event
+ }
+ }
}
}
diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+Shortcuts.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+Shortcuts.swift
new file mode 100644
index 000000000..24fe58263
--- /dev/null
+++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+Shortcuts.swift
@@ -0,0 +1,92 @@
+//
+// TextViewController+Shortcuts.swift
+// CodeEditSourceEditor
+//
+// Created by Sophia Hooley on 4/21/24.
+//
+
+import CodeEditTextView
+import AppKit
+
+extension TextViewController {
+ /// Method called when CMD + / key sequence recognized, comments cursor's current line of code
+ public func commandSlashCalled() {
+ guard let cursorPosition = cursorPositions.first else {
+ print("There is no cursor \(#function)")
+ return
+ }
+ // Many languages require a character sequence at the beginning of the line to comment the line.
+ // (ex. python #, C++ //)
+ // If such a sequence exists, we will insert that sequence at the beginning of the line
+ if !language.lineCommentString.isEmpty {
+ toggleCharsAtBeginningOfLine(chars: language.lineCommentString, lineNumber: cursorPosition.line)
+ }
+ // In other cases, languages require a character sequence at beginning and end of a line, aka a range comment
+ // (Ex. HTML )
+ // We treat the line as a one-line range to comment it out using rangeCommentStrings on both sides of the line
+ else {
+ let (openComment, closeComment) = language.rangeCommentStrings
+ toggleCharsAtEndOfLine(chars: closeComment, lineNumber: cursorPosition.line)
+ toggleCharsAtBeginningOfLine(chars: openComment, lineNumber: cursorPosition.line)
+ }
+ }
+
+ /// Toggles comment string at the beginning of a specified line (lineNumber is 1-indexed)
+ private func toggleCharsAtBeginningOfLine(chars: String, lineNumber: Int) {
+ guard let lineInfo = textView.layoutManager.textLineForIndex(lineNumber - 1) else {
+ print("There are no characters/lineInfo \(#function)")
+ return
+ }
+ guard let lineString = textView.textStorage.substring(from: lineInfo.range) else {
+ print("There are no characters/lineString \(#function)")
+ return
+ }
+ let firstNonWhiteSpaceCharIndex = lineString.firstIndex(where: {!$0.isWhitespace}) ?? lineString.startIndex
+ let numWhitespaceChars = lineString.distance(from: lineString.startIndex, to: firstNonWhiteSpaceCharIndex)
+ let firstCharsInLine = lineString.suffix(from: firstNonWhiteSpaceCharIndex).prefix(chars.count)
+ // toggle comment off
+ if firstCharsInLine == chars {
+ textView.replaceCharacters(in: NSRange(
+ location: lineInfo.range.location + numWhitespaceChars,
+ length: chars.count
+ ), with: "")
+ }
+ // toggle comment on
+ else {
+ textView.replaceCharacters(in: NSRange(
+ location: lineInfo.range.location + numWhitespaceChars,
+ length: 0
+ ), with: chars)
+ }
+ }
+
+ /// Toggles a specific string of characters at the end of a specified line. (lineNumber is 1-indexed)
+ private func toggleCharsAtEndOfLine(chars: String, lineNumber: Int) {
+ guard let lineInfo = textView.layoutManager.textLineForIndex(lineNumber - 1) else {
+ print("There are no characters/lineInfo \(#function)")
+ return
+ }
+ guard let lineString = textView.textStorage.substring(from: lineInfo.range) else {
+ print("There are no characters/lineString \(#function)")
+ return
+ }
+ let lineLastCharIndex = lineInfo.range.location + lineInfo.range.length - 1
+ let closeCommentLength = chars.count
+ let closeCommentRange = NSRange(
+ location: lineLastCharIndex - closeCommentLength,
+ length: closeCommentLength
+ )
+ let lastCharsInLine = textView.textStorage.substring(from: closeCommentRange)
+ // toggle comment off
+ if lastCharsInLine == chars {
+ textView.replaceCharacters(in: NSRange(
+ location: lineLastCharIndex - closeCommentLength,
+ length: closeCommentLength
+ ), with: "")
+ }
+ // toggle comment on
+ else {
+ textView.replaceCharacters(in: NSRange(location: lineLastCharIndex, length: 0), with: chars)
+ }
+ }
+}
diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift
index d2ae830f1..32a73f6fd 100644
--- a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift
+++ b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift
@@ -16,7 +16,6 @@ import TextFormation
///
/// A view controller class for managing a source editor. Uses ``CodeEditTextView/TextView`` for input and rendering,
/// tree-sitter for syntax highlighting, and TextFormation for live editing completions.
-///
public class TextViewController: NSViewController {
// swiftlint:disable:next line_length
public static let cursorPositionUpdatedNotification: Notification.Name = .init("TextViewController.cursorPositionNotification")