Skip to content

Draw Invisible Characters #334

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -50,9 +50,14 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
/// - useSystemCursor: If true, uses the system cursor on `>=macOS 14`.
/// - undoManager: The undo manager for the text view. Defaults to `nil`, which will create a new CEUndoManager
/// - coordinators: Any text coordinators for the view to use. See ``TextViewCoordinator`` for more information.
/// - showMinimap: Whether to show the minimap
/// - reformatAtColumn: The column to reformat at
/// - showReformattingGuide: Whether to show the reformatting guide
/// - 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.
/// See ``TextViewController/invisibleCharactersConfig`` and
/// ``InvisibleCharactersConfig`` for more information.
/// - warningCharacters: A set of characters the editor should draw with a small red border. See
/// ``TextViewController/warningCharacters`` for more information.
public init(
_ text: Binding<String>,
language: CodeLanguage,
Expand All @@ -77,7 +82,9 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
coordinators: [any TextViewCoordinator] = [],
showMinimap: Bool,
reformatAtColumn: Int,
showReformattingGuide: Bool
showReformattingGuide: Bool,
invisibleCharactersConfig: InvisibleCharactersConfig = .empty,
warningCharacters: Set<UInt16> = []
) {
self.text = .binding(text)
self.language = language
Expand Down Expand Up @@ -107,6 +114,8 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
self.showMinimap = showMinimap
self.reformatAtColumn = reformatAtColumn
self.showReformattingGuide = showReformattingGuide
self.invisibleCharactersConfig = invisibleCharactersConfig
self.warningCharacters = warningCharacters
}

/// Initializes a Text Editor
Expand Down Expand Up @@ -136,9 +145,14 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
/// See `BracketPairEmphasis` for more information. Defaults to `nil`
/// - undoManager: The undo manager for the text view. Defaults to `nil`, which will create a new CEUndoManager
/// - coordinators: Any text coordinators for the view to use. See ``TextViewCoordinator`` for more information.
/// - showMinimap: Whether to show the minimap
/// - reformatAtColumn: The column to reformat at
/// - showReformattingGuide: Whether to show the reformatting guide
/// - 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.
/// See ``TextViewController/invisibleCharactersConfig`` and
/// ``InvisibleCharactersConfig`` for more information.
/// - warningCharacters: A set of characters the editor should draw with a small red border. See
/// ``TextViewController/warningCharacters`` for more information.
public init(
_ text: NSTextStorage,
language: CodeLanguage,
Expand All @@ -163,7 +177,9 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
coordinators: [any TextViewCoordinator] = [],
showMinimap: Bool,
reformatAtColumn: Int,
showReformattingGuide: Bool
showReformattingGuide: Bool,
invisibleCharactersConfig: InvisibleCharactersConfig = .empty,
warningCharacters: Set<UInt16> = []
) {
self.text = .storage(text)
self.language = language
Expand Down Expand Up @@ -193,6 +209,8 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
self.showMinimap = showMinimap
self.reformatAtColumn = reformatAtColumn
self.showReformattingGuide = showReformattingGuide
self.invisibleCharactersConfig = invisibleCharactersConfig
self.warningCharacters = warningCharacters
}

package var text: TextAPI
Expand All @@ -219,6 +237,8 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
package var showMinimap: Bool
private var reformatAtColumn: Int
private var showReformattingGuide: Bool
private var invisibleCharactersConfig: InvisibleCharactersConfig
private var warningCharacters: Set<UInt16>

public typealias NSViewControllerType = TextViewController

Expand Down Expand Up @@ -247,7 +267,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 +373,14 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
if controller.useSystemCursor != useSystemCursor {
controller.useSystemCursor = useSystemCursor
}

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

if controller.warningCharacters != warningCharacters {
controller.warningCharacters = warningCharacters
}
}

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

Expand Down
71 changes: 58 additions & 13 deletions Sources/CodeEditSourceEditor/Controller/TextViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ public class TextViewController: NSViewController { // swiftlint:disable:this ty
var gutterView: GutterView!
var minimapView: MinimapView!

/// The reformatting guide view
var guideView: ReformattingGuideView! {
didSet {
if let oldValue = oldValue {
oldValue.removeFromSuperview()
}
}
}

var minimapXConstraint: NSLayoutConstraint?

var _undoManager: CEUndoManager!
Expand All @@ -35,6 +44,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 +65,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 +84,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 +101,7 @@ public class TextViewController: NSViewController { // swiftlint:disable:this ty
public var indentOption: IndentOption {
didSet {
setUpTextFormation()
invisibleCharactersCoordinator.indentOption = indentOption
}
}

Expand Down Expand Up @@ -256,18 +272,37 @@ public class TextViewController: NSViewController { // swiftlint:disable:this ty
}
}

/// The reformatting guide view
var guideView: ReformattingGuideView! {
didSet {
if let oldValue = oldValue {
oldValue.removeFromSuperview()
}
/// Configuration for drawing invisible characters.
///
/// See ``InvisibleCharactersConfig`` for more details.
public var invisibleCharactersConfig: InvisibleCharactersConfig {
get {
invisibleCharactersCoordinator.config
}
set {
invisibleCharactersCoordinator.config = newValue
}
}

/// 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> {
get {
invisibleCharactersCoordinator.warningCharacters
}
set {
invisibleCharactersCoordinator.warningCharacters = 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 +326,9 @@ public class TextViewController: NSViewController { // swiftlint:disable:this ty
coordinators: [TextViewCoordinator] = [],
showMinimap: Bool,
reformatAtColumn: Int = 80,
showReformattingGuide: Bool = false
showReformattingGuide: Bool = false,
invisibleCharactersConfig: InvisibleCharactersConfig = .empty,
warningCharacters: Set<UInt16> = []
) {
self.language = language
self.font = font
Expand All @@ -314,14 +351,20 @@ public class TextViewController: NSViewController { // swiftlint:disable:this ty
self.showMinimap = showMinimap
self.reformatAtColumn = reformatAtColumn
self.showReformattingGuide = showReformattingGuide
self.invisibleCharactersCoordinator = InvisibleCharactersCoordinator(
config: invisibleCharactersConfig,
warningCharacters: warningCharacters,
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 +385,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 +436,4 @@ public class TextViewController: NSViewController { // swiftlint:disable:this ty
}
localEvenMonitor = nil
}
}
} // swiftlint:disable:this file_length
Loading