import SwiftUI import AppKit import UniformTypeIdentifiers struct ContentView: View { var body: some View { VStack { Image(systemName: "doc.text") .font(.system(size: 80)) .foregroundColor(.secondary) Text("Markdown Viewer") .font(.largeTitle) .fontWeight(.medium) Text("Open markdown files from the File menu") .foregroundColor(.secondary) .padding(.top, 8) Text("⌘O") .foregroundColor(.secondary) .font(.caption) .padding(.top, 4) } .frame(maxWidth: .infinity, maxHeight: .infinity) } } struct MarkdownDocumentView: View { let originalContent: String let fileName: String let fileURL: URL? @State private var isEditMode = false @State private var editableContent: String @State private var hasUnsavedChanges = false @State private var isSaving = false init(content: String, fileName: String, fileURL: URL? = nil) { self.originalContent = content self.fileName = fileName self.fileURL = fileURL self._editableContent = State(initialValue: content) print("MarkdownDocumentView: Initialized with content length: \(content.count)") } var body: some View { VStack(spacing: 0) { // Toolbar HStack { Button(action: { isEditMode.toggle() }) { HStack { Image(systemName: isEditMode ? "eye" : "pencil") Text(isEditMode ? "Preview" : "Edit") } } .buttonStyle(.bordered) Spacer() } .padding() .background(Color(NSColor.controlBackgroundColor)) // Content area if isEditMode { HSplitView { // Editor side VStack(alignment: .leading, spacing: 0) { Text("Editor") .font(.headline) .padding(.horizontal) .padding(.top, 8) .padding(.bottom, 4) MarkdownEditor(text: $editableContent) .frame(maxWidth: .infinity, maxHeight: .infinity) } // Preview side VStack(alignment: .leading, spacing: 0) { Text("Preview") .font(.headline) .padding(.horizontal) .padding(.top, 8) .padding(.bottom, 4) MarkdownRenderer(content: editableContent) .frame(maxWidth: .infinity, maxHeight: .infinity) } } } else { MarkdownRenderer(content: editableContent) .frame(maxWidth: .infinity, maxHeight: .infinity) } } .navigationTitle(hasUnsavedChanges ? "\(fileName) • Edited" : fileName) .onChange(of: editableContent) { newValue in hasUnsavedChanges = (newValue != originalContent) } .onReceive(NotificationCenter.default.publisher(for: .saveDocument)) { _ in saveFile() } .onReceive(NotificationCenter.default.publisher(for: .saveDocumentAs)) { _ in saveAsNewFile() } } func saveFile() { guard let fileURL = fileURL else { // If no URL, show save dialog saveAsNewFile() return } guard hasUnsavedChanges else { return } isSaving = true Task { do { try editableContent.write(to: fileURL, atomically: true, encoding: .utf8) await MainActor.run { hasUnsavedChanges = false isSaving = false } } catch { await MainActor.run { isSaving = false showSaveError(error) } } } } func saveAsNewFile() { let panel = NSSavePanel() if let mdType = UTType(filenameExtension: "md") { panel.allowedContentTypes = [mdType] } else { panel.allowedContentTypes = [.plainText] } panel.nameFieldStringValue = fileName if panel.runModal() == .OK, let url = panel.url { isSaving = true Task { do { try editableContent.write(to: url, atomically: true, encoding: .utf8) await MainActor.run { hasUnsavedChanges = false isSaving = false } } catch { await MainActor.run { isSaving = false showSaveError(error) } } } } } private func showSaveError(_ error: Error) { // For now, just print the error // In a full implementation, we'd show an alert dialog print("Save error: \(error.localizedDescription)") } } struct MarkdownEditor: NSViewRepresentable { @Binding var text: String func makeNSView(context: Context) -> NSScrollView { let scrollView = NSScrollView() let textView = NSTextView() // Configure text view textView.delegate = context.coordinator textView.isEditable = true textView.isSelectable = true textView.usesRuler = false textView.isAutomaticQuoteSubstitutionEnabled = false textView.isAutomaticDashSubstitutionEnabled = false textView.isAutomaticTextReplacementEnabled = false textView.font = NSFont.monospacedSystemFont(ofSize: 14, weight: .regular) textView.textColor = NSColor.textColor textView.backgroundColor = NSColor.textBackgroundColor textView.allowsUndo = true textView.string = text // Properly set up the scroll view hierarchy first scrollView.borderType = .noBorder scrollView.hasVerticalScroller = true scrollView.hasHorizontalScroller = false scrollView.autohidesScrollers = false scrollView.documentView = textView // Configure text container for proper wrapping if let textContainer = textView.textContainer { textContainer.containerSize = NSSize(width: scrollView.contentSize.width, height: CGFloat.greatestFiniteMagnitude) textContainer.widthTracksTextView = true } // Configure text view sizing textView.isVerticallyResizable = true textView.isHorizontallyResizable = false textView.autoresizingMask = [.width] textView.minSize = NSSize(width: 0, height: scrollView.contentSize.height) textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) print("MarkdownEditor: Created with text length: \(text.count)") return scrollView } func updateNSView(_ scrollView: NSScrollView, context: Context) { guard let textView = scrollView.documentView as? NSTextView else { return } if textView.string != text { print("MarkdownEditor: Updating text from \(textView.string.count) to \(text.count) characters") textView.string = text } } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, NSTextViewDelegate { let parent: MarkdownEditor init(_ parent: MarkdownEditor) { self.parent = parent } func textDidChange(_ notification: Notification) { guard let textView = notification.object as? NSTextView else { return } DispatchQueue.main.async { self.parent.text = textView.string } } } } #Preview { ContentView() }