diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj
index 2b2a39b04a..ed8383d147 100644
--- a/CodeEdit.xcodeproj/project.pbxproj
+++ b/CodeEdit.xcodeproj/project.pbxproj
@@ -46,6 +46,8 @@
6CD3CA552C8B508200D83DCD /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 6CD3CA542C8B508200D83DCD /* CodeEditSourceEditor */; };
6CE21E872C650D2C0031B056 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = 6CE21E862C650D2C0031B056 /* SwiftTerm */; };
B6FF04782B6C08AC002C2C78 /* DefaultThemes in Resources */ = {isa = PBXBuildFile; fileRef = B6FF04772B6C08AC002C2C78 /* DefaultThemes */; };
+ CE5F10032EBA000100000001 /* SwiftSyntax in Frameworks */ = {isa = PBXBuildFile; productRef = CE5F10022EBA000100000001 /* SwiftSyntax */; };
+ CE5F10052EBA000100000001 /* SwiftParser in Frameworks */ = {isa = PBXBuildFile; productRef = CE5F10042EBA000100000001 /* SwiftParser */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -188,6 +190,8 @@
5E4485612DF600D9008BBE69 /* AboutWindow in Frameworks */,
6C6BD6F429CD142C00235D17 /* CollectionConcurrencyKit in Frameworks */,
6C85BB442C210EFD00EB5DEF /* SwiftUIIntrospect in Frameworks */,
+ CE5F10032EBA000100000001 /* SwiftSyntax in Frameworks */,
+ CE5F10052EBA000100000001 /* SwiftParser in Frameworks */,
6CB446402B6DFF3A00539ED0 /* CodeEditSourceEditor in Frameworks */,
6C73A6D32D4F1E550012D95C /* CodeEditSourceEditor in Frameworks */,
2816F594280CF50500DD548B /* CodeEditSymbols in Frameworks */,
@@ -295,7 +299,6 @@
B658FB2827DA9E0F00EA4DBD /* Sources */,
B658FB2927DA9E0F00EA4DBD /* Frameworks */,
B658FB2A27DA9E0F00EA4DBD /* Resources */,
- 2B18499A27F8A7A0005119F0 /* Mark // swiftlint:disable:all as errors | Run Script */,
04ADA0CC27E6043B00BF00B2 /* Add TODO/FIXME as warnings | Run Script */,
04C3255A2801B43A00C8DA2D /* Embed Frameworks */,
2BE487F528245162003F3F64 /* Embed Foundation Extensions */,
@@ -304,7 +307,6 @@
buildRules = (
);
dependencies = (
- 6C7B1C762A1D57CE005CBBFC /* PBXTargetDependency */,
2BE487F328245162003F3F64 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
@@ -337,6 +339,8 @@
6C76D6D32E15B91E00EF52C3 /* CodeEditSourceEditor */,
6CCF6DD22E26D48F00B94F75 /* SwiftTerm */,
6CCF73CF2E26DE3200B94F75 /* SwiftTerm */,
+ CE5F10022EBA000100000001 /* SwiftSyntax */,
+ CE5F10042EBA000100000001 /* SwiftParser */,
);
productName = CodeEdit;
productReference = B658FB2C27DA9E0F00EA4DBD /* CodeEdit.app */;
@@ -443,6 +447,7 @@
5E44855F2DF600D9008BBE69 /* XCRemoteSwiftPackageReference "AboutWindow" */,
6C76D6D22E15B91E00EF52C3 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */,
6CCF73CE2E26DE3200B94F75 /* XCRemoteSwiftPackageReference "SwiftTerm" */,
+ CE5F10012EBA000100000001 /* XCRemoteSwiftPackageReference "swift-syntax" */,
);
preferredProjectObjectVersion = 55;
productRefGroup = B658FB2D27DA9E0F00EA4DBD /* Products */;
@@ -511,25 +516,6 @@
shellPath = /bin/sh;
shellScript = "TAGS=\"TODO:|FIXME:\"\necho \"searching ${SRCROOT} for ${TAGS}\"\nfind \"${SRCROOT}\" \\( -name \"*.swift\" \\) -print0 | xargs -0 egrep --with-filename --line-number --only-matching \"($TAGS).*\\$\" | perl -p -e \"s/($TAGS)/ warning: \\$1/\"\n";
};
- 2B18499A27F8A7A0005119F0 /* Mark // swiftlint:disable:all as errors | Run Script */ = {
- isa = PBXShellScriptBuildPhase;
- alwaysOutOfDate = 1;
- buildActionMask = 2147483647;
- files = (
- );
- inputFileListPaths = (
- );
- inputPaths = (
- );
- name = "Mark // swiftlint:disable:all as errors | Run Script";
- outputFileListPaths = (
- );
- outputPaths = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- shellPath = /bin/sh;
- shellScript = "TAGS=\"\\/\\/ swiftlint:disable all\"\necho \"searching ${SRCROOT} for ${TAGS}\"\nfind \"${SRCROOT}\" \\( -name \"*.swift\" \\) -print0 | xargs -0 egrep --with-filename --line-number --only-matching \"($TAGS).*\\$\" | perl -p -e \"s/($TAGS)/ error: Usage of \\$1 is prohibited/\"\n";
- };
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -574,10 +560,6 @@
target = 2BE487EB28245162003F3F64 /* OpenWithCodeEdit */;
targetProxy = 2BE487F228245162003F3F64 /* PBXContainerItemProxy */;
};
- 6C7B1C762A1D57CE005CBBFC /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- productRef = 6C7B1C752A1D57CE005CBBFC /* SwiftLint */;
- };
B658FB3F27DA9E1000EA4DBD /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = B658FB2B27DA9E0F00EA4DBD /* CodeEdit */;
@@ -673,8 +655,8 @@
CURRENT_PROJECT_VERSION = 47;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\"";
- DEVELOPMENT_TEAM = "";
- ENABLE_APP_SANDBOX = YES;
+ DEVELOPMENT_TEAM = B9P888266K;
+ ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = NO;
@@ -874,8 +856,8 @@
CURRENT_PROJECT_VERSION = 47;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\"";
- DEVELOPMENT_TEAM = "";
- ENABLE_APP_SANDBOX = YES;
+ DEVELOPMENT_TEAM = B9P888266K;
+ ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = NO;
@@ -1147,8 +1129,8 @@
CURRENT_PROJECT_VERSION = 47;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\"";
- DEVELOPMENT_TEAM = "";
- ENABLE_APP_SANDBOX = YES;
+ DEVELOPMENT_TEAM = B9P888266K;
+ ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = NO;
@@ -1420,8 +1402,8 @@
CURRENT_PROJECT_VERSION = 47;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\"";
- DEVELOPMENT_TEAM = "";
- ENABLE_APP_SANDBOX = YES;
+ DEVELOPMENT_TEAM = B9P888266K;
+ ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = NO;
@@ -1464,8 +1446,8 @@
CURRENT_PROJECT_VERSION = 47;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\"";
- DEVELOPMENT_TEAM = "";
- ENABLE_APP_SANDBOX = YES;
+ DEVELOPMENT_TEAM = B9P888266K;
+ ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = NO;
@@ -1846,6 +1828,14 @@
kind = branch;
};
};
+ CE5F10012EBA000100000001 /* XCRemoteSwiftPackageReference "swift-syntax" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/apple/swift-syntax.git";
+ requirement = {
+ kind = exactVersion;
+ version = 509.1.1;
+ };
+ };
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@@ -1930,11 +1920,6 @@
package = 6C76D6D22E15B91E00EF52C3 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */;
productName = CodeEditSourceEditor;
};
- 6C7B1C752A1D57CE005CBBFC /* SwiftLint */ = {
- isa = XCSwiftPackageProductDependency;
- package = 287136B1292A407E00E9F5F4 /* XCRemoteSwiftPackageReference "SwiftLintPlugin" */;
- productName = "plugin:SwiftLint";
- };
6C81916A29B41DD300B75C92 /* DequeModule */ = {
isa = XCSwiftPackageProductDependency;
package = 6C147C4329A329350089B630 /* XCRemoteSwiftPackageReference "swift-collections" */;
@@ -1989,6 +1974,16 @@
isa = XCSwiftPackageProductDependency;
productName = SwiftTerm;
};
+ CE5F10022EBA000100000001 /* SwiftSyntax */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CE5F10012EBA000100000001 /* XCRemoteSwiftPackageReference "swift-syntax" */;
+ productName = SwiftSyntax;
+ };
+ CE5F10042EBA000100000001 /* SwiftParser */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CE5F10012EBA000100000001 /* XCRemoteSwiftPackageReference "swift-syntax" */;
+ productName = SwiftParser;
+ };
/* End XCSwiftPackageProductDependency section */
};
rootObject = B658FB2427DA9E0F00EA4DBD /* Project object */;
diff --git a/CodeEdit/CodeEdit.entitlements b/CodeEdit/CodeEdit.entitlements
index 5c1489ef3b..4b18e592aa 100644
--- a/CodeEdit/CodeEdit.entitlements
+++ b/CodeEdit/CodeEdit.entitlements
@@ -2,18 +2,12 @@
- com.apple.security.app-sandbox
-
- com.apple.security.files.user-selected.read-write
-
- com.apple.security.files.bookmarks.app-scope
-
- com.apple.security.network.client
-
com.apple.security.application-groups
app.codeedit.CodeEdit.shared
$(TeamIdentifierPrefix)
+ com.apple.security.files.bookmarks.app-scope
+
diff --git a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift
index e4367dcc0a..27dc9b54f6 100644
--- a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift
+++ b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift
@@ -21,13 +21,26 @@ struct EditorAreaFileView: View {
var editorInstance: EditorInstance
var codeFile: CodeFileDocument
+ @StateObject private var previewRunner = SwiftUIPreviewRunner()
+ @State private var hasSwiftUIPreview = false
@ViewBuilder var editorAreaFileView: some View {
if let utType = codeFile.utType, utType.conforms(to: .text) {
- CodeFileView(
- editorInstance: editorInstance,
- codeFile: codeFile
- )
+ if hasSwiftUIPreview {
+ HSplitView {
+ CodeFileView(
+ editorInstance: editorInstance,
+ codeFile: codeFile
+ )
+
+ SwiftUIPreviewCanvasView(runner: previewRunner)
+ }
+ } else {
+ CodeFileView(
+ editorInstance: editorInstance,
+ codeFile: codeFile
+ )
+ }
} else {
NonTextFileView(fileDocument: codeFile)
.padding(.top, edgeInsets.top - 1.74)
@@ -43,6 +56,15 @@ struct EditorAreaFileView: View {
var body: some View {
editorAreaFileView
.frame(maxWidth: .infinity, maxHeight: .infinity)
+ .onAppear {
+ updateSwiftUIPreview()
+ }
+ .onChange(of: codeFile.fileURL) { _, _ in
+ updateSwiftUIPreview()
+ }
+ .onReceive(codeFile.contentCoordinator.textUpdatePublisher) { _ in
+ updateSwiftUIPreview()
+ }
.onHover { hover in
DispatchQueue.main.async {
if hover {
@@ -53,4 +75,17 @@ struct EditorAreaFileView: View {
}
}
}
+
+ private func updateSwiftUIPreview() {
+ guard codeFile.fileURL?.pathExtension.lowercased() == "swift",
+ let source = codeFile.content?.string,
+ SwiftUIPreviewParser.firstPreview(in: source) != nil else {
+ hasSwiftUIPreview = false
+ previewRunner.clear()
+ return
+ }
+
+ hasSwiftUIPreview = true
+ previewRunner.compile(source: source, fileURL: codeFile.fileURL)
+ }
}
diff --git a/CodeEdit/Features/SwiftUIPreview/SwiftUIPreviewCanvasView.swift b/CodeEdit/Features/SwiftUIPreview/SwiftUIPreviewCanvasView.swift
new file mode 100644
index 0000000000..1250ff4242
--- /dev/null
+++ b/CodeEdit/Features/SwiftUIPreview/SwiftUIPreviewCanvasView.swift
@@ -0,0 +1,63 @@
+//
+// SwiftUIPreviewCanvasView.swift
+// CodeEdit
+//
+// Created by Aryan Rogye on 5/4/26.
+//
+
+import SwiftUI
+
+struct SwiftUIPreviewCanvasView: View {
+ @ObservedObject var runner: SwiftUIPreviewRunner
+
+ var body: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Label("Preview", systemImage: "play.rectangle")
+ .font(.headline)
+
+ Spacer()
+
+ if runner.isCompiling {
+ ProgressView()
+ .controlSize(.small)
+ }
+ }
+ .padding(.horizontal, 12)
+ .frame(height: 34)
+ .background(EffectView(.headerView))
+
+ Divider()
+
+ SwiftUIPreviewHostView(previewView: runner.previewView)
+ .overlay {
+ if runner.previewView == nil {
+ CEContentUnavailableView(
+ runner.isCompiling ? "Compiling Preview" : "No Preview Loaded"
+ )
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+
+ if !runner.logs.isEmpty {
+ Divider()
+
+ ScrollView {
+ LazyVStack(alignment: .leading, spacing: 4) {
+ ForEach(Array(runner.logs.enumerated()), id: \.offset) { _, log in
+ Text(log)
+ .font(.system(.caption, design: .monospaced))
+ .foregroundStyle(.secondary)
+ .textSelection(.enabled)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ }
+ .padding(8)
+ }
+ .frame(minHeight: 96, idealHeight: 140, maxHeight: 180)
+ }
+ }
+ .frame(minWidth: 360)
+ .background(Color(nsColor: .windowBackgroundColor))
+ }
+}
diff --git a/CodeEdit/Features/SwiftUIPreview/SwiftUIPreviewHostView.swift b/CodeEdit/Features/SwiftUIPreview/SwiftUIPreviewHostView.swift
new file mode 100644
index 0000000000..e61c6fcbdd
--- /dev/null
+++ b/CodeEdit/Features/SwiftUIPreview/SwiftUIPreviewHostView.swift
@@ -0,0 +1,38 @@
+//
+// SwiftUIPreviewHostView.swift
+// CodeEdit
+//
+// Created by Aryan Rogye on 5/4/26.
+//
+
+import AppKit
+import SwiftUI
+
+struct SwiftUIPreviewHostView: NSViewRepresentable {
+ let previewView: NSView?
+
+ func makeNSView(context: Context) -> NSView {
+ let container = NSView()
+ container.wantsLayer = true
+ container.layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
+ return container
+ }
+
+ func updateNSView(_ nsView: NSView, context: Context) {
+ nsView.subviews.forEach { $0.removeFromSuperview() }
+
+ guard let previewView else {
+ return
+ }
+
+ previewView.translatesAutoresizingMaskIntoConstraints = false
+ nsView.addSubview(previewView)
+
+ NSLayoutConstraint.activate([
+ previewView.leadingAnchor.constraint(equalTo: nsView.leadingAnchor),
+ previewView.trailingAnchor.constraint(equalTo: nsView.trailingAnchor),
+ previewView.topAnchor.constraint(equalTo: nsView.topAnchor),
+ previewView.bottomAnchor.constraint(equalTo: nsView.bottomAnchor)
+ ])
+ }
+}
diff --git a/CodeEdit/Features/SwiftUIPreview/SwiftUIPreviewParser.swift b/CodeEdit/Features/SwiftUIPreview/SwiftUIPreviewParser.swift
new file mode 100644
index 0000000000..7caafa3606
--- /dev/null
+++ b/CodeEdit/Features/SwiftUIPreview/SwiftUIPreviewParser.swift
@@ -0,0 +1,78 @@
+//
+// SwiftUIPreviewParser.swift
+// CodeEdit
+//
+// Created by Aryan Rogye on 5/4/26.
+//
+
+import Foundation
+import SwiftParser
+import SwiftSyntax
+
+struct SwiftUIPreviewSource {
+ let sourceWithoutPreviews: String
+ let previewBody: String
+}
+
+enum SwiftUIPreviewParser {
+ static func firstPreview(in source: String) -> SwiftUIPreviewSource? {
+ let tree = Parser.parse(source: source)
+ let visitor = PreviewVisitor(viewMode: .sourceAccurate)
+ visitor.walk(tree)
+
+ guard let preview = visitor.previews.first else {
+ return nil
+ }
+
+ let sourceWithoutPreviews = source.removingUTF8Ranges(
+ visitor.previews.map(\.removalRange)
+ )
+
+ return SwiftUIPreviewSource(
+ sourceWithoutPreviews: sourceWithoutPreviews,
+ previewBody: preview.body.trimmingCharacters(in: .whitespacesAndNewlines)
+ )
+ }
+}
+
+private final class PreviewVisitor: SyntaxVisitor {
+ struct Preview {
+ let body: String
+ let removalRange: Range
+ }
+
+ private(set) var previews: [Preview] = []
+
+ override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind {
+ guard node.macroName.trimmedDescription == "Preview",
+ let closure = node.trailingClosure else {
+ return .skipChildren
+ }
+
+ previews.append(
+ Preview(
+ body: closure.statements.description,
+ removalRange: node.position.utf8Offset..]) -> String {
+ guard !ranges.isEmpty else {
+ return self
+ }
+
+ var bytes = Array(utf8)
+ for range in ranges.sorted(by: { $0.lowerBound > $1.lowerBound }) {
+ guard range.lowerBound >= bytes.startIndex, range.upperBound <= bytes.endIndex else {
+ continue
+ }
+ bytes.removeSubrange(range)
+ }
+
+ return String(bytes: bytes, encoding: .utf8) ?? self
+ }
+}
diff --git a/CodeEdit/Features/SwiftUIPreview/SwiftUIPreviewRunner.swift b/CodeEdit/Features/SwiftUIPreview/SwiftUIPreviewRunner.swift
new file mode 100644
index 0000000000..30ed31dfa4
--- /dev/null
+++ b/CodeEdit/Features/SwiftUIPreview/SwiftUIPreviewRunner.swift
@@ -0,0 +1,226 @@
+//
+// SwiftUIPreviewRunner.swift
+// CodeEdit
+//
+// Created by Aryan Rogye on 5/4/26.
+//
+
+import AppKit
+import Darwin
+import Foundation
+
+@MainActor
+final class SwiftUIPreviewRunner: ObservableObject {
+ @Published var previewView: NSView?
+ @Published var logs: [String] = []
+ @Published var isCompiling = false
+
+ private var loadedLibraryHandles: [UnsafeMutableRawPointer] = []
+ private var compileTask: Task?
+
+ func compile(source: String, fileURL: URL?) {
+ compileTask?.cancel()
+ compileTask = Task { await compilePreview(source: source, fileURL: fileURL) }
+ }
+
+ func clear() {
+ compileTask?.cancel()
+ previewView = nil
+ logs.removeAll()
+ isCompiling = false
+ }
+
+ private func compilePreview(source: String, fileURL: URL?) async {
+ guard let previewSource = SwiftUIPreviewParser.firstPreview(in: source) else {
+ clear()
+ return
+ }
+
+ isCompiling = true
+ previewView = nil
+ logs = []
+ defer { isCompiling = false }
+
+ logs.append("Found #Preview in \(fileURL?.lastPathComponent ?? "current file")")
+
+ let packageURL = FileManager.default.temporaryDirectory
+ .appendingPathComponent("CodeEditPreview-\(UUID().uuidString)")
+
+ do {
+ try writePreviewPackage(previewSource, to: packageURL)
+ } catch {
+ logs.append("Failed to write preview package: \(error.localizedDescription)")
+ return
+ }
+
+ logs.append("Preview package path: \(packageURL.path)")
+
+ guard await buildPreviewPackage(at: packageURL) else {
+ return
+ }
+
+ let dylibURL = packageURL
+ .appendingPathComponent(".build")
+ .appendingPathComponent("debug")
+ .appendingPathComponent("libCodeEditPreview.dylib")
+
+ loadPreview(from: dylibURL)
+ }
+
+ private func writePreviewPackage(_ previewSource: SwiftUIPreviewSource, to packageURL: URL) throws {
+ let sourcesURL = packageURL
+ .appendingPathComponent("Sources")
+ .appendingPathComponent("CodeEditPreview")
+
+ try FileManager.default.createDirectory(
+ at: sourcesURL,
+ withIntermediateDirectories: true
+ )
+
+ try packageFile.write(
+ to: packageURL.appendingPathComponent("Package.swift"),
+ atomically: true,
+ encoding: .utf8
+ )
+
+ try previewSource.sourceWithoutPreviews.write(
+ to: sourcesURL.appendingPathComponent("PreviewSource.swift"),
+ atomically: true,
+ encoding: .utf8
+ )
+
+ try previewFactory(previewBody: previewSource.previewBody).write(
+ to: sourcesURL.appendingPathComponent("PreviewFactory.swift"),
+ atomically: true,
+ encoding: .utf8
+ )
+ }
+
+ private func buildPreviewPackage(at packageURL: URL) async -> Bool {
+ let process = Process()
+ process.executableURL = URL(fileURLWithPath: "/usr/bin/swift")
+ process.arguments = ["build", "--package-path", packageURL.path]
+
+ let pipe = Pipe()
+ process.standardOutput = pipe
+ process.standardError = pipe
+
+ let outputStream = AsyncStream { continuation in
+ pipe.fileHandleForReading.readabilityHandler = { handle in
+ let data = handle.availableData
+ guard !data.isEmpty else {
+ continuation.finish()
+ return
+ }
+ if let output = String(data: data, encoding: .utf8), !output.isEmpty {
+ continuation.yield(output)
+ }
+ }
+
+ process.terminationHandler = { _ in
+ pipe.fileHandleForReading.readabilityHandler = nil
+ continuation.finish()
+ }
+ }
+
+ do {
+ try process.run()
+ } catch {
+ logs.append("Failed to start swift build: \(error.localizedDescription)")
+ return false
+ }
+
+ for await output in outputStream {
+ guard !Task.isCancelled else {
+ process.terminate()
+ return false
+ }
+ logs.append(output)
+ }
+
+ guard process.terminationStatus == 0 else {
+ logs.append("swift build failed with exit code \(process.terminationStatus)")
+ return false
+ }
+
+ logs.append("swift build succeeded")
+ return true
+ }
+
+ private func loadPreview(from dylibURL: URL) {
+ guard FileManager.default.fileExists(atPath: dylibURL.path) else {
+ logs.append("Built dylib not found at: \(dylibURL.path)")
+ return
+ }
+
+ guard let handle = dlopen(dylibURL.path, RTLD_NOW | RTLD_LOCAL) else {
+ logs.append("dlopen failed: \(String(cString: dlerror()))")
+ return
+ }
+
+ guard let symbol = dlsym(handle, "makeCodeEditPreviewView") else {
+ logs.append("Could not find makeCodeEditPreviewView symbol")
+ return
+ }
+
+ typealias MakePreviewView = @convention(c) () -> UnsafeMutableRawPointer
+ let makePreviewView = unsafeBitCast(symbol, to: MakePreviewView.self)
+ let pointer = makePreviewView()
+
+ previewView = Unmanaged.fromOpaque(pointer).takeRetainedValue()
+ loadedLibraryHandles.append(handle)
+ logs.append("Loaded preview dylib")
+ }
+
+ private var packageFile: String {
+ """
+ // swift-tools-version: 5.9
+ import PackageDescription
+
+ let package = Package(
+ name: "CodeEditPreview",
+ platforms: [
+ .macOS(.v14)
+ ],
+ products: [
+ .library(
+ name: "CodeEditPreview",
+ type: .dynamic,
+ targets: ["CodeEditPreview"]
+ )
+ ],
+ targets: [
+ .target(name: "CodeEditPreview")
+ ]
+ )
+ """
+ }
+
+ private func previewFactory(previewBody: String) -> String {
+ """
+ import AppKit
+ import SwiftUI
+
+ private enum __CodeEditPreviewFactory {
+ @ViewBuilder
+ static func makePreview() -> some View {
+ \(previewBody.indentingPreviewBody())
+ }
+ }
+
+ @_cdecl("makeCodeEditPreviewView")
+ public func makeCodeEditPreviewView() -> UnsafeMutableRawPointer {
+ let view = NSHostingView(rootView: __CodeEditPreviewFactory.makePreview())
+ return Unmanaged.passRetained(view).toOpaque()
+ }
+ """
+ }
+}
+
+private extension String {
+ func indentingPreviewBody() -> String {
+ split(separator: "\n", omittingEmptySubsequences: false)
+ .map { " \($0)" }
+ .joined(separator: "\n")
+ }
+}