Skip to content

Folding Ribbon: Collapsed Indicators, Masking, Bracket Emphasis #332

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

Open
wants to merge 42 commits into
base: feat/code-folding
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4a73315
Initial Commit - Ribbon View & Toggles
thecoolwinter May 8, 2025
91ddcec
Correct Gutter Padding With Transparency, Dark Mode
thecoolwinter May 8, 2025
0a7519e
Revert Debugging Changes
thecoolwinter May 8, 2025
c056e4a
Add Demo Line Fold Provider (For Testing)
thecoolwinter May 8, 2025
0b840f6
Remove Some Unnecessary Hovering Stuff
thecoolwinter May 8, 2025
5c813dd
Implement Hover State and Animation
thecoolwinter May 8, 2025
8ee94b5
Move DrawingContext Struct
thecoolwinter May 8, 2025
4d9d1d0
Update View When Folds Change
thecoolwinter May 8, 2025
2c1af46
Dispatch Folding Calculation To Background
thecoolwinter May 8, 2025
2f1fdad
Use Lock For Cache, Skip Depth Changes
thecoolwinter May 9, 2025
f8433f3
Sanity Check Range
thecoolwinter May 9, 2025
d618ace
Toggle Folding State
thecoolwinter May 9, 2025
4408fac
Merge branch 'main' into code-folding/ribbon-view
thecoolwinter May 28, 2025
6cbf5e0
Merge branch 'code-folding/ribbon-view' into code-folding/better-fold…
thecoolwinter May 28, 2025
d3c03cf
Make StyledRangeStore Generalized and `Sendable`
thecoolwinter May 28, 2025
c7d7823
Rename to `RangeStore`
thecoolwinter May 28, 2025
26d2b66
Finish Rename
thecoolwinter May 28, 2025
7e7172c
Fix Doc Comment Drawing
thecoolwinter May 28, 2025
1687258
Update Test Names
thecoolwinter May 28, 2025
0acd458
Loosen `RangeStoreElement` Requirements
thecoolwinter May 28, 2025
886c7fe
Merge branch 'feat/code-folding' into code-folding/better-folding-cal…
thecoolwinter May 29, 2025
80f534e
Update Package.resolved
thecoolwinter May 29, 2025
d0c2451
Merge branch 'main' into code-folding/better-folding-calculation
thecoolwinter May 29, 2025
cd5b7c8
Remove Moved Files
thecoolwinter May 29, 2025
b1511ee
Merge branch 'make-styled-range-store-generic' into code-folding/bett…
thecoolwinter May 29, 2025
24d9b7a
Begin Transition To `RangeStore` Model
thecoolwinter May 30, 2025
45ccd75
Back to where we started! (working)
thecoolwinter May 30, 2025
0c6fe3d
Merge branch 'main' into code-folding/better-folding-calculation
thecoolwinter Jun 2, 2025
3b13a30
Move to Swift Concurrency with Async Streams
thecoolwinter Jun 3, 2025
8abf180
Remove some comments
thecoolwinter Jun 3, 2025
8976911
Fix Drawing Ordering, Use Attachment Ranges
thecoolwinter Jun 3, 2025
bab4c8e
Add Straight Line When Adjacent
thecoolwinter Jun 3, 2025
2a952fe
Merge branch 'feat/code-folding' into code-folding/better-folding-cal…
thecoolwinter Jun 3, 2025
aa89ce2
Clean Up, Add Tests
thecoolwinter Jun 3, 2025
fb70a58
Fix Lint Errors, Clean Up Calculator
thecoolwinter Jun 3, 2025
78cb70a
Document the async calculator
thecoolwinter Jun 3, 2025
b5092fe
Merge branch 'CodeEditApp:main' into code-folding/better-folding-calc…
thecoolwinter Jun 4, 2025
473a513
Collapsed Colors, Clean Up and Document
thecoolwinter Jun 4, 2025
e0ceeb1
Mask Collapsed Folds, Simplify Reasoning in Draw
thecoolwinter Jun 4, 2025
f4d4808
Emphasize Brackets Surrounding Folds
thecoolwinter Jun 5, 2025
716ebaf
lint:fix
thecoolwinter Jun 6, 2025
ba8b9da
Merge branch 'feat/code-folding' into code-folding/collapsed-indicators
thecoolwinter Jun 18, 2025
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
Expand Up @@ -11,23 +11,6 @@ import TextFormation
import TextStory

