Skip to content

Commit 5885a59

Browse files
committed
docs: add comprehensive DocC documentation for all three library products
Add DocC catalogs with landing pages for TextBuffer, TextRope, and TextBufferTesting. Create "Choosing a Buffer" and "Undo and Redo" article pages with decision tables and code examples. Add doc comments to all previously undocumented public types. Update MutableStringBuffer docs to point to SendableRopeBuffer as the recommended in-memory buffer.
1 parent 262e080 commit 5885a59

20 files changed

Lines changed: 446 additions & 17 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
- DocC documentation for all three library products: TextBuffer, TextRope, and TextBufferTesting each have a DocC catalog with a landing page and organized topic groups.
8+
- "Choosing a Buffer" article — decision guide with comparison table and code examples, positioning `SendableRopeBuffer` as the recommended in-memory buffer.
9+
- "Undo and Redo" article — explains both `UndoManager`-based and `OperationLog`-based strategies with code examples.
10+
- Doc comments for previously undocumented public types: `TextBuffer` protocol, `RopeBuffer`, `SendableRopeBuffer`, `TransferableUndoable`, `PuppetUndoManager`, `OperationLog`, `UndoGroup`, `BufferOperation`, `BufferContent`, `TextRope`, and all TextBufferTesting helpers.
11+
512
### Changed
613

714
- **BREAKING:** `InMemoryBuffer` typealias now points to `SendableRopeBuffer` (was `MutableStringBuffer`). The rope-backed, `Sendable` value type with built-in undo is the proper in-memory buffer for production use. `MutableStringBuffer` remains available by its concrete name.

Sources/TextBuffer/Buffer/BufferContent.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
// Copyright © 2024 Christian Tietze. All rights reserved. Distributed under the MIT License.
22

