Skip to content

Commit 169e4ea

Browse files
Indent Options and Clarify Tab Width (CodeEditApp#171)
<!--- IMPORTANT: If this PR addresses multiple unrelated issues, it will be closed until separated. --> ### Description > This is a near clone of CodeEditApp#147, but git got messed up on that branch. This PR improves that branch anyways. This enables configuration of the behavior when the tab key is pressed. Previously all tabs were converted to spaces and inserted `tabWidth` spaces in place of the tab character. This PR clarifies that the `tabWidth` parameter should be used for the *visual* width of tabs, and adds an `indentOption` parameter that specifies how to handle inserting tab characters. Adds an `IndentOption` enum with two cases for this behavior: - `spaces(count: Int)` - `tab` If `spaces(count: Int)` is specified, the editor will insert the given number of spaces when the tab key is pressed, otherwise the tab character will be kept. ### Related Issues <!--- REQUIRED: Tag all related issues (e.g. * CodeEditApp#123) --> <!--- If this PR resolves the issue please specify (e.g. * closes CodeEditApp#123) --> <!--- If this PR addresses multiple issues, these issues must be related to one other --> * CodeEditApp#80 - Does not close, needs an additional PR for the tab width setting. ### Checklist <!--- Add things that are not yet implemented above --> - [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 https://user-images.githubusercontent.com/35942988/228014785-85a20e2e-0465-4767-9d53-b97b4df2e11e.mov
1 parent a60580a commit 169e4ea

File tree

8 files changed

+150
-41
lines changed

8 files changed

+150
-41
lines changed

Sources/CodeEditTextView/CodeEditTextView.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
1818
/// - language: The language for syntax highlighting
1919
/// - theme: The theme for syntax highlighting
2020
/// - font: The default font
21-
/// - tabWidth: The tab width
21+
/// - tabWidth: The visual tab width in number of spaces
22+
/// - indentOption: The behavior to use when the tab key is pressed. Defaults to 4 spaces.
2223
/// - lineHeight: The line height multiplier (e.g. `1.2`)
2324
/// - wrapLines: Whether lines wrap to the width of the editor
2425
/// - editorOverscroll: The percentage for overscroll, between 0-1 (default: `0.0`)
@@ -33,6 +34,7 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
3334
theme: Binding<EditorTheme>,
3435
font: Binding<NSFont>,
3536
tabWidth: Binding<Int>,
37+
indentOption: Binding<IndentOption> = .constant(.spaces(count: 4)),
3638
lineHeight: Binding<Double>,
3739
wrapLines: Binding<Bool>,
3840
editorOverscroll: Binding<Double> = .constant(0.0),
@@ -48,6 +50,7 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
4850
self.useThemeBackground = useThemeBackground
4951
self._font = font
5052
self._tabWidth = tabWidth
53+
self._indentOption = indentOption
5154
self._lineHeight = lineHeight
5255
self._wrapLines = wrapLines
5356
self._editorOverscroll = editorOverscroll
@@ -62,6 +65,7 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
6265
@Binding private var theme: EditorTheme
6366
@Binding private var font: NSFont
6467
@Binding private var tabWidth: Int
68+
@Binding private var indentOption: IndentOption
6569
@Binding private var lineHeight: Double
6670
@Binding private var wrapLines: Bool
6771
@Binding private var editorOverscroll: Double
@@ -80,6 +84,7 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
8084
font: font,
8185
theme: theme,
8286
tabWidth: tabWidth,
87+
indentOption: indentOption,
8388
wrapLines: wrapLines,
8489
cursorPosition: $cursorPosition,
8590
editorOverscroll: editorOverscroll,
@@ -94,20 +99,25 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
9499

95100
public func updateNSViewController(_ controller: NSViewControllerType, context: Context) {
96101
controller.font = font
97-
controller.tabWidth = tabWidth
98102
controller.wrapLines = wrapLines
99103
controller.useThemeBackground = useThemeBackground
100104
controller.lineHeightMultiple = lineHeight
101105
controller.editorOverscroll = editorOverscroll
102106
controller.contentInsets = contentInsets
103107

104-
// Updating the language and theme needlessly can cause highlights to be re-calculated.
108+
// Updating the language, theme, tab width and indent option needlessly can cause highlights to be re-calculated
105109
if controller.language.id != language.id {
106110
controller.language = language
107111
}
108112
if controller.theme != theme {
109113
controller.theme = theme
110114
}
115+
if controller.indentOption != indentOption {
116+
controller.indentOption = indentOption
117+
}
118+
if controller.tabWidth != tabWidth {
119+
controller.tabWidth = tabWidth
120+
}
111121

112122
controller.reloadUI()
113123
return

Sources/CodeEditTextView/Controller/STTextViewController.swift

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,20 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
3737
/// Whether the code editor should use the theme background color or be transparent
3838
public var useThemeBackground: Bool
3939

40-
/// The number of spaces to use for a `tab '\t'` character
41-
public var tabWidth: Int
40+
/// The visual width of tab characters in the text view measured in number of spaces.
41+
public var tabWidth: Int {
42+
didSet {
43+
paragraphStyle = generateParagraphStyle()
44+
reloadUI()
45+
}
46+
}
47+
48+
/// The behavior to use when the tab key is pressed.
49+
public var indentOption: IndentOption {
50+
didSet {
51+
setUpTextFormation()
52+
}
53+
}
4254

4355
/// A multiplier for setting the line height. Defaults to `1.0`
4456
public var lineHeightMultiple: Double = 1.0
@@ -68,9 +80,6 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
6880

6981
internal var highlighter: Highlighter?
7082

71-
/// Internal variable for tracking whether or not the textView has the correct standard attributes.
72-
private var hasSetStandardAttributes: Bool = false
73-
7483
/// The provided highlight provider.
7584
private var highlightProvider: HighlightProviding?
7685

@@ -82,6 +91,7 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
8291
font: NSFont,
8392
theme: EditorTheme,
8493
tabWidth: Int,
94+
indentOption: IndentOption,
8595
wrapLines: Bool,
8696
cursorPosition: Binding<(Int, Int)>,
8797
editorOverscroll: Double,
@@ -95,6 +105,7 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
95105
self.font = font
96106
self.theme = theme
97107
self.tabWidth = tabWidth
108+
self.indentOption = indentOption
98109
self.wrapLines = wrapLines
99110
self.cursorPosition = cursorPosition
100111
self.editorOverscroll = editorOverscroll
@@ -142,6 +153,7 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
142153
scrollView.verticalRulerView = rulerView
143154
scrollView.rulersVisible = true
144155

156+
textView.typingAttributes = attributesFor(nil)
145157
textView.defaultParagraphStyle = self.paragraphStyle
146158
textView.font = self.font
147159
textView.textColor = theme.text
@@ -214,11 +226,17 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
214226
// MARK: UI
215227

216228
/// A default `NSParagraphStyle` with a set `lineHeight`
217-
private var paragraphStyle: NSMutableParagraphStyle {
229+
private lazy var paragraphStyle: NSMutableParagraphStyle = generateParagraphStyle()
230+
231+
private func generateParagraphStyle() -> NSMutableParagraphStyle {
218232
// swiftlint:disable:next force_cast
219233
let paragraph = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
220234
paragraph.minimumLineHeight = lineHeight
221235
paragraph.maximumLineHeight = lineHeight
236+
// TODO: Fix Tab widths
237+
// This adds tab stops throughout the document instead of only changing the width of tab characters
238+
// paragraph.tabStops = [NSTextTab(type: .decimalTabStopType, location: 0.0)]
239+
// paragraph.defaultTabInterval = CGFloat(tabWidth) * (" " as NSString).size(withAttributes: [.font: font]).width
222240
return paragraph
223241
}
224242

@@ -238,9 +256,6 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
238256
internal func reloadUI() {
239257
// if font or baseline has been modified, set the hasSetStandardAttributesFlag
240258
// to false to ensure attributes are updated. This allows live UI updates when changing preferences.
241-
if textView?.font != font || rulerView.baselineOffset != baselineOffset {
242-
hasSetStandardAttributes = false
243-
}
244259

245260
textView?.textColor = theme.text
246261
textView.backgroundColor = useThemeBackground ? theme.background : .clear
@@ -249,6 +264,8 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
249264
textView?.selectedLineHighlightColor = theme.lineHighlight
250265
textView?.isEditable = isEditable
251266
textView.highlightSelectedLine = isEditable
267+
textView?.typingAttributes = attributesFor(nil)
268+
textView?.defaultParagraphStyle = paragraphStyle
252269

253270
rulerView?.backgroundColor = useThemeBackground ? theme.background : .clear
254271
rulerView?.separatorColor = theme.invisibles
@@ -267,15 +284,6 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
267284
scrollView.contentInsets.bottom = bottomContentInsets + (contentInsets?.bottom ?? 0)
268285
}
269286

270-
setStandardAttributes()
271-
}
272-
273-
/// Sets the standard attributes (`font`, `baselineOffset`) to the whole text
274-
internal func setStandardAttributes() {
275-
guard let textView = textView else { return }
276-
guard !hasSetStandardAttributes else { return }
277-
hasSetStandardAttributes = true
278-
textView.addAttributes(attributesFor(nil), range: .init(0..<textView.string.count))
279287
highlighter?.invalidate()
280288
}
281289

@@ -286,7 +294,8 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
286294
return [
287295
.font: font,
288296
.foregroundColor: theme.colorFor(capture),
289-
.baselineOffset: baselineOffset
297+
.baselineOffset: baselineOffset,
298+
.paragraphStyle: paragraphStyle
290299
]
291300
}
292301

@@ -331,11 +340,14 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
331340
}
332341
}
333342

