From 97fff6d98df0169d1fed5c0b6be85e8f3b72f406 Mon Sep 17 00:00:00 2001 From: Hunter Davis Date: Tue, 2 Dec 2025 02:40:45 -0500 Subject: [PATCH 1/9] Simple Web View --- CodeEdit.xcodeproj/project.pbxproj | 92 ++- .../xcshareddata/swiftpm/Package.resolved | 6 +- .../xcshareddata/xcschemes/CodeEdit.xcscheme | 2 +- .../xcschemes/OpenWithCodeEdit.xcscheme | 2 +- CodeEdit/CodeEdit.entitlements | 4 - .../Editor/Views/EditorAreaFileView.swift | 602 +++++++++++++++++- CodeEdit/Features/Welcome/NewFileButton.swift | 15 +- 7 files changed, 685 insertions(+), 38 deletions(-) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index ff63c4974c..cd3ef1c94a 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -397,7 +397,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1330; - LastUpgradeCheck = 1640; + LastUpgradeCheck = 2610; TargetAttributes = { 2BE487EB28245162003F3F64 = { CreatedOnToolsVersion = 13.3.1; @@ -650,6 +650,7 @@ OTHER_SWIFT_FLAGS = "-D ALPHA"; RUN_DOCUMENTATION_COMPILER = YES; SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SYSTEM_FRAMEWORK_SEARCH_PATHS = ""; @@ -673,8 +674,21 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\""; DEVELOPMENT_TEAM = ""; + ENABLE_APP_SANDBOX = YES; + ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + ENABLE_USER_SELECTED_FILES = readwrite; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -690,6 +704,8 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; REGISTER_APP_GROUPS = YES; + RUNTIME_EXCEPTION_ALLOW_JIT = YES; + RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = YES; RUN_DOCUMENTATION_COMPILER = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; @@ -847,6 +863,7 @@ OTHER_SWIFT_FLAGS = "-D BETA"; RUN_DOCUMENTATION_COMPILER = YES; SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SYSTEM_FRAMEWORK_SEARCH_PATHS = ""; @@ -870,8 +887,21 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\""; DEVELOPMENT_TEAM = ""; + ENABLE_APP_SANDBOX = YES; + ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + ENABLE_USER_SELECTED_FILES = readwrite; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -887,6 +917,8 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; REGISTER_APP_GROUPS = YES; + RUNTIME_EXCEPTION_ALLOW_JIT = YES; + RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = YES; RUN_DOCUMENTATION_COMPILER = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; @@ -1115,6 +1147,7 @@ OTHER_SWIFT_FLAGS = "-D ALPHA"; RUN_DOCUMENTATION_COMPILER = YES; SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SYSTEM_FRAMEWORK_SEARCH_PATHS = ""; @@ -1139,8 +1172,21 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\""; DEVELOPMENT_TEAM = ""; + ENABLE_APP_SANDBOX = YES; + ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + ENABLE_USER_SELECTED_FILES = readwrite; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -1156,6 +1202,8 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; REGISTER_APP_GROUPS = YES; + RUNTIME_EXCEPTION_ALLOW_JIT = YES; + RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = YES; RUN_DOCUMENTATION_COMPILER = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; @@ -1319,6 +1367,7 @@ ONLY_ACTIVE_ARCH = YES; RUN_DOCUMENTATION_COMPILER = YES; SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SYSTEM_FRAMEWORK_SEARCH_PATHS = ""; @@ -1384,6 +1433,7 @@ MTL_FAST_MATH = YES; RUN_DOCUMENTATION_COMPILER = YES; SDKROOT = macosx; + STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SYSTEM_FRAMEWORK_SEARCH_PATHS = ""; @@ -1407,8 +1457,21 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\""; DEVELOPMENT_TEAM = ""; + ENABLE_APP_SANDBOX = YES; + ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + ENABLE_USER_SELECTED_FILES = readwrite; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -1424,6 +1487,8 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; REGISTER_APP_GROUPS = YES; + RUNTIME_EXCEPTION_ALLOW_JIT = YES; + RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = YES; RUN_DOCUMENTATION_COMPILER = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; @@ -1448,8 +1513,21 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\""; DEVELOPMENT_TEAM = ""; + ENABLE_APP_SANDBOX = YES; + ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite; ENABLE_HARDENED_RUNTIME = YES; + ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; + ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PRINTING = NO; + ENABLE_RESOURCE_ACCESS_USB = NO; + ENABLE_USER_SELECTED_FILES = readwrite; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -1465,6 +1543,8 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; REGISTER_APP_GROUPS = YES; + RUNTIME_EXCEPTION_ALLOW_JIT = YES; + RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = YES; RUN_DOCUMENTATION_COMPILER = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = ""; @@ -1597,7 +1677,7 @@ 28052DF32973045C00F4F90A /* Beta */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Pre; }; B658FB2727DA9E0F00EA4DBD /* Build configuration list for PBXProject "CodeEdit" */ = { isa = XCConfigurationList; @@ -1609,7 +1689,7 @@ 28052DEF2973045C00F4F90A /* Beta */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Pre; }; B658FB5127DA9E1000EA4DBD /* Build configuration list for PBXNativeTarget "CodeEdit" */ = { isa = XCConfigurationList; @@ -1621,7 +1701,7 @@ 28052DF02973045C00F4F90A /* Beta */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Pre; }; B658FB5427DA9E1000EA4DBD /* Build configuration list for PBXNativeTarget "CodeEditTests" */ = { isa = XCConfigurationList; @@ -1633,7 +1713,7 @@ 28052DF12973045C00F4F90A /* Beta */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Pre; }; B658FB5727DA9E1000EA4DBD /* Build configuration list for PBXNativeTarget "CodeEditUITests" */ = { isa = XCConfigurationList; @@ -1645,7 +1725,7 @@ 28052DF22973045C00F4F90A /* Beta */, ); defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; + defaultConfigurationName = Pre; }; /* End XCConfigurationList section */ diff --git a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 835319d36b..ebee3ff816 100644 --- a/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/CodeEdit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -229,7 +229,7 @@ { "identity" : "swiftlintplugin", "kind" : "remoteSourceControl", - "location" : "https://github.com/lukepistrol/SwiftLintPlugin", + "location" : "https://github.com/lukepistrol/SwiftLintPlugin.git", "state" : { "revision" : "3780efccceaa87f17ec39638a9d263d0e742b71c", "version" : "0.59.1" @@ -258,8 +258,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/siteline/SwiftUI-Introspect.git", "state" : { - "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", - "version" : "1.3.0" + "revision" : "668a65735751432b640260c56dfa621cec568368", + "version" : "1.2.0" } }, { diff --git a/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme b/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme index 2c80a13978..e0bdbb1fb8 100644 --- a/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme +++ b/CodeEdit.xcodeproj/xcshareddata/xcschemes/CodeEdit.xcscheme @@ -1,6 +1,6 @@ app.codeedit.CodeEdit.shared $(TeamIdentifierPrefix) - com.apple.security.cs.allow-jit - - com.apple.security.cs.disable-library-validation - diff --git a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift index e4367dcc0a..3e72ac1f55 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift @@ -9,34 +9,553 @@ import AppKit import AVKit import CodeEditSourceEditor import SwiftUI +import WebKit +import UniformTypeIdentifiers +import Combine -struct EditorAreaFileView: View { +// MARK: - Display Modes +enum EditorDisplayMode: String, CaseIterable, Identifiable { + case code = "Code" + case split = "Split" + case preview = "Preview" + var id: String { rawValue } +} + +// MARK: - Preview Source +enum PreviewSource { + case localHTML + case serverPreview +} + +// MARK: - HTML Rendering Backend Abstraction +struct HTMLRenderer { + var render: ((String) -> String)? + var renderAsync: ((String) async -> String)? + var loggingEnabled: Bool = false + + func renderHTML(from source: String) async -> String { + if loggingEnabled { print("[Preview] Rendering start. Source length: \(source.count)") } + let output: String + if let renderAsync { + output = await renderAsync(source) + } else if let render { + output = render(source) + } else { + output = source + } + if loggingEnabled { print("[Preview] Rendering done. HTML length: \(output.count)") } + return output + } +} + +// MARK: - WebKit Crash-Aware Delegate +final class PreviewNavDelegate: NSObject, WKNavigationDelegate { + var onCrash: (() -> Void)? + + func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { + print("[WebView] WebContent process terminated") + onCrash?() + } + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + print("[WebView] didFailProvisionalNavigation: \(error.localizedDescription)") + onCrash?() + } + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + print("[WebView] didFail navigation: \(error.localizedDescription)") + } + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + print("[WebView] didFinish navigation. URL: \(webView.url?.absoluteString ?? "nil")") + } +} + +// MARK: - WebView (Coordinator reuse, safe refresh) + +struct WebView: NSViewRepresentable { + let html: String + let baseURL: URL? + let onCrash: () -> Void + let allowJavaScript: Bool + + class Coordinator { + let webView: WKWebView + let navDelegate = PreviewNavDelegate() + var lastHTML: String = "" + var lastLoadAt: Date = .distantPast + + init(onCrash: @escaping () -> Void, allowJavaScript: Bool) { + let config = WKWebViewConfiguration() + config.preferences.javaScriptEnabled = allowJavaScript + config.websiteDataStore = .default() + config.suppressesIncrementalRendering = true + + let wv = WKWebView(frame: .zero, configuration: config) + wv.setValue(false, forKey: "drawsBackground") + navDelegate.onCrash = onCrash + wv.navigationDelegate = navDelegate + self.webView = wv + } + + func safeLoad(html: String, baseURL: URL?) { + let now = Date() + if now.timeIntervalSince(lastLoadAt) < 1.0, lastHTML == html { return } + lastLoadAt = now + lastHTML = html + webView.stopLoading() + webView.loadHTMLString(html, baseURL: baseURL) + } + } + + func makeCoordinator() -> Coordinator { Coordinator(onCrash: onCrash, allowJavaScript: allowJavaScript) } + func makeNSView(context: Context) -> WKWebView { context.coordinator.webView } + func updateNSView(_ webView: WKWebView, context: Context) { context.coordinator.safeLoad(html: html, baseURL: baseURL) } +} + +// MARK: - Markdown Renderer +struct MarkdownView: NSViewRepresentable { + let source: String + + func makeNSView(context: Context) -> NSScrollView { + let scroll = NSScrollView() + scroll.hasVerticalScroller = true + let textView = NSTextView() + textView.isEditable = false + textView.backgroundColor = .white + textView.textContainerInset = NSSize(width: 8, height: 8) + scroll.documentView = textView + return scroll + } + + func updateNSView(_ nsView: NSScrollView, context: Context) { + guard let textView = nsView.documentView as? NSTextView else { return } + let attributed = renderMarkdown(source) + textView.textStorage?.setAttributedString(attributed) + } + + private func renderMarkdown(_ source: String) -> NSAttributedString { + if source.isEmpty { return NSAttributedString(string: "") } + if #available(macOS 12.0, *) { + if let attributed = try? NSAttributedString(markdown: source) { + return attributed + } + } + return NSAttributedString(string: source) + } +} + +// MARK: - Server Preview Client +struct ServerPreviewClient { + let baseURL = URL(string: "http://localhost:3000")! + let path = "/preview" + let timeout: TimeInterval = 8 + + func postHTML(_ html: String, filename: String?) async throws -> String { + var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)! + components.path = path + guard let url = components.url else { throw URLError(.badURL) } + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.timeoutInterval = timeout + let payload: [String: Any] = [ + "html": html, + "filename": filename ?? "untitled.html", + "timestamp": Date().timeIntervalSince1970 + ] + let data = try JSONSerialization.data(withJSONObject: payload, options: []) + let configuration = URLSessionConfiguration.default + configuration.timeoutIntervalForRequest = timeout + configuration.timeoutIntervalForResource = timeout + let session = URLSession(configuration: configuration) + let (respData, resp) = try await session.upload(for: request, from: data) + guard let http = resp as? HTTPURLResponse else { throw URLError(.badServerResponse) } + if !(200...299).contains(http.statusCode) { + let bodyString = String(data: respData, encoding: .utf8) ?? "" + throw NSError(domain: "ServerPreview", code: http.statusCode, userInfo: [ + NSLocalizedDescriptionKey: "Server preview failed (\(http.statusCode)).", + "body": bodyString + ]) + } + return String(data: respData, encoding: .utf8) ?? "" + } +} + +extension Notification.Name { + static let CodeFileDocumentContentDidChange = Notification.Name("CodeFileDocumentContentDidChange") +} + +// MARK: - Bottom Controls Overlay +struct PreviewBottomBar: View { + let refresh: () -> Void + let reloadIgnoreCache: () -> Void + @Binding var enableJS: Bool + @Binding var previewSource: PreviewSource + let serverErrorMessage: String? + + var body: some View { + VStack(spacing: 0) { + Divider() + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + Button("Refresh") { refresh() } + .keyboardShortcut("r", modifiers: []) + Button("Reload (ignore cache)") { reloadIgnoreCache() } + // WebKit is always enabled; remove toggle. + Toggle("Enable JS", isOn: $enableJS) + Picker("Preview Source", selection: $previewSource) { + Text("Local").tag(PreviewSource.localHTML) + Text("Server").tag(PreviewSource.serverPreview) + } + .pickerStyle(.segmented) + Menu("More") { + Button("Refresh") { refresh() } + Button("Reload (ignore cache)") { reloadIgnoreCache() } + Toggle("Enable JS", isOn: $enableJS) + Picker("Preview Source", selection: $previewSource) { + Text("Local").tag(PreviewSource.localHTML) + Text("Server").tag(PreviewSource.serverPreview) + } + } + } + .padding(.horizontal, 8) + .padding(.vertical, 6) + } + .background(Color(NSColor.windowBackgroundColor)) + if let serverErrorMessage, previewSource == .serverPreview { + Text("Server error: \(serverErrorMessage)") + .foregroundColor(.red) + .font(.caption) + .padding(.horizontal, 8) + .padding(.bottom, 6) + } + } + } +} +// MARK: - Main View +struct EditorAreaFileView: View { @EnvironmentObject private var editorManager: EditorManager @EnvironmentObject private var editor: Editor @EnvironmentObject private var statusBarViewModel: StatusBarViewModel - - @Environment(\.edgeInsets) - private var edgeInsets + @Environment(\.edgeInsets) private var edgeInsets var editorInstance: EditorInstance var codeFile: CodeFileDocument + var htmlRenderer: HTMLRenderer = .init( + render: { source in + let trimmed = source.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + let looksHTML = trimmed.hasPrefix("", with: ">") + return """ + + + + + + + + +
\(escaped)
+ + """ + }, + loggingEnabled: true + ) + var enablePreviewLogging: Bool = true + + @State private var renderedHTMLState: String = "" + @State private var contentString: String = "" + @State private var displayMode: EditorDisplayMode = .split + @State private var cancellables = Set() + @State private var renderWorkItem: DispatchWorkItem? + + // WebKit is always enabled; only allow JS toggle + @State private var webViewAllowJS: Bool = false + @State private var previewSource: PreviewSource = .localHTML + + private let serverClient = ServerPreviewClient() + @State private var serverErrorMessage: String? + + @State private var webViewRefreshToken = UUID() + + // Fixed size constants + private let FIXED_PREVIEW_HEIGHT: CGFloat = 320 // adjust as needed + private let FIXED_PREVIEW_WIDTH: CGFloat = 420 // used in split right pane + + private func bindContent() { + NotificationCenter.default.publisher( + for: .CodeFileDocumentContentDidChange, + object: codeFile + ) + .compactMap { _ in codeFile.content?.string } + .removeDuplicates() + .debounce(for: .milliseconds(250), scheduler: RunLoop.main) + .sink { newText in + updatePreview(with: newText) + } + .store(in: &cancellables) + } + + private func scheduleRender(for source: String) { + renderWorkItem?.cancel() + let work = DispatchWorkItem { + Task { @MainActor in + if enablePreviewLogging { print("[Preview] Source changed. Rendering…") } + switch previewSource { + case .localHTML: + let html = await htmlRenderer.renderHTML(from: source) + serverErrorMessage = nil + if renderedHTMLState != html { + renderedHTMLState = html + webViewRefreshToken = UUID() + print("[Preview] Local HTML set. length: \(html.count)") + } else { + print("[Preview] No state change (same HTML)") + } + case .serverPreview: + let localHTML = await htmlRenderer.renderHTML(from: source) + if renderedHTMLState != localHTML { + renderedHTMLState = localHTML + print("[Preview] Fallback local HTML set. length: \(localHTML.count)") + } + await loadServerPreview(with: source) + } + } + } + renderWorkItem = work + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: work) + } + + @MainActor + private func loadServerPreview(with source: String) async { + serverErrorMessage = nil + do { + let filename = codeFile.fileURL?.lastPathComponent + let serverHTML = try await serverClient.postHTML(source, filename: filename) + if renderedHTMLState != serverHTML { + renderedHTMLState = serverHTML + print("[Preview] Server HTML set. length: \(serverHTML.count)") + } else { + print("[Preview] Server returned identical HTML; no state change.") + } + } catch { + let message: String + if let urlError = error as? URLError { + switch urlError.code { + case .cannotFindHost: message = "Cannot find server at localhost:3000." + case .timedOut: message = "Server preview timed out." + case .notConnectedToInternet: message = "No network connection." + default: message = "Network error: \(urlError.localizedDescription)" + } + } else { + message = error.localizedDescription + } + serverErrorMessage = message + print("[Preview] Server preview error: \(message)") + } + } + + private func updatePreview(with newText: String) { + if contentString != newText { + contentString = newText + scheduleRender(for: newText) + } + } + + private func refreshPreview() { + print("[Preview] Manual refresh triggered") + scheduleRender(for: contentString) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + webViewRefreshToken = UUID() + } + } + + // MARK: - Whole Editor Layout with fixed WebView below divider @ViewBuilder var editorAreaFileView: some View { - if let utType = codeFile.utType, utType.conforms(to: .text) { - CodeFileView( - editorInstance: editorInstance, - codeFile: codeFile - ) - } else { - NonTextFileView(fileDocument: codeFile) - .padding(.top, edgeInsets.top - 1.74) - .padding(.bottom, StatusBarView.height + 1.26) - .modifier(UpdateStatusBarInfo(with: codeFile.fileURL)) - .onDisappear { - statusBarViewModel.dimensions = nil - statusBarViewModel.fileSize = nil + let pathExt = codeFile.fileURL?.pathExtension.lowercased() ?? "" + let isHTML = (codeFile.utType?.conforms(to: .html) ?? false) || (["html", "htm"].contains(pathExt)) + let isMarkdown = (codeFile.utType?.identifier == "net.daringfireball.markdown") || (pathExt == "md" || pathExt == "markdown") + + VStack(spacing: 0) { + // Top row: mode picker + HStack(spacing: 8) { + Picker("Display Mode", selection: $displayMode) { + ForEach(EditorDisplayMode.allCases) { mode in + Text(mode.rawValue).tag(mode) + } + } + .pickerStyle(.segmented) + Spacer(minLength: 8) + } + .padding(.horizontal, 8) + .padding(.top, 8) + + Divider() + + // Content below divider + if isHTML { + switch displayMode { + case .code: + CodeFileView(editorInstance: editorInstance, codeFile: codeFile) + + case .preview: + // Fixed-height WebView area below the divider + VStack(spacing: 0) { + ZStack { + WebView( + html: renderedHTMLState.isEmpty + ? "

Preview Ready

" + : renderedHTMLState, + baseURL: codeFile.fileURL?.deletingLastPathComponent(), + onCrash: { /* WebKit always on; show message if needed */ }, + allowJavaScript: webViewAllowJS + ) + .id(webViewRefreshToken) + .frame(maxWidth: .infinity, minHeight: FIXED_PREVIEW_HEIGHT, maxHeight: FIXED_PREVIEW_HEIGHT) + .background(Color.white) + + // Bottom overlay bar pinned inside the fixed area + VStack(spacing: 0) { + Spacer() + PreviewBottomBar( + refresh: { refreshPreview() }, + reloadIgnoreCache: { webViewRefreshToken = UUID() }, + enableJS: $webViewAllowJS, + previewSource: $previewSource, + serverErrorMessage: serverErrorMessage + ) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + case .split: + HStack(spacing: 0) { + CodeFileView(editorInstance: editorInstance, codeFile: codeFile) + + // Right pane has fixed width; inside it, fixed-height WebView + VStack(spacing: 0) { + ZStack { + WebView( + html: renderedHTMLState.isEmpty + ? "

Preview Ready

" + : renderedHTMLState, + baseURL: codeFile.fileURL?.deletingLastPathComponent(), + onCrash: { /* WebKit always on */ }, + allowJavaScript: webViewAllowJS + ) + .id(webViewRefreshToken) + .frame(maxWidth: .infinity, minHeight: FIXED_PREVIEW_HEIGHT, maxHeight: FIXED_PREVIEW_HEIGHT) + .background(Color.white) + + VStack(spacing: 0) { + Spacer() + PreviewBottomBar( + refresh: { refreshPreview() }, + reloadIgnoreCache: { webViewRefreshToken = UUID() }, + enableJS: $webViewAllowJS, + previewSource: $previewSource, + serverErrorMessage: serverErrorMessage + ) + } + } + } + .frame(width: FIXED_PREVIEW_WIDTH) + .frame(maxHeight: .infinity) + .background(Color.white) + } + } + } else if isMarkdown { + switch displayMode { + case .code: + CodeFileView(editorInstance: editorInstance, codeFile: codeFile) + + case .preview: + VStack(spacing: 0) { + ZStack { + MarkdownView(source: contentString) + .frame(maxWidth: .infinity, minHeight: FIXED_PREVIEW_HEIGHT, maxHeight: FIXED_PREVIEW_HEIGHT) + .background(Color.white) + + VStack(spacing: 0) { + Spacer() + PreviewBottomBar( + refresh: { refreshPreview() }, + reloadIgnoreCache: { webViewRefreshToken = UUID() }, + enableJS: $webViewAllowJS, + previewSource: $previewSource, + serverErrorMessage: serverErrorMessage + ) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + case .split: + HStack(spacing: 0) { + CodeFileView(editorInstance: editorInstance, codeFile: codeFile) + VStack(spacing: 0) { + ZStack { + MarkdownView(source: contentString) + .frame(maxWidth: .infinity, minHeight: FIXED_PREVIEW_HEIGHT, maxHeight: FIXED_PREVIEW_HEIGHT) + .background(Color.white) + VStack(spacing: 0) { + Spacer() + PreviewBottomBar( + refresh: { refreshPreview() }, + reloadIgnoreCache: { webViewRefreshToken = UUID() }, + enableJS: $webViewAllowJS, + previewSource: $previewSource, + serverErrorMessage: serverErrorMessage + ) + } + } + } + .frame(minWidth: FIXED_PREVIEW_WIDTH, + idealWidth: FIXED_PREVIEW_WIDTH, + maxWidth: FIXED_PREVIEW_WIDTH, + minHeight: nil, idealHeight: nil, + maxHeight: .infinity, alignment: .center) + .background(Color.white) + } + } + } else if let utType = codeFile.utType, utType.conforms(to: .text) { + CodeFileView(editorInstance: editorInstance, codeFile: codeFile) + + } else { + NonTextFileView(fileDocument: codeFile) + .padding(.top, edgeInsets.top - 1.74) + .padding(.bottom, StatusBarView.height + 1.26) + .modifier(UpdateStatusBarInfo(with: codeFile.fileURL)) + .onDisappear { + statusBarViewModel.dimensions = nil + statusBarViewModel.fileSize = nil + } + } + } + .onAppear { + bindContent() + let sourceString = codeFile.content?.string ?? "" + contentString = sourceString + Task { @MainActor in + let html = await htmlRenderer.renderHTML(from: sourceString) + serverErrorMessage = nil + if renderedHTMLState != html { + renderedHTMLState = html + print("[Preview] Initial set. html length: \(html.count)") + } + if previewSource == .serverPreview { + await loadServerPreview(with: sourceString) } + } } } @@ -45,12 +564,51 @@ struct EditorAreaFileView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .onHover { hover in DispatchQueue.main.async { - if hover { - NSCursor.iBeam.push() - } else { - NSCursor.pop() - } + if hover { NSCursor.iBeam.push() } else { NSCursor.pop() } } } } + + struct EditorArea: View { + let editorInstance: EditorInstance + let codeFile: CodeFileDocument + + private let renderer = HTMLRenderer( + render: { source in + func isHTML(_ sourceString: String) -> Bool { + let trimed = sourceString.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + return trimed.hasPrefix("", with: ">") + return """ + + + + + + +
\(escaped)
+ + """ + }, + loggingEnabled: true + ) + + var body: some View { + EditorAreaFileView( + editorInstance: editorInstance, + codeFile: codeFile, + htmlRenderer: renderer, + enablePreviewLogging: true + ) + } + } } + diff --git a/CodeEdit/Features/Welcome/NewFileButton.swift b/CodeEdit/Features/Welcome/NewFileButton.swift index 75261faee5..7069713a34 100644 --- a/CodeEdit/Features/Welcome/NewFileButton.swift +++ b/CodeEdit/Features/Welcome/NewFileButton.swift @@ -17,9 +17,22 @@ struct NewFileButton: View { iconName: "plus.square", title: "Create New File...", action: { - let documentController = CodeEditDocumentController() + let documentController = CodeEditDocumentControllerProvider.sharedDocumentController() documentController.createAndOpenNewDocument(onCompletion: { dismissWindow() }) } ) } } + +private enum CodeEditDocumentControllerProvider { + static func sharedDocumentController() -> CodeEditDocumentController { + if let typed = NSDocumentController.shared as? CodeEditDocumentController { + return typed + } + // Fall back to our own singleton instance without mutating the system `shared` + struct Holder { + static let instance = CodeEditDocumentController() + } + return Holder.instance + } +} From 76708f5f49edec32e2cfe9e9af82749f8765e05d Mon Sep 17 00:00:00 2001 From: Hunter Davis Date: Tue, 2 Dec 2025 17:22:40 -0500 Subject: [PATCH 2/9] Live updates --- .../Editor/Views/EditorAreaFileView.swift | 167 ++++++++++++------ 1 file changed, 111 insertions(+), 56 deletions(-) diff --git a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift index 3e72ac1f55..ac16a36709 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift @@ -12,6 +12,8 @@ import SwiftUI import WebKit import UniformTypeIdentifiers import Combine +import Foundation +import Darwin // MARK: - Display Modes enum EditorDisplayMode: String, CaseIterable, Identifiable { @@ -69,7 +71,6 @@ final class PreviewNavDelegate: NSObject, WKNavigationDelegate { } // MARK: - WebView (Coordinator reuse, safe refresh) - struct WebView: NSViewRepresentable { let html: String let baseURL: URL? @@ -199,7 +200,6 @@ struct PreviewBottomBar: View { Button("Refresh") { refresh() } .keyboardShortcut("r", modifiers: []) Button("Reload (ignore cache)") { reloadIgnoreCache() } - // WebKit is always enabled; remove toggle. Toggle("Enable JS", isOn: $enableJS) Picker("Preview Source", selection: $previewSource) { Text("Local").tag(PreviewSource.localHTML) @@ -231,6 +231,59 @@ struct PreviewBottomBar: View { } } +// MARK: - Directory Watcher (helper) +final class DirectoryWatcher { + private var fd: CInt = -1 + private var source: DispatchSourceFileSystemObject? + private let queue = DispatchQueue(label: "codeedit.filewatch.queue") + private var lastEventAt: Date = .distantPast + private let debounceInterval: TimeInterval = 0.25 + + func startWatching(url: URL, onChange: @escaping () -> Void, onError: @escaping (Error) -> Void) { + stop() + + let dirURL = url.deletingLastPathComponent() + fd = open(dirURL.path, O_EVTONLY) + guard fd >= 0 else { + onError(NSError(domain: "DirectoryWatcher", code: 1, userInfo: [ + NSLocalizedDescriptionKey: "Failed to open directory: \(dirURL.path)" + ])) + return + } + + let mask: DispatchSource.FileSystemEvent = [.write, .rename, .delete, .attrib, .extend] + let src = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fd, eventMask: mask, queue: queue) + source = src + + src.setEventHandler { [weak self] in + guard let self else { return } + let now = Date() + if now.timeIntervalSince(self.lastEventAt) < self.debounceInterval { return } + self.lastEventAt = now + onChange() // caller hops to main if needed + } + + src.setCancelHandler { [weak self] in + guard let self else { return } + if self.fd >= 0 { close(self.fd) } + self.fd = -1 + self.source = nil + } + + src.resume() + print("[FS Watch] Started for directory: \(dirURL.path)") + } + + func stop() { + source?.cancel() + source = nil + if fd >= 0 { close(fd) } + fd = -1 + } + + deinit { stop() } +} + // MARK: - Main View struct EditorAreaFileView: View { @EnvironmentObject private var editorManager: EditorManager @@ -256,7 +309,6 @@ struct EditorAreaFileView: View { - - -
\(escaped)
- - """ - }, - loggingEnabled: true - ) - - var body: some View { - EditorAreaFileView( - editorInstance: editorInstance, - codeFile: codeFile, - htmlRenderer: renderer, - enablePreviewLogging: true - ) - } - } } - From 14d0b7f4806bfb4527dc2fbacba228cb4cebc254 Mon Sep 17 00:00:00 2001 From: Hunter Davis Date: Tue, 9 Dec 2025 17:13:54 -0500 Subject: [PATCH 3/9] Web view show/hide --- .../Editor/Views/EditorAreaFileView.swift | 308 ++++++++++++++---- 1 file changed, 244 insertions(+), 64 deletions(-) diff --git a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift index ac16a36709..8975bd956f 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift @@ -1,3 +1,6 @@ +// swiftlint:disable line_length +// swiftLint:disable file_length +// swiftlint:disable type_body_length // // EditorAreaFileView.swift // CodeEdit @@ -329,6 +332,9 @@ struct EditorAreaFileView: View { @State private var cancellables = Set() @State private var renderWorkItem: DispatchWorkItem? + // NEW: preview visibility toggle + @State private var showPreviewPane: Bool = true + @State private var webViewAllowJS: Bool = false @State private var previewSource: PreviewSource = .localHTML @@ -433,7 +439,7 @@ struct EditorAreaFileView: View { } } - // MARK: - FS Watch helpers + // MARK: - FS Watch helpers private func startFileWatchIfNeeded() { guard let fileURL = codeFile.fileURL else { return } @@ -501,38 +507,7 @@ struct EditorAreaFileView: View { CodeFileView(editorInstance: editorInstance, codeFile: codeFile) case .preview: - VStack(spacing: 0) { - ZStack { - WebView( - html: renderedHTMLState.isEmpty - ? "

