Skip to content

Commit 4cd926c

Browse files
author
yan
committed
docs: correct concepts the redesign made false
Three claims had drifted from the code, and a stale document is worse than a missing one because it is trusted. MEMORY #8 and STACK.md both said the CodeEditSourceEditor exposure was contained to two files. Three import it: EditorState.swift has imported it for IndentOption all along. Counted, corrected, and the rule is now 'keep it at three' with the third named and the reason it is thin. STACK.md and ARCHITECTURE.md both described a UI-to-App notification seam. There was no seam left. The sidebar lost its Open Folder button when the empty state moved to the canvas, which left .codeEditorOpenFolder with a subscriber and no publisher; and .codeEditorOpenFiles posted and observed inside CodeEditorApp, a round trip to nowhere. Both names are deleted and presentOpenFilePanel calls AppState directly. Session restore was described as restoring 'the workspace and tabs', which reads as resuming work. It persists paths and never buffer content, so quitting with unsaved changes loses them silently while the tab still reopens. Stated plainly in CURRENT_STATE, README and the type's own MEMORY entry. Also: PROCESS and PROJECT_PLAN still claimed 35 tests, README's module table predated the Core and Themes additions, ARCHITECTURE had no account of the find-panel sync, session restore or quick open flows, and two anchors in AGENTS.md pointed at headings that do not exist.
1 parent eea140c commit 4cd926c

10 files changed

Lines changed: 89 additions & 54 deletions

File tree

