From 32a82fa3c9dea2e6d84a97278ee23bd9fb929380 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Thu, 13 Jun 2024 14:54:34 -0500 Subject: [PATCH] Implement the Emacs Kill Ring --- .../TextView/TextView+Delete.swift | 3 + .../TextView/TextView+Insert.swift | 31 ++++++++ .../TextView/TextView+ReplaceCharacters.swift | 2 - .../CodeEditTextView/TextView/TextView.swift | 16 ++-- Sources/CodeEditTextView/Utils/KillRing.swift | 55 ++++++++++++++ .../CodeEditTextViewTests/KillRingTests.swift | 73 +++++++++++++++++++ 6 files changed, 170 insertions(+), 10 deletions(-) create mode 100644 Sources/CodeEditTextView/Utils/KillRing.swift create mode 100644 Tests/CodeEditTextViewTests/KillRingTests.swift diff --git a/Sources/CodeEditTextView/TextView/TextView+Delete.swift b/Sources/CodeEditTextView/TextView/TextView+Delete.swift index e8d6ecce..2691c37f 100644 --- a/Sources/CodeEditTextView/TextView/TextView+Delete.swift +++ b/Sources/CodeEditTextView/TextView/TextView+Delete.swift @@ -60,6 +60,9 @@ extension TextView { guard extendedRange.location >= 0 else { continue } textSelection.range.formUnion(extendedRange) } + KillRing.shared.kill( + strings: selectionManager.textSelections.map(\.range).compactMap({ textStorage.substring(from: $0) }) + ) replaceCharacters(in: selectionManager.textSelections.map(\.range), with: "") unmarkTextIfNeeded() } diff --git a/Sources/CodeEditTextView/TextView/TextView+Insert.swift b/Sources/CodeEditTextView/TextView/TextView+Insert.swift index f51e891d..8c4fc408 100644 --- a/Sources/CodeEditTextView/TextView/TextView+Insert.swift +++ b/Sources/CodeEditTextView/TextView/TextView+Insert.swift @@ -15,4 +15,35 @@ extension TextView { override public func insertTab(_ sender: Any?) { insertText("\t") } + + override public func yank(_ sender: Any?) { + let strings = KillRing.shared.yank() + insertMultipleString(strings) + } + + /// Not documented or in any headers, but required if kill ring size > 1. + /// From Cocoa docs: "note that yankAndSelect: is not listed in any headers" + @objc func yankAndSelect(_ sender: Any?) { + let strings = KillRing.shared.yankAndSelect() + insertMultipleString(strings) + } + + private func insertMultipleString(_ strings: [String]) { + let selectedRanges = selectionManager.textSelections.map(\.range) + + guard selectedRanges.count > 0 else { return } + + for idx in (0.. [String] { + return buffer[index] + } + + /// Yanks an item from the ring, and selects the next one in the ring. + func yankAndSelect() -> [String] { + let retVal = buffer[index] + incrementIndex() + return retVal + } + + private func incrementIndex() { + index = (index + 1) % buffer.count + } +} diff --git a/Tests/CodeEditTextViewTests/KillRingTests.swift b/Tests/CodeEditTextViewTests/KillRingTests.swift new file mode 100644 index 00000000..ad092eb4 --- /dev/null +++ b/Tests/CodeEditTextViewTests/KillRingTests.swift @@ -0,0 +1,73 @@ +import XCTest +@testable import CodeEditTextView + +class KillRingTests: XCTestCase { + func test_killRingYank() { + var ring = KillRing.shared + ring.kill(strings: ["hello"]) + for _ in 0..<100 { + XCTAssertEqual(ring.yank(), ["hello"]) + } + + ring.kill(strings: ["hello", "multiple", "strings"]) + // should never change on yank + for _ in 0..<100 { + XCTAssertEqual(ring.yank(), ["hello", "multiple", "strings"]) + } + + ring = KillRing(2) + ring.kill(strings: ["hello"]) + for _ in 0..<100 { + XCTAssertEqual(ring.yank(), ["hello"]) + } + + ring.kill(strings: ["hello", "multiple", "strings"]) + // should never change on yank + for _ in 0..<100 { + XCTAssertEqual(ring.yank(), ["hello", "multiple", "strings"]) + } + } + + func test_killRingYankAndSelect() { + let ring = KillRing(5) + ring.kill(strings: ["1"]) + ring.kill(strings: ["2"]) + ring.kill(strings: ["3", "3", "3"]) + ring.kill(strings: ["4", "4"]) + ring.kill(strings: ["5"]) + // should loop + for _ in 0..<5 { + XCTAssertEqual(ring.yankAndSelect(), ["5"]) + XCTAssertEqual(ring.yankAndSelect(), ["1"]) + XCTAssertEqual(ring.yankAndSelect(), ["2"]) + XCTAssertEqual(ring.yankAndSelect(), ["3", "3", "3"]) + XCTAssertEqual(ring.yankAndSelect(), ["4", "4"]) + } + } + + func test_textViewYank() { + let view = TextView(string: "Hello World") + view.selectionManager.setSelectedRange(NSRange(location: 0, length: 1)) + view.delete(self) + XCTAssertEqual(view.string, "ello World") + + view.yank(self) + XCTAssertEqual(view.string, "Hello World") + view.selectionManager.setSelectedRange(NSRange(location: 0, length: 0)) + view.yank(self) + XCTAssertEqual(view.string, "HHello World") + } + + func test_textViewYankMultipleCursors() { + let view = TextView(string: "Hello World") + view.selectionManager.setSelectedRanges([NSRange(location: 1, length: 0), NSRange(location: 4, length: 0)]) + view.delete(self) + XCTAssertEqual(view.string, "elo World") + + view.yank(self) + XCTAssertEqual(view.string, "Hello World") + view.selectionManager.setSelectedRanges([NSRange(location: 0, length: 0)]) + view.yank(self) + XCTAssertEqual(view.string, "H\nlHello World") + } +}