Last verified on 2026-10-10, Swift 6.4.0.34.1, macOS 27.0.1. The feature/lsp-completion
and feature/external-change-detection branches have been merged into one working branch
on main (748e7ef) and are each green on the local gate.
swift build → Build complete (1 pre-existing warning in EditorState.swift)
swift test → 115 tests in 32 suites (114 unit + 1 sourcekit end-to-end, local-only;
2 new `DocumentDiagnosticsTests`); 78 in 18 suites on
`feature/external-change-detection` (73 pre-existing + 5 new)
swift-format lint --strict → 0 warnings
Tagged 0.1.0. The interface has been redesigned since that tag; see Redesigned interface and #14.
Start here: the highest-priority next step is not one of the six issues — it is guarding unsaved work on quit, which is the only remaining path to data loss. See Next.
The window was rebuilt around native macOS chrome. The design tokens live in
CodeEditorThemes, the metrics scale in CodeEditorCore.
| Area | What it does now |
|---|---|
| Theme | Theme carries colorScheme, a Chrome group (accent, sidebar, rows, bars, panels, overlays, borders) and a Semantic group (danger, success, warning). Three palettes: Dark, Light, High Contrast. High Contrast sets usesMaterials = false. |
| Typography | Typography maps roles to semantic styles, so chrome scales with the system text size. .system(size:) is banned from chrome. The canvas stays a fixed-size SF Mono — code reflowing mid-edit is worse than the accessibility it buys. |
| Metrics | One 4pt spacing scale, radii keyed by surface, fixed chrome heights. Enforced by MetricsTests. |
| Toolbar | Unified: breadcrumb principal, create menu and quick-open/terminal toggles trailing. The sidebar's hand-rolled header is gone. |
| Sidebar | Click or arrow to a row to activate: files open, folders expand. The active document is revealed by expanding its ancestors, shown bold with an accent icon. Delete asks first. |
| Tabs | 32pt strip, active tab filled with the canvas colour and rounded so it merges with the editor. Dirty dot beside the title; close button always present. |
| Status bar | Caret, selection count, UTF-8, line ending, language, a live error/warning |
| count from LSP diagnostics (clicking opens a scrollable Problems popover with click-to-jump navigation and line emphasis), and a "Changed on disk" hint while a save conflict is | |
| pending. The byte count is gone. | |
| Empty state | One, in the canvas. With a workspace open it points at the sidebar; without one it offers Open Folder as the single prominent action. |
| Find/replace | ⌘F opens CodeEdit's own panel through SourceEditorState.findPanelVisible. |
| Quick open | ⌘P over a bounded filesystem index. Matched characters are emphasised. |
| Terminal | Drag-resized 120–600pt, persisted. Semantic status dot, icon buttons, selectable output. |
| Session | Workspace root, open tabs and active tab restore on launch. |
| Autocompletion & AI | User can choose between Language Server (LSP) and AI completions in Settings. Supports any OpenAI-compatible API (OpenAI, OpenRouter, Ollama, LM Studio), with dynamic model fetching/search chooser, Copilot-style suggestions with sparkles icon, and Tab insertion. |
File explorer. Recursive tree, lazily loaded — only expanded branches are enumerated. Create file, create folder, rename, delete, reveal in Finder, copy path. Dotfiles hidden. New items land beside the active document, or at the workspace root.
Editing. SourceEditor from CodeEditSourceEditor 0.15, bound two-way to
Document.content. Three defects found in visual review of #14
were fixed and all three are confirmed fixed in the running app: every tab showed the first
file opened (#15 — upstream does not
diff text on update), a selected folder's chevron did nothing
(#16), and the gutter drew through the
translucent tab strip while rubber-band scrolling
(#17). Tree-sitter highlighting across 34 languages, line-number
gutter, bracket-pair emphasis, indentation and reformat-at-column, word wrap, undo stack
shared with the document model. Peripherals are configured explicitly: gutter on, minimap
off by default with a toggle, folding ribbon and invisibles toggleable, smart quotes and
invisible spaces flagged as warnings.
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.
A file that changed on disk since it was opened is detected on save — save() throws
DocumentError.changedOnDisk and the dialog offers Reload from Disk / Save Anyway / Cancel,
with a "Changed on disk" hint in the status bar. #6.
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, word wrap, minimap, folding ribbon and invisibles.
Settings panel. ⌘, opens a sheet (see #4) consolidating theme, font size, tab width and the four view toggles in one Form — no new behaviour, just a discoverable surface for what was already in the Format menu.
Quit guard. Quitting with modified documents prompts first. Save saves everything and abandons the quit if any save did not take — a cancelled panel keeps the work; Don't Save discards; Cancel stays.
Commands. ⌘N, ⌘O, ⇧⌘O, ⌘F, ⌘P, ⌘S, ⌥⌘S, ⌘W, ⌘B, ⌃`, ⇧⌘L, ⌘,. All wired; none are empty closures.
Tests. 115 swift-testing cases (113 unit + 2 new diagnostics, 1 sourcekit end-to-end, local-only).
FileSystemManager and QuickOpenIndex are tested against in-memory providers, so no
non-LSP test touches the real disk; the LSP transport and completion matching are covered by
dedicated suites (see table below).
LSP completion is wired. JSONRPCTransport does real Content-Length framing,
correlates responses by id, ACKs server-initiated requests and fails parked requests on crash
or time-out instead of hanging. LSPClient completes the initialize handshake against
sourcekit-lsp, and LSPTextViewCoordinator drives open/didChange/close plus the fuzzy
completion popup (Tab/Enter to accept, inline error on server crash or a malformed
response). Diagnostics, hover and go-to-definition are #2–#3: diagnostics
from textDocument/publishDiagnostics paint underlines coloured by severity (error/warning/
information/hint) from Theme.semantic, a live error/warning count appears in the status bar,
and ⌘-click resolves a definition via JumpToDefinitionDelegate with server hover
documentation shown in the link popover. Verified for completion by LSPClientEndToEndTests
(local-only; sourcekit-lsp is too heavy to run under CI, see MEMORY.md #1).
Known limits of the LSP wiring:
Positionis UTF‑16 code units. The conversion is correct for ASCII but is not yet exercised on multi-byte runs — see MEMORY.md #21.- Document edits are synced as a full
didChangeon a 100ms debounce, not as ranges:SourceEditorexposes ranges and the coordinator doesn't bridge them yet. - Each
didOpenon aPackage.swiftworkspace triggersswift build --experimental-prepare-for-indexinginside sourcekit, which OOMs underswift test. Use a single-file document. MEMORY.md #20. - Diagnostics underlines are a solid coloured underline, not a wavy squiggle — the upstream
GutterViewandTextViewdon't expose a squiggle attribute set in 0.15. Gutter markers (the red/yellow dots in the scroll ruler) are a follow-up.
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.
The quick-open index is a snapshot, not a stream. It is a filesystem walk refreshed when the workspace root changes. Files created afterwards are not findable until the next reindex. Not FSEvents.
Session restore stores plain paths. Correct only because the app is not sandboxed. Under App Sandbox a path can go stale between launches and this would need security-scoped bookmarks. See MEMORY.md.
Untitled documents are not restored. They have no stable identity across launches.
Session restore reopens paths, not edits. The snapshot stores the workspace root, tab paths and active path — never buffer content. Quit with unsaved changes and the tab comes back showing what is on disk. Restoring buffers would mean writing them somewhere, and there is no untitled-document story yet either. The honest version of this feature is "reopens what you had open", not "resumes your session".
Quitting itself is guarded: a modified document prompts first (#18). Losing the buffer to a force-quit or a crash is still possible — nothing persists unsaved content anywhere.
No multi-cursor. TextSelectionManager models multiple selections and Document stores
them, but the text view drives a single caret.
Single window only. Window replaced WindowGroup; a second window would have shared
one AppState, one document set and one sidebar. Multi-window needs per-window state first.
CI does not build. Format and syntax only. See MEMORY.md.
| Suite | Cases | Covers |
|---|---|---|
| LineEnding | 1 | detection precedence |
| Language | 1 | extension → language |
| TextSelection | 2 | inverted ranges, NSRange |
| Document | 12 | line splitting, offset math, selection clamping, insert/delete, line endings, line ranges, selected lines |
| Document persistence | 2 | save/reload round-trip, noURL |
| Document external change | 5 | mtime guard, baseline resets on save, revert, untitled unguarded |
| Document diagnostics | 2 | setDiagnostics round-trip, range-to-NSRange conversion |
| DocumentManager | 3 | reuse, neighbour focus, dirty tracking |
| TextSelectionManager | 4 | primary selection, joined text |
| FileSystemManager | 10 | root validation, sort order, create, rename, expansion, cache keys, reveal, clearError |
| FileSystemItem | 2 | hidden files, icons |
| Metrics | 5 | spacing grid, radii ordering, chrome heights |
| ThemeManager | 4 | appearance per palette, persistence round-trip, unknown name |
| SessionStore | 4 | round-trip, empty, corrupt data, clear |
| FileOutline | 3 | expansion and depth, collapsed branch, depth cap |
| QuickOpenMatcher | 7 | subsequence, ranking, highlight ranges, folding |
| QuickOpenIndex | 4 | relative paths, skipped directories, ranking, empty query |
| TerminalSession | 4 | history, failed connect, clear |
| saveAllModified | 5 | nothing to do, saved, abandoned, mixed outcomes, only-modified visited |
| JSONRPCTransport | 15 | framing, response/error routing (incl. string-id server requests), crash + time-out, repairMalformedJSON |
| ServerCapabilities | 2 | object-form capabilities, minimal/bool fallback forms |
| CompletionMatching | 8 | fuzzy filter, sortText ranking, CompletionList array/object/extra-field decode |
| DocumentCompletion | 7 | lspPosition/completionPrefix, UTF-16 code units |
| LSPClientEndToEnd | 1 | sourcekit-lsp connect/open/complete; local-only |
Untested: the view layer in CodeEditorUI and CodeEditorApp beyond the pure logic
extracted into CodeEditorCore, and the live LSPTextViewCoordinator ↔ SourceEditor
binding — there is no headless hook into upstream's suggestion window, so completion is
verified by running the app. See MEMORY.md #18.
The redesign (#14, PR #13) changed what is cheap and what is not, so the old ordering no longer holds. Below is what I would pick next and why — this is a recommendation, not a commitment.
The data-loss guard recommended here is done — #18.
Largest and least user-visible until done. vim and top are broken today, which makes it
feel urgent, but it is C interop behind a protocol and nothing else depends on it.
- More themes. Each one is a palette someone maintains and reviews, for a choice most users make once. Three is enough.
- Multi-window. Needs per-window
AppStatefirst. TheWindowscene is the honest single-window shape, not a compromise. - Finder file associations. Needs
CFBundleDocumentTypesinApp-Info.plistand.handlesExternalEvents. Small, but it is packaging rather than capability, and it should ride along with something that needs double-clicking files to work — which is completion via ⌘P, not the other way round.
Scripts/build-app.shneeds running from a clean checkout before it is believed again. See MEMORY.md #16.
feature/terminal-pty and PR #7 exist
as a placeholder branch — plan only, no code.
| Module | Lines | Files |
|---|---|---|
| CodeEditorCore | 1619 | 10 |
| CodeEditorLSP | 1443 | 4 |
| CodeEditorUI | 2424 | 15 |
| CodeEditorApp | 592 | 3 |
| CodeEditorThemes | 439 | 5 |
| CodeEditorTerminal | 245 | 1 |
| Tests | 1723 | 13 |
LSP is the largest module after CodeEditorUI and was the least used — it is now wired in for
completion (#1). That is deliberate for now — it is the interesting part — but with it live,
the transport and the coordinator are exercised by tests and by running the app.