334-
// MARK: Key Presses
343+
// MARK: Selectors
335344

336-
/// Handles `keyDown` events in the `textView`
337345
override public func keyDown(with event: NSEvent) {
338-
// TODO: - This should be uncessecary
346+
// This should be uneccessary but if removed STTextView receives some `keydown`s twice.
347+
}
348+
349+
public override func insertTab(_ sender: Any?) {
350+
textView.insertText("\t", replacementRange: textView.selectedRange)
339351
}
340352

341353
deinit {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//
2+
// IndentOption.swift
3+
//
4+
//
5+
// Created by Khan Winter on 3/26/23.
6+
//
7+
8+
/// Represents what to insert on a tab key press.
9+
public enum IndentOption: Equatable {
10+
case spaces(count: Int)
11+
case tab
12+
13+
var stringValue: String {
14+
switch self {
15+
case .spaces(let count):
16+
return String(repeating: " ", count: count)
17+
case .tab:
18+
return "\t"
19+
}
20+
}
21+
22+
public static func == (lhs: IndentOption, rhs: IndentOption) -> Bool {
23+
switch (lhs, rhs) {
24+
case (.tab, .tab):
25+
return true
26+
case (.spaces(let lhsCount), .spaces(let rhsCount)):
27+
return lhsCount == rhsCount
28+
default:
29+
return false
30+
}
31+
}
32+
}

Sources/CodeEditTextView/Extensions/STTextView+/STTextView+TextInterface.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,7 @@ extension STTextView: TextInterface {
4242
textContentStorage.performEditingTransaction {
4343
textContentStorage.applyMutation(mutation)
4444
}
45+
46+
didChangeText()
4547
}
4648
}

