Skip to content
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

Add NSTextStorage Initializer #248

Merged
merged 3 commits into from
May 29, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// CodeEditSourceEditor+Coordinator.swift
// CodeEditSourceEditor
//
// Created by Khan Winter on 5/20/24.
//

import Foundation
import CodeEditTextView

extension CodeEditSourceEditor {
@MainActor
public class Coordinator: NSObject {
var parent: CodeEditSourceEditor
weak var controller: TextViewController?
var isUpdatingFromRepresentable: Bool = false
var isUpdateFromTextView: Bool = false

init(parent: CodeEditSourceEditor) {
self.parent = parent
super.init()

NotificationCenter.default.addObserver(
self,
selector: #selector(textViewDidChangeText(_:)),
name: TextView.textDidChangeNotification,
object: nil
)

NotificationCenter.default.addObserver(
self,
selector: #selector(textControllerCursorsDidUpdate(_:)),
name: TextViewController.cursorPositionUpdatedNotification,
object: nil
)
}

@objc func textViewDidChangeText(_ notification: Notification) {
guard let textView = notification.object as? TextView,
let controller,
controller.textView === textView else {
return
}
if case .binding(let binding) = parent.text {
binding.wrappedValue = textView.string
}
parent.coordinators.forEach {
$0.textViewDidChangeText(controller: controller)
}
}

@objc func textControllerCursorsDidUpdate(_ notification: Notification) {
guard !isUpdatingFromRepresentable else { return }
self.isUpdateFromTextView = true
self.parent.cursorPositions.wrappedValue = self.controller?.cursorPositions ?? []
if self.controller != nil {
self.parent.coordinators.forEach {
$0.textViewDidChangeSelection(
controller: self.controller!,
newPositions: self.controller!.cursorPositions
)
}
}
}

deinit {
parent.coordinators.forEach {
$0.destroy()
}
parent.coordinators.removeAll()
NotificationCenter.default.removeObserver(self)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@
// Created by Lukas Pistrol on 24.05.22.
//

import AppKit
import SwiftUI
import CodeEditTextView
import CodeEditLanguages

/// A SwiftUI View that provides source editing functionality.
public struct CodeEditSourceEditor: NSViewControllerRepresentable {
package enum TextAPI {
case binding(Binding<String>)
case storage(NSTextStorage)
}

/// Initializes a Text Editor
/// - Parameters:
Expand All @@ -22,7 +28,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
/// - lineHeight: The line height multiplier (e.g. `1.2`)
/// - wrapLines: Whether lines wrap to the width of the editor
/// - editorOverscroll: The distance to overscroll the editor by.
/// - cursorPosition: The cursor's position in the editor, measured in `(lineNum, columnNum)`
/// - cursorPositions: The cursor's position in the editor, measured in `(lineNum, columnNum)`
/// - useThemeBackground: Determines whether the editor uses the theme's background color, or a transparent
/// background color
/// - highlightProvider: A class you provide to perform syntax highlighting. Leave this as `nil` to use the
Expand All @@ -37,6 +43,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
/// - bracketPairHighlight: The type of highlight to use to highlight bracket pairs.
/// See `BracketPairHighlight` 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.
public init(
_ text: Binding<String>,
language: CodeLanguage,
Expand All @@ -58,7 +65,76 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
undoManager: CEUndoManager? = nil,
coordinators: [any TextViewCoordinator] = []
) {
self._text = text
self.text = .binding(text)
self.language = language
self.theme = theme
self.useThemeBackground = useThemeBackground
self.font = font
self.tabWidth = tabWidth
self.indentOption = indentOption
self.lineHeight = lineHeight
self.wrapLines = wrapLines
self.editorOverscroll = editorOverscroll
self.cursorPositions = cursorPositions
self.highlightProvider = highlightProvider
self.contentInsets = contentInsets
self.isEditable = isEditable
self.isSelectable = isSelectable
self.letterSpacing = letterSpacing
self.bracketPairHighlight = bracketPairHighlight
self.undoManager = undoManager
self.coordinators = coordinators
}

/// Initializes a Text Editor
/// - Parameters:
/// - text: The text content
/// - language: The language for syntax highlighting
/// - theme: The theme for syntax highlighting
/// - font: The default font
/// - 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 distance to overscroll the editor by.
/// - cursorPositions: The cursor's position in the editor, measured in `(lineNum, columnNum)`
/// - useThemeBackground: Determines whether the editor uses the theme's background color, or a transparent
/// background color
/// - highlightProvider: A class you provide to perform syntax highlighting. Leave this as `nil` to use the
/// built-in `TreeSitterClient` highlighter.
/// - contentInsets: Insets to use to offset the content in the enclosing scroll view. Leave as `nil` to let the
/// scroll view automatically adjust content insets.
/// - isEditable: A Boolean value that controls whether the text view allows the user to edit text.
/// - isSelectable: A Boolean value that controls whether the text view allows the user to select text. If this
/// value is true, and `isEditable` is false, the editor is selectable but not editable.
/// - letterSpacing: The amount of space to use between letters, as a percent. Eg: `1.0` = no space, `1.5` = 1/2 a
/// character's width between characters, etc. Defaults to `1.0`
/// - bracketPairHighlight: The type of highlight to use to highlight bracket pairs.
/// See `BracketPairHighlight` 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.
public init(
_ text: NSTextStorage,
language: CodeLanguage,
theme: EditorTheme,
font: NSFont,
tabWidth: Int,
indentOption: IndentOption = .spaces(count: 4),
lineHeight: Double,
wrapLines: Bool,
editorOverscroll: CGFloat = 0,
cursorPositions: Binding<[CursorPosition]>,
useThemeBackground: Bool = true,
highlightProvider: HighlightProviding? = nil,
contentInsets: NSEdgeInsets? = nil,
isEditable: Bool = true,
isSelectable: Bool = true,
letterSpacing: Double = 1.0,
bracketPairHighlight: BracketPairHighlight? = nil,
undoManager: CEUndoManager? = nil,
coordinators: [any TextViewCoordinator] = []
) {
self.text = .storage(text)
self.language = language
self.theme = theme
self.useThemeBackground = useThemeBackground
Expand All @@ -68,7 +144,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
self.lineHeight = lineHeight
self.wrapLines = wrapLines
self.editorOverscroll = editorOverscroll
self._cursorPositions = cursorPositions
self.cursorPositions = cursorPositions
self.highlightProvider = highlightProvider
self.contentInsets = contentInsets
self.isEditable = isEditable
Expand All @@ -79,7 +155,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
self.coordinators = coordinators
}

@Binding private var text: String
package var text: TextAPI
private var language: CodeLanguage
private var theme: EditorTheme
private var font: NSFont
Expand All @@ -88,7 +164,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
private var lineHeight: Double
private var wrapLines: Bool
private var editorOverscroll: CGFloat
@Binding private var cursorPositions: [CursorPosition]
package var cursorPositions: Binding<[CursorPosition]>
private var useThemeBackground: Bool
private var highlightProvider: HighlightProviding?
private var contentInsets: NSEdgeInsets?
Expand All @@ -97,21 +173,21 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
private var letterSpacing: Double
private var bracketPairHighlight: BracketPairHighlight?
private var undoManager: CEUndoManager?
private var coordinators: [any TextViewCoordinator]
package var coordinators: [any TextViewCoordinator]

public typealias NSViewControllerType = TextViewController

public func makeNSViewController(context: Context) -> TextViewController {
let controller = TextViewController(
string: text,
string: "",
language: language,
font: font,
theme: theme,
tabWidth: tabWidth,
indentOption: indentOption,
lineHeight: lineHeight,
wrapLines: wrapLines,
cursorPositions: cursorPositions,
cursorPositions: cursorPositions.wrappedValue,
editorOverscroll: editorOverscroll,
useThemeBackground: useThemeBackground,
highlightProvider: highlightProvider,
Expand All @@ -122,11 +198,17 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
bracketPairHighlight: bracketPairHighlight,
undoManager: undoManager
)
switch text {
case .binding(let binding):
controller.textView.setText(binding.wrappedValue)
case .storage(let textStorage):
controller.textView.setTextStorage(textStorage)
}
if controller.textView == nil {
controller.loadView()
}
if !cursorPositions.isEmpty {
controller.setCursorPositions(cursorPositions)
controller.setCursorPositions(cursorPositions.wrappedValue)
}

context.coordinator.controller = controller
Expand All @@ -144,7 +226,7 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
if !context.coordinator.isUpdateFromTextView {
// Prevent infinite loop of update notifications
context.coordinator.isUpdatingFromRepresentable = true
controller.setCursorPositions(cursorPositions)
controller.setCursorPositions(cursorPositions.wrappedValue)
context.coordinator.isUpdatingFromRepresentable = false
} else {
context.coordinator.isUpdateFromTextView = false
Expand Down Expand Up @@ -216,67 +298,6 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
controller.letterSpacing == letterSpacing &&
controller.bracketPairHighlight == bracketPairHighlight
}

@MainActor
public class Coordinator: NSObject {
var parent: CodeEditSourceEditor
weak var controller: TextViewController?
var isUpdatingFromRepresentable: Bool = false
var isUpdateFromTextView: Bool = false

init(parent: CodeEditSourceEditor) {
self.parent = parent
super.init()

NotificationCenter.default.addObserver(
self,
selector: #selector(textViewDidChangeText(_:)),
name: TextView.textDidChangeNotification,
object: nil
)

NotificationCenter.default.addObserver(
self,
selector: #selector(textControllerCursorsDidUpdate(_:)),
name: TextViewController.cursorPositionUpdatedNotification,
object: nil
)
}

@objc func textViewDidChangeText(_ notification: Notification) {
guard let textView = notification.object as? TextView,
let controller,
controller.textView === textView else {
return
}
parent.text = textView.string
parent.coordinators.forEach {
$0.textViewDidChangeText(controller: controller)
}
}

@objc func textControllerCursorsDidUpdate(_ notification: Notification) {
guard !isUpdatingFromRepresentable else { return }
self.isUpdateFromTextView = true
self.parent._cursorPositions.wrappedValue = self.controller?.cursorPositions ?? []
if self.controller != nil {
self.parent.coordinators.forEach {
$0.textViewDidChangeSelection(
controller: self.controller!,
newPositions: self.controller!.cursorPositions
)
}
}
}

deinit {
parent.coordinators.forEach {
$0.destroy()
}
parent.coordinators.removeAll()
NotificationCenter.default.removeObserver(self)
}
}
}

// swiftlint:disable:next line_length
Expand Down
36 changes: 25 additions & 11 deletions Sources/CodeEditSourceEditor/Documentation.docc/Documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,50 @@ A code editor with syntax highlighting powered by tree-sitter.

## Overview

![logo](codeedittextview-logo)
![logo](codeeditsourceeditor-logo)

An Xcode-inspired code editor view written in Swift powered by tree-sitter for [CodeEdit](https://github.com/CodeEditApp/CodeEdit). Features include syntax highlighting (based on the provided theme), code completion, find and replace, text diff, validation, current line highlighting, minimap, inline messages (warnings and errors), bracket matching, and more.

This package includes both `AppKit` and `SwiftUI` components. It also relies on the `CodeEditLanguages` and `Theme` module.

![banner](preview)

## Syntax Highlighting
This package includes both `AppKit` and `SwiftUI` components. It also relies on the [`CodeEditLanguages`](https://github.com/CodeEditApp/CodeEditLanguages) for optional syntax highlighting using tree-sitter.

> **CodeEditSourceEditor is currently in development and it is not ready for production use.** <br> Please check back later for updates on this project. Contributors are welcome as we build out the features mentioned above!

``CodeEditSourceEditor`` uses `tree-sitter` for syntax highlighting. A list of already supported languages can be found [here](https://github.com/CodeEditApp/CodeEditSourceEditor/issues/15).
## Currently Supported Languages

New languages need to be added to the [CodeEditLanguages](https://github.com/CodeEditApp/CodeEditLanguages) repo.
See this issue [CodeEditLanguages#10](https://github.com/CodeEditApp/CodeEditLanguages/issues/10) on `CodeEditLanguages` for more information on supported languages.

## Dependencies

Special thanks to both [Marcin Krzyzanowski](https://twitter.com/krzyzanowskim) & [Matt Massicotte](https://twitter.com/mattie) for the great work they've done!
Special thanks to [Matt Massicotte](https://twitter.com/mattie) for the great work he's done!

| Package | Source | Author |
| - | - | - |
| `STTextView` | [GitHub](https://github.com/krzyzanowskim/STTextView) | [Marcin Krzyzanowski](https://twitter.com/krzyzanowskim) |
| :- | :- | :- |
| `SwiftTreeSitter` | [GitHub](https://github.com/ChimeHQ/SwiftTreeSitter) | [Matt Massicotte](https://twitter.com/mattie) |

## License

Licensed under the [MIT license](https://github.com/CodeEditApp/CodeEdit/blob/main/LICENSE.md).

## Topics

### Text View

- ``CodeEditSourceEditor/CodeEditSourceEditor``
- ``CodeEditSourceEditor/TextViewController``
- ``TextViewController``
- ``GutterView``

### Theme
### Themes

- ``EditorTheme``

### Text Coordinators

- <doc:TextViewCoordinators>
- ``TextViewCoordinator``
- ``CombineCoordinator``

### Cursors

- ``CursorPosition``
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Loading