3+
/// A type whose instances can report their length, used by buffer protocols to measure content.
4+
///
5+
/// `String` conforms to `BufferContent` with its `length` returning `utf16.count`,
6+
/// matching Foundation's `NSString` indexing used throughout the buffer API.
37
public protocol BufferContent<Length> {
48
associatedtype Length
59
var length: Length { get }

Sources/TextBuffer/Buffer/MutableStringBuffer.swift

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@
22

33
import Foundation
44

5-
/// A self-contained ``Buffer`` implementation, backed by `NSMutableString` as the UTF-16-offset indexed storage.
5+
/// A ``Buffer`` implementation backed by `NSMutableString` as the UTF-16-offset indexed storage.
66
///
7-
/// Used as in-memory buffers, you can apply changes to off-screen textual content in a way that is consistent with text views, but actually independent of these. Opposed to the platform's Text Kit views, which are large class clusters with a lot of automatic behavior pertaining layout, keeping a ``MutableStringBuffer`` in memory produces little overhead. (In fact, only as much overhead as a `NSMutableString` will, plus storing the selected range.)
7+
/// `MutableStringBuffer` is a lightweight reference-type buffer useful for simple tests and for
8+
/// copying snapshots of other buffers. For general-purpose in-memory text manipulation,
9+
/// prefer ``SendableRopeBuffer``, which offers O(log n) mutations, built-in undo, and `Sendable` safety.
810
///
9-
/// To adapt other buffers and copy their content, use ``MutableStringBuffer/init(copying:)``.
10-
///
11-
/// ## Utility for Apps
12-
///
13-
/// - Use ``MutableStringBuffer`` in unit tests.
14-
/// - Maintain multiple text buffers in memory while only ever rendering one buffer as a text view on screen, e.g. for opening multiple files in your app.
11+
/// To copy another buffer's content, use ``MutableStringBuffer/init(copying:)``.
1512
public final class MutableStringBuffer: Buffer, TextAnalysisCapable {
1613
public typealias Range = NSRange
1714
public typealias Content = String

Sources/TextBuffer/Buffer/PuppetUndoManager.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ protocol PuppetUndoManagerDelegate: AnyObject {
1010
var puppetRedoActionName: String { get }
1111
}
1212

13+
/// An `UndoManager` subclass that delegates undo and redo to a ``TransferableUndoable``'s ``OperationLog``.
14+
///
15+
/// You don't create instances directly. Instead, call
16+
/// ``TransferableUndoable/enableSystemUndoIntegration()`` to obtain one. Assign the returned
17+
/// undo manager to your window or document to enable AppKit's Edit menu undo/redo items.
1318
@MainActor
1419
public final class PuppetUndoManager: UndoManager {
1520
weak var owner: (any PuppetUndoManagerDelegate)?

Sources/TextBuffer/Buffer/RopeBuffer.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
import Foundation
22
import TextRope
33

4+
/// A ``Buffer`` implementation backed by a ``TextRope`` for efficient manipulation of large texts.
5+
///
6+
/// `RopeBuffer` provides O(log n) insert, delete, and replace operations, making it a better choice
7+
/// than ``MutableStringBuffer`` when working with very large documents.
8+
///
9+
/// `RopeBuffer` is a reference type and is **not** `Sendable`. For a thread-safe value-type alternative,
10+
/// use ``SendableRopeBuffer``.
11+
///
12+
/// `RopeBuffer` does not include built-in undo support. Wrap it in ``Undoable`` or ``TransferableUndoable``
13+
/// to add undo/redo.
14+
///
15+
/// To copy another buffer's content, use ``init(copying:)``.
416
public final class RopeBuffer: Buffer, TextAnalysisCapable {
517
public typealias Range = NSRange
618
public typealias Content = String

Sources/TextBuffer/Buffer/TextBuffer.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
/// Synchronous text buffer protocol for value types.
2+
///
3+
/// ``TextBuffer`` is the counterpart to ``Buffer`` (which targets reference types and refines ``AsyncBuffer``).
4+
/// Both protocols expose the same API surface — content access, selection management, and text mutations —
5+
/// but ``TextBuffer`` uses `mutating` methods, making it suitable for structs like ``SendableRopeBuffer``.
6+
///
7+
/// The primary associated type is `Range`, which determines the range representation.
8+
/// `Location` is derived as `Range.Position`.
19
public protocol TextBuffer<Range> {
210
associatedtype Range: BufferRange
311
typealias Location = Range.Position

Sources/TextBuffer/Buffer/TransferableUndoable.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
import Foundation
22

3+
/// Decorator of any ``Buffer`` to add undo/redo functionality through an ``OperationLog``.
4+
///
5+
/// Unlike ``Undoable``, which relies on Foundation's `UndoManager`, `TransferableUndoable` records
6+
/// all mutations as ``BufferOperation`` values for replay-based undo. This makes the undo history
7+
/// inspectable, serializable, and transferable between buffer instances.
8+
///
9+
/// ## Snapshots and State Transfer
10+
///
11+
/// - ``snapshot()`` captures the current content, selection, and operation log as a new
12+
/// `TransferableUndoable<MutableStringBuffer>`.
13+
/// - ``sendableSnapshot()`` produces a ``SendableRopeBuffer`` that can cross actor boundaries.
14+
/// - `represent(_:)` restores state from another `TransferableUndoable` or a ``SendableRopeBuffer``.
15+
///
16+
/// ## System Undo Integration
17+
///
18+
/// Call ``enableSystemUndoIntegration()`` to obtain an `UndoManager` that bridges to this buffer's
19+
/// ``OperationLog``, enabling AppKit undo menu items and the responder chain.
320
@MainActor
421
public final class TransferableUndoable<Base>: @MainActor Buffer where Base: Buffer, Base.Range == NSRange, Base.Content == String {
522
public typealias Range = NSRange
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Choosing a Buffer
2+
3+
Pick the right buffer implementation for your use case.
4+
5+
## Overview
6+
7+
The TextBuffer library provides several concrete buffer types that all share the same read-and-mutate API.
8+
They differ in backing storage, value vs. reference semantics, `Sendable` conformance, and built-in undo support.
9+
10+
For most use cases, ``SendableRopeBuffer`` (aliased as `InMemoryBuffer`) is the recommended starting point:
11+
it's a `Sendable` value type with efficient rope-backed storage, built-in undo/redo, and O(log n) mutations
12+
that scale to large documents.
13+
14+
## At a Glance
15+
16+
| Type | Backing | Semantics | Built-in Undo | Sendable | Best for |
17+
|------|---------|-----------|---------------|----------|----------|
18+
| ``SendableRopeBuffer`` | ``TextRope`` | value | ``OperationLog`` | yes | general-purpose in-memory buffer |
19+
| ``MutableStringBuffer`` | `NSMutableString` | reference | no | no | simple tests |
20+
| ``RopeBuffer`` | ``TextRope`` | reference | no | no | reference-type rope without undo |
21+
| ``NSTextViewBuffer`` | `NSTextView` | reference, `@MainActor` | no | no | driving an AppKit text view |
22+
23+
All four conform to ``TextAnalysisCapable``, so you get ``TextAnalysisCapable/wordRange(for:)``
24+
and ``TextAnalysisCapable/lineRange(for:)`` on every buffer.
25+
26+
## The In-Memory Buffer: SendableRopeBuffer
27+
28+
``SendableRopeBuffer`` is the default choice for in-memory text manipulation. It combines:
29+
30+
- **Efficient storage** via ``TextRope`` — O(log n) insert, delete, and replace, even for large documents.
31+
- **Built-in undo/redo** via ``OperationLog`` — no decorator needed.
32+
- **Value semantics and `Sendable`** — safe to pass across actor boundaries.
33+
34+
```swift
35+
var buffer = SendableRopeBuffer("Hello, World!")
36+
try buffer.insert("!", at: 13)
37+
print(buffer.content) // "Hello, World!!"
38+
39+
buffer.undo()
40+
print(buffer.content) // "Hello, World!"
41+
```
42+
43+
For apps that need a reference-type buffer with `UndoManager` integration or snapshot/transfer
44+
capabilities, wrap a ``RopeBuffer`` in ``TransferableUndoable`` (aliased as `EditingBuffer`):
45+
46+
```swift
47+
let editing = TransferableUndoable(RopeBuffer("Document text"))
48+
```
49+
50+
## Other Buffer Types
51+
52+
``MutableStringBuffer`` is backed by `NSMutableString`. It's lightweight and useful in simple tests,
53+
but its O(n) mutations don't scale to large texts. It has no built-in undo — wrap it in
54+
``Undoable`` or ``TransferableUndoable`` if needed.
55+
56+
``RopeBuffer`` gives you rope-backed O(log n) performance as a reference type, but without built-in undo.
57+
Use it when you need a mutable reference-type buffer to wrap in ``Undoable`` or ``TransferableUndoable``.
58+
59+
``NSTextViewBuffer`` adapts an `NSTextView` for use through the buffer API. All mutations go through
60+
`NSTextStorage` wrapped in `beginEditing()`/`endEditing()`. It's `@MainActor`-isolated.
61+
62+
```swift
63+
let textViewBuffer = NSTextViewBuffer(textView: myTextView)
64+
try textViewBuffer.replace(range: textViewBuffer.selectedRange, with: "replacement")
65+
```
66+
67+
## Writing Generic Code
68+
69+
Constrain on ``Buffer`` for reference-type buffers (classes) or ``TextBuffer`` for value-type buffers (structs).
70+
Use ``TextAnalysisCapable`` when you need word or line analysis.
71+
72+
```swift
73+
func wordAtInsertion<B: Buffer>(in buffer: B) throws -> String
74+
where B.Range == NSRange, B.Content == String, B: TextAnalysisCapable {
75+
let word = try buffer.wordRange(for: buffer.selectedRange)
76+
return try buffer.content(in: word)
77+
}
78+
```
79+
80+
## Copying Between Buffer Types
81+
82+
Every buffer can be copied into a ``MutableStringBuffer`` or ``RopeBuffer`` via `init(copying:)`:
83+
84+
```swift
85+
let copy = MutableStringBuffer(copying: someBuffer)
86+
let ropeCopy = RopeBuffer(copying: someBuffer)
87+
```
88+
89+
For crossing actor boundaries, ``TransferableUndoable`` provides ``TransferableUndoable/sendableSnapshot()``
90+
to produce a ``SendableRopeBuffer``, and ``TransferableUndoable/represent(_:)-(SendableRopeBuffer)`` to restore
91+
from one:
92+
93+
```swift
94+
// On @MainActor
95+
let snapshot = transferableBuffer.sendableSnapshot()
96+
97+
// On another actor / in a Task
98+
await process(snapshot)
99+
100+
// Back on @MainActor
101+
transferableBuffer.represent(snapshot)
102+
```

Sources/TextBuffer/Documentation.docc/Documentation.md

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,50 @@ Text buffer abstractions to power your text editor, in UI or in memory.
44

55
## Overview
66

7-
To simulate modifications and insertion point movement or selection changes in text views, you need to create the actual UI component. This is both rather resource intensive and constrained to the `MainActor`.
7+
TextBuffer provides a uniform API for reading, mutating, and selecting text — whether backed by
8+
an `NSMutableString`, a rope, or an `NSTextView`. All buffers share the same operations: access
9+
content, manage a selection or insertion point, and insert, delete, or replace text.
810

9-
With in-memory buffers, you get the same behavior, but without the UI overhead.
11+
The library defines two parallel protocol hierarchies. ``Buffer`` (which refines ``AsyncBuffer``)
12+
targets reference-type conformers like ``MutableStringBuffer``, ``RopeBuffer``, and
13+
``NSTextViewBuffer``. ``TextBuffer`` targets value types like ``SendableRopeBuffer``.
14+
Both hierarchies expose the same API surface; ``TextAnalysisCapable`` adds word and line range
15+
analysis to either.
1016

1117
## Topics
1218

13-
### Buffers
19+
### Essentials
1420

15-
A `Buffer` is an abstraction of textual content and a selection.
21+
- <doc:ChoosingABuffer>
22+
- <doc:UndoAndRedo>
23+
24+
### Protocols
1625

1726
- ``Buffer``
18-
- ``MutableStringBuffer``
19-
- ``Undoable``
27+
- ``TextBuffer``
28+
- ``AsyncBuffer``
29+
- ``TextAnalysisCapable``
30+
- ``BufferRange``
31+
- ``BufferContent``
2032

21-
### Platform-Specific Buffer Adapters
33+
### In-Memory Buffers
34+
35+
- ``MutableStringBuffer``
36+
- ``RopeBuffer``
37+
- ``SendableRopeBuffer``
2238

23-
Text Kit's text views behave as buffers, but offer a much wider surface API to perform layout and typesetting. Opposed to these, a `Buffer` is a lightweight API to perform changes like a user would in an interactive text view, which we expose as adapters.
39+
### Platform Adapters
2440

2541
- ``NSTextViewBuffer``
2642

43+
### Undo Support
44+
45+
- ``Undoable``
46+
- ``TransferableUndoable``
47+
- ``OperationLog``
48+
- ``UndoGroup``
49+
- ``BufferOperation``
50+
51+
### Error Handling
52+
53+
- ``BufferAccessFailure``
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Undo and Redo
2+
3+
Add undo/redo to your buffers using UndoManager or OperationLog.
4+
5+
## Overview
6+
7+
TextBuffer offers two undo strategies. Choose based on whether you need AppKit `UndoManager` integration
8+
or a portable, `Sendable` undo history.
9+
10+
## UndoManager-Based: Undoable
11+
12+
``Undoable`` is a decorator that wraps any ``Buffer`` and registers inverse actions with Foundation's
13+
`UndoManager`. It integrates directly with AppKit's Edit menu and the responder chain.
14+
15+
```swift
16+
let buffer = MutableStringBuffer("Hello")
17+
let undoable = Undoable(buffer)
18+
19+
undoable.undoGrouping(actionName: "Greet") {
20+
try! undoable.delete(in: undoable.range)
21+
try! undoable.insert("Hi, World!")
22+
}
23+
print(buffer.content) // "Hi, World!"
24+
25+
undoable.undo()
26+
print(buffer.content) // "Hello"
27+
28+
undoable.redo()
29+
print(buffer.content) // "Hi, World!"
30+
```
31+
32+
> Warning: You must keep the ``Undoable`` instance alive for undo to work.
33+
> It removes all registered undo actions from its ``Undoable/undoManager`` on deinitialization
34+
> to avoid crashes from dangling `unowned` references.
35+
36+
## OperationLog-Based: TransferableUndoable and SendableRopeBuffer
37+
38+
``OperationLog`` records each mutation as a ``BufferOperation`` value. Undo and redo work by
39+
replaying operations in reverse or forward order. This makes the history inspectable,
40+
serializable, and transferable.
41+
42+
### TransferableUndoable
43+
44+
``TransferableUndoable`` is a decorator like ``Undoable``, but backed by ``OperationLog``
45+
instead of `UndoManager`. It supports snapshotting and state transfer:
46+
47+
```swift
48+
let base = RopeBuffer("Document text")
49+
let buffer = TransferableUndoable(base)
50+
51+
buffer.undoGrouping(actionName: "Edit") {
52+
try! buffer.replace(range: NSRange(location: 0, length: 8), with: "New")
53+
}
54+
55+
// Snapshot for transfer across actors
56+
let snapshot = buffer.sendableSnapshot()
57+
58+
// Restore from snapshot
59+
buffer.represent(snapshot)
60+
61+
// Bridge to system undo for AppKit menus
62+
let undoManager = buffer.enableSystemUndoIntegration()
63+
myWindow.undoManager = undoManager
64+
```
65+
66+
### SendableRopeBuffer
67+
68+
``SendableRopeBuffer`` is a `Sendable` value type with ``OperationLog`` built in.
69+
No decorator needed — undo/redo is part of the buffer itself:
70+
71+
```swift
72+
var buffer = SendableRopeBuffer("Hello")
73+
try buffer.insert(", World", at: 5)
74+
75+
buffer.undoGrouping(actionName: "Replace") { buf in
76+
try! buf.delete(in: buf.range)
77+
try! buf.insert("Goodbye")
78+
}
79+
print(buffer.content) // "Goodbye"
80+
81+
buffer.undo()
82+
print(buffer.content) // "Hello, World"
83+
84+
buffer.redo()
85+
print(buffer.content) // "Goodbye"
86+
```
87+
88+
## Choosing an Undo Strategy
89+
90+
| Strategy | Type | Sendable | AppKit Integration | Snapshots |
91+
|----------|------|----------|--------------------|-----------|
92+
| `UndoManager` | ``Undoable`` | no | built-in | no |
93+
| `OperationLog` | ``TransferableUndoable`` | no (but can snapshot) | via ``TransferableUndoable/enableSystemUndoIntegration()`` | yes |
94+
| `OperationLog` | ``SendableRopeBuffer`` | yes | no | is the snapshot |
95+
96+
Use ``Undoable`` when you already have an `UndoManager` (e.g., document-based apps) and want
97+
zero-configuration AppKit integration.
98+
99+
Use ``TransferableUndoable`` when you need to snapshot buffer state, transfer it across actors,
100+
or want an inspectable operation history while still wrapping a reference-type buffer.
101+
102+
Use ``SendableRopeBuffer`` when you need a fully self-contained, `Sendable` buffer with undo —
103+
for example, in background processing or when the buffer itself crosses isolation boundaries.

0 commit comments

Comments
 (0)