Skip to content

stoicswe/curryide

Repository files navigation

Curry IDE

main_ui

A native macOS IDE for Haskell, built in SwiftUI with a Liquid Glass UI. Curry drives user-installed GHC via GHCup, Stack, or Cabal; talks to haskell-language-server over LSP for autocomplete and diagnostics; ships a built-in GHCi REPL for quick snippets; and layers on Apple Intelligence suggestions (on supported Macs) via Apple's on-device Foundation Models.

Why Developer ID, not the Mac App Store

Curry is distributed via Developer ID + notarization. This isn't an oversight — it's a hard constraint of the product. The Mac App Store requires App Sandbox on every Mach-O in the bundle, and the kernel refuses any child process that doesn't carry exactly com.apple.security.app-sandbox + com.apple.security.inherit and nothing else. The GHC, HLS, Stack, Cabal, and GHCup binaries are signed upstream with Developer ID and don't — can't — satisfy that rule, so a sandboxed Curry simply cannot exec them. Every serious commercial IDE on macOS (Nova, VS Code, JetBrains, Fleet, Zed, Sublime Text) reached the same conclusion.

See Sources/CurryIDE/Resources/CurryIDE.entitlements for the exact Hardened Runtime relaxations we request (JIT, library validation disabled, DYLD env vars), and the research document bundled in the original spec for citations.

What's in the box

Subsystem Path
App entry + chrome Sources/CurryIDE/App/
Liquid Glass styles Sources/CurryIDE/Views/LiquidGlass.swift
Project sidebar Sources/CurryIDE/Views/Sidebar/
Tabbed / split editor Sources/CurryIDE/Views/Editor/
Haskell highlighter Sources/CurryIDE/Views/Editor/HaskellSyntaxHighlighter.swift
GHCi console Sources/CurryIDE/Views/Console/
GHCup / Stack / Cabal Sources/CurryIDE/Services/
HLS LSP client Sources/CurryIDE/LSP/
Apple Intelligence Sources/CurryIDE/AppleIntelligence/
Workspace model Sources/CurryIDE/Models/Workspace.swift

Architecture

┌──────────────────────── Curry (SwiftUI) ────────────────────────┐
│  RootView                                                       │
│  ├── ProjectSidebar        (dismissable, ⌘0)                    │
│  ├── EditorSurface                                              │
│  │     └── recursive HSplit/VSplit of EditorPaneView            │
│  │           └── NSTextView-backed CodeEditorView               │
│  │                 ├── HaskellSyntaxHighlighter                 │
│  │                 └── completions (HLS + keywords + AI hint)   │
│  └── ConsoleSurface                                             │
│        ├── GHCi         → GHCiSession (spawned `ghci`)          │
│        ├── Build        → BuildRunner → stack/cabal             │
│        └── Problems     → parsed GHC diagnostics                │
│                                                                 │
│  Services                                                       │
│  ├── ToolchainManager   → GHCupService (`ghcup list/install`)   │
│  ├── HLSClient          → LSPTransport (stdio JSON-RPC)         │
│  └── IntelligenceCoord. → FoundationModels.LanguageModelSession │
└─────────────────────────────────────────────────────────────────┘

The editor talks to compilers only through a service layer. This is the "build server" split the research recommends: if a sandboxed learning variant is ever needed, swap the services out without touching the UI.

Requirements

  • macOS 15.1 (Sequoia) or newer for Apple Intelligence; the rest of the IDE runs on macOS 13+.
  • Apple silicon for on-device Foundation Models. On Intel Macs the AI panel reports "unavailable" and the editor falls back to HLS + keyword completion.
  • GHCup installed at ~/.ghcup/bin/ghcup. Curry guides the user through install on first launch if it's missing.

Building

Curry is an Xcode project, generated from project.yml by XcodeGen. The .xcodeproj itself is gitignored — project.yml is the source of truth.

# One-time:
brew install xcodegen

# Every time you pull or edit project.yml:
xcodegen generate

