Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
52df0c5
chore(app): script the .app bundle build and the app icon
Oct 9, 2026
5db6ef3
chore(app): merge the .app bundle tooling
Oct 9, 2026
bb7f278
fix(themes): derive appearance from the palette, not channel arithmetic
Oct 9, 2026
1f50869
feat(themes): add chrome and semantic token groups
Oct 9, 2026
7622593
feat(core): one spacing grid, one radius scale, one set of chrome hei…
Oct 9, 2026
6564838
feat(themes): a semantic type ramp for the chrome
Oct 9, 2026
b8bf354
feat(core): reveal a file's ancestors and clear the manager error
Oct 9, 2026
f1e7dca
feat(ui): rewrite the sidebar as a native navigator
Oct 9, 2026
13891da
feat(ui): unified toolbar, merged tabs, honest status bar, one empty …
Oct 9, 2026
4005612
feat(core): file outline, fuzzy matcher and quick-open index
Oct 9, 2026
fd2dbc9
feat(ui): find, quick open, a resizable panel and session restore
Oct 9, 2026
53949d1
fix(ui): click a folder to expand it, arrow past it without toggling
Oct 9, 2026
963b208
fix(ui): drive tree activation from selection, not a row tap gesture
Oct 9, 2026
7ac3f76
docs: record the redesigned interface and its invariants
Oct 9, 2026
a09d909
fix(app): make build-app.sh actually build; stop claiming a bare care…
Oct 9, 2026
eea140c
docs: record the build-script and selection-summary traps
Oct 9, 2026
4cd926c
docs: correct concepts the redesign made false
Oct 9, 2026
1cb974a
docs: record the redesign issue and a recommendation for what comes next
Oct 9, 2026
dcf4acf
fix(ui): three defects found in visual review
Oct 9, 2026
e9939ea
docs: record the three review findings as issues and memory
Oct 10, 2026
85038e5
docs: the editor binding is now a hand-check in the definition of done
Oct 10, 2026
528f813
feat(app): guard unsaved work on quit
Oct 10, 2026
1593f8b
docs: the quit guard is shipped, and it is not the end of data loss
Oct 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ Never imply in a commit message or PR that CI verified the build. It did not.
- **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.
- **Chrome never borrows a syntax token.** Colours come from `Theme.chrome` or
`Theme.semantic`. The accent was once `Theme.keyword`, which made the tab underline, folder
icons, the terminal prompt and terminal errors change meaning with the syntax palette.
- **No magic numbers in chrome.** Spacing, radii and fixed heights come from `Metrics`
(`CodeEditorCore`); the 4pt grid is enforced by a test. Chrome type comes from
`Typography` — semantic styles only, so it scales with the user's text size. Code keeps a
fixed-size SF Mono. See [MEMORY.md](docs/MEMORY.md#11-never-derive-appearance-from-colour-arithmetic)
for why these rules exist.

## Traps that will bite

Expand All @@ -98,6 +106,11 @@ Never imply in a commit message or PR that CI verified the build. It did not.
- Do not give a `@MainActor` class a `deinit` that touches its state.
- `SourceEditor` takes a `Binding<String>`; there is no push path from `Document` to the
view. See [MEMORY.md #4](docs/MEMORY.md).
- A `.onTapGesture` on a row inside a `List(selection:)` never fires. Drive row behaviour
from the selection change. See [MEMORY.md #12](docs/MEMORY.md#12-a-tap-gesture-inside-a-selection-list-is-swallowed).
- Do not put a `.frame` on a `NavigationSplitView` column; `.navigationSplitViewColumnWidth`
already sizes it. See
[MEMORY.md #13](docs/MEMORY.md#13-navigationsplitview-columns-are-sized-in-one-place).

## Before you call it done

Expand All @@ -106,6 +119,7 @@ Never imply in a commit message or PR that CI verified the build. It did not.
- [ ] `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
- [ ] View code with real logic moved into `CodeEditorCore` and tested there
- [ ] The `.md` files above are still true

That last item is not a formality. It is the difference between this repository and one
Expand Down
13 changes: 9 additions & 4 deletions PROJECT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,29 @@ tracked in GitHub, ordered in [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md).
| 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 |
| 4 | [Settings panel](https://github.com/Yan123-tech/code-editor/issues/4) | Move persisted preferences out of the Format menu into a discoverable surface |
| 14 | [Interface redesign](https://github.com/Yan123-tech/code-editor/issues/14) | Landed: native macOS chrome and a real design system |

## Quality gates

- [x] Builds with no warnings
- [x] `swift test` passes (35 tests)
- [x] `swift test` passes (68 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
- [x] Unsaved work is guarded on quit — [#18](https://github.com/Yan123-tech/code-editor/issues/18). Force-quit and crashes still lose buffers
- [x] The redesign has been reviewed by someone who can see it — it found #15–#17, all since fixed

## Risks

| Risk | Mitigation |
|---|---|
| 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) |
| Source editor API churn (upstream is pre-1.0) | Pinned `from: 0.15.2`; three files under `Editor/` name upstream types and no more. [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) |
| 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) |
| A build script that passes while doing nothing | `Scripts/build-app.sh` shipped with `--show-bin-path` on its build commands and only ever failed from a clean checkout. Run it from clean. [MEMORY.md #16](docs/MEMORY.md#16-scriptsbuild-appsh-only-fails-from-a-clean-checkout) |
| A document that outlives its code | The redesign broke three claims in `docs/`, one of them describing a notification seam that no longer had a poster. Auditing the `.md` files against the code is part of the work, not after it. [AGENTS.md](AGENTS.md) |
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ let package = Package(
// MARK: - Tests
.testTarget(
name: "CodeEditorTests",
dependencies: ["CodeEditorCore", "CodeEditorTerminal", "CodeEditorLSP"],
dependencies: ["CodeEditorCore", "CodeEditorTerminal", "CodeEditorLSP", "CodeEditorThemes"],
path: "Tests/CodeEditorTests",
swiftSettings: [.enableUpcomingFeature("ApproachableConcurrency")]
)
Expand Down
60 changes: 53 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,31 @@ an embedded shell in the same window.
- **Tabs** — open documents, dirty-state dots, close with a save prompt.
- **Save / Save All**, with a location prompt for untitled documents. Line endings are
detected on load and preserved on save.
- **Find and replace** — ⌘F, backed by the source editor's own panel.
- **Quick open** — ⌘P, fuzzy-matching files across the workspace with the matched
characters highlighted.
- **Embedded terminal** — runs your `$SHELL`, with scrollback capped at 2000 lines,
command history on ↑/↓, and separate stdout/stderr coloring.
command history on ↑/↓, selectable output, and separate stdout/stderr coloring. Drag
the divider above it to resize; the height is remembered.
- **Themes** — Dark, Light and High Contrast, persisted across launches along with font
size, tab width and word wrap.
- **Menu commands** — ⌘N, ⌘O, ⇧⌘O, ⌘S, ⌥⌘S, ⌘W, ⌘B, ⌃`, ⇧⌘L for theme.
size, tab width, word wrap, minimap, folding ribbon and invisibles.
- **Session restore** — reopens your workspace and tabs on launch. It restores *what you
had open*, not your edits: unsaved buffers are not persisted, and untitled documents
have no path to replay.
- **Quit guard** — quitting with unsaved changes asks first. Choosing Save abandons the
quit if anything failed to save, so a cancelled dialog never costs you the work.
- **Menu commands** — ⌘N, ⌘O, ⇧⌘O, ⌘F, ⌘P, ⌘S, ⌥⌘S, ⌘W, ⌘B, ⌃`, ⇧⌘L for theme.

### Interface notes

The window uses native macOS chrome: a unified toolbar with a breadcrumb, a navigator that
opens files and expands folders on click or arrow key, a tab strip whose active tab merges
into the editor, and a single empty state that adapts to whether a workspace is open.

Design tokens are not sprinkled through the views. Colours live in `Theme.chrome` and
`Theme.semantic` — chrome never borrows a syntax token. Spacing, radii and chrome heights
live in `Metrics` in `CodeEditorCore`, on a 4pt grid. Chrome type is semantic, so it scales
with your system text size; the code font stays fixed-size on purpose.

## Requirements

Expand All @@ -47,6 +67,30 @@ Run the tests with:
swift test
```

### Build the .app bundle

`swift run` gives you the binary, not an application — no Dock tile, no menu bar, no
double-clickable file. To get `build/Code Editor.app`, run:

```bash
Scripts/build-app.sh
```

The script builds in release, lays out `Contents/{MacOS,Resources}`, copies every SwiftPM
resource bundle into `Contents/Resources` (CodeEditLanguages grammars and CodeEditSymbols
assets are resolved through `Bundle.module`, which looks them up under `Bundle.main.resourceURL`),
renders `AppIcon.icns` from `Scripts/make-icon.swift`, and ad-hoc signs the bundle.

| Flag | Effect |
|---|---|
| `--debug` | Build the debug product instead of release |
| `--install` | Also copy the bundle to `/Applications` |
| `--open` | Launch it when the build finishes |

Ad-hoc signing is enough locally and for handing the bundle to another Mac. To distribute it
further, swap the `codesign` identity in the script for a Developer ID and notarize the zipped
bundle.

Format and lint with `swift-format`, which ships inside the Xcode toolchain:

```bash
Expand Down Expand Up @@ -77,12 +121,12 @@ add a `build` job and drop the caveat.

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

Dependencies flow one way: `Core` ← `UI` ← `App`. `Themes`, `Terminal` and `LSP` are
leaves that `UI` composes.
Expand All @@ -94,6 +138,8 @@ leaves that `UI` composes.
| `⌘N` | New file |
| `⌘O` | Open folder |
| `⇧⌘O` | Open files |
| `⌘F` | Find |
| `⌘P` | Quick open |
| `⌘S` / `⌥⌘S` | Save / Save all |
| `⌘W` | Close tab |
| `⌘B` | Toggle sidebar |
Expand Down
38 changes: 38 additions & 0 deletions Resources/App-Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>CodeEditorApp</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>com.yan123tech.codeeditor</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Code Editor</string>
<key>CFBundleDisplayName</key>
<string>Code Editor</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSSupportsAutomaticTermination</key>
<true/>
<key>NSSupportsSuddenTermination</key>
<true/>
</dict>
</plist>
98 changes: 98 additions & 0 deletions Scripts/build-app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/bin/bash
#
# Assembles build/Code Editor.app from the SwiftPM release binary.
#
# Scripts/build-app.sh [--debug] [--install] [--open]
#
# SwiftPM produces a bare Mach-O executable. macOS needs the bundle layout —
# Info.plist plus Contents/MacOS — before it will give the process a Dock tile,
# a menu bar and a document-based open-panel identity. This script builds the
# release binary, lays out the bundle, copies in every SwiftPM resource bundle
# (CodeEditLanguages grammars, CodeEditSymbols assets) and ad-hoc signs the
# result so it launches without a Gatekeeper prompt.
#
# Ad-hoc signing is enough for local use and for handing the bundle to another
# Mac. For distribution beyond the machine that built it, replace the signing
# identity with a Developer ID and notarize the zipped bundle.
set -euo pipefail

CONFIGURATION="release"
INSTALL=0
LAUNCH=0

for argument in "$@"; do
case "$argument" in
--debug) CONFIGURATION="debug" ;;
--install) INSTALL=1 ;;
--open) LAUNCH=1 ;;
*) echo "unknown option: $argument" >&2; exit 2 ;;
esac
done

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
APP_NAME="Code Editor"
BUNDLE_ID="com.yan123tech.codeeditor"
DESTINATION="$ROOT/build/$APP_NAME.app"
CONTENTS="$DESTINATION/Contents"
EXECUTABLE="CodeEditorApp"

cd "$ROOT"

echo "==> Building $EXECUTABLE ($CONFIGURATION)"
swift build -c "$CONFIGURATION" --product "$EXECUTABLE"
BIN_PATH="$(swift build -c "$CONFIGURATION" --product "$EXECUTABLE" --show-bin-path)"

echo "==> Laying out $DESTINATION"
rm -rf "$DESTINATION"
mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Resources"

install -m 755 "$BIN_PATH/$EXECUTABLE" "$CONTENTS/MacOS/$EXECUTABLE"
install -m 644 "$ROOT/Resources/App-Info.plist" "$CONTENTS/Info.plist"

# SwiftPM writes resource bundles next to the binary and Bundle.module looks
# them up under Bundle.main.resourceURL, which is Contents/Resources here.
shopt -s nullglob
for bundle in "$BIN_PATH"/*.bundle; do
echo "==> Embedding $(basename "$bundle")"
cp -R "$bundle" "$CONTENTS/Resources/"
done
shopt -u nullglob

echo "==> Rendering icon"
TEMP_DIR="$(mktemp -d -t code-editor-app)"
trap 'rm -rf "$TEMP_DIR"' EXIT
ICON_PNG="$TEMP_DIR/AppIcon.png"
ICONSET="$TEMP_DIR/AppIcon.iconset"
mkdir -p "$ICONSET"

swift "$ROOT/Scripts/make-icon.swift" "$ICON_PNG"

for size in 16 32 128 256 512; do
sips -z "$size" "$size" "$ICON_PNG" --out "$ICONSET/icon_${size}x${size}.png" > /dev/null
double=$((size * 2))
sips -z "$double" "$double" "$ICON_PNG" --out "$ICONSET/icon_${size}x${size}@2x.png" > /dev/null
done
iconutil -c icns "$ICONSET" -o "$CONTENTS/Resources/AppIcon.icns"

echo "==> Signing (ad-hoc)"
codesign --force --sign - --timestamp=none "$DESTINATION"
codesign --verify --verbose=2 "$DESTINATION"

# LaunchServices caches the bundle identity, so a rebuild can keep showing the
# previous icon or menu-bar name until this runs.
touch "$DESTINATION"

echo "==> Built $DESTINATION"
if [[ "$INSTALL" == "1" ]]; then
rm -rf "/Applications/$APP_NAME.app"
cp -R "$DESTINATION" /Applications/
echo "==> Installed /Applications/$APP_NAME.app"
DESTINATION="/Applications/$APP_NAME.app"
fi

if [[ "$LAUNCH" == "1" ]]; then
open "$DESTINATION"
fi

plutil -p "$CONTENTS/Info.plist" | grep -E "CFBundleIdentifier|CFBundleShortVersionString|CFBundleExecutable"
du -sh "$DESTINATION"
Loading
Loading