Skip to content

Commit

Permalink
Indent Options and Clarify Tab Width (#171)
Browse files Browse the repository at this point in the history
<!--- IMPORTANT: If this PR addresses multiple unrelated issues, it will
be closed until separated. -->

### Description

> This is a near clone of #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. * #123) -->
<!--- If this PR resolves the issue please specify (e.g. * closes #123)
-->
<!--- If this PR addresses multiple issues, these issues must be related
to one other -->

* #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
  • Loading branch information
thecoolwinter authored Mar 28, 2023
1 parent a60580a commit 169e4ea
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 41 deletions.
16 changes: 13 additions & 3 deletions Sources/CodeEditTextView/CodeEditTextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
/// - language: The language for syntax highlighting
/// - theme: The theme for syntax highlighting
/// - font: The default font
/// - tabWidth: The tab width
/// - tabWidth: The visual tab width in number of spaces
/// - indentOption: The behavior to use when the tab key is pressed. Defaults to 4 spaces.
/// - lineHeight: The line height multiplier (e.g. `1.2`)
/// - wrapLines: Whether lines wrap to the width of the editor
/// - editorOverscroll: The percentage for overscroll, between 0-1 (default: `0.0`)
Expand All @@ -33,6 +34,7 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
theme: Binding<EditorTheme>,
font: Binding<NSFont>,
tabWidth: Binding<Int>,
indentOption: Binding<IndentOption> = .constant(.spaces(count: 4)),
lineHeight: Binding<Double>,
wrapLines: Binding<Bool>,
editorOverscroll: Binding<Double> = .constant(0.0),
Expand All @@ -48,6 +50,7 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
self.useThemeBackground = useThemeBackground
self._font = font
self._tabWidth = tabWidth
self._indentOption = indentOption
self._lineHeight = lineHeight
self._wrapLines = wrapLines
self._editorOverscroll = editorOverscroll
Expand All @@ -62,6 +65,7 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
@Binding private var theme: EditorTheme
@Binding private var font: NSFont
@Binding private var tabWidth: Int
@Binding private var indentOption: IndentOption
@Binding private var lineHeight: Double
@Binding private var wrapLines: Bool
@Binding private var editorOverscroll: Double
Expand All @@ -80,6 +84,7 @@ public struct CodeEditTextView: NSViewControllerRepresentable {
font: font,
theme: theme,
tabWidth: tabWidth,
indentOption: indentOption,
wrapLines: wrapLines,
cursorPosition: $cursorPosition,
editorOverscroll: editorOverscroll,
Expand All @@ -94,20 +99,25 @@ public struct CodeEditTextView: NSViewControllerRepresentable {

public func updateNSViewController(_ controller: NSViewControllerType, context: Context) {
controller.font = font
controller.tabWidth = tabWidth
controller.wrapLines = wrapLines
controller.useThemeBackground = useThemeBackground
controller.lineHeightMultiple = lineHeight
controller.editorOverscroll = editorOverscroll
controller.contentInsets = contentInsets

// Updating the language and theme needlessly can cause highlights to be re-calculated.
// Updating the language, theme, tab width and indent option needlessly can cause highlights to be re-calculated
if controller.language.id != language.id {
controller.language = language
}
if controller.theme != theme {
controller.theme = theme
}
if controller.indentOption != indentOption {
controller.indentOption = indentOption
}
if controller.tabWidth != tabWidth {
controller.tabWidth = tabWidth
}

controller.reloadUI()
return
Expand Down
56 changes: 34 additions & 22 deletions Sources/CodeEditTextView/Controller/STTextViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,20 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
/// Whether the code editor should use the theme background color or be transparent
public var useThemeBackground: Bool

/// The number of spaces to use for a `tab '\t'` character
public var tabWidth: Int
/// The visual width of tab characters in the text view measured in number of spaces.
public var tabWidth: Int {
didSet {
paragraphStyle = generateParagraphStyle()
reloadUI()
}
}

/// The behavior to use when the tab key is pressed.
public var indentOption: IndentOption {
didSet {
setUpTextFormation()
}
}

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

internal var highlighter: Highlighter?

/// Internal variable for tracking whether or not the textView has the correct standard attributes.
private var hasSetStandardAttributes: Bool = false

/// The provided highlight provider.
private var highlightProvider: HighlightProviding?

Expand All @@ -82,6 +91,7 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
font: NSFont,
theme: EditorTheme,
tabWidth: Int,
indentOption: IndentOption,
wrapLines: Bool,
cursorPosition: Binding<(Int, Int)>,
editorOverscroll: Double,
Expand All @@ -95,6 +105,7 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
self.font = font
self.theme = theme
self.tabWidth = tabWidth
self.indentOption = indentOption
self.wrapLines = wrapLines
self.cursorPosition = cursorPosition
self.editorOverscroll = editorOverscroll
Expand Down Expand Up @@ -142,6 +153,7 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
scrollView.verticalRulerView = rulerView
scrollView.rulersVisible = true

textView.typingAttributes = attributesFor(nil)
textView.defaultParagraphStyle = self.paragraphStyle
textView.font = self.font
textView.textColor = theme.text
Expand Down Expand Up @@ -214,11 +226,17 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
// MARK: UI

/// A default `NSParagraphStyle` with a set `lineHeight`
private var paragraphStyle: NSMutableParagraphStyle {
private lazy var paragraphStyle: NSMutableParagraphStyle = generateParagraphStyle()

private func generateParagraphStyle() -> NSMutableParagraphStyle {
// swiftlint:disable:next force_cast
let paragraph = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
paragraph.minimumLineHeight = lineHeight
paragraph.maximumLineHeight = lineHeight
// TODO: Fix Tab widths
// This adds tab stops throughout the document instead of only changing the width of tab characters
// paragraph.tabStops = [NSTextTab(type: .decimalTabStopType, location: 0.0)]
// paragraph.defaultTabInterval = CGFloat(tabWidth) * (" " as NSString).size(withAttributes: [.font: font]).width
return paragraph
}

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

textView?.textColor = theme.text
textView.backgroundColor = useThemeBackground ? theme.background : .clear
Expand All @@ -249,6 +264,8 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
textView?.selectedLineHighlightColor = theme.lineHighlight
textView?.isEditable = isEditable
textView.highlightSelectedLine = isEditable
textView?.typingAttributes = attributesFor(nil)
textView?.defaultParagraphStyle = paragraphStyle

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

setStandardAttributes()
}

/// Sets the standard attributes (`font`, `baselineOffset`) to the whole text
internal func setStandardAttributes() {
guard let textView = textView else { return }
guard !hasSetStandardAttributes else { return }
hasSetStandardAttributes = true
textView.addAttributes(attributesFor(nil), range: .init(0..<textView.string.count))
highlighter?.invalidate()
}

Expand All @@ -286,7 +294,8 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
return [
.font: font,
.foregroundColor: theme.colorFor(capture),
.baselineOffset: baselineOffset
.baselineOffset: baselineOffset,
.paragraphStyle: paragraphStyle
]
}

Expand Down Expand Up @@ -331,11 +340,14 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt
}
}

// MARK: Key Presses
// MARK: Selectors

/// Handles `keyDown` events in the `textView`
override public func keyDown(with event: NSEvent) {
// TODO: - This should be uncessecary
// This should be uneccessary but if removed STTextView receives some `keydown`s twice.
}

public override func insertTab(_ sender: Any?) {
textView.insertText("\t", replacementRange: textView.selectedRange)
}

deinit {
Expand Down
32 changes: 32 additions & 0 deletions Sources/CodeEditTextView/Enums/IndentOption.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// IndentOption.swift
//
//
// Created by Khan Winter on 3/26/23.
//

/// Represents what to insert on a tab key press.
public enum IndentOption: Equatable {
case spaces(count: Int)
case tab

var stringValue: String {
switch self {
case .spaces(let count):
return String(repeating: " ", count: count)
case .tab:
return "\t"
}
}

public static func == (lhs: IndentOption, rhs: IndentOption) -> Bool {
switch (lhs, rhs) {
case (.tab, .tab):
return true
case (.spaces(let lhsCount), .spaces(let rhsCount)):
return lhsCount == rhsCount
default:
return false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,7 @@ extension STTextView: TextInterface {
textContentStorage.performEditingTransaction {
textContentStorage.applyMutation(mutation)
}

didChangeText()
}
}
13 changes: 7 additions & 6 deletions Sources/CodeEditTextView/Filters/DeleteWhitespaceFilter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import TextStory

/// Filter for quickly deleting indent whitespace
struct DeleteWhitespaceFilter: Filter {
let indentationUnit: String
let indentOption: IndentOption

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

Expand All @@ -26,13 +26,14 @@ struct DeleteWhitespaceFilter: Filter {
return .none
}

let indentLength = indentOption.stringValue.count
let length = mutation.range.max - preceedingNonWhitespace
let numberOfExtraSpaces = length % indentationUnit.count
let numberOfExtraSpaces = length % indentLength

if numberOfExtraSpaces == 0 && length >= indentationUnit.count {
if numberOfExtraSpaces == 0 && length >= indentLength {
interface.applyMutation(
TextMutation(delete: NSRange(location: mutation.range.max - indentationUnit.count,
length: indentationUnit.count),
TextMutation(delete: NSRange(location: mutation.range.max - indentLength,
length: indentLength),
limit: mutation.limit)
)
return .discard
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ extension STTextViewController {
internal func setUpTextFormation() {
textFilters = []

let indentationUnit = String(repeating: " ", count: tabWidth)
let indentationUnit = indentOption.stringValue

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

setUpOpenPairFilters(pairs: pairsToHandle, whitespaceProvider: whitespaceProvider)
setUpNewlineTabFilters(whitespaceProvider: whitespaceProvider,
indentationUnit: indentationUnit)
indentOption: indentOption)
setUpDeletePairFilters(pairs: pairsToHandle)
setUpDeleteWhitespaceFilter(indentationUnit: indentationUnit)
setUpDeleteWhitespaceFilter(indentOption: indentOption)
}

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

textFilters.append(contentsOf: [newlineFilter, tabReplacementFilter])
}
Expand All @@ -86,8 +86,8 @@ extension STTextViewController {
}

/// Configures up the delete whitespace filter.
private func setUpDeleteWhitespaceFilter(indentationUnit: String) {
let filter = DeleteWhitespaceFilter(indentationUnit: indentationUnit)
private func setUpDeleteWhitespaceFilter(indentOption: IndentOption) {
let filter = DeleteWhitespaceFilter(indentOption: indentOption)
textFilters.append(filter)
}

Expand Down
6 changes: 3 additions & 3 deletions Sources/CodeEditTextView/Filters/TabReplacementFilter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import TextStory
/// Filter for replacing tab characters with the user-defined indentation unit.
/// - Note: The undentation unit can be another tab character, this is merely a point at which this can be configured.
struct TabReplacementFilter: Filter {
let indentationUnit: String
let indentOption: IndentOption

func processMutation(_ mutation: TextMutation, in interface: TextInterface) -> FilterAction {
if mutation.string == "\t" {
interface.applyMutation(TextMutation(insert: indentationUnit,
if mutation.string == "\t" && indentOption != .tab && mutation.delta > 0 {
interface.applyMutation(TextMutation(insert: indentOption.stringValue,
at: mutation.range.location,
limit: mutation.limit))
return .discard
Expand Down
Loading

0 comments on commit 169e4ea

Please sign in to comment.