Skip to content

Commit 2cba052

Browse files
Integrate TextFormation (#135)
1 parent 311e324 commit 2cba052

File tree

8 files changed

+341
-20
lines changed

8 files changed

+341
-20
lines changed

Package.resolved

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,18 @@ let package = Package(
2525
url: "https://github.com/lukepistrol/SwiftLintPlugin",
2626
from: "0.2.2"
2727
),
28+
.package(
29+
url: "https://github.com/ChimeHQ/TextFormation",
30+
from: "0.6.7"
31+
)
2832
],
2933
targets: [
3034
.target(
3135
name: "CodeEditTextView",
3236
dependencies: [
3337
"STTextView",
3438
"CodeEditLanguages",
39+
"TextFormation"
3540
],
3641
plugins: [
3742
.plugin(name: "SwiftLint", package: "SwiftLintPlugin")
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// STTextView+TextInterface.swift
3+
//
4+
//
5+
// Created by Khan Winter on 1/26/23.
6+
//
7+
8+
import AppKit
9+
import STTextView
10+
import TextStory
11+
import TextFormation
12+
13+
extension STTextView: TextInterface {
14+
public var selectedRange: NSRange {
15+
get {
16+
return self.selectedRange()
17+
}
18+
set {
19+
if let textRange = NSTextRange(newValue, provider: textContentStorage) {
20+
self.setSelectedRange(textRange)
21+
}
22+
}
23+
}
24+
25+
public var length: Int {
26+
textContentStorage.length
27+
}
28+
29+
public func substring(from range: NSRange) -> String? {
30+
return textContentStorage.substring(from: range)
31+
}
32+
33+
public func applyMutation(_ mutation: TextStory.TextMutation) {
34+
if let manager = undoManager {
35+
let inverse = inverseMutation(for: mutation)
36+
37+
manager.registerUndo(withTarget: self, handler: { (storable) in
38+
storable.applyMutation(inverse)
39+
})
40+
}
41+
42+
textContentStorage.performEditingTransaction {
43+
textContentStorage.applyMutation(mutation)
44+
}
45+
}
46+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//
2+
// DeleteWhitespaceFilter.swift
3+
//
4+
//
5+
// Created by Khan Winter on 1/28/23.
6+
//
7+
8+
import Foundation
9+
import TextFormation
10+
import TextStory
11+
12+
/// Filter for quickly deleting indent whitespace
13+
struct DeleteWhitespaceFilter: Filter {
14+
let indentationUnit: String
15+
16+
func processMutation(_ mutation: TextMutation, in interface: TextInterface) -> FilterAction {
17+
guard mutation.string == "" && mutation.range.length == 1 else {
18+
return .none
19+
}
20+
21+
// Walk backwards from the mutation, grabbing as much whitespace as possible
22+
guard let preceedingNonWhitespace = interface.findPrecedingOccurrenceOfCharacter(
23+
in: CharacterSet.whitespaces.inverted,
24+
from: mutation.range.max
25+
) else {
26+
return .none
27+
}
28+
29+
let length = mutation.range.max - preceedingNonWhitespace
30+
let numberOfExtraSpaces = length % indentationUnit.count
31+
32+
if numberOfExtraSpaces == 0 && length >= indentationUnit.count {
33+
interface.applyMutation(
34+
TextMutation(delete: NSRange(location: mutation.range.max - indentationUnit.count,
35+
length: indentationUnit.count),
36+
limit: mutation.limit)
37+
)
38+
return .discard
39+
}
40+
41+
return .none
42+
}
43+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// NewlineFilter.swift
3+
//
4+
//
5+
// Created by Khan Winter on 1/28/23.
6+
//
7+
8+
import Foundation
9+
import TextFormation
10+
import TextStory
11+
12+
/// A newline filter almost entirely similar to `TextFormation`s standard implementation.
13+
struct NewlineFilter: Filter {
14+
private let recognizer: ConsecutiveCharacterRecognizer
15+
let providers: WhitespaceProviders
16+
17+
init(whitespaceProviders: WhitespaceProviders) {
18+
self.recognizer = ConsecutiveCharacterRecognizer(matching: "\n")
19+
self.providers = whitespaceProviders
20+
}
21+
22+
func processMutation(_ mutation: TextStory.TextMutation,
23+
in interface: TextFormation.TextInterface) -> TextFormation.FilterAction {
24+
recognizer.processMutation(mutation)
25+
26+
switch recognizer.state {
27+
case .triggered:
28+
return filterHandler(mutation, in: interface)
29+
case .tracking, .idle:
30+
return .none
31+
}
32+
}
33+
34+
private func filterHandler(_ mutation: TextMutation, in interface: TextInterface) -> FilterAction {
35+
interface.applyMutation(mutation)
36+
37+
let range = NSRange(location: mutation.postApplyRange.max, length: 0)
38+
39+
let value = providers.leadingWhitespace(range, interface)
40+
41+
interface.insertString(value, at: mutation.postApplyRange.max)
42+
43+
return .discard
44+
}
45+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
//
2+
// STTextViewController+TextFormation.swift
3+
//
4+
//
5+
// Created by Khan Winter on 1/26/23.
6+
//
7+
8+
import AppKit
9+
import STTextView
10+
import TextFormation
11+
import TextStory
12+
13+
extension STTextViewController {
14+
15+
// MARK: - Filter Configuration
16+
17+
/// Initializes any filters for text editing.
18+
internal func setUpTextFormation() {
19+
textFilters = []
20+
21+
let indentationUnit = String(repeating: " ", count: tabWidth)
22+
23+
let pairsToHandle: [(String, String)] = [
24+
("{", "}"),
25+
("[", "]"),
26+
("(", ")"),
27+
("<", ">")
28+
]
29+
30+
let indenter: TextualIndenter = getTextIndenter()
31+
let whitespaceProvider = WhitespaceProviders(
32+
leadingWhitespace: indenter.substitionProvider(indentationUnit: indentationUnit,
33+
width: tabWidth),
34+
trailingWhitespace: { _, _ in "" }
35+
)
36+
37+
// Filters
38+
39+
setUpOpenPairFilters(pairs: pairsToHandle, whitespaceProvider: whitespaceProvider)
40+
setUpNewlineTabFilters(whitespaceProvider: whitespaceProvider,
41+
indentationUnit: indentationUnit)
42+
setUpDeletePairFilters(pairs: pairsToHandle)
43+
setUpDeleteWhitespaceFilter(indentationUnit: indentationUnit)
44+
}
45+
46+
/// Returns a `TextualIndenter` based on available language configuration.
47+
private func getTextIndenter() -> TextualIndenter {
48+
switch language.id {
49+
case .python:
50+
return TextualIndenter(patterns: TextualIndenter.pythonPatterns)
51+
case .ruby:
52+
return TextualIndenter(patterns: TextualIndenter.rubyPatterns)
53+
default:
54+
return TextualIndenter(patterns: TextualIndenter.basicPatterns)
55+
}
56+
}
57+
58+
/// Configures pair filters and adds them to the `textFilters` array.
59+
/// - Parameters:
60+
/// - pairs: The pairs to configure. Eg: `{` and `}`
61+
/// - whitespaceProvider: The whitespace providers to use.
62+
private func setUpOpenPairFilters(pairs: [(String, String)], whitespaceProvider: WhitespaceProviders) {
63+
for pair in pairs {
64+
let filter = StandardOpenPairFilter(open: pair.0, close: pair.1, whitespaceProviders: whitespaceProvider)
65+
textFilters.append(filter)
66+
}
67+
}
68+
69+
/// Configures newline and tab replacement filters.
70+
/// - Parameters:
71+
/// - whitespaceProvider: The whitespace providers to use.
72+
/// - indentationUnit: The unit of indentation to use.
73+
private func setUpNewlineTabFilters(whitespaceProvider: WhitespaceProviders, indentationUnit: String) {
74+
let newlineFilter: Filter = NewlineFilter(whitespaceProviders: whitespaceProvider)
75+
let tabReplacementFilter: Filter = TabReplacementFilter(indentationUnit: indentationUnit)
76+
77+
textFilters.append(contentsOf: [newlineFilter, tabReplacementFilter])
78+
}
79+
80+
/// Configures delete pair filters.
81+
private func setUpDeletePairFilters(pairs: [(String, String)]) {
82+
for pair in pairs {
83+
let filter = DeleteCloseFilter(open: pair.0, close: pair.1)
84+
textFilters.append(filter)
85+
}
86+
}
87+
88+
/// Configures up the delete whitespace filter.
89+
private func setUpDeleteWhitespaceFilter(indentationUnit: String) {
90+
let filter = DeleteWhitespaceFilter(indentationUnit: indentationUnit)
91+
textFilters.append(filter)
92+
}
93+
94+
// MARK: - Delegate Methods
95+
96+
public func textView(_ textView: STTextView,
97+
shouldChangeTextIn affectedCharRange: NSTextRange,
98+
replacementString: String?) -> Bool {
99+
guard let range = affectedCharRange.nsRange(using: textView.textContentStorage) else {
100+
return true
101+
}
102+
103+
let mutation = TextMutation(string: replacementString ?? "",
104+
range: range,
105+
limit: textView.textContentStorage.length)
106+
107+
textView.undoManager?.beginUndoGrouping()
108+
109+
let result = shouldApplyMutation(mutation, to: textView)
110+
111+
textView.undoManager?.endUndoGrouping()
112+
113+
return result
114+
}
115+
116+
/// Determines whether or not a text mutation should be applied.
117+
/// - Parameters:
118+
/// - mutation: The text mutation.
119+
/// - textView: The textView to use.
120+
/// - Returns: Return whether or not the mutation should be applied.
121+
private func shouldApplyMutation(_ mutation: TextMutation, to textView: STTextView) -> Bool {
122+
// don't perform any kind of filtering during undo operations
123+
if textView.undoManager?.isUndoing ?? false || textView.undoManager?.isRedoing ?? false {
124+
return true
125+
}
126+
127+
for filter in textFilters {
128+
let action = filter.processMutation(mutation, in: textView)
129+
130+
switch action {
131+
case .none:
132+
break
133+
case .stop:
134+
return true
135+
case .discard:
136+
return false
137+
}
138+
}
139+
140+
return true
141+
}
142+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
//
2+
// TabReplacementFilter.swift
3+
//
4+
//
5+
// Created by Khan Winter on 1/28/23.
6+
//
7+
8+
import Foundation
9+
import TextFormation
10+
import TextStory
11+
12+
/// Filter for replacing tab characters with the user-defined indentation unit.
13+
/// - Note: The undentation unit can be another tab character, this is merely a point at which this can be configured.
14+
struct TabReplacementFilter: Filter {
15+
let indentationUnit: String
16+
17+
func processMutation(_ mutation: TextMutation, in interface: TextInterface) -> FilterAction {
18+
if mutation.string == "\t" {
19+
interface.applyMutation(TextMutation(insert: indentationUnit,
20+
at: mutation.range.location,
21+
limit: mutation.limit))
22+
return .discard
23+
} else {
24+
return .none
25+
}
26+
}
27+
}

0 commit comments

Comments
 (0)