Sources/CodeEditTextView/Filters/DeleteWhitespaceFilter.swift

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import TextStory
1111

1212
/// Filter for quickly deleting indent whitespace
1313
struct DeleteWhitespaceFilter: Filter {
14-
let indentationUnit: String
14+
let indentOption: IndentOption
1515

1616
func processMutation(_ mutation: TextMutation, in interface: TextInterface) -> FilterAction {
17-
guard mutation.string == "" && mutation.range.length == 1 else {
17+
guard mutation.string == "" && mutation.range.length == 1 && indentOption != .tab else {
1818
return .none
1919
}
2020

@@ -26,13 +26,14 @@ struct DeleteWhitespaceFilter: Filter {
2626
return .none
2727
}
2828

29+
let indentLength = indentOption.stringValue.count
2930
let length = mutation.range.max - preceedingNonWhitespace
30-
let numberOfExtraSpaces = length % indentationUnit.count
31+
let numberOfExtraSpaces = length % indentLength
3132

32-
if numberOfExtraSpaces == 0 && length >= indentationUnit.count {
33+
if numberOfExtraSpaces == 0 && length >= indentLength {
3334
interface.applyMutation(
34-
TextMutation(delete: NSRange(location: mutation.range.max - indentationUnit.count,
35-
length: indentationUnit.count),
35+
TextMutation(delete: NSRange(location: mutation.range.max - indentLength,
36+
length: indentLength),
3637
limit: mutation.limit)
3738
)
3839
return .discard

Sources/CodeEditTextView/Filters/STTextViewController+TextFormation.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ extension STTextViewController {
1818
internal func setUpTextFormation() {
1919
textFilters = []
2020

21-
let indentationUnit = String(repeating: " ", count: tabWidth)
21+
let indentationUnit = indentOption.stringValue
2222

2323
let pairsToHandle: [(String, String)] = [
2424
("{", "}"),
@@ -38,9 +38,9 @@ extension STTextViewController {
3838

3939
setUpOpenPairFilters(pairs: pairsToHandle, whitespaceProvider: whitespaceProvider)
4040
setUpNewlineTabFilters(whitespaceProvider: whitespaceProvider,
41-
indentationUnit: indentationUnit)
41+
indentOption: indentOption)
4242
setUpDeletePairFilters(pairs: pairsToHandle)
43-
setUpDeleteWhitespaceFilter(indentationUnit: indentationUnit)
43+
setUpDeleteWhitespaceFilter(indentOption: indentOption)
4444
}
4545

4646
/// Returns a `TextualIndenter` based on available language configuration.
@@ -70,9 +70,9 @@ extension STTextViewController {
7070
/// - Parameters:
7171
/// - whitespaceProvider: The whitespace providers to use.
7272
/// - indentationUnit: The unit of indentation to use.
73-
private func setUpNewlineTabFilters(whitespaceProvider: WhitespaceProviders, indentationUnit: String) {
73+
private func setUpNewlineTabFilters(whitespaceProvider: WhitespaceProviders, indentOption: IndentOption) {
7474
let newlineFilter: Filter = NewlineProcessingFilter(whitespaceProviders: whitespaceProvider)
75-
let tabReplacementFilter: Filter = TabReplacementFilter(indentationUnit: indentationUnit)
75+
let tabReplacementFilter: Filter = TabReplacementFilter(indentOption: indentOption)
7676

7777
textFilters.append(contentsOf: [newlineFilter, tabReplacementFilter])
7878
}
@@ -86,8 +86,8 @@ extension STTextViewController {
8686
}
8787

8888
/// Configures up the delete whitespace filter.
89-
private func setUpDeleteWhitespaceFilter(indentationUnit: String) {
90-
let filter = DeleteWhitespaceFilter(indentationUnit: indentationUnit)
89+
private func setUpDeleteWhitespaceFilter(indentOption: IndentOption) {
90+
let filter = DeleteWhitespaceFilter(indentOption: indentOption)
9191
textFilters.append(filter)
9292
}
9393

Sources/CodeEditTextView/Filters/TabReplacementFilter.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ import TextStory
1212
/// Filter for replacing tab characters with the user-defined indentation unit.
1313
/// - Note: The undentation unit can be another tab character, this is merely a point at which this can be configured.
1414
struct TabReplacementFilter: Filter {
15-
let indentationUnit: String
15+
let indentOption: IndentOption
1616

1717
func processMutation(_ mutation: TextMutation, in interface: TextInterface) -> FilterAction {
18-
if mutation.string == "\t" {
19-
interface.applyMutation(TextMutation(insert: indentationUnit,
18+
if mutation.string == "\t" && indentOption != .tab && mutation.delta > 0 {
19+
interface.applyMutation(TextMutation(insert: indentOption.stringValue,
2020
at: mutation.range.location,
2121
limit: mutation.limit))
2222
return .discard

0 commit comments

Comments
 (0)