‎AGENTS.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ Never imply in a commit message or PR that CI verified the build. It did not.
9494
- **No magic numbers in chrome.** Spacing, radii and fixed heights come from `Metrics`
9595
(`CodeEditorCore`); the 4pt grid is enforced by a test. Chrome type comes from
9696
`Typography` — semantic styles only, so it scales with the user's text size. Code keeps a
97-
fixed-size SF Mono. See [MEMORY.md](docs/MEMORY.md#11) for why these rules exist.
97+
fixed-size SF Mono. See [MEMORY.md](docs/MEMORY.md#11-never-derive-appearance-from-colour-arithmetic)
98+
for why these rules exist.
9899

99100
## Traps that will bite
100101

@@ -108,7 +109,8 @@ Never imply in a commit message or PR that CI verified the build. It did not.
108109
- A `.onTapGesture` on a row inside a `List(selection:)` never fires. Drive row behaviour
109110
from the selection change. See [MEMORY.md #12](docs/MEMORY.md#12-a-tap-gesture-inside-a-selection-list-is-swallowed).
110111
- Do not put a `.frame` on a `NavigationSplitView` column; `.navigationSplitViewColumnWidth`
111-
already sizes it. See [MEMORY.md #13](docs/MEMORY.md).
112+
already sizes it. See
113+
[MEMORY.md #13](docs/MEMORY.md#13-navigationsplitview-columns-are-sized-in-one-place).
112114

113115
## Before you call it done
114116

‎PROJECT_PLAN.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ tracked in GitHub, ordered in [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md).
3030
## Quality gates
3131

3232
- [x] Builds with no warnings
33-
- [x] `swift test` passes (35 tests)
33+
- [x] `swift test` passes (68 tests)
3434
- [x] `swift-format lint --strict` clean
3535
- [ ] CI builds the package — blocked, see [docs/MEMORY.md](docs/MEMORY.md#1-ci-cannot-build-this-package-do-not-try-to-make-it)
3636
- [ ] Completion works for Swift

‎README.md‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ an embedded shell in the same window.
2929
the divider above it to resize; the height is remembered.
3030
- **Themes** — Dark, Light and High Contrast, persisted across launches along with font
3131
size, tab width, word wrap, minimap, folding ribbon and invisibles.
32-
- **Session restore** — reopens your workspace and tabs on launch. Untitled documents are
33-
not restored.
32+
- **Session restore** — reopens your workspace and tabs on launch. It restores *what you
33+
had open*, not your edits: unsaved buffers are not persisted, and untitled documents
34+
have no path to replay.
3435
- **Menu commands** — ⌘N, ⌘O, ⇧⌘O, ⌘F, ⌘P, ⌘S, ⌥⌘S, ⌘W, ⌘B, ⌃`, ⇧⌘L for theme.
3536

3637
### Interface notes
@@ -118,12 +119,12 @@ add a `build` job and drop the caveat.
118119

119120
| Module | Responsibility |
120121
|---|---|
121-
| `CodeEditorCore` | `Document`, `DocumentManager`, `TextSelectionManager`, `FileSystemManager`. No UI. |
122-
| `CodeEditorThemes` | `Theme`, `EditorColor`, `ThemeManager` with persisted selection. |
122+
| `CodeEditorCore` | `Document`, `DocumentManager`, `TextSelectionManager`, `FileSystemManager`, `FileOutline`, `QuickOpenMatcher`, `QuickOpenIndex`, `SessionStore`, `Metrics`. No UI — Foundation only. |
123+
| `CodeEditorThemes` | `Theme`, `Chrome`, `Semantic`, `Typography`, `EditorColor`, `ThemeManager` with persisted selection. |
123124
| `CodeEditorTerminal` | `TerminalSession`: shell subprocess, line-buffered scrollback, history. |
124125
| `CodeEditorLSP` | `JSONRPCTransport` (Content-Length framing) and `LSPClient`. Not yet wired into the UI. |
125-
| `CodeEditorUI` | SwiftUI views: editor, sidebar, terminal, plus `AppState`. |
126-
| `CodeEditorApp` | The `@main` App, menu commands, window layout. |
126+
| `CodeEditorUI` | SwiftUI views: editor, sidebar, tabs, status bar, empty state, quick open, terminal, plus `AppState`. |
127+
| `CodeEditorApp` | The `@main` App, menu commands, window layout, toolbar. |
127128

128129
Dependencies flow one way: `Core` ← `UI` ← `App`. `Themes`, `Terminal` and `LSP` are
129130
leaves that `UI` composes.

‎Sources/CodeEditorApp/Sources/CodeEditorApp.swift‎

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,6 @@ struct CodeEditorApp: App {
1414
Window("Code Editor", id: "main-window") {
1515
MainWindowView(appState: appState)
1616
.frame(minWidth: 720, minHeight: 480)
17-
.onReceive(
18-
NotificationCenter.default.publisher(for: .codeEditorOpenFolder)
19-
) { _ in
20-
appState.openFolder()
21-
}
22-
.onReceive(
23-
NotificationCenter.default.publisher(for: .codeEditorOpenFiles)
24-
) { notification in
25-
guard let urls = notification.object as? [URL] else { return }
26-
for url in urls {
27-
appState.openFile(url)
28-
}
29-
}
3017
}
3118
.defaultSize(width: 1200, height: 800)
3219
.commands { commands }
@@ -182,9 +169,8 @@ struct CodeEditorApp: App {
182169
panel.prompt = "Open"
183170

184171
guard panel.runModal() == .OK else { return }
185-
NotificationCenter.default.post(
186-
name: .codeEditorOpenFiles,
187-
object: panel.urls
188-
)
172+
for url in panel.urls {
173+
appState.openFile(url)
174+
}
189175
}
190176
}

‎Sources/CodeEditorUI/Sources/Sidebar/FileExplorerView.swift‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -264,13 +264,3 @@ public struct FileExplorerView: View {
264264
}
265265
}
266266
}
267-
268-
// MARK: - Notifications
269-
270-
extension Notification.Name {
271-
/// Posted by the sidebar to ask the app to present an open-folder panel.
272-
public static let codeEditorOpenFolder = Notification.Name("CodeEditor.openFolder")
273-
274-
/// Posted by the app with an array of `URL`s to open in the editor.
275-
public static let codeEditorOpenFiles = Notification.Name("CodeEditor.openFiles")
276-
}

‎docs/ARCHITECTURE.md‎

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,26 @@ would need a re-entrancy guard against the text view writing while SwiftUI is mi
8181
**Consequence:** `Document` mutations that originate outside the text view (nothing today)
8282
would fight the binding. If you add one, route it through the view, not around it.
8383

84+
## Three more flows worth knowing
85+
86+
**Find panel.** One flag, two owners. `EditorState.isFindVisible` is ours;
87+
`SourceEditorState.findPanelVisible` is the editor's. `CodeEditorView` syncs them both ways,
88+
and the panel is the authority: when the user dismisses the panel, the coordinator writes
89+
`findPanelVisible = false`, which we mirror back onto `EditorState`. ⌘F only ever sets our
90+
flag. Nothing in the app draws a find UI.
91+
92+
**Session restore.** `AppState` is the `DocumentManagerDelegate`, so opening and closing a
93+
document persists a `SessionSnapshot` — workspace root, open tab paths, active path — to
94+
`UserDefaults` via `SessionStore`. On launch it replays it. Untitled documents are skipped
95+
because they have no path to replay. A vanished root clears the snapshot rather than leaving
96+
a window that restores nothing.
97+
98+
**Quick open.** `QuickOpenIndex` walks the workspace off the main actor in batches and
99+
publishes each batch only if its generation is still current, so a superseded walk is
100+
dropped rather than interleaved. `QuickOpenMatcher` scores candidates per keystroke against
101+
what is already indexed; the palette renders and nothing else. It is a snapshot — see
102+
[CURRENT_STATE.md](CURRENT_STATE.md).
103+
84104
## Caching and identity
85105

86106
`FileSystemManager` keys its directory cache on `url.standardizedFileURL.path`, not `URL`.
@@ -166,8 +186,12 @@ Anything that grows a filesystem dependency should use it.
166186
state disagrees with it. The panel stays the authority on its own visibility.
167187
- `TreeSitterClient.Constants.longParse` — a notification name from the upstream module.
168188
The editor surfaces it as a parse indicator; nothing else depends on it.
169-
- `Notification.Name.codeEditorOpenFolder` / `codeEditorOpenFiles` — `UI` posts, `App`
170-
observes. Replace with a closure when a second consumer appears.
189+
- `Notification.Name.codeEditorOpenFolder` / `codeEditorOpenFiles` — **removed.** The seam
190+
existed because the sidebar used to own an "Open Folder" button and could not call
191+
`AppState` directly. It can now: the sidebar takes an `onCreateIn` closure and the canvas
192+
empty state takes `onOpenFolder`, and the panel itself lives in `AppState`. Both
193+
notifications posted and observed inside `CodeEditorApp`, which is a round trip to
194+
nowhere. Call closures.
171195
- `FileSystemManager.load` returns rows synchronously from a cache while populating it
172196
asynchronously. The view re-renders as children arrive. Fine for a tree; would need
173197
thought for a list that must not reflow.

‎docs/CURRENT_STATE.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ security-scoped bookmarks. See [MEMORY.md](MEMORY.md).
8282

8383
**Untitled documents are not restored.** They have no stable identity across launches.
8484

85+
**Session restore reopens paths, not edits.** The snapshot stores the workspace root, tab
86+
paths and active path — never buffer content. Quit with unsaved changes and the tab comes
87+
back showing what is on disk, with the edits gone and no prompt on the way out. Restoring
88+
buffers would mean writing them somewhere, and there is no untitled-document story yet
89+
either. The honest version of this feature is "reopens what you had open", not "resumes
90+
your session".
91+
8592
**No multi-cursor.** `TextSelectionManager` models multiple selections and `Document` stores
8693
them, but the text view drives a single caret.
8794

‎docs/MEMORY.md‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,21 @@ interop and belongs behind a testable protocol.
156156

157157
Pinned `from: "0.15.2"`, so 0.16 lands automatically. That is intentional.
158158

159-
Exposure is contained to two files:
159+
Exposure is contained to three files, all in `Editor/`:
160160

161-
- `Sources/CodeEditorUI/Sources/Editor/EditorTheme+Bridge.swift` — the only place that
162-
constructs `EditorTheme.Attribute`
163-
- `Sources/CodeEditorUI/Sources/Editor/CodeEditorView.swift` — `SourceEditor` and
164-
`SourceEditorConfiguration`
161+
- `EditorTheme+Bridge.swift` — the only place that constructs `EditorTheme.Attribute`
162+
- `CodeEditorView.swift` — `SourceEditor`, `SourceEditorConfiguration` and its nested
163+
`Appearance` / `Behavior` / `Peripherals`, `InvisibleCharactersConfiguration`,
164+
`BracketPairEmphasis`
165+
- `EditorState.swift` — `IndentOption` only, because indent is an editor preference
165166

166-
Keep it that way. If a third file starts naming upstream types, the boundary has leaked.
167+
The first two are the deliberate boundary. The third is a thin type reference; if you move
168+
`IndentOption` out of the editor's configuration, that import can go too.
169+
170+
**Keep it at three.** If a fourth file starts naming upstream types, the boundary has
171+
leaked. The count was wrong in the docs for a while — they said two while `EditorState.swift`
172+
had been importing it all along — which is the reason to go and count instead of repeating
173+
the claim.
167174

168175
Known break: 0.8.1 → 0.15 changed the API completely (`CodeEditorSourceEditor` with
169176
`cursorPositions:` became `SourceEditor` with `state: SourceEditorState`), and 0.8.1 no
@@ -305,6 +312,12 @@ about a *state*, not about a *range*. Deriving it from the range without checkin
305312
the range is degenerate is how you get a readout that is wrong in the one case everybody
306313
sees first.
307314

315+
The same shape bit the session snapshot, and it is worth checking every "restore" feature
316+
against: it persists *paths*, never buffer content. Quit with unsaved changes and the tab
317+
returns showing what is on disk — the edits are gone and nothing said so on the way out.
318+
A feature named "restore" will be read as "resume"; say which one it is, in the docs and
319+
in the UI.
320+
308321
---
309322

310323
## Quick reference

‎docs/PROCESS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ swift build && swift test
2020
swift-format lint --recursive --strict --configuration .swift-format Sources Tests
2121
```
2222

23-
All three, before every push. 35 tests, about 30 seconds.
23+
All three, before every push. 68 tests in 16 suites, about 30 seconds.
2424

2525
`swift-format format --in-place --recursive --configuration .swift-format Sources Tests`
2626
when lint complains about formatting.

‎docs/STACK.md‎

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
|---|---|---|
77
| Swift | 6.4 (6.4.0.34.1) | `swift-tools-version: 6.4`, Swift 6 language mode |
88
| Xcode | 16+ | `ApproachableConcurrency` enabled per target |
9-
| macOS deployment | 14.0 | `@Observable`, `.onChange(of:initial:)` |
9+
| macOS deployment | 14.0 | `@Observable`, `.onChange(of:initial:)`, `.onKeyPress`, `.formStyle(.grouped)` |
1010
| Build system | SwiftPM | no Xcode project, no workspace |
1111

1212
Developed against macOS 27.0.1 arm64. Nothing in the code is version-specific above 14.0;
@@ -39,9 +39,17 @@ wired, and running two text engines in one app buys nothing.
3939
the `SourceEditor` + `SourceEditorConfiguration` shape this code targets.
4040

4141
Pinning is `from: "0.15.2"`, so 0.16 will be picked up automatically. That is deliberate:
42-
the upstream package is pre-1.0 and moves. The exposure is contained to one file —
43-
`Sources/CodeEditorUI/Sources/Editor/EditorTheme+Bridge.swift` — which is the only place
44-
that touches upstream types beyond `SourceEditor` itself. See [MEMORY.md](MEMORY.md).
42+
the upstream package is pre-1.0 and moves. The exposure is contained to three files under
43+
`Sources/CodeEditorUI/Sources/Editor/`: `CodeEditorView.swift` (`SourceEditor`,
44+
`SourceEditorConfiguration` and its nested `Appearance`/`Behavior`/`Peripherals`,
45+
`InvisibleCharactersConfiguration`, `BracketPairEmphasis`), `EditorTheme+Bridge.swift`
46+
(`EditorTheme` and its `Attribute`), and `EditorState.swift` for `IndentOption` alone. A
47+
fourth file naming upstream types means the boundary has leaked. See
48+
[MEMORY.md](MEMORY.md#8-codeeditsourceeditor-is-pre-10-and-moves).
49+
50+
0.15 also ships the pieces this app had no reason to rebuild: a find-and-replace panel
51+
driven by `SourceEditorState.findPanelVisible`, code-folding ribbon, minimap, invisibles
52+
and warning-character rendering. Finding them meant not designing a ⌘F UI at all.
4553

4654
## Tooling
4755

@@ -69,10 +77,11 @@ compile errors in the original scaffold were exactly this.
6977
**`NavigationSplitView` for the layout.** Resizable sidebar, system-standard behaviour, free
7078
on macOS 14+.
7179

72-
**Notifications between `UI` and `App`.** `FileExplorerView` posts `.codeEditorOpenFolder`;
73-
`CodeEditorApp` observes it. This keeps `NSOpenPanel` out of the view and avoids `UI`
74-
importing `AppKit` panel code. It is a small seam to replace with a closure when a second
75-
consumer appears.
80+
**No notifications between `UI` and `App`.** There used to be: the sidebar posted
81+
`.codeEditorOpenFolder` because it could not call `AppState`. It can — views take closures
82+
(`onOpenFile`, `onCreateIn`, `onOpenFolder`) and the panel logic lives in `AppState`, not in
83+
a view. Both notification names are gone. `NSOpenPanel` still does not appear in the view
84+
layer, because `AppState` owns the commands.
7685

7786
## Alternatives rejected
7887

@@ -82,4 +91,7 @@ consumer appears.
8291
| Write our own text view | NSTextView already does selection, undo, IME, accessibility. Reimplementing is a year. |
8392
| Runestone | Duplicate text engine, no tree-sitter integration. |
8493
| SwiftLSPClient | Abandoned upstream. The LSP layer here is small and worth owning. |
85-
| `develop` branch | One line of work. Doubles merge cost for nothing. See [BRANCHING.md](BRANCHING.md). |
94+
| `develop` branch | One line of work. Doubles merge cost for nothing. See [BRANCHING.md](BRANCHING.md). |
95+
| Hand-rolled find/replace | 0.15 ships one. See above. |
96+
| Hand-rolled folding ribbon, minimap, invisibles | 0.15 ships all three as `Peripherals`. |
97+
| More built-in themes | Each one is a palette someone has to maintain and review, for a choice most users make once. Three is enough. |

0 commit comments

Comments
 (0)