Preview Ready

" - : renderedHTMLState, - baseURL: codeFile.fileURL?.deletingLastPathComponent(), - onCrash: { /* WebKit always on; show message if needed */ }, - allowJavaScript: webViewAllowJS - ) - .id(webViewRefreshToken) - .frame(maxWidth: .infinity, minHeight: FIXED_PREVIEW_HEIGHT, maxHeight: FIXED_PREVIEW_HEIGHT) - .background(Color.white) - - VStack(spacing: 0) { - Spacer() - PreviewBottomBar( - refresh: { refreshPreview() }, - reloadIgnoreCache: { webViewRefreshToken = UUID() }, - enableJS: $webViewAllowJS, - previewSource: $previewSource, - serverErrorMessage: serverErrorMessage - ) - } - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - - case .split: - HStack(spacing: 0) { - CodeFileView(editorInstance: editorInstance, codeFile: codeFile) - + if showPreviewPane { VStack(spacing: 0) { ZStack { WebView( @@ -540,7 +515,7 @@ struct EditorAreaFileView: View { ? "

Preview Ready

" : renderedHTMLState, baseURL: codeFile.fileURL?.deletingLastPathComponent(), - onCrash: { /* WebKit always on */ }, + onCrash: { /* WebKit always on; show message if needed */ }, allowJavaScript: webViewAllowJS ) .id(webViewRefreshToken) @@ -558,46 +533,144 @@ struct EditorAreaFileView: View { ) } } + // Ensure overlay respects safe area and is not clipped + .overlay(alignment: .topTrailing) { + Button { + withAnimation(.easeInOut(duration: 0.18)) { + showPreviewPane = false + } + } label: { + Image(systemName: "eye.slash") + .font(.system(size: 14, weight: .medium)) + .padding(6) + .background(Color.black.opacity(0.12)) + .clipShape(Circle()) + } + .buttonStyle(.plain) + .help("Hide Preview") + .padding(.top, edgeInsets.top + 8) + .padding(.trailing, 8) + .zIndex(10) + } } - .frame(width: FIXED_PREVIEW_WIDTH) - .frame(maxHeight: .infinity) - .background(Color.white) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + // Preview hidden in Preview mode — show a center "show" button + VStack { + Spacer() + Button { + withAnimation(.easeInOut(duration: 0.18)) { + showPreviewPane = true + } + } label: { + Image(systemName: "eye") + .font(.system(size: 20, weight: .semibold)) + .padding(10) + .background(Color.black.opacity(0.08)) + .clipShape(Circle()) + } + .buttonStyle(.plain) + .help("Show Preview") + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(NSColor.windowBackgroundColor)) } - } - } else if isMarkdown { - switch displayMode { - case .code: - CodeFileView(editorInstance: editorInstance, codeFile: codeFile) - case .preview: - VStack(spacing: 0) { + case .split: + HStack(spacing: 0) { + // Wrap code view so we can show the "show preview" button when preview is hidden ZStack { - MarkdownView(source: contentString) - .frame(maxWidth: .infinity, minHeight: FIXED_PREVIEW_HEIGHT, maxHeight: FIXED_PREVIEW_HEIGHT) - .background(Color.white) + CodeFileView(editorInstance: editorInstance, codeFile: codeFile) + + if !showPreviewPane { + // small overlay button at top-right of code area to restore preview + Button { + withAnimation(.easeInOut(duration: 0.18)) { + showPreviewPane = true + } + } label: { + Image(systemName: "eye") + .font(.system(size: 14, weight: .medium)) + .padding(6) + .background(Color.black.opacity(0.12)) + .clipShape(Circle()) + } + .buttonStyle(.plain) + .help("Show Preview") + .padding(.top, edgeInsets.top + 8) + .padding(.trailing, 8) + .zIndex(10) + } + } + .frame(minWidth: 200) + if showPreviewPane { VStack(spacing: 0) { - Spacer() - PreviewBottomBar( - refresh: { refreshPreview() }, - reloadIgnoreCache: { webViewRefreshToken = UUID() }, - enableJS: $webViewAllowJS, - previewSource: $previewSource, - serverErrorMessage: serverErrorMessage - ) + ZStack { + WebView( + html: renderedHTMLState.isEmpty + ? "

