Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ struct ContentView: View {
@State private var indentOption: IndentOption = .spaces(count: 4)
@AppStorage("reformatAtColumn") private var reformatAtColumn: Int = 80
@AppStorage("showReformattingGuide") private var showReformattingGuide: Bool = false
@State private var invisibleCharactersConfig: InvisibleCharactersConfig = .empty

init(document: Binding<CodeEditSourceEditorExampleDocument>, fileURL: URL?) {
self._document = document
Expand All @@ -56,7 +57,8 @@ struct ContentView: View {
useSystemCursor: useSystemCursor,
showMinimap: showMinimap,
reformatAtColumn: reformatAtColumn,
showReformattingGuide: showReformattingGuide
showReformattingGuide: showReformattingGuide,
invisibleCharactersConfig: invisibleCharactersConfig
)
.overlay(alignment: .bottom) {
StatusBar(
Expand All @@ -71,7 +73,8 @@ struct ContentView: View {
showMinimap: $showMinimap,
indentOption: $indentOption,
reformatAtColumn: $reformatAtColumn,
showReformattingGuide: $showReformattingGuide
showReformattingGuide: $showReformattingGuide,
invisibles: $invisibleCharactersConfig
)
}
.ignoresSafeArea()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ struct StatusBar: View {
@Binding var indentOption: IndentOption
@Binding var reformatAtColumn: Int
@Binding var showReformattingGuide: Bool
@Binding var invisibles: InvisibleCharactersConfig

var body: some View {
HStack {
Expand All @@ -50,6 +51,33 @@ struct StatusBar: View {
.disabled(true)
.help("macOS 14 required")
}

Menu {
Toggle("Spaces", isOn: $invisibles.showSpaces)
Toggle("Tabs", isOn: $invisibles.showTabs)
Toggle("Line Endings", isOn: $invisibles.showLineEndings)
Divider()
Toggle(
"Warning Characters",
isOn: Binding(
get: {
!invisibles.warningCharacters.isEmpty
},
set: { newValue in
// In this example app, we only add one character
// For real apps, consider providing a table where users can add UTF16
// char codes to warn about, as well as a set of good defaults.
if newValue {
invisibles.warningCharacters.insert(0x200B) // zero-width space
} else {
invisibles.warningCharacters.removeAll()
}
}
)
)
} label: {
Text("Invisibles")
}
} label: {}
.background {
Image(systemName: "switch.2")
Expand Down
4 changes: 2 additions & 2 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ let package = Package(
// A fast, efficient, text view for code.
.package(
url: "https://github.com/CodeEditApp/CodeEditTextView.git",
from: "0.11.1"
from: "0.11.2"
),
// tree-sitter languages
.package(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
/// - showMinimap: Whether to show the minimap
/// - reformatAtColumn: The column to reformat at
/// - showReformattingGuide: Whether to show the reformatting guide
/// - invisibleCharactersConfig: Configuration for displaying invisible characters. Defaults to an empty object.
public init(
_ text: Binding<String>,
language: CodeLanguage,
Expand All @@ -77,7 +78,8 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
coordinators: [any TextViewCoordinator] = [],
showMinimap: Bool,
reformatAtColumn: Int,
showReformattingGuide: Bool
showReformattingGuide: Bool,
invisibleCharactersConfig: InvisibleCharactersConfig = .empty
) {
self.text = .binding(text)
self.language = language
Expand Down Expand Up @@ -107,6 +109,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
self.showMinimap = showMinimap
self.reformatAtColumn = reformatAtColumn
self.showReformattingGuide = showReformattingGuide
self.invisibleCharactersConfig = invisibleCharactersConfig
}

/// Initializes a Text Editor
Expand Down Expand Up @@ -139,6 +142,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
/// - showMinimap: Whether to show the minimap
/// - reformatAtColumn: The column to reformat at
/// - showReformattingGuide: Whether to show the reformatting guide
/// - invisibleCharactersConfig: Configuration for displaying invisible characters. Defaults to an empty object.
public init(
_ text: NSTextStorage,
language: CodeLanguage,
Expand All @@ -163,7 +167,8 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
coordinators: [any TextViewCoordinator] = [],
showMinimap: Bool,
reformatAtColumn: Int,
showReformattingGuide: Bool
showReformattingGuide: Bool,
invisibleCharactersConfig: InvisibleCharactersConfig = .empty
) {
self.text = .storage(text)
self.language = language
Expand Down Expand Up @@ -193,6 +198,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
self.showMinimap = showMinimap
self.reformatAtColumn = reformatAtColumn
self.showReformattingGuide = showReformattingGuide
self.invisibleCharactersConfig = invisibleCharactersConfig
}

package var text: TextAPI
Expand All @@ -219,6 +225,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
package var showMinimap: Bool
private var reformatAtColumn: Int
private var showReformattingGuide: Bool
private var invisibleCharactersConfig: InvisibleCharactersConfig

public typealias NSViewControllerType = TextViewController

Expand Down Expand Up @@ -247,7 +254,8 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
coordinators: coordinators,
showMinimap: showMinimap,
reformatAtColumn: reformatAtColumn,
showReformattingGuide: showReformattingGuide
showReformattingGuide: showReformattingGuide,
invisibleCharactersConfig: invisibleCharactersConfig
)
switch text {
case .binding(let binding):
Expand Down Expand Up @@ -352,6 +360,10 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
if controller.useSystemCursor != useSystemCursor {
controller.useSystemCursor = useSystemCursor
}

if controller.invisibleCharactersConfig != invisibleCharactersConfig {
controller.invisibleCharactersConfig = invisibleCharactersConfig
}
}

private func updateThemeAndLanguage(_ controller: TextViewController) {
Expand Down Expand Up @@ -397,6 +409,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
controller.showMinimap == showMinimap &&
controller.reformatAtColumn == reformatAtColumn &&
controller.showReformattingGuide == showReformattingGuide &&
controller.invisibleCharactersConfig == invisibleCharactersConfig &&
areHighlightProvidersEqual(controller: controller, coordinator: coordinator)
}

Expand Down
44 changes: 37 additions & 7 deletions Sources/CodeEditSourceEditor/Controller/TextViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ public class TextViewController: NSViewController { // swiftlint:disable:this ty
var localEvenMonitor: Any?
var isPostingCursorNotification: Bool = false

/// Middleman between the text view to our invisible characters config, with knowledge of things like the
/// user's theme and indent option to help correctly draw invisible character placeholders.
var invisibleCharactersCoordinator: InvisibleCharactersCoordinator

/// The string contents.
public var string: String {
textView.string
Expand All @@ -52,6 +56,7 @@ public class TextViewController: NSViewController { // swiftlint:disable:this ty
public var font: NSFont {
didSet {
textView.font = font
invisibleCharactersCoordinator.font = font
highlighter?.invalidate()
}
}
Expand All @@ -70,6 +75,7 @@ public class TextViewController: NSViewController { // swiftlint:disable:this ty
gutterView.selectedLineTextColor = theme.text.color
minimapView.setTheme(theme)
guideView?.setTheme(theme)
invisibleCharactersCoordinator.theme = theme
}
}

Expand All @@ -86,6 +92,7 @@ public class TextViewController: NSViewController { // swiftlint:disable:this ty
public var indentOption: IndentOption {
didSet {
setUpTextFormation()
invisibleCharactersCoordinator.indentOption = indentOption
}
}

Expand Down Expand Up @@ -265,9 +272,24 @@ public class TextViewController: NSViewController { // swiftlint:disable:this ty
}
}

/// Configuration for drawing invisible characters.
///
/// See ``InvisibleCharactersConfig`` for more details.
var invisibleCharactersConfig: InvisibleCharactersConfig {
get {
invisibleCharactersCoordinator.config
}
set {
invisibleCharactersCoordinator.config = newValue
}
}

// MARK: Init

init(
// Disabling function body length warning for now. There's an open issue for combining a lot of these parameters
// into a single config object.

init( // swiftlint:disable:this function_body_length
string: String,
language: CodeLanguage,
font: NSFont,
Expand All @@ -291,7 +313,8 @@ public class TextViewController: NSViewController { // swiftlint:disable:this ty
coordinators: [TextViewCoordinator] = [],
showMinimap: Bool,
reformatAtColumn: Int = 80,
showReformattingGuide: Bool = false
showReformattingGuide: Bool = false,
invisibleCharactersConfig: InvisibleCharactersConfig = .empty
) {
self.language = language
self.font = font
Expand All @@ -314,14 +337,19 @@ public class TextViewController: NSViewController { // swiftlint:disable:this ty
self.showMinimap = showMinimap
self.reformatAtColumn = reformatAtColumn
self.showReformattingGuide = showReformattingGuide
self.invisibleCharactersCoordinator = InvisibleCharactersCoordinator(
config: invisibleCharactersConfig,
indentOption: indentOption,
theme: theme,
font: font
)

super.init(nibName: nil, bundle: nil)

let platformGuardedSystemCursor: Bool
if #available(macOS 14, *) {
platformGuardedSystemCursor = useSystemCursor
let platformGuardedSystemCursor: Bool = if #available(macOS 14, *) {
useSystemCursor
} else {
platformGuardedSystemCursor = false
false
}

if let idx = highlightProviders.firstIndex(where: { $0 is TreeSitterClient }),
Expand All @@ -342,6 +370,8 @@ public class TextViewController: NSViewController { // swiftlint:disable:this ty
delegate: self
)

textView.layoutManager.invisibleCharacterDelegate = invisibleCharactersCoordinator

// Initialize guide view
self.guideView = ReformattingGuideView(column: reformatAtColumn, isVisible: showReformattingGuide, theme: theme)

Expand Down Expand Up @@ -391,4 +421,4 @@ public class TextViewController: NSViewController { // swiftlint:disable:this ty
}
localEvenMonitor = nil
}
}
} // swiftlint:disable:this file_length
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//
// InvisibleCharactersConfig.swift
// CodeEditSourceEditor
//
// Created by Khan Winter on 6/11/25.
//

/// Configuration for how the editor draws invisible characters.
///
/// Enable specific categories using the ``showSpaces``, ``showTabs``, and ``showLineEndings`` toggles. Customize
/// drawing further with the ``spaceReplacement`` and family variables.
public struct InvisibleCharactersConfig: Equatable, Hashable, Sendable, Codable {
/// An empty configuration.
public static var empty: InvisibleCharactersConfig {
InvisibleCharactersConfig(showSpaces: false, showTabs: false, showLineEndings: false, warningCharacters: [])
}

/// Set to true to draw spaces with a dot.
public var showSpaces: Bool

/// Set to true to draw tabs with a small arrow.
public var showTabs: Bool

/// Set to true to draw line endings.
public var showLineEndings: Bool

/// Replacement when drawing the space character, enabled by ``showSpaces``.
public var spaceReplacement: String = "·"
/// Replacement when drawing the tab character, enabled by ``showTabs``.
public var tabReplacement: String = "→"
/// Replacement when drawing the carriage return character, enabled by ``showLineEndings``.
public var carriageReturnReplacement: String = "↵"
/// Replacement when drawing the line feed character, enabled by ``showLineEndings``.
public var lineFeedReplacement: String = "¬"
/// Replacement when drawing the paragraph separator character, enabled by ``showLineEndings``.
public var paragraphSeparatorReplacement: String = "¶"
/// Replacement when drawing the line separator character, enabled by ``showLineEndings``.
public var lineSeparatorReplacement: String = "⏎"

/// A set of characters the editor should draw with a small red border.
///
/// Indicates characters that the user may not have meant to insert, such as a zero-width space: `(0x200D)` or a
/// non-standard quote character: `“ (0x201C)`.
public var warningCharacters: Set<UInt16>

public init(
showSpaces: Bool,
showTabs: Bool,
showLineEndings: Bool,
warningCharacters: Set<UInt16>
) {
self.showSpaces = showSpaces
self.showTabs = showTabs
self.showLineEndings = showLineEndings
self.warningCharacters = warningCharacters
}

/// Determines what characters should trigger a custom drawing action.
func triggerCharacters() -> Set<UInt16> {
var set = Set<UInt16>()

if showSpaces {
set.insert(Symbols.space)
}

if showTabs {
set.insert(Symbols.tab)
}

if showLineEndings {
set.insert(Symbols.lineFeed)
set.insert(Symbols.carriageReturn)
set.insert(Symbols.paragraphSeparator)
set.insert(Symbols.lineSeparator)
}

set.formUnion(warningCharacters)

return set
}

/// Some commonly used whitespace symbols in their unichar representation.
public enum Symbols {
public static let space: UInt16 = 0x20
public static let tab: UInt16 = 0x9
public static let lineFeed: UInt16 = 0xA // \n
public static let carriageReturn: UInt16 = 0xD // \r
public static let paragraphSeparator: UInt16 = 0x2029 // ¶
public static let lineSeparator: UInt16 = 0x2028 // line separator
}
}
Loading