diff --git a/Sources/CodeEditTextView/CodeEditTextView.swift b/Sources/CodeEditTextView/CodeEditTextView.swift index c2d9bb5d3..9a80e4bad 100644 --- a/Sources/CodeEditTextView/CodeEditTextView.swift +++ b/Sources/CodeEditTextView/CodeEditTextView.swift @@ -17,7 +17,6 @@ public struct CodeEditTextView: NSViewControllerRepresentable { /// - text: The text content /// - language: The language for syntax highlighting /// - theme: The theme for syntax highlighting - /// - useThemeBackground: Whether CodeEditTextView uses theme background color or is transparent /// - font: The default font /// - tabWidth: The tab width /// - lineHeight: The line height multiplier (e.g. `1.2`) @@ -37,7 +36,7 @@ public struct CodeEditTextView: NSViewControllerRepresentable { lineHeight: Binding, wrapLines: Binding, editorOverscroll: Binding = .constant(0.0), - cursorPosition: Published<(Int, Int)>.Publisher? = nil, + cursorPosition: Binding<(Int, Int)>, useThemeBackground: Bool = true, highlightProvider: HighlightProviding? = nil, contentInsets: NSEdgeInsets? = nil, @@ -52,7 +51,7 @@ public struct CodeEditTextView: NSViewControllerRepresentable { self._lineHeight = lineHeight self._wrapLines = wrapLines self._editorOverscroll = editorOverscroll - self.cursorPosition = cursorPosition + self._cursorPosition = cursorPosition self.highlightProvider = highlightProvider self.contentInsets = contentInsets self.isEditable = isEditable @@ -66,7 +65,7 @@ public struct CodeEditTextView: NSViewControllerRepresentable { @Binding private var lineHeight: Double @Binding private var wrapLines: Bool @Binding private var editorOverscroll: Double - private var cursorPosition: Published<(Int, Int)>.Publisher? + @Binding private var cursorPosition: (Int, Int) private var useThemeBackground: Bool private var highlightProvider: HighlightProviding? private var contentInsets: NSEdgeInsets? @@ -82,7 +81,7 @@ public struct CodeEditTextView: NSViewControllerRepresentable { theme: theme, tabWidth: tabWidth, wrapLines: wrapLines, - cursorPosition: cursorPosition, + cursorPosition: $cursorPosition, editorOverscroll: editorOverscroll, useThemeBackground: useThemeBackground, highlightProvider: highlightProvider, diff --git a/Sources/CodeEditTextView/Controller/STTextViewController+Cursor.swift b/Sources/CodeEditTextView/Controller/STTextViewController+Cursor.swift new file mode 100644 index 000000000..84160cfba --- /dev/null +++ b/Sources/CodeEditTextView/Controller/STTextViewController+Cursor.swift @@ -0,0 +1,103 @@ +// +// STTextViewController+Cursor.swift +// +// +// Created by Elias Wahl on 15.03.23. +// + +import Foundation +import AppKit + +extension STTextViewController { + func setCursorPosition(_ position: (Int, Int)) { + guard let provider = textView.textLayoutManager.textContentManager else { + return + } + + var (line, column) = position + let string = textView.string + if line > 0 { + if string.isEmpty { + // If the file is blank, automatically place the cursor in the first index. + let range = NSRange(string.startIndex.. Bool + in + var col = 1 + /// If the cursor is at the end of the document: + if textLayoutManager.offset(from: insertionPointLocation, to: documentEndLocation) == 0 { + /// If document is empty: + if textLayoutManager.offset(from: documentStartLocation, to: documentEndLocation) == 0 { + self.cursorPosition.wrappedValue = (1, 1) + return false + } + guard let cursorTextFragment = textLayoutManager.textLayoutFragment(for: textSegmentFrame.origin), + let cursorTextLineFragment = cursorTextFragment.textLineFragments.last + else { return false } + + col = cursorTextLineFragment.characterRange.length + 1 + if col == 1 { line += 1 } + } else { + guard let cursorTextLineFragment = textLayoutManager.textLineFragment(at: insertionPointLocation) + else { return false } + + /// +1, because we start with the first character with 1 + let tempCol = cursorTextLineFragment.characterIndex(for: textSegmentFrame.origin) + let result = tempCol.addingReportingOverflow(1) + + if !result.overflow { col = result.partialValue } + /// If cursor is at end of line add 1: + if cursorTextLineFragment.characterRange.length != 1 && + (cursorTextLineFragment.typographicBounds.width == (textSegmentFrame.maxX + 5.0)) { + col += 1 + } + + /// If cursor is at first character of line, the current line is not being included + if col == 1 { line += 1 } + } + + self.cursorPosition.wrappedValue = (line, col) + return false + } + } +} diff --git a/Sources/CodeEditTextView/STTextViewController.swift b/Sources/CodeEditTextView/Controller/STTextViewController.swift similarity index 86% rename from Sources/CodeEditTextView/STTextViewController.swift rename to Sources/CodeEditTextView/Controller/STTextViewController.swift index 179722af8..0dbee4f5a 100644 --- a/Sources/CodeEditTextView/STTextViewController.swift +++ b/Sources/CodeEditTextView/Controller/STTextViewController.swift @@ -46,6 +46,9 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt /// The font to use in the `textView` public var font: NSFont + /// The current cursor position e.g. (1, 1) + public var cursorPosition: Binding<(Int, Int)> + /// The editorOverscroll to use for the textView over scroll public var editorOverscroll: Double @@ -80,7 +83,7 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt theme: EditorTheme, tabWidth: Int, wrapLines: Bool, - cursorPosition: Published<(Int, Int)>.Publisher? = nil, + cursorPosition: Binding<(Int, Int)>, editorOverscroll: Double, useThemeBackground: Bool, highlightProvider: HighlightProviding? = nil, @@ -175,9 +178,7 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt setHighlightProvider(self.highlightProvider) setUpTextFormation() - self.cursorPositionCancellable = self.cursorPosition?.sink(receiveValue: { value in - self.setCursorPosition(value) - }) + self.setCursorPosition(self.cursorPosition.wrappedValue) } public override func viewDidLoad() { @@ -189,6 +190,14 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt guard let self = self else { return } (self.view as? NSScrollView)?.contentView.contentInsets.bottom = self.bottomContentInsets } + + NotificationCenter.default.addObserver( + forName: STTextView.didChangeSelectionNotification, + object: nil, + queue: .main + ) { [weak self] _ in + self?.updateCursorPosition() + } } public override func viewDidAppear() { @@ -323,45 +332,6 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt // TODO: - This should be uncessecary } - // MARK: Cursor Position - - private var cursorPosition: Published<(Int, Int)>.Publisher? - private var cursorPositionCancellable: AnyCancellable? - - private func setCursorPosition(_ position: (Int, Int)) { - guard let provider = textView.textLayoutManager.textContentManager else { - return - } - - var (line, column) = position - let string = textView.string - if line > 0 { - if string.isEmpty { - // If the file is blank, automatically place the cursor in the first index. - let range = NSRange(string.startIndex..