From c5b6d1aa5f676c6e4a296dac804dedfdfa498ecc Mon Sep 17 00:00:00 2001 From: yan Date: Fri, 9 Oct 2026 16:12:24 +0300 Subject: [PATCH] docs: document the project across six focused files plus AGENTS.md Replaces the single PROJECT_PLAN.md with documents that each answer one question, so a reader loads only what they need: - IDEA.md what this is for, what it deliberately is not, principles - CURRENT_STATE what works, what does not, coverage, ordered next steps - STACK.md toolchain and dependency versions with the reasoning - ARCHITECTURE module boundaries, data flow, concurrency, coupling - PROCESS.md local gate, branching, PRs, definition of done, releasing - MEMORY.md constraints that are expensive to rediscover - AGENTS.md instructions for coding agents PROJECT_PLAN.md is now a pointer table so old links resolve, with the roadmap and risk table intact. MEMORY.md records what actually cost time and would again: the four failed CI attempts and why lowering swift-tools-version does not work, why FileSystemManager keys on paths rather than URLs, why Document is ambiguous next to SwiftUI.Document, the ObservableObject/@Observable trap, and which two files are allowed to touch upstream SourceEditor types. AGENTS.md leads with those three documents and states that fixing a stale .md file is part of the change, with a table mapping change type to the file that must be updated. A stale document is worse than a missing one: a missing file announces ignorance, a wrong file gets trusted. --- AGENTS.md | 112 +++++++++++++++++++++++ PROJECT_PLAN.md | 87 ++++++++---------- README.md | 37 ++++++-- docs/ARCHITECTURE.md | 150 ++++++++++++++++++++++++++++++ docs/BRANCHING.md | 9 +- docs/CURRENT_STATE.md | 107 ++++++++++++++++++++++ docs/IDEA.md | 84 +++++++++++++++++ docs/MEMORY.md | 206 ++++++++++++++++++++++++++++++++++++++++++ docs/PROCESS.md | 142 +++++++++++++++++++++++++++++ docs/STACK.md | 85 +++++++++++++++++ 10 files changed, 961 insertions(+), 58 deletions(-) create mode 100644 AGENTS.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/CURRENT_STATE.md create mode 100644 docs/IDEA.md create mode 100644 docs/MEMORY.md create mode 100644 docs/PROCESS.md create mode 100644 docs/STACK.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..185a28e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,112 @@ +# AGENTS.md + +Instructions for coding agents working in this repository. + +## Read this first + +Before changing any code: + +1. **[docs/MEMORY.md](docs/MEMORY.md)** — constraints that are expensive to rediscover. + Entry #1 alone will cost you an hour if you touch `Package.swift` without it. +2. **[docs/CURRENT_STATE.md](docs/CURRENT_STATE.md)** — what works, what does not, what is + next. Do not re-implement something that already works. +3. **[docs/IDEA.md](docs/IDEA.md)** — what this project is for, and what it deliberately is not. + +--- + +## Keeping the documentation true is part of the work + +**The `.md` files in `docs/` describe the current state of the code. If your change makes any +of them wrong, fixing that file is not optional cleanup — it is part of the change.** + +This matters more here than in most projects, because `docs/MEMORY.md` is the only record of +several constraints that cost real time to discover. A stale entry does not just misinform a +reader; it sends the next person — or the next agent — into the same dead end, and they have +no way to tell the file is stale. + +### When to update which file + +| Your change | Update | +|---|---| +| Anything in `Sources/` or `Tests/` that changes what works | **CURRENT_STATE.md** — features, tests table, sizes | +| Worked around a dependency, a toolchain limit, an SDK quirk | **MEMORY.md** — new numbered entry, or amend the relevant one | +| Found a trap you fell into | **MEMORY.md** — what happened, what it cost, what to do | +| Added, removed or upgraded a dependency | **STACK.md** — versions table and the rationale below it | +| Added or removed a module, or changed a data flow | **ARCHITECTURE.md** | +| Changed how work is expected to happen | **PROCESS.md**, **BRANCHING.md**, **COMMITTING.md** | +| Changed what the project is for | **IDEA.md** | +| Changed the public surface, CI, or setup | **README.md** | + +### The rule + +If you had to read something to understand your change, it belonged in a `.md` file and +probably does not exist yet. Write it. + +If you found something that contradicts a `.md` file, fix the file in the same commit. A +stale document is a defect, not a matter of taste — and it is worse than a missing one, +because a missing file announces ignorance while a wrong file is trusted. + +### When the current state is knowingly wrong + +Do not paper over it. Write it down as a limitation with an issue number, the way +`TerminalSession`'s pipes-not-a-PTY is handled: + +``` +## Not working + +**The terminal has no PTY.** Pipes, so no job control and no TTY. +[#5](https://github.com/Yan123-tech/code-editor/issues/5) +``` + +An honest gap is useful. A hidden gap is a trap for whoever inherits this. + +--- + +## Local gate — CI does not build + +**Run all three before every push. CI checks formatting and syntax only.** + +```bash +swift build && swift test +swift-format lint --recursive --strict --configuration .swift-format Sources Tests +``` + +Reason: no hosted runner has SwiftPM 6.4, which `CodeEditSourceEditor` 0.15 needs to +generate its resource bundles. Full details and the failed attempts in +[docs/MEMORY.md](docs/MEMORY.md#1-ci-cannot-build-this-package-do-not-try-to-make-it). + +Never imply in a commit message or PR that CI verified the build. It did not. + +--- + +## Conventions + +- **Conventional Commits**, scopes are modules: `core`, `themes`, `terminal`, `lsp`, `ui`, + `app`, `ci`, `docs`. See [docs/COMMITTING.md](docs/COMMITTING.md). +- **One concern per branch.** `main` is protected: PR required, linear history, no + force-push. See [docs/BRANCHING.md](docs/BRANCHING.md). +- **4-space indent, 120 columns.** Enforced by `.swift-format`. +- **`CodeEditorCore` imports `Foundation` only** — no SwiftUI, no AppKit. That is what makes + it testable without a window server, and what makes offset arithmetic testable at all. + +## Traps that will bite + +- `Document` is ambiguous inside modules that import SwiftUI. Write `CodeEditorCore.Document`. +- Never key a dictionary or set by `URL` where the path is what matters. See + [MEMORY.md #3](docs/MEMORY.md). +- Do not add `ObservableObject` to anything. It is `@Observable` throughout. +- Do not give a `@MainActor` class a `deinit` that touches its state. +- `SourceEditor` takes a `Binding`; there is no push path from `Document` to the + view. See [MEMORY.md #4](docs/MEMORY.md). + +## Before you call it done + +- [ ] `swift build` passes +- [ ] `swift test` passes +- [ ] `swift-format lint --strict` clean +- [ ] Behaviour change has a test; bug fix has a test that fails without the fix +- [ ] New public APIs have doc comments that state the contract +- [ ] The `.md` files above are still true + +That last item is not a formality. It is the difference between this repository and one +where the next hour is wasted. \ No newline at end of file diff --git a/PROJECT_PLAN.md b/PROJECT_PLAN.md index 88475bd..1d86bee 100644 --- a/PROJECT_PLAN.md +++ b/PROJECT_PLAN.md @@ -1,59 +1,48 @@ -# CodeEditor MVP — Project Plan +# Project plan -## Current state +Superseded by the documents in [`docs/`](docs/). This file is kept only so old links resolve. -The MVP is feature-complete and building green. `swift build` and `swift test` (35 tests) -both pass. `PROJECT_PLAN.md` originally described an eight-week roadmap whose Phase 1 goal -was to hand-roll syntax highlighting; that was replaced by -[CodeEditSourceEditor](https://github.com/CodeEditApp/CodeEditSourceEditor), which provides -the text view, tree-sitter highlighting, gutter and undo handling out of the box. - -Shipped: - -- File explorer with lazy recursive tree, create / rename / delete -- Editing via `SourceEditor` with tree-sitter highlighting for ~25 languages -- Document tabs, dirty tracking, save with untitled-location prompt -- Line-ending detection and preservation -- Embedded shell terminal with history and 2000-line scrollback -- Three themes with persisted editor preferences -- Menu commands and keyboard shortcuts -- `CodeEditorLSP` transport with real Content-Length framing (not yet wired to the UI) - -## Next up - -| Issue | Scope | +| Looking for | Read | |---|---| -| LSP completion | Connect `LSPClient` to `SourceEditor` via a `TextViewCoordinator`; fuzzy-filtered popup, Tab/Enter to accept | -| Diagnostics | Consume `textDocument/publishDiagnostics`; underline plus gutter markers | -| Hover and go-to-definition | Tooltips and ⌘-click navigation using `textDocument/hover` and `definition` | -| Settings panel | Persisted preferences UI replacing the current menu-only controls | +| What this project is for | [docs/IDEA.md](docs/IDEA.md) | +| What works and what does not | [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) | +| Versions and dependency rationale | [docs/STACK.md](docs/STACK.md) | +| Module boundaries and data flow | [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | +| How work gets done | [docs/PROCESS.md](docs/PROCESS.md) | +| Constraints worth knowing before touching anything | [docs/MEMORY.md](docs/MEMORY.md) | +| Branch and commit conventions | [docs/BRANCHING.md](docs/BRANCHING.md), [docs/COMMITTING.md](docs/COMMITTING.md) | + +The original eight-week roadmap planned to hand-roll syntax highlighting in Phase 1. +CodeEditSourceEditor provides it, so that work is gone and the remaining plan is six issues +tracked in GitHub, ordered in [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md). + +## Roadmap + +| # | Issue | Scope | +|---|---|---| +| 1 | [LSP completion](https://github.com/Yan123-tech/code-editor/issues/1) | Connect `LSPClient` via a `TextViewCoordinator`; fuzzy-filtered popup, Tab/Enter to accept | +| 2 | [Diagnostics](https://github.com/Yan123-tech/code-editor/issues/2) | Consume `textDocument/publishDiagnostics`; underline plus gutter markers | +| 3 | [Hover and go-to-definition](https://github.com/Yan123-tech/code-editor/issues/3) | Tooltips and ⌘-click navigation | +| 6 | [External change detection](https://github.com/Yan123-tech/code-editor/issues/6) | Reload clean files, prompt on dirty ones | +| 5 | [PTY terminal](https://github.com/Yan123-tech/code-editor/issues/5) | Real pseudo-terminal, ANSI parsing, SIGINT forwarding | +| 4 | [Settings panel](https://github.com/Yan123-tech/code-editor/issues/4) | Replace menu-only preference controls | -## Module boundaries - -``` -CodeEditorCore (no dependencies) -CodeEditorThemes (no dependencies) -CodeEditorTerminal (no dependencies) -CodeEditorLSP (no dependencies) -CodeEditorUI → Core, Themes, Terminal, LSP, CodeEditSourceEditor -CodeEditorApp → UI, Core, Themes, Terminal, LSP -``` +## Quality gates -`CodeEditorCore` must stay UI-free so it remains testable without a window server. +- [x] Builds with no warnings +- [x] `swift test` passes (35 tests) +- [x] `swift-format lint --strict` clean +- [ ] CI builds the package — blocked, see [docs/MEMORY.md](docs/MEMORY.md#1-ci-cannot-build-this-package-do-not-try-to-make-it) +- [ ] Completion works for Swift +- [ ] Diagnostics render +- [ ] No leaks in a 30-minute session ## Risks | Risk | Mitigation | |---|---| -| Source editor API churn (upstream is pre-1.0) | Pinned to `from: 0.15.2`; the `Theme.editorTheme` bridge in `Sources/CodeEditorUI/Sources/Editor/EditorTheme+Bridge.swift` isolates the surface we depend on | -| `sourcekit-lsp` is Xcode-toolchain specific | `LanguageServerRegistry.sourceKitLSPPath` probes known locations and returns `nil` rather than failing to launch | -| URL equality treats `/tmp/x` and `/tmp/x/` as distinct | `FileSystemManager` keys its cache on `standardizedFileURL.path` | -| Terminal uses pipes, not a PTY | Documented in `TerminalSession`; no job control or terminal echo | - -## Quality gates - -- [x] Builds with no warnings -- [x] `swift test` passes -- [ ] Completion works for Swift -- [ ] Diagnostics render -- [ ] No leaks in a 30-minute session \ No newline at end of file +| Source editor API churn (upstream is pre-1.0) | Pinned `from: 0.15.2`; `Theme.editorTheme` in `EditorTheme+Bridge.swift` isolates the surface we touch. [MEMORY.md #8](docs/MEMORY.md) | +| `sourcekit-lsp` is Xcode-toolchain specific | `LanguageServerRegistry.sourceKitLSPPath` probes known paths and returns `nil` rather than failing to launch | +| URL equality treats `/tmp/x` and `/tmp/x/` as distinct | `FileSystemManager` keys its cache on `standardizedFileURL.path`. [MEMORY.md #3](docs/MEMORY.md) | +| Terminal uses pipes, not a PTY | Documented in `TerminalSession`, tracked as issue #5 | +| LSP is the largest module and unused | If issues 1–3 do not land, delete it rather than keep it aspirational. [CURRENT_STATE.md](docs/CURRENT_STATE.md) | \ No newline at end of file diff --git a/README.md b/README.md index b9ecdc1..1e4773a 100644 --- a/README.md +++ b/README.md @@ -100,19 +100,39 @@ leaves that `UI` composes. | `` ⌃` `` | Toggle terminal | | `⇧⌘L` | Toggle dark/light theme | +## Documentation + +| Document | What it covers | +|---|---| +| [docs/IDEA.md](docs/IDEA.md) | What this is for, what it deliberately is not, design principles | +| [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) | What works, what does not, test coverage, next steps | +| [docs/STACK.md](docs/STACK.md) | Toolchain and dependency versions, with the reasoning for each choice | +| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Module boundaries, data flow, concurrency, where coupling lives | +| [docs/PROCESS.md](docs/PROCESS.md) | Local gate, branching, PRs, definition of done, releasing | +| [docs/MEMORY.md](docs/MEMORY.md) | Constraints that are expensive to rediscover | +| [docs/BRANCHING.md](docs/BRANCHING.md) | Branch naming and the PR flow | +| [docs/COMMITTING.md](docs/COMMITTING.md) | Conventional Commits types and scopes | +| [AGENTS.md](AGENTS.md) | Instructions for coding agents working here | + +`docs/MEMORY.md` is the one to read first. It records why CI cannot build this package, why +`FileSystemManager` keys on paths rather than `URL`s, and which source editor API is safe to +depend on — each of which cost real time to work out. + +If a change makes any of these documents wrong, fixing the document is part of the change. +See [AGENTS.md](AGENTS.md). + ## Roadmap `CodeEditorLSP` has a working transport — real `Content-Length` framing, request/response correlating by id, notification routing — and a `sourcekit-lsp` descriptor that resolves -Xcode's bundled binary. It is not connected to the editor yet. Planned, in order: - -1. Completion popup with fuzzy filtering and keyboard navigation -2. Diagnostics overlay with gutter markers -3. Hover tooltips -4. Go-to-definition -5. Settings panel +Xcode's bundled binary. It is not connected to the editor yet. -See [`PROJECT_PLAN.md`](PROJECT_PLAN.md) for the full roadmap. +1. [Completion popup](https://github.com/Yan123-tech/code-editor/issues/1) +2. [Diagnostics overlay](https://github.com/Yan123-tech/code-editor/issues/2) +3. [Hover and go-to-definition](https://github.com/Yan123-tech/code-editor/issues/3) +4. [External change detection](https://github.com/Yan123-tech/code-editor/issues/6) +5. [PTY terminal](https://github.com/Yan123-tech/code-editor/issues/5) +6. [Settings panel](https://github.com/Yan123-tech/code-editor/issues/4) ## Contributing @@ -122,6 +142,7 @@ See [`PROJECT_PLAN.md`](PROJECT_PLAN.md) for the full roadmap. - Branching model and PR flow: [docs/BRANCHING.md](docs/BRANCHING.md) - Commit types and scopes: [docs/COMMITTING.md](docs/COMMITTING.md) +- Full workflow: [docs/PROCESS.md](docs/PROCESS.md) Before pushing, run `swift build && swift test` locally — CI checks formatting and syntax only, for the toolchain reason described above. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..ffcb756 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,150 @@ +# Architecture + +## Modules + +Six targets, dependencies flowing one way. + +``` +CodeEditorCore ┐ +CodeEditorThemes │ +CodeEditorTerminal ├─→ CodeEditorUI ─→ CodeEditorApp +CodeEditorLSP ┘ +``` + +`Core`, `Themes`, `Terminal` and `LSP` have no dependencies on each other. `UI` composes +them. `App` is the only target that owns a window. + +| Module | Lines | Owns | +|---|---|---| +| `CodeEditorCore` | 958 | `Document`, `DocumentManager`, `TextSelectionManager`, `FileSystemManager`, `Language`, `LineEnding`, `TextSelection` | +| `CodeEditorLSP` | 1143 | `JSONRPCTransport`, `LSPClient`, wire types | +| `CodeEditorUI` | 966 | `CodeEditorView`, `FileExplorerView`, `TerminalView`, `EditorState`, `AppState`, `Theme.editorTheme` | +| `CodeEditorApp` | 370 | `MainWindowView`, `CodeEditorApp`, menu commands | +| `CodeEditorTerminal` | 245 | `TerminalSession` | +| `CodeEditorThemes` | 225 | `Theme`, `EditorColor`, `ThemeManager` | + +## The rule that matters + +**`CodeEditorCore` must not import SwiftUI or AppKit.** It is the layer worth testing +without a window server, and it is where the subtle logic lives — offset arithmetic, line +ending conversion, path-keyed caching. If a `NSRange` becomes inconvenient, that is a +signal the model wants a better API, not an `import AppKit`. + +`CodeEditorCore` imports `Foundation` only. Everything else may import freely. + +## Data flow + +The document is the source of truth. The text view does not own content. + +``` +user types + │ + ▼ +SourceEditor ──text binding──→ Document.setContent(_:) + │ │ + │ ├─→ lines[] recomputed + │ ├─→ isModified = true + │ └─→ delegate.documentDidModify + │ + └──cursorPositions──→ Document.setSelection(location:) + │ + ▼ + EditorState.syncSelection + │ + ▼ + status bar reads caretLine/caretColumn +``` + +The reverse — `Document` notifying the view — is deliberately absent. `SourceEditor` takes a +`Binding`, so the text view reads `Document.content` on every render. A push model +would need a re-entrancy guard against the text view writing while SwiftUI is mid-update. + +**Consequence:** `Document` mutations that originate outside the text view (nothing today) +would fight the binding. If you add one, route it through the view, not around it. + +## Caching and identity + +`FileSystemManager` keys its directory cache on `url.standardizedFileURL.path`, not `URL`. + +This is not a micro-optimisation. `URL` equality treats `/tmp/x` and `/tmp/x/` as different +values — same file, different `absoluteString`. Keying on `URL` produced two cache entries +for one directory, so after a rename the sidebar kept showing the old name from the entry +keyed on the pre-rename URL. There is a test for this +(`FileSystemManager.cacheKeysIgnoreTrailingSlash`); do not "simplify" it back to `URL`. + +```swift +childrenByPath[pathKey(for: url)] // stable across trailing slashes and `.` segments +loadingPaths: Set // in-flight guard, same key space +expandedPaths: Set // expansion state, same key space +``` + +All three use the same key type deliberately. + +## Concurrency + +Every observable model is `@MainActor @Observable`. Views observe them; none of them own +threads. + +Two things cross the boundary: + +- **`JSONRPCTransport` is an `actor`.** It owns a `Process` and drains pipes on a background + queue, so reads cannot race. Responses are `Data` that the client decodes. +- **`Task.detached` for file I/O.** `Document.load` and `Document.save` hop off the main + actor and back. Reading a 500 KB file should not stutter the UI. + +`deinit` is the trap. A `@MainActor` class cannot touch its isolated state from `deinit`, +which is why `TerminalSession.disconnect()` exists and callers use it explicitly. + +## LSP layering + +Three layers, each independently testable: + +``` +LSPClient protocol lifecycle, MainActor, observable + │ connect / openDocument / completion / hover / definition + ▼ +JSONRPCTransport actor: framing, id correlation, notification routing + │ does not know what a Position is + ▼ +Process + pipes xcrun sourcekit-lsp +``` + +The transport is the piece worth reading. It accumulates bytes into a buffer and emits only +complete `Content-Length`-framed messages, so a response split across two pipe reads still +parses. It resumes parked continuations by id, and resumes *all* of them with an error when +the server dies — otherwise a crashed language server would hang the editor forever. + +`Types/LSPTypes.swift` holds the wire types and nothing else. It was split out of a +1169-line `LSPClient.swift` that mixed the two. + +`CompletionList` decodes from either a bare array or the wrapper object, because servers +disagree on which they send. `Hover` only decodes `MarkupContent`, not the three-way +`MarkedString | MarkupContent | MarkedString[]` union — sourcekit-lsp does not send the +others, and the union adds decoding branches nothing exercises. + +## Tests + +`CodeEditorCore` and `CodeEditorTerminal` are tested; `CodeEditorUI` and `CodeEditorApp` are +not, because they are view code. + +The useful pattern is `StubProvider`: an in-memory `FileSystemProvider`, so file-system tests +assert on URLs and names and never touch the real disk or depend on a fixture directory. +Anything that grows a filesystem dependency should use it. + +## Where the coupling is + +- `SourceEditor` and `EditorTheme.Attribute` — contained in `CodeEditorView.swift` and + `EditorTheme+Bridge.swift`. Both are upstream, both are pre-1.0. See [STACK.md](STACK.md). +- `Notification.Name.codeEditorOpenFolder` / `codeEditorOpenFiles` — `UI` posts, `App` + observes. Replace with a closure when a second consumer appears. +- `FileSystemManager.load` returns rows synchronously from a cache while populating it + asynchronously. The view re-renders as children arrive. Fine for a tree; would need + thought for a list that must not reflow. + +## Adding a module + +If a new concern does not fit the six: give it no dependencies, put the model in it, and add +it to `UI`'s dependency list. Do not add it to `App` — anything `App` imports directly cannot +be tested without the app target. + +If the concern is UI-only, it belongs in `CodeEditorUI` alongside the views it serves. \ No newline at end of file diff --git a/docs/BRANCHING.md b/docs/BRANCHING.md index b4f4198..979a33a 100644 --- a/docs/BRANCHING.md +++ b/docs/BRANCHING.md @@ -29,13 +29,20 @@ Name branches after the issue where one exists: `feature/lsp-completion` for iss 1. `git checkout main && git pull` 2. `git checkout -b feature/lsp-completion` 3. Work, committing in Conventional Commits form. -4. `swift build && swift test` locally before pushing. +4. `swift build && swift test` and `swift-format lint --strict` locally before pushing. 5. `git push -u origin feature/lsp-completion` 6. Open a PR against `main`. Fill in the template. 7. After review, squash-merge or merge with a merge commit. Squash for a branch with noisy intermediate commits; merge when reviewers want the sequence preserved. 8. Delete the branch. `main` moves on. +**If a PR changes what the code does, update the `.md` files in the same PR.** See +[AGENTS.md](../AGENTS.md) for which file covers which kind of change. A stale +[CURRENT_STATE.md](CURRENT_STATE.md) or [MEMORY.md](MEMORY.md) costs the next person real +time. + +**Never rewrite history on a pushed branch.** Use `git revert`. + ## Why no `develop` branch The project has one active line of work. A `develop` branch means every change has to be diff --git a/docs/CURRENT_STATE.md b/docs/CURRENT_STATE.md new file mode 100644 index 0000000..766ebdb --- /dev/null +++ b/docs/CURRENT_STATE.md @@ -0,0 +1,107 @@ +# Current state + +Last verified against `main` on 2026-10-09, Swift 6.4, macOS 27.0.1. + +``` +swift build → Build complete +swift test → 35 tests in 10 suites passed +swift-format lint --strict → 0 warnings +``` + +Tagged **0.1.0**. + +## Working + +**File explorer.** Recursive tree, lazily loaded — only expanded branches are enumerated. +Create file, create folder, rename, delete, reveal in Finder, copy path. Dotfiles hidden. +Double-click a file to open it; single-click a folder to set the create-target. + +**Editing.** `SourceEditor` from CodeEditSourceEditor 0.15, bound two-way to +`Document.content`. Tree-sitter highlighting across roughly 25 languages, line-number +gutter, bracket-pair emphasis, indentation and reformat-at-column, word wrap, undo stack +shared with the document model. + +**Documents.** Tabs with dirty-state dots. Save, Save All, close with a save prompt. +Untitled documents get a location prompt. Line endings detected on load, preserved on save. +UTF-8 byte and line counts in the status bar. + +**Terminal.** Runs `$SHELL -i` in a subprocess. Line-buffered scrollback capped at 2000 +lines, command history on ↑/↓, stdout and stderr colored separately. + +**Themes.** Dark, Light, High Contrast. Selection persists to `UserDefaults`, along with +font size, tab width and word wrap. + +**Commands.** ⌘N, ⌘O, ⇧⌘O, ⌘S, ⌥⌘S, ⌘W, ⌘B, ⌃`, ⇧⌘L. All wired; none are empty closures. + +**Tests.** 35 swift-testing cases. `FileSystemManager` is tested against an in-memory +provider, so no test touches the real disk. + +## Not working + +**LSP is implemented but not connected.** `JSONRPCTransport` does real `Content-Length` +framing, correlates responses by id, routes notifications, and fails parked requests rather +than hanging when the server dies. `LSPClient` completes the initialize handshake against +`sourcekit-lsp`. Nothing calls either one. Completion, diagnostics, hover and +go-to-definition are [#1](https://github.com/Yan123-tech/code-editor/issues/1)–[#3](https://github.com/Yan123-tech/code-editor/issues/3). + +**The terminal has no PTY.** Pipes, so no job control and no TTY. `vim` renders wrong, +`top` exits immediately, Ctrl-C does not reliably interrupt. [#5](https://github.com/Yan123-tech/code-editor/issues/5). + +**No external-change detection.** If a file changes on disk while open, the editor does not +notice and will overwrite it. [#6](https://github.com/Yan123-tech/code-editor/issues/6). + +**No multi-cursor.** `TextSelectionManager` models multiple selections and `Document` stores +them, but the text view drives a single caret. + +**No find/replace panel.** `SourceEditorState` has the fields for it +(`findText`, `replaceText`, `findPanelVisible`); nothing surfaces them. + +**CI does not build.** Format and syntax only. See [MEMORY.md](MEMORY.md). + +## Test coverage + +| Suite | Cases | Covers | +|---|---|---| +| LineEnding | 1 | detection precedence | +| Language | 1 | extension → language | +| TextSelection | 2 | inverted ranges, NSRange | +| Document | 9 | line splitting, offset math, selection clamping, insert/delete, line endings, line ranges | +| Document persistence | 2 | save/reload round-trip, `noURL` | +| DocumentManager | 3 | reuse, neighbour focus, dirty tracking | +| TextSelectionManager | 4 | primary selection, joined text | +| FileSystemManager | 6 | root validation, sort order, create, rename, expansion, cache keys | +| FileSystemItem | 2 | hidden files, icons | +| TerminalSession | 4 | history, failed connect, clear | + +Untested: `CodeEditorUI` and `CodeEditorApp` entirely (they are view code), the LSP +transport, and `ThemeManager` persistence. + +## Next + +In order, by issue: + +1. [#1](https://github.com/Yan123-tech/code-editor/issues/1) — connect `LSPClient` via a `TextViewCoordinator`; completion popup +2. [#2](https://github.com/Yan123-tech/code-editor/issues/2) — diagnostics overlay and gutter markers +3. [#3](https://github.com/Yan123-tech/code-editor/issues/3) — hover and go-to-definition +4. [#6](https://github.com/Yan123-tech/code-editor/issues/6) — external change detection +5. [#5](https://github.com/Yan123-tech/code-editor/issues/5) — PTY terminal +6. [#4](https://github.com/Yan123-tech/code-editor/issues/4) — settings panel + +`feature/terminal-pty` and [PR #7](https://github.com/Yan123-tech/code-editor/pull/7) exist +as a placeholder branch — plan only, no code. + +## Size + +| Module | Lines | Files | +|---|---|---| +| CodeEditorCore | 958 | 3 | +| CodeEditorLSP | 1143 | 3 | +| CodeEditorUI | 966 | 6 | +| CodeEditorApp | 370 | 2 | +| CodeEditorTerminal | 245 | 1 | +| CodeEditorThemes | 225 | 1 | +| Tests | 498 | 2 | + +LSP is the largest module and the least used. That is deliberate for now — it is the +interesting part — but if the next three issues do not land, it becomes dead weight that +should be deleted rather than kept aspirational. \ No newline at end of file diff --git a/docs/IDEA.md b/docs/IDEA.md new file mode 100644 index 0000000..83ddc98 --- /dev/null +++ b/docs/IDEA.md @@ -0,0 +1,84 @@ +# Idea + +## What this is + +A native macOS code editor: open a folder, browse it, edit files with real syntax +highlighting, save them, and run a shell in the same window. Nothing else. + +## Why it exists + +Two reasons, and it matters that they are separate. + +**A working editor.** There is no good reason this project should not produce a usable +editor on a Mac. The interesting constraint is that everything should be native: SwiftUI +and AppKit, no web view, no Electron, no runtime to install. What you get is a binary that +starts in under a second, uses system text rendering, and behaves like a Mac app rather +than a website in a frame. + +**A testbed for LSP.** Most editor features above syntax highlighting are language-server +features, not editor features. Completion, diagnostics, hover and go-to-definition are the +same LSP requests in every editor; the hard part is the transport and the lifecycle, not the +UI. `CodeEditorLSP` implements that layer from scratch — framing, request correlation, +notification routing, process lifecycle — so it can be read, tested and reasoned about +without an IDE around it. + +## What it is not + +- **Not a replacement for Xcode or Zed.** It has no build system integration, no refactoring, + no multi-cursor editing, no extension system. Those are large projects. +- **Not a general-purpose IDE.** The focus is text editing plus language intelligence. +- **Not trying to out-feature anything.** Each feature should earn its place by being either + cheap and genuinely useful, or a demonstration that the underlying machinery works. + +## Shape of the thing + +``` +┌──────────────────────────────────────────────────┐ +│ ⌘ commands · theme · preferences │ +├────────────┬─────────────────────────────────────┤ +│ │ tabs │ +│ explorer ├─────────────────────────────────────┤ +│ (tree) │ │ +│ │ editor + tree-sitter highlighting │ +│ │ gutter · bracket emphasis · undo │ +│ ├─────────────────────────────────────┤ +│ │ status bar: Ln/Col, encoding │ +├────────────┴─────────────────────────────────────┤ +│ terminal (toggleable) │ +└──────────────────────────────────────────────────┘ +``` + +Three panels, one window, no chrome that is not doing work. The sidebar is resizable, the +terminal hides entirely, and the editor takes whatever is left. + +## Principles + +**Native or nothing.** If a capability requires a web view or a subprocess bridge to do, it +does not belong here. + +**The document model is the truth.** `CodeEditorCore.Document` owns content, line endings, +selection and dirty state. The text view binds to it; it does not shadow it. Anything that +duplicates editor state is a bug waiting to happen. + +**Write the boring layer properly.** LSP framing and file-system caching have no UI to hide +behind. They are where correctness actually gets tested. + +**Say what does not work.** A documented limitation is worth more than a silent gap. The +terminal uses pipes rather than a PTY, and [issue #5](https://github.com/Yan123-tech/code-editor/issues/5) +exists because of it. + +## Where it is going + +Near term, connect the LSP client that already works: completion, diagnostics, hover, +go-to-definition. That is issues [#1](https://github.com/Yan123-tech/code-editor/issues/1)–[#3](https://github.com/Yan123-tech/code-editor/issues/3). + +Longer term it is unclear whether this grows into a full editor or stays a focused tool. That +is a decision to make when there is more evidence about what is missing, not now. + +## Related documents + +- [CURRENT_STATE.md](CURRENT_STATE.md) — what works, what does not, what is next +- [STACK.md](STACK.md) — versions and dependency rationale +- [ARCHITECTURE.md](ARCHITECTURE.md) — module boundaries and data flow +- [PROCESS.md](PROCESS.md) — how work gets done here +- [MEMORY.md](MEMORY.md) — constraints that are easy to forget and expensive to rediscover \ No newline at end of file diff --git a/docs/MEMORY.md b/docs/MEMORY.md new file mode 100644 index 0000000..4f47323 --- /dev/null +++ b/docs/MEMORY.md @@ -0,0 +1,206 @@ +# Memory + +Constraints and facts that are expensive to rediscover. Each entry says what happened, what +it cost, and what to do about it. + +**This file is the reason the other documents exist.** If a change makes something here +wrong, fix the file in the same PR. + +--- + +## 1. CI cannot build this package. Do not try to make it. + +**The constraint.** `Package.swift` declares `swift-tools-version: 6.4`. No hosted GitHub +runner carries SwiftPM 6.4. + +**What was tried, all failing:** + +| Attempt | Runner | Toolchain | Result | +|---|---|---|---| +| `xcode-select -s /Applications/Xcode_16.app` | macos-latest | — | `invalid developer directory` — path absent from the image | +| default toolchain | macos-latest | 6.3.3 | manifest 6.4 rejected | +| `setup-xcode` `latest-stable` | macos-15 | 6.2.4 | older still | +| Xcode 26 explicit | macos-26 | 6.3.3 | still short of 6.4 | + +**Why lowering the manifest does not work.** Setting `swift-tools-version: 6.3` gets past +SwiftPM, then fails inside a dependency: + +``` +CodeEditSymbols/CodeEditSymbols.swift:14:42: error: type 'Bundle' has no member 'module' +``` + +`CodeEditSymbols` 0.2.3 has `Sources/CodeEditSymbols/Symbols.xcassets` but its own +`Package.swift` declares **no** `resources:`. SwiftPM 6.4 infers the bundle and synthesises +`Bundle.module`; 6.3 does not. Every version of that package, 0.1.1 through 0.2.3, has the +same omission — there is no version to pin to. + +**Why downgrading the editor does not work.** `CodeEditSourceEditor` gained the +`CodeEditSymbols` dependency at 0.13.0, so 0.12.x avoids it. But 0.12.x requires a +CodeEditTextView 0.10 API that no longer exists: + +``` +MinimapLineFragmentView.swift:46: error: method does not override any method from its superclass +MinimapLineRenderer.swift:32: error: incorrect argument label in call +``` + +The 0.11.x line fails the same way on a different API change. There is no working +combination on an old toolchain. + +**Current state.** CI runs two jobs that work everywhere: + +- **Format** — `swift-format lint --strict`, then `format --in-place` and fail if + `git diff` is non-empty. `swift-format` ships in the Xcode toolchain, so it is present + regardless of Swift version. +- **Syntax** — `swiftc -parse` on every file. Parsing does not resolve the dependency graph, + so it needs no SwiftPM. + +Branch protection has **no** required status check, because there is nothing that can pass. + +**The cost of this is real.** CI does not catch type errors or failing tests. Every +contributor must run the local gate. Say so in PRs rather than implying CI covered it. + +**What unblocks it.** A runner with Xcode 26.4. Then add a `build` job and re-add +`build-and-test` as a required check in one commit. + +--- + +## 2. `swift-format` needs the config or it reports nothing useful + +`.swift-format` sets 4-space indentation, 120 columns. + +Without it, `swift-format` defaults to 2 spaces and reports **3301** `Indentation` warnings +across 18 files. The real warnings — `OrderedImports`, `DoNotUseSemicolons`, `TrailingComma` +— are invisible in that noise. + +With the config: 75 warnings, all real, all fixable. Zero after applying. + +Applying the formatter also surfaced two genuine defects the original code had, which +suggests it is worth running before review, not only in CI: + +- Semicolon-separated statements in the sidebar context menu +- Unordered import blocks + +--- + +## 3. `URL` equality is not path equality + +`URL(fileURLWithPath: "/tmp/x") != URL(fileURLWithPath: "/tmp/x/")`. Same file, different +`absoluteString`. + +`FileSystemManager` keys its cache on `url.standardizedFileURL.path` for this reason. Keyed +on `URL`, a rename left the stale entry under the pre-rename key and the sidebar kept +displaying the old filename — the tree appeared to ignore the rename entirely. + +Test: `FileSystemManager.cacheKeysIgnoreTrailingSlash`. **Do not simplify it back to `URL`.** + +Related trap: `deletingLastPathComponent()` returns a URL *with* a trailing slash. So +`root.appendingPathComponent("x").deletingLastPathComponent() != root` even though the paths +are identical. + +--- + +## 4. The document is the source of truth; the text view binds to it + +`SourceEditor` takes a `Binding`. `CodeEditorView` supplies +`Binding(get: { document.content }, set: { document.setContent($0) })`. + +There is no push path from `Document` to the text view. That is deliberate: a push model +would need a re-entrancy guard against the text view writing while SwiftUI is mid-update. + +**Consequence:** a `Document` mutation originating outside the text view will fight the +binding. Route it through the view. If you add one, you will need to think about this. + +`Document` mutations all funnel through one private `apply(_:)`, which recomputes `lines`, +sets `isModified` and notifies the delegate in one place. Keep it that way — the original had +four methods that each did a subset, and the line cache drifted from the content. + +--- + +## 5. `@Observable` and `ObservableObject` are mutually exclusive here + +The original `TerminalSession` declared both. That stops `@StateObject` from working — the +type is no longer `ObservableObject`, so `@StateObject` will not accept it, and the error +points at the property wrapper rather than the conformance. + +Everything is `@Observable` now. Views hold models via `@State` or `@Bindable`. + +Same class of trap: a `@MainActor` class cannot touch its isolated state from `deinit`. +`TerminalSession` has no `deinit`; callers use `disconnect()`. + +--- + +## 6. `Document` is ambiguous with `SwiftUI.Document` + +Inside `CodeEditorUI` and `CodeEditorApp`, which import SwiftUI, the bare name `Document` +resolves ambiguously. Use `CodeEditorCore.Document`. + +This appears in type positions: parameters, stored properties, `ForEach` element types. +It does *not* shadow value-level uses like `document.content`. + +--- + +## 7. The terminal is pipes, not a PTY + +`TerminalSession` spawns `$SHELL -i` with three `Pipe()`s. No job control, no TTY. + +Consequences, all expected: `vim` renders wrong, `top` exits immediately, Ctrl-C writes +`0x03` into a pipe the shell does not read as a signal. + +This is documented in the type's doc comment and tracked as +[#5](https://github.com/Yan123-tech/code-editor/issues/5). Not an oversight — a PTY layer is C +interop and belongs behind a testable protocol. + +--- + +## 8. CodeEditSourceEditor is pre-1.0 and moves + +Pinned `from: "0.15.2"`, so 0.16 lands automatically. That is intentional. + +Exposure is contained to two files: + +- `Sources/CodeEditorUI/Sources/Editor/EditorTheme+Bridge.swift` — the only place that + constructs `EditorTheme.Attribute` +- `Sources/CodeEditorUI/Sources/Editor/CodeEditorView.swift` — `SourceEditor` and + `SourceEditorConfiguration` + +Keep it that way. If a third file starts naming upstream types, the boundary has leaked. + +Known break: 0.8.1 → 0.15 changed the API completely (`CodeEditorSourceEditor` with +`cursorPositions:` became `SourceEditor` with `state: SourceEditorState`), and 0.8.1 no +longer compiles against CodeEditTextView 0.12 at all. + +--- + +## 9. Xcode-build-system leftovers in `.build` + +`.build/out/` holds artifacts from an Xcode-driven build, and `swift build` will use +`-D Xcode`. This is stale state from before the package was restructured. If a build behaves +inexplicably, `rm -rf .build` before debugging anything else. + +`.build/` is gitignored. `/build` is also ignored because Xcode writes there. + +--- + +## 10. History was rebuilt once + +The first published history contained commits that captured stale file contents, caused by +a `git reset --hard` mid-task. The published `main` was reconstructed from scratch and +verified green, so what is on the remote is correct — but part of that work was redone. + +The lesson is procedural and it is in [BRANCHING.md](BRANCHING.md): work on feature branches +and merge, rather than rewriting history in place. It is much harder to lose a day's work +that way. + +--- + +## Quick reference + +| Question | Answer | +|---|---| +| Why doesn't CI build? | No runner has SwiftPM 6.4. #1 above. | +| What must I run before pushing? | `swift build && swift test`, plus `swift-format lint --strict`. | +| Can I change `Package.swift`? | Only if you have a Swift 6.3+ toolchain to verify against. See #1. | +| Why is the sidebar slow to show subfolders? | `load` is async and re-renders as children arrive. By design. | +| Why does `Document` not resolve? | `SwiftUI.Document`. Qualify as `CodeEditorCore.Document`. | +| Can I bump CodeEditSourceEditor? | Yes; watch the two files listed in #8. | +| Where do I write "this does not work"? | Here, and [CURRENT_STATE.md](CURRENT_STATE.md). | \ No newline at end of file diff --git a/docs/PROCESS.md b/docs/PROCESS.md new file mode 100644 index 0000000..8001194 --- /dev/null +++ b/docs/PROCESS.md @@ -0,0 +1,142 @@ +# Process + +How work gets done in this repository. + +## Before you start + +Read [MEMORY.md](MEMORY.md). It holds the constraints that are expensive to rediscover — +the Swift 6.4 toolchain problem in particular will cost you an hour if you do not know it +before you touch `Package.swift`. + +Then read [CURRENT_STATE.md](CURRENT_STATE.md) so you know what already works and do not +re-implement it. + +## Local gate + +This is the gate. CI does not build — see [MEMORY.md](MEMORY.md). + +```bash +swift build && swift test +swift-format lint --recursive --strict --configuration .swift-format Sources Tests +``` + +All three, before every push. 35 tests, about 30 seconds. + +`swift-format format --in-place --recursive --configuration .swift-format Sources Tests` +when lint complains about formatting. + +## Branching + +`main` is the only long-lived branch. Full model in [BRANCHING.md](BRANCHING.md). + +``` +feature/ new capability +fix/ bug fix +refactor/ behaviour-preserving +chore/ build, CI, dependencies +docs/ documentation +test/ tests +``` + +One concern per branch. Name it after the issue when there is one: +`feature/lsp-completion` for #1. + +```bash +git checkout main && git pull +git checkout -b feature/lsp-completion +# work, committing +git push -u origin feature/lsp-completion +gh pr create --fill +``` + +## Commits + +[Conventional Commits](https://www.conventionalcommits.org/). Full guide in +[COMMITTING.md](COMMITTING.md). + +``` +(): +``` + +Scopes are modules: `core`, `themes`, `terminal`, `lsp`, `ui`, `app`, `ci`, `docs`. + +The body explains **why**, not what. The diff already says what. Write one when the +reasoning is not obvious, or when the obvious approach was wrong and you want the next +reader not to repeat it. + +``` +fix(core): key the directory cache on standardized paths + +URL equality treats /tmp/x and /tmp/x/ as different values, so a rename +left a stale entry under the old key and the sidebar kept showing the old +name. There is a test for this; do not simplify it back to URL. +``` + +## Pull requests + +Fill in the template. Reviewer needs: what changed, why, how it was verified. + +- One concern. Split unrelated changes. +- Say what you did *not* verify. +- Link the issue with `Refs #1`, not `Fixes #1` unless the PR actually closes it. + +`main` requires a PR and linear history. Force-push is blocked. + +## Definition of done + +- [ ] `swift build` passes +- [ ] `swift test` passes +- [ ] `swift-format lint --strict` clean +- [ ] New public APIs have doc comments explaining the contract, not restating the name +- [ ] Behaviour change has a test; bug fix has a test that fails without the fix +- [ ] Limitations documented in the PR body or [CURRENT_STATE.md](CURRENT_STATE.md) + +## Issues + +Every non-trivial piece of work has an issue before it has a branch. The body carries +acceptance criteria as checkboxes — that is the definition of done, written before the work +starts. + +Good issue bodies here name modules touched and call out the trap. See +[#1](https://github.com/Yan123-tech/code-editor/issues/1) for the shape. + +## Releasing + +```bash +# local gate first +swift build && swift test + +git tag -a 0.2.0 -m "0.2.0 — " +git push origin main --tags +``` + +The tag message states what is in the release and what is *not* — the 0.1.0 tag lists the +six open issues by number, because someone reading the tag six months later needs to know +what the release does not do. + +There is no automation here yet. A release workflow that runs the build on a runner with the +right toolchain is worth adding; it is blocked on the same toolchain problem as CI. + +## When something is broken + +1. Reproduce it. A test that fails for a reason you cannot state is not a test yet. +2. Write the failing test first if the fix is not obvious. +3. Fix it on a `fix/` branch. Reference the issue. +4. If it needs a workaround, put it in the source next to a comment saying what breaks if it + can be removed. + +Never rewrite history on a pushed branch. `git revert` and explain why in the body. + +## When you get stuck + +The two things that have consumed the most time in this project: + +1. **A dependency that will not build.** Check the dependency's own `Package.swift` for the + versions it expects, and check whether the failure is a toolchain difference rather than + your code. See [MEMORY.md](MEMORY.md). +2. **A model that shadows editor state.** If a view keeps its own copy of something + `Document` also holds, one of them is stale. Find which one is authoritative before + patching the symptom. + +Both cost time because the symptom was far from the cause. Log it in [MEMORY.md](MEMORY.md) +when it happens again. \ No newline at end of file diff --git a/docs/STACK.md b/docs/STACK.md new file mode 100644 index 0000000..46ca133 --- /dev/null +++ b/docs/STACK.md @@ -0,0 +1,85 @@ +# Stack + +## Toolchain + +| Component | Version | Note | +|---|---|---| +| Swift | 6.4 (6.4.0.34.1) | `swift-tools-version: 6.4`, Swift 6 language mode | +| Xcode | 16+ | `ApproachableConcurrency` enabled per target | +| macOS deployment | 14.0 | `@Observable`, `.onChange(of:initial:)` | +| Build system | SwiftPM | no Xcode project, no workspace | + +Developed against macOS 27.0.1 arm64. Nothing in the code is version-specific above 14.0; +the 27 SDK is just what is installed. + +## Runtime + +| Library | Version | Why this one | +|---|---|---| +| [CodeEditSourceEditor](https://github.com/CodeEditApp/CodeEditSourceEditor) | 0.15.2 | The editor. `NSTextView`-backed, tree-sitter highlighting, gutter, bracket emphasis, undo. Built for CodeEdit, so its API is the one that fits a native editor most closely. | +| [CodeEditLanguages](https://github.com/CodeEditApp/CodeEditLanguages.git) | 0.1.20 | Language definitions and tree-sitter queries for ~25 languages. A direct dependency because the UI target needs `CodeLanguage` for editor language resolution. | +| [CodeEditTextView](https://github.com/CodeEditApp/CodeEditTextView.git) | 0.12.1 | Text view primitives. Direct dependency because the UI target needs `CEUndoManager` to share one undo stack between the text view and `Document`. | +| tree-sitter (via SwiftTreeSitter 0.25.0) | transitive | Incremental parsing for highlighting | + +Also pulled transitively: `CodeEditSymbols` 0.2.3, `TextFormation` 0.9.0, `TextStory` 0.9.2, +`Rearrange` 2.1.1, `SwiftLintPlugin` 0.65.0. + +### On Runestone and the original dependencies + +The first manifest pulled in Runestone and SwiftTreeSitter directly, and CodeEditSourceEditor +at `from: "0.1.0"`. Both direct dependencies were unused and produced +`dependency 'runestone' is not used by any target` warnings. Runestone was dropped in favour +of CodeEditTextView: it has the tree-sitter integration and the text formations already +wired, and running two text engines in one app buys nothing. + +### Why SourceEditor 0.15 specifically + +0.8.1 does not compile against CodeEditTextView 0.12 — it calls +`TextLayoutManager.beginTransaction`, removed in 0.9. 0.15 is the current release and has +the `SourceEditor` + `SourceEditorConfiguration` shape this code targets. + +Pinning is `from: "0.15.2"`, so 0.16 will be picked up automatically. That is deliberate: +the upstream package is pre-1.0 and moves. The exposure is contained to one file — +`Sources/CodeEditorUI/Sources/Editor/EditorTheme+Bridge.swift` — which is the only place +that touches upstream types beyond `SourceEditor` itself. See [MEMORY.md](MEMORY.md). + +## Tooling + +| Tool | Source | Used for | +|---|---|---| +| `swift-format` | ships in the Xcode toolchain | formatting and lint | +| swift-testing | ships in the toolchain | the test suite | +| GitHub Actions | `.github/workflows/ci.yml` | format + syntax checks | + +`.swift-format` sets 4-space indentation and a 120-column limit to match the existing +sources. Without it swift-format defaults to 2 spaces and reports 3300+ `Indentation` +warnings, which buries the warnings that matter. + +## Decisions + +**`@Observable`, not `ObservableObject`.** The original code declared both on the same class, +which stops `@StateObject` from working. Observation, not Combine. + +**`ApproachableConcurrency` per target.** Defaults the code assumes — `nonisolated(nonsending)` +parameters, non-isolated deinit — so concurrency is not sprinkled through every signature. + +**Swift 6 language mode, strict concurrency.** Errors rather than warnings. Most of the +compile errors in the original scaffold were exactly this. + +**`NavigationSplitView` for the layout.** Resizable sidebar, system-standard behaviour, free +on macOS 14+. + +**Notifications between `UI` and `App`.** `FileExplorerView` posts `.codeEditorOpenFolder`; +`CodeEditorApp` observes it. This keeps `NSOpenPanel` out of the view and avoids `UI` +importing `AppKit` panel code. It is a small seam to replace with a closure when a second +consumer appears. + +## Alternatives rejected + +| Considered | Why not | +|---|---| +| Web view (Electron/Tauri) | Not native. Defeats the point. | +| Write our own text view | NSTextView already does selection, undo, IME, accessibility. Reimplementing is a year. | +| Runestone | Duplicate text engine, no tree-sitter integration. | +| SwiftLSPClient | Abandoned upstream. The LSP layer here is small and worth owning. | +| `develop` branch | One line of work. Doubles merge cost for nothing. See [BRANCHING.md](BRANCHING.md). | \ No newline at end of file