Add live preview while editing feature

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 <noreply@anthropic.com>
This commit is contained in:
0x8664b2
2025-06-20 10:40:43 -07:00
parent ad55b2308a
commit 732f633086
2 changed files with 142 additions and 4 deletions

View File

@@ -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

View File

@@ -1,4 +1,5 @@
import SwiftUI
import AppKit
struct ContentView: View {
var body: some View {
@@ -24,12 +25,149 @@ 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)
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
}
}
}
}
#Preview {