Preview Ready

" + : renderedHTMLState, + baseURL: codeFile.fileURL?.deletingLastPathComponent(), + onCrash: { /* WebKit always on */ }, + allowJavaScript: webViewAllowJS + ) + .id(webViewRefreshToken) + .frame(maxWidth: .infinity, minHeight: FIXED_PREVIEW_HEIGHT, maxHeight: FIXED_PREVIEW_HEIGHT) + .background(Color.white) + + VStack(spacing: 0) { + Spacer() + PreviewBottomBar( + refresh: { refreshPreview() }, + reloadIgnoreCache: { webViewRefreshToken = UUID() }, + enableJS: $webViewAllowJS, + previewSource: $previewSource, + serverErrorMessage: serverErrorMessage + ) + } + + // drag / divider area (kept minimal here, you can replace with more advanced drag logic) + } + .overlay(alignment: .topTrailing) { + Button { + withAnimation(.easeInOut(duration: 0.18)) { + showPreviewPane = false + } + } label: { + Image(systemName: "eye.slash") + .font(.system(size: 14, weight: .medium)) + .padding(6) + .background(Color.black.opacity(0.12)) + .clipShape(Circle()) + } + .buttonStyle(.plain) + .help("Hide Preview") + .padding(.top, edgeInsets.top + 8) + .padding(.trailing, 8) + .zIndex(10) + } } + .frame(width: FIXED_PREVIEW_WIDTH) + .frame(maxHeight: .infinity) + .background(Color.white) } } - .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } else if isMarkdown { + switch displayMode { + case .code: + CodeFileView(editorInstance: editorInstance, codeFile: codeFile) - case .split: - HStack(spacing: 0) { - CodeFileView(editorInstance: editorInstance, codeFile: codeFile) + case .preview: + if showPreviewPane { VStack(spacing: 0) { ZStack { MarkdownView(source: contentString) .frame(maxWidth: .infinity, minHeight: FIXED_PREVIEW_HEIGHT, maxHeight: FIXED_PREVIEW_HEIGHT) .background(Color.white) + VStack(spacing: 0) { Spacer() PreviewBottomBar( @@ -609,12 +682,119 @@ struct EditorAreaFileView: View { ) } } + .overlay(alignment: .topTrailing) { + Button { + withAnimation(.easeInOut(duration: 0.18)) { + showPreviewPane = false + } + } label: { + Image(systemName: "eye.slash") + .font(.system(size: 14, weight: .medium)) + .padding(6) + .background(Color.black.opacity(0.12)) + .clipShape(Circle()) + } + .buttonStyle(.plain) + .help("Hide Preview") + .padding(.top, edgeInsets.top + 8) + .padding(.trailing, 8) + .zIndex(10) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + // Preview hidden in Preview mode — show a center "show" button + VStack { + Spacer() + Button { + withAnimation(.easeInOut(duration: 0.18)) { + showPreviewPane = true + } + } label: { + Image(systemName: "eye") + .font(.system(size: 20, weight: .semibold)) + .padding(10) + .background(Color.black.opacity(0.08)) + .clipShape(Circle()) + } + .buttonStyle(.plain) + .help("Show Preview") + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(NSColor.windowBackgroundColor)) + } + + case .split: + HStack(spacing: 0) { + // Wrap code view so we can show the "show preview" button when preview is hidden + ZStack { + CodeFileView(editorInstance: editorInstance, codeFile: codeFile) + + if !showPreviewPane { + Button { + withAnimation(.easeInOut(duration: 0.18)) { + showPreviewPane = true + } + } label: { + Image(systemName: "eye") + .font(.system(size: 14, weight: .medium)) + .padding(6) + .background(Color.black.opacity(0.12)) + .clipShape(Circle()) + } + .buttonStyle(.plain) + .help("Show Preview") + .padding(.top, edgeInsets.top + 8) + .padding(.trailing, 8) + .zIndex(10) + } + } + .frame(minWidth: 200) + + if showPreviewPane { + VStack(spacing: 0) { + ZStack { + MarkdownView(source: contentString) + .frame(maxWidth: .infinity, minHeight: FIXED_PREVIEW_HEIGHT, maxHeight: FIXED_PREVIEW_HEIGHT) + .background(Color.white) + + VStack(spacing: 0) { + Spacer() + PreviewBottomBar( + refresh: { refreshPreview() }, + reloadIgnoreCache: { webViewRefreshToken = UUID() }, + enableJS: $webViewAllowJS, + previewSource: $previewSource, + serverErrorMessage: serverErrorMessage + ) + } + } + .overlay(alignment: .topTrailing) { + Button { + withAnimation(.easeInOut(duration: 0.18)) { + showPreviewPane = false + } + } label: { + Image(systemName: "eye.slash") + .font(.system(size: 14, weight: .medium)) + .padding(6) + .background(Color.black.opacity(0.12)) + .clipShape(Circle()) + } + .buttonStyle(.plain) + .help("Hide Preview") + .padding(.top, edgeInsets.top + 8) + .padding(.trailing, 8) + .zIndex(10) + } + } + .frame(minWidth: FIXED_PREVIEW_WIDTH, + idealWidth: FIXED_PREVIEW_WIDTH, + maxWidth: FIXED_PREVIEW_WIDTH, + maxHeight: .infinity, alignment: .center) + .background(Color.white) } - .frame(minWidth: FIXED_PREVIEW_WIDTH, - idealWidth: FIXED_PREVIEW_WIDTH, - maxWidth: FIXED_PREVIEW_WIDTH, - maxHeight: .infinity, alignment: .center) - .background(Color.white) } } } else if let utType = codeFile.utType, utType.conforms(to: .text) { From 4499fdd38adfb53b8f8618e106403898b773e323 Mon Sep 17 00:00:00 2001 From: Hunter Davis Date: Wed, 10 Dec 2025 22:59:51 -0500 Subject: [PATCH 4/9] Added Resizing --- .../Editor/Views/EditorAreaFileView.swift | 143 +++++++++++------- 1 file changed, 89 insertions(+), 54 deletions(-) diff --git a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift index 8975bd956f..6fd4c38fca 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift @@ -53,7 +53,7 @@ struct HTMLRenderer { } } -// MARK: - WebKit Crash-Aware Delegate +// MARK: - WebKit Crash Delegate final class PreviewNavDelegate: NSObject, WKNavigationDelegate { var onCrash: (() -> Void)? @@ -73,7 +73,7 @@ final class PreviewNavDelegate: NSObject, WKNavigationDelegate { } } -// MARK: - WebView (Coordinator reuse, safe refresh) +// MARK: - WebView struct WebView: NSViewRepresentable { let html: String let baseURL: URL? @@ -347,9 +347,13 @@ struct EditorAreaFileView: View { @State private var watcher = DirectoryWatcher() @State private var lastKnownFileMTime: Date? - // Fixed size constants - private let FIXED_PREVIEW_HEIGHT: CGFloat = 320 - private let FIXED_PREVIEW_WIDTH: CGFloat = 420 + // Width-resize state + @State private var previewWidth: CGFloat = 420 // default width for split mode + @State private var dragStartPreviewWidth: CGFloat? + + // Min/max constraints for width-only resizing + private let MIN_PREVIEW_WIDTH: CGFloat = 200 + private let MAX_PREVIEW_WIDTH: CGFloat = 1400 private func bindContent() { NotificationCenter.default.publisher( @@ -501,6 +505,26 @@ struct EditorAreaFileView: View { Divider() + // thin draggable vertical divider between code and preview (width-resize) + let verticalDivider: some View = Rectangle() + .fill(Color(NSColor.separatorColor)) + .frame(width: 2) + // give a larger transparent hit area so it is easy to grab + .padding(.horizontal, 6) + .contentShape(Rectangle()) + .gesture(DragGesture(minimumDistance: 1).onChanged { value in + if dragStartPreviewWidth == nil { dragStartPreviewWidth = previewWidth } + let start = dragStartPreviewWidth ?? previewWidth + let delta = value.location.x - value.startLocation.x + let newW = start - delta + previewWidth = max(MIN_PREVIEW_WIDTH, min(MAX_PREVIEW_WIDTH, newW)) + }.onEnded { _ in + dragStartPreviewWidth = nil + }) + .onHover { hovering in + if hovering { NSCursor.resizeLeftRight.push() } else { NSCursor.pop() } + } + if isHTML { switch displayMode { case .code: @@ -519,7 +543,9 @@ struct EditorAreaFileView: View { allowJavaScript: webViewAllowJS ) .id(webViewRefreshToken) - .frame(maxWidth: .infinity, minHeight: FIXED_PREVIEW_HEIGHT, maxHeight: FIXED_PREVIEW_HEIGHT) + .frame(maxWidth: .infinity, minHeight: 320, maxHeight: .infinity) + // ensure top safe area isn't covered — add top padding to push web content down + .padding(.top, edgeInsets.top) .background(Color.white) VStack(spacing: 0) { @@ -579,7 +605,7 @@ struct EditorAreaFileView: View { case .split: HStack(spacing: 0) { - // Wrap code view so we can show the "show preview" button when preview is hidden + // Code area ZStack { CodeFileView(editorInstance: editorInstance, codeFile: codeFile) @@ -606,53 +632,57 @@ struct EditorAreaFileView: View { .frame(minWidth: 200) if showPreviewPane { - VStack(spacing: 0) { - ZStack { - WebView( - html: renderedHTMLState.isEmpty - ? "