# Then either:
open CurryIDE.xcodeproj        # develop in Xcode 16+
# or, for a command-line build:
xcodebuild -project CurryIDE.xcodeproj -scheme CurryIDE build

Running from Xcode uses "Sign to Run Locally" by default — it'll launch on your machine without a paid Developer account. For a shippable .app:

  1. Set DEVELOPMENT_TEAM in project.yml to your Developer ID team, regenerate.
  2. Archive in Xcode (Product ▸ Archive) or with xcodebuild archive.
  3. Run the archive through notarytool (xcrun notarytool submit …) and staple (xcrun stapler staple).

The hand-authored Info.plist and CurryIDE.entitlements at Sources/CurryIDE/Resources/ are wired in via INFOPLIST_FILE and CODE_SIGN_ENTITLEMENTS build settings, so they remain the authoritative source — XcodeGen does not regenerate them.

Feature map

Editor

  • Tabs per pane; drag-and-drop reordering is a TODO
  • Split views via the pane menu: Split Right / Split Down. The same file can be open in multiple splits (shared SourceDocument buffer).
  • Dismissable sidebar via ⌘0 or the toolbar icon.
  • Line numbers rendered by a custom NSRulerView.
  • Find via the built-in NSTextView find bar.

Syntax highlighting

Regex-driven in HaskellSyntaxHighlighter.swift. Handles block/line comments, pragmas, strings, chars, numbers, reserved keywords, constructors, and operators (->, =>, <-, ::, |). The highlighter only reprocesses the line range around the edit, keeping edits cheap on multi-thousand-line files.

For higher fidelity later: either adopt tree-sitter-haskell via TreeSitter.xcframework, or consume HLS's semantic tokens response.

Autocomplete

Three sources, merged:

  1. HLS textDocument/completion (authoritative types, in-scope symbols).
  2. HaskellKeywords.all — reserved words + common Prelude.
  3. Apple Intelligence ghost-text suggestions from IntelligenceCoordinator.suggest(...).

HLS completions are cached per-document so the synchronous NSTextView completions callback can serve immediately while a background request refreshes the cache.

GHCup integration

GHCupService wraps the ghcup CLI for listing, installing, and switching toolchains. The Haskell ▸ Manage Toolchains… sheet walks users through installing GHCup itself (we don't pipe get-ghcup.haskell.org automatically — it deserves a deliberate click) and then through picking a GHC + HLS.

Stack / Cabal projects

ProjectScaffolder runs stack new or cabal init --non-interactive. The ⌘⇧N sheet picks a name, location, and build system.

GHCi

GHCiSession spawns ghci, pipes its stdout/stderr into a SwiftUI transcript view, and feeds lines back through stdin. Use ⌘⇧G to load the current file; the Haskell ▸ menu also exposes a scratch session.

Apple Intelligence

IntelligenceCoordinator keeps one LanguageModelSession warm (on macOS 15.1+, Apple silicon, AI enabled). Every edit schedules a debounced 450 ms request that sends the ±1600 characters around the caret plus a compact symbol table produced by HaskellProjectIndexer. The returned snippet becomes document.inlineHint, which the editor can render as ghost text — wire that surface in when you're ready.

Falls back silently to HLS + keyword completion on unsupported hardware.

Keyboard shortcuts

Shortcut Action
⌘⇧N New Haskell Project…
⌘O Open folder…
⌘0 Toggle sidebar
⌘B Build
⌘R Run
⌘⇧G Open GHCi for current file

Things this scaffold intentionally leaves out

  • A packaged .xcodeproj (SPM + Xcode 16 handles everything the app needs).
  • Ghost-text rendering — the SourceDocument.inlineHint value is populated but the editor doesn't paint it yet. NSTextView's drawInsertionPoint(in:color:turnedOn:) override is the usual seam.
  • publishDiagnosticsWorkspace.diagnostics wiring.
  • Drag-and-drop reorder of editor tabs.
  • Debugger integration (would be GHCi breakpoints today).

Each is a small, localized addition on top of the interfaces already defined.