mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
Changes to the Sidebar. And Fixing Errors in main window
This commit is contained in:
parent
d3bd921823
commit
b353a09064
1 changed files with 230 additions and 216 deletions
|
|
@ -5,218 +5,6 @@ import Combine
|
|||
import SwiftData
|
||||
import AppKit // Explicitly import for NSColor
|
||||
|
||||
// MARK: - ContentView
|
||||
// This is the main view that displays the editor interface with a sidebar and tabbed content area
|
||||
struct ContentView: View {
|
||||
@EnvironmentObject var viewModel: ContentViewModel // Injects the view model to manage state and data
|
||||
@Environment(\.modelContext) private var modelContext // Provides access to the SwiftData context for persistence
|
||||
@State private var isRenaming: [UUID: Bool] = [:] // Tracks which tab is being renamed
|
||||
@FocusState private var isFocused: Bool // Manages focus state for renaming
|
||||
|
||||
var body: some View {
|
||||
// Horizontal stack to layout sidebar and content side by side
|
||||
HStack(spacing: 0) {
|
||||
// Sidebar section to list and manage tabs
|
||||
List(viewModel.tabs, id: \.id, selection: $viewModel.selectedTab) { item in
|
||||
// Horizontal stack for each tab item in the list
|
||||
HStack {
|
||||
if isRenaming[item.id, default: false] {
|
||||
TextField("Tab Name", text: Binding(
|
||||
get: { item.name },
|
||||
set: { viewModel.renameTab(item, to: $0) }
|
||||
))
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.foregroundColor(.black) // Changed to black for renaming text
|
||||
.background(Color(nsColor: .windowBackgroundColor).opacity(0.85)) // Match sidebar background
|
||||
.focused($isFocused) // Enable focus management
|
||||
.onSubmit {
|
||||
isRenaming[item.id] = false // Commit rename on Enter
|
||||
isFocused = false
|
||||
}
|
||||
.onExitCommand {
|
||||
isRenaming[item.id] = false // Commit on focus loss
|
||||
isFocused = false
|
||||
}
|
||||
} else {
|
||||
Text(item.name) // Displays the tab's name
|
||||
.foregroundColor(.black) // Changed to black
|
||||
.font(.system(size: 12)) // Smaller font size
|
||||
}
|
||||
Spacer() // Pushes the close button to the right
|
||||
Button("x") {
|
||||
viewModel.removeTab(item) // Calls method to remove the tab
|
||||
}
|
||||
.buttonStyle(.borderless) // Removes default button styling
|
||||
.foregroundColor(.red) // Colors the close button red
|
||||
}
|
||||
.onTapGesture(count: 2) {
|
||||
isRenaming[item.id] = true // Enable renaming on double-click
|
||||
isFocused = true // Focus the text field
|
||||
}
|
||||
}
|
||||
.frame(width: 200) // Fixed width for the sidebar
|
||||
.background(Color(nsColor: .windowBackgroundColor).opacity(0.85)) // Applies a semi-transparent background
|
||||
.listStyle(SidebarListStyle()) // Applies a sidebar-specific list style
|
||||
.frame(minHeight: 0) // Ensure minimum height to prevent collapse
|
||||
|
||||
// Tabbed content area to display the selected tab's content with scroll
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
if let selectedItem = viewModel.selectedTab {
|
||||
// Language picker for the selected tab
|
||||
Picker("Language", selection: Binding(
|
||||
get: { viewModel.selectedLanguage }, // Gets the current language
|
||||
set: { viewModel.selectedLanguage = $0 } // Sets the new language
|
||||
)) {
|
||||
ForEach(viewModel.languages, id: \.self) { lang in
|
||||
Text(lang).tag(lang.lowercased()) // Creates options for language selection
|
||||
}
|
||||
}
|
||||
.pickerStyle(MenuPickerStyle()) // Uses a menu-style picker
|
||||
.padding(.horizontal) // Adds horizontal padding
|
||||
.frame(height: 30) // Fixed height for the picker
|
||||
.background(Color(nsColor: .windowBackgroundColor).opacity(0.85)) // Semi-transparent background
|
||||
|
||||
// Custom text editor for the selected tab's content
|
||||
CustomTextEditor(text: Binding(
|
||||
get: { selectedItem.content }, // Gets the content of the selected item
|
||||
set: { viewModel.updateContent($0) } // Updates content when changed
|
||||
), language: Binding(
|
||||
get: { selectedItem.language }, // Gets the language of the selected item
|
||||
set: { viewModel.updateLanguage($0) } // Updates language when changed
|
||||
), highlightr: HighlightrViewModel().highlightr)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity) // Expands to fill available space
|
||||
} else {
|
||||
Text("No tabs open") // Displayed when no tabs are selected
|
||||
.foregroundColor(.primary) // Dynamic color for light/dark mode using SwiftUI's .primary
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity) // Expands to fill remaining space
|
||||
}
|
||||
.frame(minWidth: 1000, minHeight: 600) // Minimum dimensions for the window
|
||||
.background(Color(nsColor: .textBackgroundColor).opacity(0.85)) // Semi-transparent background
|
||||
// Temporarily removed overlay to test if it blocks interactions
|
||||
// .overlay(
|
||||
// Rectangle()
|
||||
// .fill(Color(nsColor: .windowBackgroundColor).opacity(0.85))
|
||||
// .edgesIgnoringSafeArea(.all)
|
||||
// )
|
||||
.onAppear {
|
||||
// Load tabs when the view appears
|
||||
print("ContentView appeared, loading tabs. Tabs count: \(viewModel.tabs.count)")
|
||||
viewModel.loadTabs()
|
||||
print("After load, tabs count: \(viewModel.tabs.count), selectedTab: \(String(describing: viewModel.selectedTab))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomTextEditor
|
||||
// A custom NSViewRepresentable to integrate NSTextView with syntax highlighting
|
||||
struct CustomTextEditor: NSViewRepresentable {
|
||||
@Binding var text: String // Binding to the text content
|
||||
@Binding var language: String // Binding to the language for highlighting
|
||||
let highlightr: Highlightr // Highlightr instance for syntax highlighting
|
||||
|
||||
func makeNSView(context: Context) -> NSTextView {
|
||||
// Creates a text storage with Highlightr for syntax highlighting
|
||||
let textStorage = CodeAttributedString(highlightr: highlightr)
|
||||
let layoutManager = NSLayoutManager()
|
||||
textStorage.addLayoutManager(layoutManager)
|
||||
let textContainer = NSTextContainer()
|
||||
textContainer.widthTracksTextView = true
|
||||
textContainer.heightTracksTextView = true
|
||||
textContainer.containerSize = CGSize(width: 0, height: CGFloat.greatestFiniteMagnitude) // Enable vertical scrolling
|
||||
layoutManager.addTextContainer(textContainer)
|
||||
|
||||
// Configures the NSTextView
|
||||
let textView = NSTextView(frame: .zero, textContainer: textContainer)
|
||||
textView.isEditable = true // Allows editing
|
||||
textView.isSelectable = true // Allows selection
|
||||
textView.font = NSFont.monospacedSystemFont(ofSize: 14, weight: .regular) // Monospaced font
|
||||
textView.backgroundColor = NSColor.textBackgroundColor.withAlphaComponent(0.85) // Semi-transparent background
|
||||
textView.textColor = NSColor(white: 0.2, alpha: 0.85) // Adjusted to dark grey to match intended theme
|
||||
textView.delegate = context.coordinator // Sets the coordinator as delegate
|
||||
|
||||
// Adds a scroll view to handle large content
|
||||
let scrollView = NSScrollView(frame: textView.bounds)
|
||||
scrollView.documentView = textView
|
||||
scrollView.hasVerticalScroller = true // Enables vertical scrolling
|
||||
scrollView.hasHorizontalScroller = true // Enables horizontal scrolling
|
||||
scrollView.autoresizingMask = [.width, .height] // Resizes with the view
|
||||
scrollView.autohidesScrollers = true // Hides scrollers when not needed
|
||||
|
||||
// Initializes the text view with the current text and language
|
||||
DispatchQueue.main.async {
|
||||
textView.string = text
|
||||
textStorage.beginEditing()
|
||||
if let highlightedText = highlightr.highlight(text, as: language) {
|
||||
textStorage.setAttributedString(highlightedText)
|
||||
}
|
||||
textStorage.endEditing()
|
||||
textStorage.language = language
|
||||
}
|
||||
|
||||
return textView
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSTextView, context: Context) {
|
||||
// Updates the text view when the bound text or language changes
|
||||
if nsView.string != text {
|
||||
nsView.string = text
|
||||
let textStorage = nsView.textStorage as! CodeAttributedString
|
||||
textStorage.beginEditing()
|
||||
if let highlightedText = highlightr.highlight(text, as: language) {
|
||||
textStorage.setAttributedString(highlightedText)
|
||||
}
|
||||
textStorage.endEditing()
|
||||
textStorage.language = language
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
// Creates a coordinator to handle text view delegate methods
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, NSTextViewDelegate {
|
||||
var parent: CustomTextEditor // Reference to the parent view
|
||||
var language: String // Tracks the current language
|
||||
|
||||
init(_ parent: CustomTextEditor) {
|
||||
self.parent = parent
|
||||
self.language = parent.language
|
||||
}
|
||||
|
||||
func textDidChange(_ notification: Notification) {
|
||||
// Updates the bound text when the text view changes
|
||||
if let textView = notification.object as? NSTextView {
|
||||
parent.text = textView.string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HighlightrViewModel
|
||||
// Manages the Highlightr instance for syntax highlighting
|
||||
class HighlightrViewModel {
|
||||
let highlightr: Highlightr = {
|
||||
// Initializes Highlightr with the custom Vibrant Light theme
|
||||
if let h = Highlightr() {
|
||||
if let themePath = Bundle.main.path(forResource: "Vibrant Light", ofType: "xccolortheme") {
|
||||
print("Loading theme from: \(themePath)") // Debug log for theme path
|
||||
h.setTheme(to: "Vibrant Light")
|
||||
} else {
|
||||
print("Failed to find Vibrant Light theme. Falling back to vs2015.")
|
||||
h.setTheme(to: "vs2015")
|
||||
}
|
||||
return h
|
||||
} else {
|
||||
fatalError("Highlightr initialization failed. Ensure the package is correctly added via Swift Package Manager.")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// MARK: - ContentViewModel
|
||||
// Manages the state and logic for the editor, including tabs and SwiftData persistence
|
||||
class ContentViewModel: ObservableObject {
|
||||
|
|
@ -239,9 +27,10 @@ class ContentViewModel: ObservableObject {
|
|||
"json": "json"
|
||||
] // Maps file extensions to languages
|
||||
|
||||
@Environment(\.modelContext) private var modelContext // Access to SwiftData context
|
||||
private var modelContext: ModelContext // Store context passed from view
|
||||
|
||||
init() {
|
||||
init(modelContext: ModelContext) {
|
||||
self.modelContext = modelContext
|
||||
// Initialize by loading existing tabs
|
||||
loadTabs()
|
||||
}
|
||||
|
|
@ -281,7 +70,7 @@ class ContentViewModel: ObservableObject {
|
|||
func saveFile() {
|
||||
// Saves the content of the selected tab to a file
|
||||
guard let selectedItem = selectedTab else { return }
|
||||
let savePanel = NSSavePanel()
|
||||
let savePanel = NSOpenPanel()
|
||||
savePanel.allowedContentTypes = [
|
||||
.text,
|
||||
.sourceCode,
|
||||
|
|
@ -304,7 +93,7 @@ class ContentViewModel: ObservableObject {
|
|||
|
||||
func addNewTab() {
|
||||
// Adds a new empty tab
|
||||
let newItem = Item(name: "Untitled-\(tabs.count + 1)", content: "", language: "swift")
|
||||
let newItem = Item(name: "Note \(tabs.count + 1)", content: "", language: "swift")
|
||||
tabs.append(newItem)
|
||||
selectedTab = newItem
|
||||
saveToSwiftData(newItem.name)
|
||||
|
|
@ -384,3 +173,228 @@ class ContentViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ContentView
|
||||
// This is the main view that displays the editor interface with a sidebar and tabbed content area
|
||||
struct ContentView: View {
|
||||
@Environment(\.modelContext) private var modelContext // Provides access to the SwiftData context for persistence
|
||||
@StateObject private var viewModel = ContentViewModel(modelContext: ModelContext(using: ModelContainer(for: Item.self)))
|
||||
@State private var isRenaming: [UUID: Bool] = [:] // Tracks which tab is being renamed
|
||||
@FocusState private var isFocused: Bool // Manages focus state for renaming
|
||||
|
||||
var body: some View {
|
||||
// Horizontal stack to layout sidebar and content side by side
|
||||
HStack(spacing: 0) {
|
||||
// Sidebar section to list and manage tabs
|
||||
List(viewModel.tabs, id: \Item.id, selection: $viewModel.selectedTab) { item in
|
||||
// Horizontal stack for each tab item in the list
|
||||
HStack {
|
||||
if isRenaming[item.id] ?? false { // Use optional binding with nil coalescing
|
||||
TextField("Tab Name", text: Binding(
|
||||
get: { item.name },
|
||||
set: { viewModel.renameTab(item, to: $0) }
|
||||
))
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.foregroundColor(.black) // Changed to black for renaming text
|
||||
.background(Color(nsColor: .windowBackgroundColor).opacity(0.85)) // Match sidebar background
|
||||
.focused($isFocused) // Enable focus management
|
||||
.onSubmit {
|
||||
isRenaming[item.id] = false // Commit rename on Enter
|
||||
isFocused = false
|
||||
}
|
||||
.onExitCommand {
|
||||
isRenaming[item.id] = false // Commit on focus loss
|
||||
isFocused = false
|
||||
}
|
||||
} else {
|
||||
Text(item.name) // Displays the tab's name
|
||||
.foregroundColor(.black) // Changed to black
|
||||
.font(.system(size: 12)) // Smaller font size
|
||||
}
|
||||
Spacer() // Pushes the close button to the right
|
||||
Button("x") {
|
||||
viewModel.removeTab(item) // Calls method to remove the tab
|
||||
}
|
||||
.buttonStyle(.borderless) // Removes default button styling
|
||||
.foregroundColor(.red) // Colors the close button red
|
||||
}
|
||||
.listRowSeparator(.hidden) // Hide default separator
|
||||
.padding(.vertical, 4) // Add vertical padding to separate tabs
|
||||
.background(Color(nsColor: .windowBackgroundColor).opacity(0.7)) // Darker background to distinguish tabs
|
||||
.cornerRadius(4) // Slight rounding for separation
|
||||
.onTapGesture(count: 2) {
|
||||
isRenaming[item.id] = true // Enable renaming on double-click
|
||||
isFocused = true // Focus the text field
|
||||
}
|
||||
.onTapGesture(count: 1) {
|
||||
if isRenaming[item.id] ?? false {
|
||||
isRenaming[item.id] = false // Return to normal on single click away
|
||||
isFocused = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: 200) // Fixed width for the sidebar
|
||||
.background(Color(nsColor: .windowBackgroundColor).opacity(0.85)) // Applies a semi-transparent background
|
||||
.listStyle(SidebarListStyle()) // Applies a sidebar-specific list style
|
||||
.frame(minHeight: 0) // Ensure minimum height to prevent collapse
|
||||
|
||||
// Tabbed content area to display the selected tab's content with scroll
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
if let selectedItem = viewModel.selectedTab {
|
||||
// Language picker for the selected tab
|
||||
Picker("Language", selection: Binding(
|
||||
get: { viewModel.selectedLanguage }, // Gets the current language
|
||||
set: { viewModel.selectedLanguage = $0 } // Sets the new language
|
||||
)) {
|
||||
ForEach(viewModel.languages, id: \.self) { lang in
|
||||
Text(lang).tag(lang.lowercased()) // Creates options for language selection
|
||||
}
|
||||
}
|
||||
.pickerStyle(MenuPickerStyle()) // Uses a menu-style picker
|
||||
.padding(.horizontal) // Adds horizontal padding
|
||||
.frame(height: 30) // Fixed height for the picker
|
||||
.background(Color(nsColor: .windowBackgroundColor).opacity(0.85)) // Semi-transparent background
|
||||
|
||||
// Custom text editor for the selected tab's content
|
||||
CustomTextEditor(text: Binding(
|
||||
get: { selectedItem.content }, // Gets the content of the selected item
|
||||
set: { viewModel.updateContent($0); print("Content updated to: \($0)") } // Updates content when changed with debug
|
||||
), language: Binding(
|
||||
get: { selectedItem.language }, // Gets the language of the selected item
|
||||
set: { viewModel.updateLanguage($0) } // Updates language when changed
|
||||
), highlightr: HighlightrViewModel().highlightr)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity) // Expands to fill available space
|
||||
} else {
|
||||
Text("No tabs open") // Displayed when no tabs are selected
|
||||
.foregroundColor(.primary) // Dynamic color for light/dark mode using SwiftUI's .primary
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity) // Expands to fill remaining space
|
||||
}
|
||||
.frame(minWidth: 1000, minHeight: 600) // Minimum dimensions for the window
|
||||
.background(Color(nsColor: .textBackgroundColor).opacity(0.85)) // Semi-transparent background
|
||||
// Temporarily removed overlay to test if it blocks interactions
|
||||
// .overlay(
|
||||
// Rectangle()
|
||||
// .fill(Color(nsColor: .windowBackgroundColor).opacity(0.85))
|
||||
// .edgesIgnoringSafeArea(.all)
|
||||
// )
|
||||
.onAppear {
|
||||
// Load tabs when the view appears
|
||||
print("ContentView appeared, loading tabs. Tabs count: \(viewModel.tabs.count)")
|
||||
viewModel.loadTabs()
|
||||
print("After load, tabs count: \(viewModel.tabs.count), selectedTab: \(String(describing: viewModel.selectedTab))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomTextEditor
|
||||
// A custom NSViewRepresentable to integrate NSTextView with syntax highlighting
|
||||
struct CustomTextEditor: NSViewRepresentable {
|
||||
@Binding var text: String // Binding to the text content
|
||||
@Binding var language: String // Binding to the language for highlighting
|
||||
let highlightr: Highlightr // Highlightr instance for syntax highlighting
|
||||
|
||||
func makeNSView(context: Context) -> NSTextView {
|
||||
// Creates a text storage with Highlightr for syntax highlighting
|
||||
let textStorage = CodeAttributedString(highlightr: highlightr)
|
||||
let layoutManager = NSLayoutManager()
|
||||
textStorage.addLayoutManager(layoutManager)
|
||||
let textContainer = NSTextContainer()
|
||||
textContainer.widthTracksTextView = true
|
||||
textContainer.heightTracksTextView = true
|
||||
textContainer.containerSize = CGSize(width: 0, height: CGFloat.greatestFiniteMagnitude) // Enable vertical scrolling
|
||||
layoutManager.addTextContainer(textContainer)
|
||||
|
||||
// Configures the NSTextView
|
||||
let textView = NSTextView(frame: .zero, textContainer: textContainer)
|
||||
textView.isEditable = true // Allows editing
|
||||
textView.isSelectable = true // Allows selection
|
||||
textView.font = NSFont.monospacedSystemFont(ofSize: 14, weight: .regular) // Monospaced font
|
||||
textView.backgroundColor = NSColor.textBackgroundColor.withAlphaComponent(0.85) // Semi-transparent background
|
||||
textView.textColor = NSColor(white: 0.2, alpha: 0.85) // Adjusted to dark grey to match intended theme
|
||||
textView.delegate = context.coordinator // Sets the coordinator as delegate
|
||||
|
||||
// Adds a scroll view to handle large content
|
||||
let scrollView = NSScrollView(frame: textView.bounds)
|
||||
scrollView.documentView = textView
|
||||
scrollView.hasVerticalScroller = true // Enables vertical scrolling
|
||||
scrollView.hasHorizontalScroller = true // Enables horizontal scrolling
|
||||
scrollView.autoresizingMask = [.width, .height] // Resizes with the view
|
||||
scrollView.autohidesScrollers = true // Hides scrollers when not needed
|
||||
|
||||
// Initializes the text view with the current text and language
|
||||
DispatchQueue.main.async {
|
||||
textView.string = self.text
|
||||
textStorage.beginEditing()
|
||||
if let highlightedText = self.highlightr.highlight(self.text, as: self.language) {
|
||||
textStorage.setAttributedString(highlightedText)
|
||||
}
|
||||
textStorage.endEditing()
|
||||
textStorage.language = self.language
|
||||
print("TextEditor initialized with text: \(self.text)") // Debug log
|
||||
}
|
||||
|
||||
return textView
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSTextView, context: Context) {
|
||||
// Updates the text view when the bound text or language changes
|
||||
if nsView.string != text {
|
||||
nsView.string = text
|
||||
let textStorage = nsView.textStorage as! CodeAttributedString
|
||||
textStorage.beginEditing()
|
||||
if let highlightedText = highlightr.highlight(text, as: language) {
|
||||
textStorage.setAttributedString(highlightedText)
|
||||
}
|
||||
textStorage.endEditing()
|
||||
textStorage.language = language
|
||||
print("TextEditor updated with text: \(text)") // Debug log
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
// Creates a coordinator to handle text view delegate methods
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, NSTextViewDelegate {
|
||||
var parent: CustomTextEditor // Reference to the parent view
|
||||
var language: String // Tracks the current language
|
||||
|
||||
init(_ parent: CustomTextEditor) {
|
||||
self.parent = parent
|
||||
self.language = parent.language
|
||||
}
|
||||
|
||||
func textDidChange(_ notification: Notification) {
|
||||
// Updates the bound text when the text view changes
|
||||
if let textView = notification.object as? NSTextView {
|
||||
parent.text = textView.string
|
||||
print("Text changed to: \(textView.string)") // Debug log
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HighlightrViewModel
|
||||
// Manages the Highlightr instance for syntax highlighting
|
||||
class HighlightrViewModel {
|
||||
let highlightr: Highlightr = {
|
||||
// Initializes Highlightr with the custom Vibrant Light theme
|
||||
if let h = Highlightr() {
|
||||
if let themePath = Bundle.main.path(forResource: "Vibrant Light", ofType: "xccolortheme") {
|
||||
print("Loading theme from: \(themePath)") // Debug log for theme path
|
||||
h.setTheme(to: "Vibrant Light")
|
||||
} else {
|
||||
print("Failed to find Vibrant Light theme. Falling back to vs2015.")
|
||||
h.setTheme(to: "vs2015")
|
||||
}
|
||||
return h
|
||||
} else {
|
||||
fatalError("Highlightr initialization failed. Ensure the package is correctly added via Swift Package Manager.")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue