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")