Preview Ready

" - : renderedHTMLState, - baseURL: codeFile.fileURL?.deletingLastPathComponent(), - onCrash: { /* WebKit always on */ }, - allowJavaScript: webViewAllowJS - ) - .id(webViewRefreshToken) - .frame(maxWidth: .infinity, minHeight: FIXED_PREVIEW_HEIGHT, maxHeight: FIXED_PREVIEW_HEIGHT) - .background(Color.white) + // divider placed between code and preview (gesture here) + verticalDivider + .zIndex(5) - VStack(spacing: 0) { - Spacer() - PreviewBottomBar( - refresh: { refreshPreview() }, - reloadIgnoreCache: { webViewRefreshToken = UUID() }, - enableJS: $webViewAllowJS, - previewSource: $previewSource, - serverErrorMessage: serverErrorMessage - ) - } + // Preview container constrained to adjustable width + ZStack { + WebView( + html: renderedHTMLState.isEmpty + ? "

Preview Ready

" + : renderedHTMLState, + baseURL: codeFile.fileURL?.deletingLastPathComponent(), + onCrash: { /* WebKit always on */ }, + allowJavaScript: webViewAllowJS + ) + .id(webViewRefreshToken) + .frame(maxWidth: .infinity, minHeight: 320, maxHeight: .infinity) + // push content below top chrome so h1 isn't cut off + .padding(.top, edgeInsets.top) + .background(Color.white) - // drag / divider area (kept minimal here, you can replace with more advanced drag logic) + VStack(spacing: 0) { + Spacer() + PreviewBottomBar( + refresh: { refreshPreview() }, + reloadIgnoreCache: { webViewRefreshToken = UUID() }, + enableJS: $webViewAllowJS, + previewSource: $previewSource, + serverErrorMessage: serverErrorMessage + ) } - .overlay(alignment: .topTrailing) { - Button { - withAnimation(.easeInOut(duration: 0.18)) { - showPreviewPane = false - } - } label: { - Image(systemName: "eye.slash") - .font(.system(size: 14, weight: .medium)) - .padding(6) - .background(Color.black.opacity(0.12)) - .clipShape(Circle()) + } + .overlay(alignment: .topTrailing) { + Button { + withAnimation(.easeInOut(duration: 0.18)) { + showPreviewPane = false } - .buttonStyle(.plain) - .help("Hide Preview") - .padding(.top, edgeInsets.top + 8) - .padding(.trailing, 8) - .zIndex(10) + } label: { + Image(systemName: "eye.slash") + .font(.system(size: 14, weight: .medium)) + .padding(6) + .background(Color.black.opacity(0.12)) + .clipShape(Circle()) } + .buttonStyle(.plain) + .help("Hide Preview") + .padding(.top, edgeInsets.top + 8) + .padding(.trailing, 8) + .zIndex(10) } - .frame(width: FIXED_PREVIEW_WIDTH) + // Constrain preview to the adjustable width + .frame(width: previewWidth) .frame(maxHeight: .infinity) .background(Color.white) } @@ -668,7 +698,8 @@ struct EditorAreaFileView: View { VStack(spacing: 0) { ZStack { MarkdownView(source: contentString) - .frame(maxWidth: .infinity, minHeight: FIXED_PREVIEW_HEIGHT, maxHeight: FIXED_PREVIEW_HEIGHT) + .frame(maxWidth: .infinity, minHeight: 320, maxHeight: .infinity) + .padding(.top, edgeInsets.top) .background(Color.white) VStack(spacing: 0) { @@ -727,7 +758,7 @@ struct EditorAreaFileView: View { case .split: HStack(spacing: 0) { - // Wrap code view so we can show the "show preview" button when preview is hidden + // Code area ZStack { CodeFileView(editorInstance: editorInstance, codeFile: codeFile) @@ -753,10 +784,14 @@ struct EditorAreaFileView: View { .frame(minWidth: 200) if showPreviewPane { + verticalDivider + .zIndex(5) + VStack(spacing: 0) { ZStack { MarkdownView(source: contentString) - .frame(maxWidth: .infinity, minHeight: FIXED_PREVIEW_HEIGHT, maxHeight: FIXED_PREVIEW_HEIGHT) + .frame(maxWidth: .infinity, minHeight: 320, maxHeight: .infinity) + .padding(.top, edgeInsets.top) .background(Color.white) VStack(spacing: 0) { @@ -789,9 +824,9 @@ struct EditorAreaFileView: View { .zIndex(10) } } - .frame(minWidth: FIXED_PREVIEW_WIDTH, - idealWidth: FIXED_PREVIEW_WIDTH, - maxWidth: FIXED_PREVIEW_WIDTH, + .frame(minWidth: previewWidth, + idealWidth: previewWidth, + maxWidth: previewWidth, maxHeight: .infinity, alignment: .center) .background(Color.white) } From 78d8a16c713f8284ad98e24045789b61a97e18fb Mon Sep 17 00:00:00 2001 From: Hunter Davis Date: Thu, 11 Dec 2025 20:23:09 -0500 Subject: [PATCH 5/9] Disabling app sandbox --- CodeEdit.xcodeproj/project.pbxproj | 70 +++--------------------------- 1 file changed, 5 insertions(+), 65 deletions(-) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index cd3ef1c94a..4b371707f7 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -674,21 +674,9 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\""; DEVELOPMENT_TEAM = ""; - ENABLE_APP_SANDBOX = YES; - ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; - ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; - ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; - ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; - ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; - ENABLE_RESOURCE_ACCESS_CALENDARS = NO; - ENABLE_RESOURCE_ACCESS_CAMERA = NO; - ENABLE_RESOURCE_ACCESS_CONTACTS = NO; - ENABLE_RESOURCE_ACCESS_LOCATION = NO; - ENABLE_RESOURCE_ACCESS_PRINTING = NO; - ENABLE_RESOURCE_ACCESS_USB = NO; - ENABLE_USER_SELECTED_FILES = readwrite; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -887,21 +875,9 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\""; DEVELOPMENT_TEAM = ""; - ENABLE_APP_SANDBOX = YES; - ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; - ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; - ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; - ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; - ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; - ENABLE_RESOURCE_ACCESS_CALENDARS = NO; - ENABLE_RESOURCE_ACCESS_CAMERA = NO; - ENABLE_RESOURCE_ACCESS_CONTACTS = NO; - ENABLE_RESOURCE_ACCESS_LOCATION = NO; - ENABLE_RESOURCE_ACCESS_PRINTING = NO; - ENABLE_RESOURCE_ACCESS_USB = NO; - ENABLE_USER_SELECTED_FILES = readwrite; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -1172,21 +1148,9 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\""; DEVELOPMENT_TEAM = ""; - ENABLE_APP_SANDBOX = YES; - ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; - ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; - ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; - ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; - ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; - ENABLE_RESOURCE_ACCESS_CALENDARS = NO; - ENABLE_RESOURCE_ACCESS_CAMERA = NO; - ENABLE_RESOURCE_ACCESS_CONTACTS = NO; - ENABLE_RESOURCE_ACCESS_LOCATION = NO; - ENABLE_RESOURCE_ACCESS_PRINTING = NO; - ENABLE_RESOURCE_ACCESS_USB = NO; - ENABLE_USER_SELECTED_FILES = readwrite; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -1457,21 +1421,9 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\""; DEVELOPMENT_TEAM = ""; - ENABLE_APP_SANDBOX = YES; - ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; - ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; - ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; - ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; - ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; - ENABLE_RESOURCE_ACCESS_CALENDARS = NO; - ENABLE_RESOURCE_ACCESS_CAMERA = NO; - ENABLE_RESOURCE_ACCESS_CONTACTS = NO; - ENABLE_RESOURCE_ACCESS_LOCATION = NO; - ENABLE_RESOURCE_ACCESS_PRINTING = NO; - ENABLE_RESOURCE_ACCESS_USB = NO; - ENABLE_USER_SELECTED_FILES = readwrite; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -1513,21 +1465,9 @@ DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"CodeEdit/Preview Content\""; DEVELOPMENT_TEAM = ""; - ENABLE_APP_SANDBOX = YES; - ENABLE_FILE_ACCESS_DOWNLOADS_FOLDER = readwrite; + ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; - ENABLE_INCOMING_NETWORK_CONNECTIONS = YES; - ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_PREVIEWS = YES; - ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; - ENABLE_RESOURCE_ACCESS_BLUETOOTH = NO; - ENABLE_RESOURCE_ACCESS_CALENDARS = NO; - ENABLE_RESOURCE_ACCESS_CAMERA = NO; - ENABLE_RESOURCE_ACCESS_CONTACTS = NO; - ENABLE_RESOURCE_ACCESS_LOCATION = NO; - ENABLE_RESOURCE_ACCESS_PRINTING = NO; - ENABLE_RESOURCE_ACCESS_USB = NO; - ENABLE_USER_SELECTED_FILES = readwrite; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; From e600316424d7cb772e5c49a41ae74097da3f21c7 Mon Sep 17 00:00:00 2001 From: Hunter Davis Date: Mon, 5 Jan 2026 14:05:20 -0500 Subject: [PATCH 6/9] Fixed SwiftLint warnings --- CodeEdit.xcodeproj/project.pbxproj | 55 +++ CodeEdit/CodeEdit.entitlements | 10 +- .../Editor/Views/EditorAreaFileView.swift | 361 +++++++++++++----- 3 files changed, 321 insertions(+), 105 deletions(-) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 23cad1267c..6fde2a01b9 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -665,6 +665,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "${CE_APPICON_NAME}"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + AUTOMATION_APPLE_EVENTS = NO; CODE_SIGN_ENTITLEMENTS = CodeEdit/CodeEdit.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; @@ -677,6 +678,12 @@ ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -692,7 +699,11 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; REGISTER_APP_GROUPS = YES; + RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; RUNTIME_EXCEPTION_ALLOW_JIT = YES; + RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO; + RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO; + RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO; RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = YES; RUN_DOCUMENTATION_COMPILER = NO; SWIFT_EMIT_LOC_STRINGS = YES; @@ -866,6 +877,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "${CE_APPICON_NAME}"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + AUTOMATION_APPLE_EVENTS = NO; CODE_SIGN_ENTITLEMENTS = CodeEdit/CodeEdit.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; @@ -878,6 +890,12 @@ ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -893,7 +911,11 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; REGISTER_APP_GROUPS = YES; + RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; RUNTIME_EXCEPTION_ALLOW_JIT = YES; + RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO; + RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO; + RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO; RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = YES; RUN_DOCUMENTATION_COMPILER = NO; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1138,6 +1160,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "${CE_APPICON_NAME}"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + AUTOMATION_APPLE_EVENTS = NO; CE_APPICON_NAME = AppIconPre; CODE_SIGN_ENTITLEMENTS = CodeEdit/CodeEdit.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; @@ -1151,6 +1174,12 @@ ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -1166,7 +1195,11 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; REGISTER_APP_GROUPS = YES; + RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; RUNTIME_EXCEPTION_ALLOW_JIT = YES; + RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO; + RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO; + RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO; RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = YES; RUN_DOCUMENTATION_COMPILER = NO; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1412,6 +1445,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "${CE_APPICON_NAME}"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + AUTOMATION_APPLE_EVENTS = NO; CODE_SIGN_ENTITLEMENTS = CodeEdit/CodeEdit.entitlements; CODE_SIGN_IDENTITY = "-"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; @@ -1424,6 +1458,12 @@ ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -1439,7 +1479,11 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; REGISTER_APP_GROUPS = YES; + RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; RUNTIME_EXCEPTION_ALLOW_JIT = YES; + RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO; + RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO; + RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO; RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = YES; RUN_DOCUMENTATION_COMPILER = NO; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1456,6 +1500,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = "${CE_APPICON_NAME}"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + AUTOMATION_APPLE_EVENTS = NO; CODE_SIGN_ENTITLEMENTS = CodeEdit/CodeEdit.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; @@ -1468,6 +1513,12 @@ ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; @@ -1483,7 +1534,11 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; REGISTER_APP_GROUPS = YES; + RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; RUNTIME_EXCEPTION_ALLOW_JIT = YES; + RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO; + RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO; + RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO; RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = YES; RUN_DOCUMENTATION_COMPILER = NO; SWIFT_EMIT_LOC_STRINGS = YES; 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 6fd4c38fca..16cffc969d 100644 --- a/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift +++ b/CodeEdit/Features/Editor/Views/EditorAreaFileView.swift @@ -1,6 +1,5 @@ -// swiftlint:disable line_length -// swiftLint:disable file_length -// swiftlint:disable type_body_length +// swiftlint:disable file_length + // // EditorAreaFileView.swift // CodeEdit @@ -17,6 +16,13 @@ import UniformTypeIdentifiers import Combine import Foundation import Darwin +import os + +// Lightweight logger to replace `print` calls (avoids SwiftLint's print_usage warning) +private let previewLogger = Logger( + subsystem: Bundle.main.bundleIdentifier ?? "CodeEdit", + category: "EditorPreview" +) // MARK: - Display Modes enum EditorDisplayMode: String, CaseIterable, Identifiable { @@ -39,7 +45,11 @@ struct HTMLRenderer { var loggingEnabled: Bool = false func renderHTML(from source: String) async -> String { - if loggingEnabled { print("[Preview] Rendering start. Source length: \(source.count)") } + if loggingEnabled { + previewLogger.debug( + "[Preview] Rendering start. Source length: \(source.count)" + ) + } let output: String if let renderAsync { output = await renderAsync(source) @@ -48,7 +58,11 @@ struct HTMLRenderer { } else { output = source } - if loggingEnabled { print("[Preview] Rendering done. HTML length: \(output.count)") } + if loggingEnabled { + previewLogger.debug( + "[Preview] Rendering done. HTML length: \(output.count)" + ) + } return output } } @@ -58,60 +72,98 @@ final class PreviewNavDelegate: NSObject, WKNavigationDelegate { var onCrash: (() -> Void)? func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { - print("[WebView] WebContent process terminated") + previewLogger.warning("[WebView] WebContent process terminated") onCrash?() } - func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { - print("[WebView] didFailProvisionalNavigation: \(error.localizedDescription)") + + func webView( + _ webView: WKWebView, + didFailProvisionalNavigation navigation: WKNavigation!, + withError error: Error + ) { + previewLogger.error( + "[WebView] didFailProvisionalNavigation: \(error.localizedDescription)" + ) onCrash?() } - func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { - print("[WebView] didFail navigation: \(error.localizedDescription)") + + func webView( + _ webView: WKWebView, + didFail navigation: WKNavigation!, + withError error: Error + ) { + previewLogger.error( + "[WebView] didFail navigation: \(error.localizedDescription)" + ) } + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - print("[WebView] didFinish navigation. URL: \(webView.url?.absoluteString ?? "nil")") + previewLogger.debug( + "[WebView] didFinish navigation. URL: \(webView.url?.absoluteString ?? "nil")" + ) } } -// MARK: - WebView +// MARK: - WebView struct WebView: NSViewRepresentable { let html: String let baseURL: URL? let onCrash: () -> Void let allowJavaScript: Bool - class Coordinator { + final class Coordinator { let webView: WKWebView let navDelegate = PreviewNavDelegate() - var lastHTML: String = "" - var lastLoadAt: Date = .distantPast + private(set) var lastHTML: String = "" + private(set) var lastLoadAt: Date = .distantPast + var allowJavaScript: Bool init(onCrash: @escaping () -> Void, allowJavaScript: Bool) { + self.allowJavaScript = allowJavaScript + let config = WKWebViewConfiguration() - config.preferences.javaScriptEnabled = allowJavaScript config.websiteDataStore = .default() config.suppressesIncrementalRendering = true - let wv = WKWebView(frame: .zero, configuration: config) - wv.setValue(false, forKey: "drawsBackground") + let webViewInstance = WKWebView(frame: .zero, configuration: config) + webViewInstance.setValue(false, forKey: "drawsBackground") navDelegate.onCrash = onCrash - wv.navigationDelegate = navDelegate - self.webView = wv + webViewInstance.navigationDelegate = navDelegate + self.webView = webViewInstance } func safeLoad(html: String, baseURL: URL?) { let now = Date() - if now.timeIntervalSince(lastLoadAt) < 1.0, lastHTML == html { return } + // Avoid redundant reloads + if now.timeIntervalSince(lastLoadAt) < 1.0, lastHTML == html { + return + } lastLoadAt = now lastHTML = html + + if #available(macOS 11.0, *) { + webView.configuration.defaultWebpagePreferences.allowsContentJavaScript = allowJavaScript + } else { + // Fallback for older macOS versions: no action + // previewLogger.warning("[WebView] JavaScript enable/disable not supported on macOS < 11") + } + webView.stopLoading() webView.loadHTMLString(html, baseURL: baseURL) } } - func makeCoordinator() -> Coordinator { Coordinator(onCrash: onCrash, allowJavaScript: allowJavaScript) } - func makeNSView(context: Context) -> WKWebView { context.coordinator.webView } - func updateNSView(_ webView: WKWebView, context: Context) { context.coordinator.safeLoad(html: html, baseURL: baseURL) } + func makeCoordinator() -> Coordinator { + Coordinator(onCrash: onCrash, allowJavaScript: allowJavaScript) + } + + func makeNSView(context: Context) -> WKWebView { + context.coordinator.webView + } + + func updateNSView(_ webView: WKWebView, context: Context) { + context.coordinator.safeLoad(html: html, baseURL: baseURL) + } } // MARK: - Markdown Renderer @@ -153,38 +205,55 @@ struct ServerPreviewClient { let timeout: TimeInterval = 8 func postHTML(_ html: String, filename: String?) async throws -> String { - var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)! + guard var components = URLComponents( + url: baseURL, + resolvingAgainstBaseURL: false + ) else { + throw URLError(.badURL) + } components.path = path guard let url = components.url else { throw URLError(.badURL) } + var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.timeoutInterval = timeout + let payload: [String: Any] = [ "html": html, "filename": filename ?? "untitled.html", "timestamp": Date().timeIntervalSince1970 ] let data = try JSONSerialization.data(withJSONObject: payload, options: []) + let configuration = URLSessionConfiguration.default configuration.timeoutIntervalForRequest = timeout configuration.timeoutIntervalForResource = timeout let session = URLSession(configuration: configuration) + let (respData, resp) = try await session.upload(for: request, from: data) - guard let http = resp as? HTTPURLResponse else { throw URLError(.badServerResponse) } - if !(200...299).contains(http.statusCode) { + guard let http = resp as? HTTPURLResponse else { + throw URLError(.badServerResponse) + } + guard (200...299).contains(http.statusCode) else { let bodyString = String(data: respData, encoding: .utf8) ?? "" - throw NSError(domain: "ServerPreview", code: http.statusCode, userInfo: [ - NSLocalizedDescriptionKey: "Server preview failed (\(http.statusCode)).", - "body": bodyString - ]) + throw NSError( + domain: "ServerPreview", + code: http.statusCode, + userInfo: [ + NSLocalizedDescriptionKey: "Server preview failed (\(http.statusCode)).", + "body": bodyString + ] + ) } return String(data: respData, encoding: .utf8) ?? "" } } extension Notification.Name { - static let CodeFileDocumentContentDidChange = Notification.Name("CodeFileDocumentContentDidChange") + static let CodeFileDocumentContentDidChange = Notification.Name( + "CodeFileDocumentContentDidChange" + ) } // MARK: - Bottom Controls Overlay @@ -236,70 +305,92 @@ struct PreviewBottomBar: View { // MARK: - Directory Watcher (helper) final class DirectoryWatcher { - private var fd: CInt = -1 + private var fileDescriptor: CInt = -1 private var source: DispatchSourceFileSystemObject? private let queue = DispatchQueue(label: "codeedit.filewatch.queue") private var lastEventAt: Date = .distantPast private let debounceInterval: TimeInterval = 0.25 - func startWatching(url: URL, onChange: @escaping () -> Void, onError: @escaping (Error) -> Void) { + func startWatching( + url: URL, + onChange: @escaping () -> Void, + onError: @escaping (Error) -> Void + ) { stop() let dirURL = url.deletingLastPathComponent() - fd = open(dirURL.path, O_EVTONLY) - guard fd >= 0 else { - onError(NSError(domain: "DirectoryWatcher", code: 1, userInfo: [ - NSLocalizedDescriptionKey: "Failed to open directory: \(dirURL.path)" - ])) + fileDescriptor = open(dirURL.path, O_EVTONLY) + guard fileDescriptor >= 0 else { + onError( + NSError( + domain: "DirectoryWatcher", + code: 1, + userInfo: [ + NSLocalizedDescriptionKey: "Failed to open directory: \(dirURL.path)" + ] + ) + ) return } - let mask: DispatchSource.FileSystemEvent = [.write, .rename, .delete, .attrib, .extend] - let src = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fd, eventMask: mask, queue: queue) + let mask: DispatchSource.FileSystemEvent = [ + .write, .rename, .delete, .attrib, .extend + ] + let src = DispatchSource.makeFileSystemObjectSource( + fileDescriptor: fileDescriptor, + eventMask: mask, + queue: queue + ) source = src src.setEventHandler { [weak self] in - guard let self else { return } + guard let strongSelf = self else { return } let now = Date() - if now.timeIntervalSince(self.lastEventAt) < self.debounceInterval { return } - self.lastEventAt = now + if now.timeIntervalSince(strongSelf.lastEventAt) < strongSelf.debounceInterval { + return + } + strongSelf.lastEventAt = now onChange() // caller hops to main if needed } src.setCancelHandler { [weak self] in - guard let self else { return } - if self.fd >= 0 { close(self.fd) } - self.fd = -1 - self.source = nil + guard let strongSelf = self else { return } + if strongSelf.fileDescriptor >= 0 { close(strongSelf.fileDescriptor) } + strongSelf.fileDescriptor = -1 + strongSelf.source = nil } src.resume() - print("[FS Watch] Started for directory: \(dirURL.path)") + previewLogger.debug("[FS Watch] Started for directory: \(dirURL.path)") } func stop() { source?.cancel() source = nil - if fd >= 0 { close(fd) } - fd = -1 + if fileDescriptor >= 0 { close(fileDescriptor) } + fileDescriptor = -1 } deinit { stop() } } // MARK: - Main View +// swiftlint:disable:next type_body_length struct EditorAreaFileView: View { @EnvironmentObject private var editorManager: EditorManager @EnvironmentObject private var editor: Editor @EnvironmentObject private var statusBarViewModel: StatusBarViewModel - @Environment(\.edgeInsets) private var edgeInsets + @Environment(\.edgeInsets) + private var edgeInsets var editorInstance: EditorInstance var codeFile: CodeFileDocument var htmlRenderer: HTMLRenderer = .init( render: { source in - let trimmed = source.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + let trimmed = source + .trimmingCharacters(in: .whitespacesAndNewlines) + .lowercased() let looksHTML = trimmed.hasPrefix(" @@ -324,6 +421,7 @@ struct EditorAreaFileView: View { }, loggingEnabled: true ) + var enablePreviewLogging: Bool = true @State private var renderedHTMLState: String = "" @@ -352,8 +450,8 @@ struct EditorAreaFileView: View { @State private var dragStartPreviewWidth: CGFloat? // Min/max constraints for width-only resizing - private let MIN_PREVIEW_WIDTH: CGFloat = 200 - private let MAX_PREVIEW_WIDTH: CGFloat = 1400 + private let minPreviewWidth: CGFloat = 200 + private let maxPreviewWidth: CGFloat = 1400 private func bindContent() { NotificationCenter.default.publisher( @@ -373,7 +471,9 @@ struct EditorAreaFileView: View { renderWorkItem?.cancel() let work = DispatchWorkItem { Task { @MainActor in - if enablePreviewLogging { print("[Preview] Source changed. Rendering…") } + if enablePreviewLogging { + previewLogger.debug("[Preview] Source changed. Rendering…") + } switch previewSource { case .localHTML: let html = await htmlRenderer.renderHTML(from: source) @@ -381,15 +481,19 @@ struct EditorAreaFileView: View { if renderedHTMLState != html { renderedHTMLState = html webViewRefreshToken = UUID() - print("[Preview] Local HTML set. length: \(html.count)") + previewLogger.debug( + "[Preview] Local HTML set. length: \(html.count)" + ) } else { - print("[Preview] No state change (same HTML)") + previewLogger.debug("[Preview] No state change (same HTML)") } case .serverPreview: let localHTML = await htmlRenderer.renderHTML(from: source) if renderedHTMLState != localHTML { renderedHTMLState = localHTML - print("[Preview] Fallback local HTML set. length: \(localHTML.count)") + previewLogger.debug( + "[Preview] Fallback local HTML set. length: \(localHTML.count)" + ) } await loadServerPreview(with: source) } @@ -407,24 +511,32 @@ struct EditorAreaFileView: View { let serverHTML = try await serverClient.postHTML(source, filename: filename) if renderedHTMLState != serverHTML { renderedHTMLState = serverHTML - print("[Preview] Server HTML set. length: \(serverHTML.count)") + previewLogger.debug( + "[Preview] Server HTML set. length: \(serverHTML.count)" + ) } else { - print("[Preview] Server returned identical HTML; no state change.") + previewLogger.debug( + "[Preview] Server returned identical HTML; no state change." + ) } } catch { let message: String if let urlError = error as? URLError { switch urlError.code { - case .cannotFindHost: message = "Cannot find server at localhost:3000." - case .timedOut: message = "Server preview timed out." - case .notConnectedToInternet: message = "No network connection." - default: message = "Network error: \(urlError.localizedDescription)" + case .cannotFindHost: + message = "Cannot find server at localhost:3000." + case .timedOut: + message = "Server preview timed out." + case .notConnectedToInternet: + message = "No network connection." + default: + message = "Network error: \(urlError.localizedDescription)" } } else { message = error.localizedDescription } serverErrorMessage = message - print("[Preview] Server preview error: \(message)") + previewLogger.error("[Preview] Server preview error: \(message)") } } @@ -436,7 +548,7 @@ struct EditorAreaFileView: View { } private func refreshPreview() { - print("[Preview] Manual refresh triggered") + previewLogger.debug("[Preview] Manual refresh triggered") scheduleRender(for: contentString) DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { webViewRefreshToken = UUID() @@ -448,11 +560,15 @@ struct EditorAreaFileView: View { guard let fileURL = codeFile.fileURL else { return } // Record current mtime to filter unrelated directory events - lastKnownFileMTime = (try? fileURL.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate + lastKnownFileMTime = (try? fileURL.resourceValues( + forKeys: [.contentModificationDateKey] + ))?.contentModificationDate watcher.startWatching(url: fileURL) { [fileURL] in // Compute new mtime off the main thread - let mtime = (try? fileURL.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate + let mtime = (try? fileURL.resourceValues( + forKeys: [.contentModificationDateKey] + ))?.contentModificationDate // Hop to main to mutate state and refresh UI DispatchQueue.main.async { @@ -467,7 +583,9 @@ struct EditorAreaFileView: View { let newText = String(data: data, encoding: .utf8) ?? "" self.updatePreview(with: newText) } catch { - print("[FS Watch] Failed reading file: \(error.localizedDescription)") + previewLogger.error( + "[FS Watch] Failed reading file: \(error.localizedDescription)" + ) } } @@ -475,7 +593,7 @@ struct EditorAreaFileView: View { } } onError: { err in DispatchQueue.main.async { - print("[FS Watch] Error: \(err.localizedDescription)") + previewLogger.error("[FS Watch] Error: \(err.localizedDescription)") } } } @@ -487,8 +605,10 @@ struct EditorAreaFileView: View { // MARK: - Layout @ViewBuilder var editorAreaFileView: some View { let pathExt = codeFile.fileURL?.pathExtension.lowercased() ?? "" - let isHTML = (codeFile.utType?.conforms(to: .html) ?? false) || (["html", "htm"].contains(pathExt)) - let isMarkdown = (codeFile.utType?.identifier == "net.daringfireball.markdown") || (pathExt == "md" || pathExt == "markdown") + let isHTML = (codeFile.utType?.conforms(to: .html) ?? false) + || (["html", "htm"].contains(pathExt)) + let isMarkdown = (codeFile.utType?.identifier == "net.daringfireball.markdown") + || (pathExt == "md" || pathExt == "markdown") VStack(spacing: 0) { HStack(spacing: 8) { @@ -512,17 +632,30 @@ struct EditorAreaFileView: View { // give a larger transparent hit area so it is easy to grab .padding(.horizontal, 6) .contentShape(Rectangle()) - .gesture(DragGesture(minimumDistance: 1).onChanged { value in - if dragStartPreviewWidth == nil { dragStartPreviewWidth = previewWidth } - let start = dragStartPreviewWidth ?? previewWidth - let delta = value.location.x - value.startLocation.x - let newW = start - delta - previewWidth = max(MIN_PREVIEW_WIDTH, min(MAX_PREVIEW_WIDTH, newW)) - }.onEnded { _ in - dragStartPreviewWidth = nil - }) + .gesture( + DragGesture(minimumDistance: 1) + .onChanged { value in + if dragStartPreviewWidth == nil { + dragStartPreviewWidth = previewWidth + } + let start = dragStartPreviewWidth ?? previewWidth + let delta = value.location.x - value.startLocation.x + let newW = start - delta + previewWidth = max( + minPreviewWidth, + min(maxPreviewWidth, newW) + ) + } + .onEnded { _ in + dragStartPreviewWidth = nil + } + ) .onHover { hovering in - if hovering { NSCursor.resizeLeftRight.push() } else { NSCursor.pop() } + if hovering { + NSCursor.resizeLeftRight.push() + } else { + NSCursor.pop() + } } if isHTML { @@ -536,14 +669,22 @@ struct EditorAreaFileView: View { ZStack { WebView( html: renderedHTMLState.isEmpty - ? "

