Skip to content

Commit

Permalink
Merge branch 'main' into feat/system-cursor
Browse files Browse the repository at this point in the history
  • Loading branch information
thecoolwinter authored Feb 21, 2024
2 parents 1c171b2 + e4d4853 commit 3bdfb22
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 18 deletions.
9 changes: 8 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,20 @@ let package = Package(
name: "CodeEditTextView",
dependencies: [
"TextStory",
.product(name: "Collections", package: "swift-collections")
.product(name: "Collections", package: "swift-collections"),
"CodeEditTextViewObjC"
],
plugins: [
.plugin(name: "SwiftLint", package: "SwiftLintPlugin")
]
),

// ObjC addons
.target(
name: "CodeEditTextViewObjC",
publicHeadersPath: "include"
),

// Tests for the text view
.testTarget(
name: "CodeEditTextViewTests",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,12 @@ public class TextLayoutManager: NSObject {

/// Lays out all visible lines
func layoutLines() { // swiftlint:disable:this function_body_length
guard let visibleRect = delegate?.visibleRect, !isInTransaction, let textStorage else { return }
guard layoutView?.superview != nil,
let visibleRect = delegate?.visibleRect,
!isInTransaction,
let textStorage else {
return
}
CATransaction.begin()
let minY = max(visibleRect.minY, 0)
let maxY = max(visibleRect.maxY, 0)
Expand Down
17 changes: 14 additions & 3 deletions Sources/CodeEditTextView/TextLine/LineFragmentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import AppKit
import CodeEditTextViewObjC

/// Displays a line fragment.
final class LineFragmentView: NSView {
Expand All @@ -23,7 +24,6 @@ final class LineFragmentView: NSView {
override func prepareForReuse() {
super.prepareForReuse()
lineFragment = nil

}

/// Set a new line fragment for this view, updating view size.
Expand All @@ -39,13 +39,24 @@ final class LineFragmentView: NSView {
return
}
context.saveGState()
context.setAllowsFontSmoothing(true)
context.setShouldSmoothFonts(true)

context.setAllowsAntialiasing(true)
context.setShouldAntialias(true)
context.setAllowsFontSmoothing(false)
context.setShouldSmoothFonts(false)
context.setAllowsFontSubpixelPositioning(true)
context.setShouldSubpixelPositionFonts(true)
context.setAllowsFontSubpixelQuantization(true)
context.setShouldSubpixelQuantizeFonts(true)

ContextSetHiddenSmoothingStyle(context, 16)

context.textMatrix = .init(scaleX: 1, y: -1)
context.textPosition = CGPoint(
x: 0,
y: lineFragment.height - lineFragment.descent + (lineFragment.heightDifference/2)
).pixelAligned

CTLineDraw(lineFragment.ctLine, context)
context.restoreGState()
}
Expand Down
29 changes: 26 additions & 3 deletions Sources/CodeEditTextView/TextLine/Typesetter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,18 @@ final class Typesetter {
startingOffset: Int,
constrainingWidth: CGFloat
) -> Int {
let breakIndex = startingOffset + CTTypesetterSuggestClusterBreak(typesetter, startingOffset, constrainingWidth)
if breakIndex >= string.length || (breakIndex - 1 > 0 && ensureCharacterCanBreakLine(at: breakIndex - 1)) {
var breakIndex = startingOffset + CTTypesetterSuggestClusterBreak(typesetter, startingOffset, constrainingWidth)

let isBreakAtEndOfString = breakIndex >= string.length

let isNextCharacterCarriageReturn = checkIfLineBreakOnCRLF(breakIndex)
if isNextCharacterCarriageReturn {
breakIndex += 1
}

let canLastCharacterBreak = (breakIndex - 1 > 0 && ensureCharacterCanBreakLine(at: breakIndex - 1))

if isBreakAtEndOfString || canLastCharacterBreak {
// Breaking either at the end of the string, or on a whitespace.
return breakIndex
} else if breakIndex - 1 > 0 {
Expand All @@ -208,7 +218,20 @@ final class Typesetter {
let set = CharacterSet(
charactersIn: string.attributedSubstring(from: NSRange(location: index, length: 1)).string
)
return set.isSubset(of: .whitespaces) || set.isSubset(of: .punctuationCharacters)
return set.isSubset(of: .whitespacesAndNewlines) || set.isSubset(of: .punctuationCharacters)
}

/// Check if the break index is on a CRLF (`\r\n`) character, indicating a valid break position.
/// - Parameter breakIndex: The index to check in the string.
/// - Returns: True, if the break index lies after the `\n` character in a `\r\n` sequence.
private func checkIfLineBreakOnCRLF(_ breakIndex: Int) -> Bool {
guard breakIndex - 1 > 0 && breakIndex + 1 <= string.length else {
return false
}
let substringRange = NSRange(location: breakIndex - 1, length: 2)
let substring = string.attributedSubstring(from: substringRange).string

return substring == LineEnding.carriageReturnLineFeed.rawValue
}

deinit {
Expand Down
16 changes: 12 additions & 4 deletions Sources/CodeEditTextView/TextView/TextView+ReplaceCharacters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@ extension TextView {
NotificationCenter.default.post(name: Self.textWillChangeNotification, object: self)
layoutManager.beginTransaction()
textStorage.beginEditing()
_undoManager?.beginGrouping()

var shouldEndGrouping = false
if !(_undoManager?.isGrouping ?? false) {
_undoManager?.beginGrouping()
shouldEndGrouping = true
}

// Can't insert an empty string into an empty range. One must be not empty
for range in ranges.sorted(by: { $0.location > $1.location }) where
(delegate?.textView(self, shouldReplaceContentsIn: range, with: string) ?? true)
&& (!range.isEmpty || !string.isEmpty) {
(!range.isEmpty || !string.isEmpty) &&
(delegate?.textView(self, shouldReplaceContentsIn: range, with: string) ?? true) {
delegate?.textView(self, willReplaceContentsIn: range, with: string)

layoutManager.willReplaceCharactersInRange(range: range, with: string)
Expand All @@ -41,7 +46,10 @@ extension TextView {
delegate?.textView(self, didReplaceContentsIn: range, with: string)
}

_undoManager?.endGrouping()
if shouldEndGrouping {
_undoManager?.endGrouping()
}

layoutManager.endTransaction()
textStorage.endEditing()
selectionManager.notifyAfterEdit()
Expand Down
20 changes: 14 additions & 6 deletions Sources/CodeEditTextView/Utils/CEUndoManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,11 @@ public class CEUndoManager {
return
}
isUndoing = true
NotificationCenter.default.post(name: .NSUndoManagerWillUndoChange, object: self.manager)
for mutation in item.mutations.reversed() {
NotificationCenter.default.post(name: .NSUndoManagerWillUndoChange, object: self.manager)
textView.insertText(mutation.inverse.string, replacementRange: mutation.inverse.range)
NotificationCenter.default.post(name: .NSUndoManagerDidUndoChange, object: self.manager)
textView.replaceCharacters(in: mutation.inverse.range, with: mutation.inverse.string)
}
NotificationCenter.default.post(name: .NSUndoManagerDidUndoChange, object: self.manager)
redoStack.append(item)
isUndoing = false
}
Expand All @@ -111,11 +111,11 @@ public class CEUndoManager {
return
}
isRedoing = true
NotificationCenter.default.post(name: .NSUndoManagerWillRedoChange, object: self.manager)
for mutation in item.mutations {
NotificationCenter.default.post(name: .NSUndoManagerWillRedoChange, object: self.manager)
textView.insertText(mutation.mutation.string, replacementRange: mutation.mutation.range)
NotificationCenter.default.post(name: .NSUndoManagerDidRedoChange, object: self.manager)
textView.replaceCharacters(in: mutation.mutation.range, with: mutation.mutation.string)
}
NotificationCenter.default.post(name: .NSUndoManagerDidRedoChange, object: self.manager)
undoStack.append(item)
isRedoing = false
}
Expand Down Expand Up @@ -159,11 +159,19 @@ public class CEUndoManager {

/// Groups all incoming mutations.
public func beginGrouping() {
guard !isGrouping else {
assertionFailure("UndoManager already in a group. Call `endGrouping` before this can be called.")
return
}
isGrouping = true
}

/// Stops grouping all incoming mutations.
public func endGrouping() {
guard isGrouping else {
assertionFailure("UndoManager not in a group. Call `beginGrouping` before this can be called.")
return
}
isGrouping = false
}

Expand Down
15 changes: 15 additions & 0 deletions Sources/CodeEditTextViewObjC/CGContextHidden.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// CGContextHidden.m
// CodeEditTextViewObjC
//
// Created by Khan Winter on 2/12/24.
//

#import <Cocoa/Cocoa.h>
#import "CGContextHidden.h"

extern void CGContextSetFontSmoothingStyle(CGContextRef, int);

void ContextSetHiddenSmoothingStyle(CGContextRef context, int style) {
CGContextSetFontSmoothingStyle(context, style);
}
15 changes: 15 additions & 0 deletions Sources/CodeEditTextViewObjC/include/CGContextHidden.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// CGContextHidden.h
// CodeEditTextViewObjC
//
// Created by Khan Winter on 2/12/24.
//

#ifndef CGContextHidden_h
#define CGContextHidden_h

#import <Cocoa/Cocoa.h>

void ContextSetHiddenSmoothingStyle(CGContextRef context, int style);

#endif /* CGContextHidden_h */
3 changes: 3 additions & 0 deletions Sources/CodeEditTextViewObjC/include/module.modulemap
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module CodeEditTextViewObjC {
header "CGContextHidden.h"
}
74 changes: 74 additions & 0 deletions Tests/CodeEditTextViewTests/TypesetterTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import XCTest
@testable import CodeEditTextView

// swiftlint:disable all

class TypesetterTests: XCTestCase {
let limitedLineWidthDisplayData = TextLine.DisplayData(maxWidth: 150, lineHeightMultiplier: 1.0, estimatedLineHeight: 20.0)
let unlimitedLineWidthDisplayData = TextLine.DisplayData(maxWidth: .infinity, lineHeightMultiplier: 1.0, estimatedLineHeight: 20.0)

func test_LineFeedBreak() {
let typesetter = Typesetter()
typesetter.typeset(
NSAttributedString(string: "testline\n"),
displayData: unlimitedLineWidthDisplayData,
breakStrategy: .word,
markedRanges: nil
)

XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.")

typesetter.typeset(
NSAttributedString(string: "testline\n"),
displayData: unlimitedLineWidthDisplayData,
breakStrategy: .character,
markedRanges: nil
)

XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.")
}

func test_carriageReturnBreak() {
let typesetter = Typesetter()
typesetter.typeset(
NSAttributedString(string: "testline\r"),
displayData: unlimitedLineWidthDisplayData,
breakStrategy: .word,
markedRanges: nil
)

XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.")

typesetter.typeset(
NSAttributedString(string: "testline\r"),
displayData: unlimitedLineWidthDisplayData,
breakStrategy: .character,
markedRanges: nil
)

XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.")
}

func test_carriageReturnLineFeedBreak() {
let typesetter = Typesetter()
typesetter.typeset(
NSAttributedString(string: "testline\r\n"),
displayData: unlimitedLineWidthDisplayData,
breakStrategy: .word,
markedRanges: nil
)

XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.")

typesetter.typeset(
NSAttributedString(string: "testline\r\n"),
displayData: unlimitedLineWidthDisplayData,
breakStrategy: .character,
markedRanges: nil
)

XCTAssertEqual(typesetter.lineFragments.count, 1, "Typesetter typeset incorrect number of lines.")
}
}

// swiftlint:enable all

0 comments on commit 3bdfb22

Please sign in to comment.