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
|
- [ ] Export to PDF feature
|
||||||
- [ ] Dark mode support
|
- [ ] Dark mode support
|
||||||
- [ ] Custom CSS themes
|
- [ ] Custom CSS themes
|
||||||
- [ ] Live preview while editing
|
- [x] Live preview while editing
|
||||||
- [ ] Search within document
|
- [ ] Search within document
|
||||||
- [ ] Zoom in/out functionality
|
- [ ] Zoom in/out functionality
|
||||||
- [ ] Recent files menu
|
- [ ] Recent files menu
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import AppKit
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -24,11 +25,148 @@ struct ContentView: View {
|
|||||||
struct MarkdownDocumentView: View {
|
struct MarkdownDocumentView: View {
|
||||||
let content: String
|
let content: String
|
||||||
let fileName: 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 {
|
var body: some View {
|
||||||
MarkdownRenderer(content: content)
|
VStack(spacing: 0) {
|
||||||
.navigationTitle(fileName)
|
// Toolbar
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user