Preview Ready

" + ? """ + +

Preview Ready

+ + """ : renderedHTMLState, baseURL: codeFile.fileURL?.deletingLastPathComponent(), onCrash: { /* WebKit always on; show message if needed */ }, allowJavaScript: webViewAllowJS ) .id(webViewRefreshToken) - .frame(maxWidth: .infinity, minHeight: 320, maxHeight: .infinity) + .frame( + maxWidth: .infinity, + minHeight: 320, + maxHeight: .infinity + ) // ensure top safe area isn't covered — add top padding to push web content down .padding(.top, edgeInsets.top) .background(Color.white) @@ -640,14 +781,22 @@ struct EditorAreaFileView: View { ZStack { WebView( html: renderedHTMLState.isEmpty - ? "

Preview Ready

" + ? """ + +

Preview Ready

+ + """ : renderedHTMLState, baseURL: codeFile.fileURL?.deletingLastPathComponent(), onCrash: { /* WebKit always on */ }, allowJavaScript: webViewAllowJS ) .id(webViewRefreshToken) - .frame(maxWidth: .infinity, minHeight: 320, maxHeight: .infinity) + .frame( + maxWidth: .infinity, + minHeight: 320, + maxHeight: .infinity + ) // push content below top chrome so h1 isn't cut off .padding(.top, edgeInsets.top) .background(Color.white) @@ -698,7 +847,11 @@ struct EditorAreaFileView: View { VStack(spacing: 0) { ZStack { MarkdownView(source: contentString) - .frame(maxWidth: .infinity, minHeight: 320, maxHeight: .infinity) + .frame( + maxWidth: .infinity, + minHeight: 320, + maxHeight: .infinity + ) .padding(.top, edgeInsets.top) .background(Color.white) @@ -763,6 +916,7 @@ struct EditorAreaFileView: View { CodeFileView(editorInstance: editorInstance, codeFile: codeFile) if !showPreviewPane { + // overlay button to restore preview Button { withAnimation(.easeInOut(duration: 0.18)) { showPreviewPane = true @@ -790,7 +944,11 @@ struct EditorAreaFileView: View { VStack(spacing: 0) { ZStack { MarkdownView(source: contentString) - .frame(maxWidth: .infinity, minHeight: 320, maxHeight: .infinity) + .frame( + maxWidth: .infinity, + minHeight: 320, + maxHeight: .infinity + ) .padding(.top, edgeInsets.top) .background(Color.white) @@ -824,10 +982,13 @@ struct EditorAreaFileView: View { .zIndex(10) } } - .frame(minWidth: previewWidth, - idealWidth: previewWidth, - maxWidth: previewWidth, - maxHeight: .infinity, alignment: .center) + .frame( + minWidth: previewWidth, + idealWidth: previewWidth, + maxWidth: previewWidth, + maxHeight: .infinity, + alignment: .center + ) .background(Color.white) } } @@ -855,7 +1016,9 @@ struct EditorAreaFileView: View { serverErrorMessage = nil if renderedHTMLState != html { renderedHTMLState = html - print("[Preview] Initial set. html length: \(html.count)") + previewLogger.debug( + "[Preview] Initial set. html length: \(html.count)" + ) } if previewSource == .serverPreview { await loadServerPreview(with: sourceString) @@ -877,7 +1040,11 @@ struct EditorAreaFileView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .onHover { hover in DispatchQueue.main.async { - if hover { NSCursor.iBeam.push() } else { NSCursor.pop() } + if hover { + NSCursor.iBeam.push() + } else { + NSCursor.pop() + } } } } From 7bafca0498c39c3e02f0680f8754b8eb256db515 Mon Sep 17 00:00:00 2001 From: Hunter Davis Date: Mon, 5 Jan 2026 16:51:40 -0500 Subject: [PATCH 7/9] Fixed entitlements --- CodeEdit/CodeEdit.entitlements | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/CodeEdit/CodeEdit.entitlements b/CodeEdit/CodeEdit.entitlements index 4b18e592aa..0c67376eba 100644 --- a/CodeEdit/CodeEdit.entitlements +++ b/CodeEdit/CodeEdit.entitlements @@ -1,13 +1,5 @@ - - com.apple.security.application-groups - - app.codeedit.CodeEdit.shared - $(TeamIdentifierPrefix) - - com.apple.security.files.bookmarks.app-scope - - + From 825ddc2341763f6a59d0192cfdf6f4a1cf462b6f Mon Sep 17 00:00:00 2001 From: Hunter Davis Date: Mon, 5 Jan 2026 20:20:16 -0500 Subject: [PATCH 8/9] Establishment Fix --- CodeEdit/CodeEdit.entitlements | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CodeEdit/CodeEdit.entitlements b/CodeEdit/CodeEdit.entitlements index 0c67376eba..2616100cf5 100644 --- a/CodeEdit/CodeEdit.entitlements +++ b/CodeEdit/CodeEdit.entitlements @@ -1,5 +1,12 @@ + + com.apple.security.application-groups + + app.codeedit.CodeEdit.shared + $(TeamIdentifierPrefix) + + From 3a78f05f9908a17f90a9968b5735fe170374d31c Mon Sep 17 00:00:00 2001 From: Hunter Davis Date: Tue, 6 Jan 2026 12:44:17 -0500 Subject: [PATCH 9/9] Entitlements fix --- CodeEdit.xcodeproj/project.pbxproj | 25 ------------------------- CodeEdit/CodeEdit.entitlements | 15 +++++++-------- 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/CodeEdit.xcodeproj/project.pbxproj b/CodeEdit.xcodeproj/project.pbxproj index 6fde2a01b9..f6d9a281da 100644 --- a/CodeEdit.xcodeproj/project.pbxproj +++ b/CodeEdit.xcodeproj/project.pbxproj @@ -678,11 +678,6 @@ ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; - ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; - ENABLE_RESOURCE_ACCESS_CALENDARS = NO; - ENABLE_RESOURCE_ACCESS_CAMERA = NO; - ENABLE_RESOURCE_ACCESS_CONTACTS = NO; - ENABLE_RESOURCE_ACCESS_LOCATION = NO; ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; @@ -890,11 +885,6 @@ ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; - ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; - ENABLE_RESOURCE_ACCESS_CALENDARS = NO; - ENABLE_RESOURCE_ACCESS_CAMERA = NO; - ENABLE_RESOURCE_ACCESS_CONTACTS = NO; - ENABLE_RESOURCE_ACCESS_LOCATION = NO; ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; @@ -1174,11 +1164,6 @@ ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; - ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; - ENABLE_RESOURCE_ACCESS_CALENDARS = NO; - ENABLE_RESOURCE_ACCESS_CAMERA = NO; - ENABLE_RESOURCE_ACCESS_CONTACTS = NO; - ENABLE_RESOURCE_ACCESS_LOCATION = NO; ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; @@ -1458,11 +1443,6 @@ ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; - ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; - ENABLE_RESOURCE_ACCESS_CALENDARS = NO; - ENABLE_RESOURCE_ACCESS_CAMERA = NO; - ENABLE_RESOURCE_ACCESS_CONTACTS = NO; - ENABLE_RESOURCE_ACCESS_LOCATION = NO; ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; @@ -1513,11 +1493,6 @@ ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; - ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; - ENABLE_RESOURCE_ACCESS_CALENDARS = NO; - ENABLE_RESOURCE_ACCESS_CAMERA = NO; - ENABLE_RESOURCE_ACCESS_CONTACTS = NO; - ENABLE_RESOURCE_ACCESS_LOCATION = NO; ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = CodeEdit/Info.plist; diff --git a/CodeEdit/CodeEdit.entitlements b/CodeEdit/CodeEdit.entitlements index 2616100cf5..c0c4f5e905 100644 --- a/CodeEdit/CodeEdit.entitlements +++ b/CodeEdit/CodeEdit.entitlements @@ -1,12 +1,11 @@ - - com.apple.security.application-groups - - app.codeedit.CodeEdit.shared - $(TeamIdentifierPrefix) - - - + + com.apple.security.application-groups + + app.codeedit.CodeEdit.shared + $(TeamIdentifierPrefix) + +