From 732f633086c99a82b2c33eb749cc892cdaeb4f90 Mon Sep 17 00:00:00 2001 From: 0x8664b2 <0x8664b2@pm.me> Date: Fri, 20 Jun 2025 10:40:43 -0700 Subject: [PATCH] Add live preview while editing feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented split-view editor with real-time markdown preview: - Added Edit/Preview toggle button with toolbar - Created MarkdownEditor component using NSTextView for raw markdown editing - Implemented HSplitView with editor on left and live preview on right - Fixed NSTextView width constraints to prevent unwanted scaling - Added proper text container configuration for word wrapping - Text changes update preview in real-time via SwiftUI binding 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 2 +- MarkdownViewer/ContentView.swift | 144 ++++++++++++++++++++++++++++++- 2 files changed, 142 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 005a699..c6b9028 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,7 +37,7 @@ This repository contains a macOS markdown viewer application built with SwiftUI. - [ ] Export to PDF feature - [ ] Dark mode support - [ ] Custom CSS themes -- [ ] Live preview while editing +- [x] Live preview while editing - [ ] Search within document - [ ] Zoom in/out functionality - [ ] Recent files menu diff --git a/MarkdownViewer/ContentView.swift b/MarkdownViewer/ContentView.swift index 503de45..d50661d 100644 --- a/MarkdownViewer/ContentView.swift +++ b/MarkdownViewer/ContentView.swift @@ -1,4 +1,5 @@ import SwiftUI +import AppKit struct ContentView: View { var body: some View { @@ -24,11 +25,148 @@ struct ContentView: View { struct MarkdownDocumentView: View { let content: String let fileName: String + @State private var isEditMode = false + @State private var editableContent: String + + init(content: String, fileName: String) { + self.content = content + self.fileName = fileName + self._editableContent = State(initialValue: content) + print("MarkdownDocumentView: Initialized with content length: \(content.count)") + } var body: some View { - MarkdownRenderer(content: content) - .navigationTitle(fileName) - .frame(maxWidth: .infinity, maxHeight: .infinity) + VStack(spacing: 0) { + // Toolbar + HStack { + Button(action: { isEditMode.toggle() }) { + HStack { + Image(systemName: isEditMode ? "eye" : "pencil") + Text(isEditMode ? "Preview" : "Edit") + } + } + .buttonStyle(.bordered) + + Spacer() + + if isEditMode { + Button("Save") { + // TODO: Implement save functionality + } + .buttonStyle(.borderedProminent) + } + } + .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(fileName) + } +} + +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.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 + } + } } }