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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user