From 0368bceed0dec279bc1cb65d6e7ab6effbd6f459 Mon Sep 17 00:00:00 2001 From: Elias Wahl Date: Thu, 26 Jan 2023 12:20:07 +0100 Subject: [PATCH 1/8] Added function to compute the cursor position as (line, column), Changed cursor position to binding --- .../CodeEditTextView/CodeEditTextView.swift | 4 +- .../STTextViewController.swift | 76 +++++++++++++++++-- .../STTextViewControllerTests.swift | 1 + 3 files changed, 73 insertions(+), 8 deletions(-) diff --git a/Sources/CodeEditTextView/CodeEditTextView.swift b/Sources/CodeEditTextView/CodeEditTextView.swift index c2d9bb5d3..db4873051 100644 --- a/Sources/CodeEditTextView/CodeEditTextView.swift +++ b/Sources/CodeEditTextView/CodeEditTextView.swift @@ -66,7 +66,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 +82,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/STTextViewController.swift b/Sources/CodeEditTextView/STTextViewController.swift index 179722af8..0fe50c6da 100644 --- a/Sources/CodeEditTextView/STTextViewController.swift +++ b/Sources/CodeEditTextView/STTextViewController.swift @@ -80,7 +80,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 +175,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 +187,12 @@ 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 + guard let textLayoutManager = self?.textView.textLayoutManager else { return } + self?.updateCursorPosition(with: textLayoutManager) + } } public override func viewDidAppear() { @@ -325,8 +329,7 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt // MARK: Cursor Position - private var cursorPosition: Published<(Int, Int)>.Publisher? - private var cursorPositionCancellable: AnyCancellable? + private var cursorPosition: Binding<(Int, Int)> private func setCursorPosition(_ position: (Int, Int)) { guard let provider = textView.textLayoutManager.textContentManager else { @@ -362,6 +365,67 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt } } + private func updateCursorPosition(with textLayoutManager: NSTextLayoutManager) { + /// Current cursor location as NSTextLocation + guard let insertionPointLocation = textLayoutManager.insertionPointLocation else { return } + + var lineWrapsCount = 0 + + // TODO: Find a more performant approach, that does not scale with line count + /// Count the line wraps prior to the cursor line + textLayoutManager.enumerateTextLayoutFragments(from: textLayoutManager.documentRange.location, + options: [.ensuresLayout, .ensuresExtraLineFragment] + ) { textLayoutFragment in + + guard let cursorTextLineFragment = textLayoutManager.textLineFragment(at: insertionPointLocation) + else { return false } + + /// Check whether the textLayoutFragment has line wraps + if textLayoutFragment.textLineFragments.count > 1 { + for lineFragment in textLayoutFragment.textLineFragments { + lineWrapsCount += 1 + /// Do not count lineFragments after the lineFragment where the cursor is placed + if lineFragment == cursorTextLineFragment { break } + } + + /// The first lineFragment will be counted as an actual line + lineWrapsCount -= 1 + } + + if textLayoutFragment.textLineFragments.contains(cursorTextLineFragment) { + return false + } + return true + } + + /// Translate to line and column value + textLayoutManager.enumerateTextSegments(in: NSTextRange(location: insertionPointLocation), + type: .standard, + options: + [.rangeNotRequired, + .upstreamAffinity] + ) { _, textSegmentFrame, _, _ -> Bool + in + var line = Int(textSegmentFrame.maxY / textSegmentFrame.height) + + line -= lineWrapsCount + + guard let cursorTextLineFragment = textLayoutManager.textLineFragment(at: insertionPointLocation) + else { return false } + + /// +1, because we start with the first character with 1 + var col = cursorTextLineFragment.characterIndex(for: textSegmentFrame.origin) + 1 + /// If the cursor is at the last character of the line + if textSegmentFrame.origin.x + 5.0 == cursorTextLineFragment.typographicBounds.size.width { + col += 1 + } + + self.cursorPosition.wrappedValue = (line, col) + + return false + } + } + deinit { textView = nil highlighter = nil diff --git a/Tests/CodeEditTextViewTests/STTextViewControllerTests.swift b/Tests/CodeEditTextViewTests/STTextViewControllerTests.swift index 5a71412e5..c90d94e34 100644 --- a/Tests/CodeEditTextViewTests/STTextViewControllerTests.swift +++ b/Tests/CodeEditTextViewTests/STTextViewControllerTests.swift @@ -34,6 +34,7 @@ final class STTextViewControllerTests: XCTestCase { theme: theme, tabWidth: 4, wrapLines: true, + cursorPosition: .constant((1, 1)), editorOverscroll: 0.5, useThemeBackground: true, isEditable: true From fd73286170092ca3fd3cc483bb6bc2416b858e6b Mon Sep 17 00:00:00 2001 From: Elias Wahl <82230675+Eliulm@users.noreply.github.com> Date: Thu, 26 Jan 2023 13:29:16 +0100 Subject: [PATCH 2/8] Update Sources/CodeEditTextView/STTextViewController.swift Co-authored-by: Lukas Pistrol --- Sources/CodeEditTextView/STTextViewController.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/CodeEditTextView/STTextViewController.swift b/Sources/CodeEditTextView/STTextViewController.swift index 0fe50c6da..078b29277 100644 --- a/Sources/CodeEditTextView/STTextViewController.swift +++ b/Sources/CodeEditTextView/STTextViewController.swift @@ -188,8 +188,11 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt (self.view as? NSScrollView)?.contentView.contentInsets.bottom = self.bottomContentInsets } - NotificationCenter.default.addObserver(forName: STTextView.didChangeSelectionNotification, - object: nil, queue: .main) { [weak self] _ in + NotificationCenter.default.addObserver( + forName: STTextView.didChangeSelectionNotification, + object: nil, + queue: .main + ) { [weak self] _ in guard let textLayoutManager = self?.textView.textLayoutManager else { return } self?.updateCursorPosition(with: textLayoutManager) } From b4a72ec61d63387dfe2d8f9e772c3a373e053a35 Mon Sep 17 00:00:00 2001 From: Elias Wahl <82230675+Eliulm@users.noreply.github.com> Date: Thu, 26 Jan 2023 13:29:26 +0100 Subject: [PATCH 3/8] Update Sources/CodeEditTextView/STTextViewController.swift Co-authored-by: Lukas Pistrol --- Sources/CodeEditTextView/STTextViewController.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/CodeEditTextView/STTextViewController.swift b/Sources/CodeEditTextView/STTextViewController.swift index 078b29277..42243c177 100644 --- a/Sources/CodeEditTextView/STTextViewController.swift +++ b/Sources/CodeEditTextView/STTextViewController.swift @@ -376,8 +376,9 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt // TODO: Find a more performant approach, that does not scale with line count /// Count the line wraps prior to the cursor line - textLayoutManager.enumerateTextLayoutFragments(from: textLayoutManager.documentRange.location, - options: [.ensuresLayout, .ensuresExtraLineFragment] + textLayoutManager.enumerateTextLayoutFragments( + from: textLayoutManager.documentRange.location, + options: [.ensuresLayout, .ensuresExtraLineFragment] ) { textLayoutFragment in guard let cursorTextLineFragment = textLayoutManager.textLineFragment(at: insertionPointLocation) From ab9d27e55f9d624de37db6a71d0b1f1b75bfc788 Mon Sep 17 00:00:00 2001 From: Elias Wahl <82230675+Eliulm@users.noreply.github.com> Date: Thu, 26 Jan 2023 13:29:34 +0100 Subject: [PATCH 4/8] Update Sources/CodeEditTextView/STTextViewController.swift Co-authored-by: Lukas Pistrol --- Sources/CodeEditTextView/STTextViewController.swift | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Sources/CodeEditTextView/STTextViewController.swift b/Sources/CodeEditTextView/STTextViewController.swift index 42243c177..f9fb69be7 100644 --- a/Sources/CodeEditTextView/STTextViewController.swift +++ b/Sources/CodeEditTextView/STTextViewController.swift @@ -403,11 +403,10 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt } /// Translate to line and column value - textLayoutManager.enumerateTextSegments(in: NSTextRange(location: insertionPointLocation), - type: .standard, - options: - [.rangeNotRequired, - .upstreamAffinity] + textLayoutManager.enumerateTextSegments( + in: NSTextRange(location: insertionPointLocation), + type: .standard, + options: [.rangeNotRequired, .upstreamAffinity] ) { _, textSegmentFrame, _, _ -> Bool in var line = Int(textSegmentFrame.maxY / textSegmentFrame.height) From c5e72f149024a2fe54bfc92d8af6416b908fb4b9 Mon Sep 17 00:00:00 2001 From: Elias Wahl Date: Sun, 29 Jan 2023 13:59:17 +0100 Subject: [PATCH 5/8] Fixed SwiftLint violations, Extended CodeEditTextView DocC, moved textLayoutManager guard --- .../CodeEditTextView/CodeEditTextView.swift | 1 - .../STTextViewController.swift | 23 ++++++++----------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/Sources/CodeEditTextView/CodeEditTextView.swift b/Sources/CodeEditTextView/CodeEditTextView.swift index db4873051..0d926725c 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`) diff --git a/Sources/CodeEditTextView/STTextViewController.swift b/Sources/CodeEditTextView/STTextViewController.swift index f9fb69be7..648ef11a5 100644 --- a/Sources/CodeEditTextView/STTextViewController.swift +++ b/Sources/CodeEditTextView/STTextViewController.swift @@ -190,11 +190,10 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt NotificationCenter.default.addObserver( forName: STTextView.didChangeSelectionNotification, - object: nil, + object: nil, queue: .main ) { [weak self] _ in - guard let textLayoutManager = self?.textView.textLayoutManager else { return } - self?.updateCursorPosition(with: textLayoutManager) + self?.updateCursorPosition() } } @@ -368,9 +367,12 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt } } - private func updateCursorPosition(with textLayoutManager: NSTextLayoutManager) { + private func updateCursorPosition() { /// Current cursor location as NSTextLocation - guard let insertionPointLocation = textLayoutManager.insertionPointLocation else { return } + guard let textLayoutManager = textView.textLayoutManager as NSTextLayoutManager?, + let insertionPointLocation = textLayoutManager.insertionPointLocation else { + return + } var lineWrapsCount = 0 @@ -409,9 +411,7 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt options: [.rangeNotRequired, .upstreamAffinity] ) { _, textSegmentFrame, _, _ -> Bool in - var line = Int(textSegmentFrame.maxY / textSegmentFrame.height) - - line -= lineWrapsCount + var line = Int(textSegmentFrame.maxY / textSegmentFrame.height) - lineWrapsCount guard let cursorTextLineFragment = textLayoutManager.textLineFragment(at: insertionPointLocation) else { return false } @@ -419,13 +419,10 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt /// +1, because we start with the first character with 1 var col = cursorTextLineFragment.characterIndex(for: textSegmentFrame.origin) + 1 /// If the cursor is at the last character of the line - if textSegmentFrame.origin.x + 5.0 == cursorTextLineFragment.typographicBounds.size.width { - col += 1 - } + if textSegmentFrame.origin.x + 5.0 == cursorTextLineFragment.typographicBounds.size.width { col += 1 } self.cursorPosition.wrappedValue = (line, col) - - return false + return false } } From f061ee84894e2783c7493012e1f768d2e4a855c4 Mon Sep 17 00:00:00 2001 From: Elias Wahl Date: Sun, 19 Feb 2023 20:36:37 +0100 Subject: [PATCH 6/8] Adapted updateCursorPosition() to benefit from viewport rendering. --- .../CodeEditTextView/CodeEditTextView.swift | 4 +- .../STTextViewController.swift | 62 ++++++------------- 2 files changed, 22 insertions(+), 44 deletions(-) diff --git a/Sources/CodeEditTextView/CodeEditTextView.swift b/Sources/CodeEditTextView/CodeEditTextView.swift index 0d926725c..9a80e4bad 100644 --- a/Sources/CodeEditTextView/CodeEditTextView.swift +++ b/Sources/CodeEditTextView/CodeEditTextView.swift @@ -36,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, @@ -51,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 diff --git a/Sources/CodeEditTextView/STTextViewController.swift b/Sources/CodeEditTextView/STTextViewController.swift index 648ef11a5..be803b2fa 100644 --- a/Sources/CodeEditTextView/STTextViewController.swift +++ b/Sources/CodeEditTextView/STTextViewController.swift @@ -368,58 +368,36 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt } private func updateCursorPosition() { - /// Current cursor location as NSTextLocation - guard let textLayoutManager = textView.textLayoutManager as NSTextLayoutManager?, - let insertionPointLocation = textLayoutManager.insertionPointLocation else { - return - } - - var lineWrapsCount = 0 - - // TODO: Find a more performant approach, that does not scale with line count - /// Count the line wraps prior to the cursor line - textLayoutManager.enumerateTextLayoutFragments( - from: textLayoutManager.documentRange.location, - options: [.ensuresLayout, .ensuresExtraLineFragment] - ) { textLayoutFragment in - - guard let cursorTextLineFragment = textLayoutManager.textLineFragment(at: insertionPointLocation) - else { return false } - - /// Check whether the textLayoutFragment has line wraps - if textLayoutFragment.textLineFragments.count > 1 { - for lineFragment in textLayoutFragment.textLineFragments { - lineWrapsCount += 1 - /// Do not count lineFragments after the lineFragment where the cursor is placed - if lineFragment == cursorTextLineFragment { break } - } - - /// The first lineFragment will be counted as an actual line - lineWrapsCount -= 1 - } + guard let textLayoutManager = textView.textLayoutManager as NSTextLayoutManager?, + let textContentManager = textLayoutManager.textContentManager as NSTextContentManager?, + let insertionPointLocation = textLayoutManager.insertionPointLocation, + let cursorTextLineFragment = textLayoutManager.textLineFragment(at: insertionPointLocation) + else { + return + } - if textLayoutFragment.textLineFragments.contains(cursorTextLineFragment) { - return false - } - return true - } + let textElements = textContentManager.textElements( + for: NSTextRange(location: textLayoutManager.documentRange.location, end: insertionPointLocation)!) + var line = textElements.count - /// Translate to line and column value - textLayoutManager.enumerateTextSegments( + textLayoutManager.enumerateTextSegments( in: NSTextRange(location: insertionPointLocation), type: .standard, options: [.rangeNotRequired, .upstreamAffinity] ) { _, textSegmentFrame, _, _ -> Bool in - var line = Int(textSegmentFrame.maxY / textSegmentFrame.height) - lineWrapsCount - - guard let cursorTextLineFragment = textLayoutManager.textLineFragment(at: insertionPointLocation) - else { return false } /// +1, because we start with the first character with 1 var col = cursorTextLineFragment.characterIndex(for: textSegmentFrame.origin) + 1 - /// If the cursor is at the last character of the line - if textSegmentFrame.origin.x + 5.0 == cursorTextLineFragment.typographicBounds.size.width { col += 1 } + + /// 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 From ea8e4624b0d9b9e2614c77c15e3986c7d9a4facb Mon Sep 17 00:00:00 2001 From: Elias Wahl Date: Sun, 5 Mar 2023 14:44:13 +0100 Subject: [PATCH 7/8] Added functionality to compute cursor position at last line --- .../STTextViewController.swift | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/Sources/CodeEditTextView/STTextViewController.swift b/Sources/CodeEditTextView/STTextViewController.swift index be803b2fa..9835e72dc 100644 --- a/Sources/CodeEditTextView/STTextViewController.swift +++ b/Sources/CodeEditTextView/STTextViewController.swift @@ -371,7 +371,8 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt guard let textLayoutManager = textView.textLayoutManager as NSTextLayoutManager?, let textContentManager = textLayoutManager.textContentManager as NSTextContentManager?, let insertionPointLocation = textLayoutManager.insertionPointLocation, - let cursorTextLineFragment = textLayoutManager.textLineFragment(at: insertionPointLocation) + let documentStartLocation = textLayoutManager.documentRange.location as NSTextLocation?, + let documentEndLocation = textLayoutManager.documentRange.endLocation as NSTextLocation? else { return } @@ -386,19 +387,39 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt options: [.rangeNotRequired, .upstreamAffinity] ) { _, textSegmentFrame, _, _ -> 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 } - /// +1, because we start with the first character with 1 - var col = cursorTextLineFragment.characterIndex(for: textSegmentFrame.origin) + 1 + 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 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 } } - /// 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 } From 4edd7faf2699d0d7bb2d1ea257789e0908de4c0c Mon Sep 17 00:00:00 2001 From: Elias Wahl Date: Wed, 15 Mar 2023 21:48:02 +0100 Subject: [PATCH 8/8] Moved STTextviewController to new folder. Moved cursor logic to STTextviewController+Cursor --- .../STTextViewController+Cursor.swift | 103 ++++++++++++++++++ .../STTextViewController.swift | 99 +---------------- 2 files changed, 106 insertions(+), 96 deletions(-) create mode 100644 Sources/CodeEditTextView/Controller/STTextViewController+Cursor.swift rename Sources/CodeEditTextView/{ => Controller}/STTextViewController.swift (72%) 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 72% rename from Sources/CodeEditTextView/STTextViewController.swift rename to Sources/CodeEditTextView/Controller/STTextViewController.swift index 9835e72dc..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 @@ -329,102 +332,6 @@ public class STTextViewController: NSViewController, STTextViewDelegate, ThemeAt // TODO: - This should be uncessecary } - // MARK: Cursor Position - - private var cursorPosition: Binding<(Int, Int)> - - 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.. 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 - } - } - deinit { textView = nil highlighter = nil