extension TextViewController {

internal enum BracketPairs {
static let allValues: [(String, String)] = [
("{", "}"),
("[", "]"),
("(", ")"),
("\"", "\""),
("'", "'")
]

static let emphasisValues: [(String, String)] = [
("{", "}"),
("[", "]"),
("(", ")")
]
}

// MARK: - Filter Configuration

/// Initializes any filters for text editing.
Expand Down
29 changes: 29 additions & 0 deletions Sources/CodeEditSourceEditor/Enums/BracketPairs.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// BracketPairs.swift
// CodeEditSourceEditor
//
// Created by Khan Winter on 6/5/25.
//

enum BracketPairs {
static let allValues: [(String, String)] = [
("{", "}"),
("[", "]"),
("(", ")"),
("\"", "\""),
("'", "'")
]

static let emphasisValues: [(String, String)] = [
("{", "}"),
("[", "]"),
("(", ")")
]

/// Checks if the given string is a matchable emphasis string.
/// - Parameter potentialMatch: The string to check for matches.
/// - Returns: True if a match was found with either start or end bracket pairs.
static func matches(_ potentialMatch: String) -> Bool {
allValues.contains(where: { $0.0 == potentialMatch || $0.1 == potentialMatch })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ extension NSBezierPath {
public static let bottomLeft = Corners(rawValue: 1 << 1)
public static let topRight = Corners(rawValue: 1 << 2)
public static let bottomRight = Corners(rawValue: 1 << 3)
public static let all: Corners = Corners(rawValue: 0b1111)
}

// swiftlint:disable:next function_body_length
Expand Down
23 changes: 23 additions & 0 deletions Sources/CodeEditSourceEditor/Extensions/NSColor+LightDark.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// NSColor+LightDark.swift
// CodeEditSourceEditor
//
// Created by Khan Winter on 6/4/25.
//

import AppKit

extension NSColor {
convenience init(light: NSColor, dark: NSColor) {
self.init(name: nil) { appearance in
return switch appearance.name {
case .aqua:
light
case .darkAqua:
dark
default:
NSColor()
}
}
}
}
19 changes: 19 additions & 0 deletions Sources/CodeEditSourceEditor/Extensions/NSRect+Transform.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// NSRect+Transform.swift
// CodeEditSourceEditor
//
// Created by Khan Winter on 6/4/25.
//

import AppKit

extension NSRect {
func transform(x xVal: CGFloat = 0, y yVal: CGFloat = 0, width: CGFloat = 0, height: CGFloat = 0) -> NSRect {
NSRect(
x: self.origin.x + xVal,
y: self.origin.y + yVal,
width: self.width + width,
height: self.height + height
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ actor LineFoldCalculator {

private var valueStreamContinuation: AsyncStream<LineFoldStorage>.Continuation
private var textChangedTask: Task<Void, Never>?

/// Create a new calculator object that listens to a given stream for text changes.
/// - Parameters:
/// - foldProvider: The object to use to calculate fold regions.
Expand All @@ -39,7 +39,7 @@ actor LineFoldCalculator {
deinit {
textChangedTask?.cancel()
}

/// Sets up an attached task to listen to values on a stream of text changes.
/// - Parameter textChangedStream: A stream of text changes.
private func listenToTextChanges(textChangedStream: AsyncStream<(NSRange, Int)>) {
Expand Down Expand Up @@ -105,7 +105,7 @@ actor LineFoldCalculator {

await yieldNewStorage(newFolds: foldCache, controller: controller, documentRange: documentRange)
}

/// Yield a new storage value on the value stream using a new set of folds.
/// - Parameters:
/// - newFolds: The new folds to yield with the storage value.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import Combine
/// - Loop through the list, creating nested folds as indents go up and down.
///
class LineFoldingModel: NSObject, NSTextStorageDelegate, ObservableObject {
static let emphasisId = "lineFolding"


/// An ordered tree of fold ranges in a document. Can be traversed using ``FoldRange/parent``
/// and ``FoldRange/subFolds``.
@Published var foldCache: LineFoldStorage = LineFoldStorage(documentLength: 0)
Expand Down Expand Up @@ -92,4 +95,35 @@ class LineFoldingModel: NSObject, NSTextStorageDelegate, ObservableObject {
}
return deepestFold
}

func emphasizeBracketsForFold(_ fold: FoldRange) {
clearEmphasis()

// Find the text object, make sure there's available characters around the fold.
guard let text = controller?.textView.textStorage.string as? NSString,
fold.range.lowerBound > 0 && fold.range.upperBound < text.length - 1 else {
return
}

let firstRange = NSRange(location: fold.range.lowerBound - 1, length: 1)
let secondRange = NSRange(location: fold.range.upperBound, length: 1)

// Check if these are emphasizable bracket pairs.
guard BracketPairs.matches(text.substring(from: firstRange) ?? "")
&& BracketPairs.matches(text.substring(from: secondRange) ?? "") else {
return
}

controller?.textView.emphasisManager?.addEmphases(
[
Emphasis(range: firstRange, style: .standard, flash: false, inactive: false, selectInDocument: false),
Emphasis(range: secondRange, style: .standard, flash: false, inactive: false, selectInDocument: false),
],
for: Self.emphasisId
)
}

func clearEmphasis() {
controller?.textView.emphasisManager?.removeEmphases(for: Self.emphasisId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,40 @@ import CodeEditTextView

class LineFoldPlaceholder: TextAttachment {
let fold: FoldRange
let charWidth: CGFloat
var isSelected: Bool = false

init(fold: FoldRange) {
init(fold: FoldRange, charWidth: CGFloat) {
self.fold = fold
self.charWidth = charWidth
}

var width: CGFloat { 17 }
var width: CGFloat {
charWidth * 5
}

func draw(in context: CGContext, rect: NSRect) {
context.saveGState()

let centerY = rect.midY - 1.5

if isSelected {
context.setFillColor(NSColor.controlAccentColor.cgColor)
context.addPath(
NSBezierPath(
rect: rect.transform(x: 2.0, y: 3.0, width: -4.0, height: -6.0 ),
roundedCorners: .all,
cornerRadius: 2
).cgPathFallback
)
context.fillPath()
}

context.setFillColor(NSColor.secondaryLabelColor.cgColor)
context.addEllipse(in: CGRect(x: rect.minX + 2, y: centerY, width: 3, height: 3))
context.addEllipse(in: CGRect(x: rect.minX + 7, y: centerY, width: 3, height: 3))
context.addEllipse(in: CGRect(x: rect.minX + 12, y: centerY, width: 3, height: 3))
let size = charWidth / 2
context.addEllipse(in: CGRect(x: rect.minX + charWidth * 1.25, y: centerY, width: size, height: size))
context.addEllipse(in: CGRect(x: rect.minX + (charWidth * 2.25), y: centerY, width: size, height: size))
context.addEllipse(in: CGRect(x: rect.minX + (charWidth * 3.25), y: centerY, width: size, height: size))
context.fillPath()

context.restoreGState()
Expand Down
Loading