New approach for Tabbar. Still Xcode Beta Issues for build Universal app

This commit is contained in:
Rodric Krogh 2025-08-27 13:33:45 +02:00
parent 2e8b3703b9
commit 9190efbbda
5 changed files with 398 additions and 341 deletions

View file

@ -5,297 +5,215 @@ import Combine
import SwiftData
import AppKit
// MARK: - ContentViewModel
class ContentViewModel: ObservableObject {
@Published var text: String = ""
@Published var selectedLanguage: String = "swift"
@Published var tabs: [Item] = []
@Published var selectedTab: Item? = nil
@Published var showSidebar = true
@Published var selectedTOCItem: String? // Track selected table of contents item
let languages: [String] = ["HTML", "C", "Swift", "Python", "C++", "Java", "Bash", "JSON", "Markdown"]
let languageMap: [String: String] = [
"html": "html",
"htm": "html",
"c": "c",
"h": "c",
"swift": "swift",
"py": "python",
"cpp": "cpp",
"java": "java",
"sh": "bash",
"json": "json",
"md": "markdown",
"markdown": "markdown"
]
private var modelContext: ModelContext?
func setModelContext(_ context: ModelContext) {
self.modelContext = context
loadTabs()
}
func openFile() {
let openPanel = NSOpenPanel()
openPanel.allowedContentTypes = [.text, .sourceCode, UTType.html, UTType.cSource, UTType.swiftSource, UTType.pythonScript, UTType("public.shell-script")!, UTType("public.markdown")!]
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = false
openPanel.canChooseFiles = true
if openPanel.runModal() == .OK, let url = openPanel.url {
do {
let content = try String(contentsOf: url, encoding: .utf8)
let lang = languageMap[url.pathExtension.lowercased()] ?? "plaintext"
selectedLanguage = lang
let newItem = Item(name: url.lastPathComponent, content: content, language: selectedLanguage)
tabs.append(newItem)
selectedTab = newItem
saveToSwiftData(url.lastPathComponent)
} catch {
print("Error opening file: \(error)")
}
}
}
func saveFile() {
guard let selectedItem = selectedTab else { return }
let savePanel = NSSavePanel()
savePanel.allowedContentTypes = [.text, .sourceCode, UTType.html, UTType.cSource, UTType.swiftSource, UTType.pythonScript, UTType("public.shell-script")!, UTType("public.markdown")!]
savePanel.nameFieldStringValue = selectedItem.name
if savePanel.runModal() == .OK, let url = savePanel.url {
do {
try selectedItem.content.write(to: url, atomically: true, encoding: .utf8)
saveToSwiftData(selectedItem.name)
} catch {
print("Error saving file: \(error)")
}
}
}
func saveAsFile() {
guard let selectedItem = selectedTab else { return }
let savePanel = NSSavePanel()
savePanel.allowedContentTypes = [.text, .sourceCode, UTType.html, UTType.cSource, UTType.swiftSource, UTType.pythonScript, UTType("public.shell-script")!, UTType("public.markdown")!]
savePanel.nameFieldStringValue = selectedItem.name
if savePanel.runModal() == .OK, let url = savePanel.url {
do {
try selectedItem.content.write(to: url, atomically: true, encoding: .utf8)
saveToSwiftData(selectedItem.name)
} catch {
print("Error saving as file: \(error)")
}
}
}
func addNewTab() {
let newItem = Item(name: "Note", content: "", language: selectedLanguage)
tabs.append(newItem)
selectedTab = newItem
saveToSwiftData(newItem.name)
}
func removeTab(_ item: Item) {
if let index = tabs.firstIndex(of: item) {
tabs.remove(at: index)
if selectedTab == item {
selectedTab = tabs.last ?? nil
}
if let context = modelContext {
do {
try context.save()
context.delete(item)
} catch {
print("Failed to delete from SwiftData: \(error)")
}
}
}
}
func updateContent(_ content: String) {
if let selectedItem = selectedTab {
selectedItem.content = content
saveToSwiftData(selectedItem.name)
print("ViewModel updated content to: \(content)")
}
}
func updateLanguage(_ language: String) {
if let selectedItem = selectedTab {
selectedItem.language = language
saveToSwiftData(selectedItem.name)
}
}
private func saveToSwiftData(_ name: String) {
if let selectedItem = selectedTab, let context = modelContext {
do {
try context.save()
if let existingItem = try context.fetch(FetchDescriptor<Item>(sortBy: [SortDescriptor(\Item.name)]))
.first(where: { $0.name == name }) {
existingItem.content = selectedItem.content
existingItem.language = selectedItem.language
} else {
context.insert(selectedItem)
}
} catch {
print("Failed to save to SwiftData: \(error)")
}
}
}
func loadTabs() {
if let context = modelContext {
do {
let descriptor = FetchDescriptor<Item>(sortBy: [SortDescriptor(\Item.name)])
tabs = try context.fetch(descriptor)
if let firstTab = tabs.first {
selectedTab = firstTab
} else {
addNewTab()
}
} catch {
print("Failed to load tabs: \(error)")
addNewTab()
}
} else {
addNewTab()
}
}
func toggleSidebar() {
showSidebar = !showSidebar
}
// Generate table of contents based on selected language and content
func generateTableOfContents() -> [String] {
guard let selectedItem = selectedTab, !selectedItem.content.isEmpty else { return ["No content"] }
switch selectedItem.language.lowercased() {
case "markdown":
return selectedItem.content.components(separatedBy: .newlines)
.filter { $0.hasPrefix("#") }
.map { $0.trimmingCharacters(in: .whitespaces).replacingOccurrences(of: "^#+\\s*", with: "", options: .regularExpression) }
case "swift", "c", "cpp", "java", "python":
return selectedItem.content.components(separatedBy: .newlines)
.filter { $0.contains("func") || $0.contains("def") || $0.contains("class") }
.map { line in
let components = line.components(separatedBy: .whitespaces)
return components.last { !$0.isEmpty && !["func", "def", "class"].contains($0) } ?? line
}
default:
return ["No table of contents available for \(selectedItem.language)"]
}
}
// Find line number for a given TOC item
func lineNumber(for tocItem: String) -> Int? {
guard let selectedItem = selectedTab, !selectedItem.content.isEmpty else { return nil }
let lines = selectedItem.content.components(separatedBy: .newlines)
return lines.firstIndex { $0.contains(tocItem) }
}
}
// MARK: - SidebarView
struct SidebarView: View {
@ObservedObject var viewModel: ContentViewModel
var body: some View {
List(viewModel.generateTableOfContents(), id: \.self, selection: $viewModel.selectedTOCItem) { item in
Text(item)
.foregroundColor(Color.gray)
.onTapGesture {
viewModel.selectedTOCItem = item
if let lineNumber = viewModel.lineNumber(for: item) {
NotificationCenter.default.post(name: .moveCursorToLine, object: lineNumber)
}
}
}
.frame(width: 200)
.background(Color(nsColor: .windowBackgroundColor).opacity(0.85))
.listStyle(SidebarListStyle())
.frame(minHeight: 0)
}
}
// MARK: - ContentView
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@StateObject private var viewModel = ContentViewModel()
@EnvironmentObject var viewModel: ViewModel
@State private var showSidebar: Bool = true
@State private var selectedTOCItem: String? = nil
var body: some View {
TabView(selection: $viewModel.selectedTab) {
ForEach(viewModel.tabs, id: \.id) { tab in
VStack(spacing: 0) {
VStack {
HStack(spacing: 0) {
if viewModel.showSidebar {
SidebarView(viewModel: viewModel)
if showSidebar {
SidebarView(content: tab.content, language: tab.language, selectedTOCItem: $selectedTOCItem)
}
VStack(spacing: 0) {
CustomTextEditor(text: Binding(
get: { tab.content },
set: { viewModel.updateContent($0) }
), language: $viewModel.selectedLanguage, highlightr: HighlightrViewModel().highlightr)
.frame(maxWidth: .infinity, maxHeight: .infinity)
CustomTextEditor(text: Binding(
get: { tab.content },
set: { tab.content = $0 }
), language: Binding(
get: { tab.language },
set: { tab.language = $0 }
), highlightr: HighlightrViewModel().highlightr)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.frame(minWidth: 1000, minHeight: 600)
.background(Color(nsColor: .textBackgroundColor).opacity(0.85))
.toolbar {
ToolbarItemGroup {
HStack(spacing: 10) {
Picker("", selection: Binding(
get: { tab.language },
set: { tab.language = $0 }
)) {
ForEach(["HTML", "C", "Swift", "Python", "C++", "Java", "Bash", "JSON", "Markdown"], id: \.self) { lang in
Text(lang).tag(lang.lowercased())
}
}
.labelsHidden()
.onChange(of: tab.language) { _, newValue in
NotificationCenter.default.post(name: .languageChanged, object: newValue)
}
Button(action: { showSidebar.toggle() }) {
Image(systemName: "sidebar.left")
}
.buttonStyle(.borderless)
Button(action: { openFile(tab) }) {
Image(systemName: "folder.badge.plus")
}
.buttonStyle(.borderless)
Button(action: { saveFile(tab) }) {
Image(systemName: "floppydisk")
}
.buttonStyle(.borderless)
Button(action: { saveAsFile(tab) }) {
Image(systemName: "square.and.arrow.down")
}
.buttonStyle(.borderless)
Spacer()
}
.padding(.horizontal, 10)
.background(Color(nsColor: .windowBackgroundColor))
.cornerRadius(8)
}
}
}
.tabItem {
Text(tab.name)
}
.tag(tab as Item?)
}
}
.frame(minWidth: 1000, minHeight: 600)
.background(Color(nsColor: .textBackgroundColor).opacity(0.85))
.toolbar {
ToolbarItemGroup(placement: .automatic) {
HStack(spacing: 10) {
Text("Neon Vision Editor")
.foregroundColor(.white)
Picker("", selection: $viewModel.selectedLanguage) {
ForEach(viewModel.languages, id: \.self) { lang in
Text(lang).tag(lang.lowercased())
}
}
.labelsHidden()
Button(action: viewModel.toggleSidebar) {
Image(systemName: "sidebar.left")
}
.buttonStyle(.borderless)
Button(action: viewModel.openFile) {
Image(systemName: "folder.badge.plus")
}
.buttonStyle(.borderless)
Button(action: viewModel.saveFile) {
Image(systemName: "floppydisk")
}
.buttonStyle(.borderless)
Button(action: viewModel.saveAsFile) {
Image(systemName: "square.and.arrow.down")
}
.buttonStyle(.borderless)
Button(action: viewModel.addNewTab) {
Image(systemName: "plus")
}
.buttonStyle(.borderless)
Spacer()
}
.padding(.horizontal, 10)
.background(Color(nsColor: .windowBackgroundColor))
.cornerRadius(8)
.tag(tab as Tab?)
}
}
.tabViewStyle(.automatic) // Enables native macOS tabbing
.onAppear {
viewModel.setModelContext(modelContext)
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))")
}
}
func openFile(_ tab: Tab) {
let openPanel = NSOpenPanel()
openPanel.allowedContentTypes = [.text, .sourceCode, UTType.html, UTType.cSource, UTType.swiftSource, UTType.pythonScript, UTType("public.shell-script")!, UTType("public.markdown")!]
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = false
openPanel.canChooseFiles = true
guard openPanel.runModal() == .OK, let url = openPanel.url else { return }
do {
let content = try String(contentsOf: url, encoding: .utf8)
let language = languageMap[url.pathExtension.lowercased()] ?? "plaintext"
tab.content = content
tab.language = language
tab.name = url.lastPathComponent
if let window = NSApplication.shared.windows.first {
window.title = tab.name
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
window.title = tab.name
}
}
} catch {
print("Error opening file: \(error)")
}
}
func saveFile(_ tab: Tab) {
let savePanel = NSSavePanel()
savePanel.allowedContentTypes = [.text, .sourceCode, UTType.html, UTType.cSource, UTType.swiftSource, UTType.pythonScript, UTType("public.shell-script")!, UTType("public.markdown")!]
savePanel.nameFieldStringValue = tab.name
guard savePanel.runModal() == .OK, let url = savePanel.url else { return }
do {
try tab.content.write(to: url, atomically: true, encoding: .utf8)
try viewModel.saveTab(tab)
tab.name = url.lastPathComponent
if let window = NSApplication.shared.windows.first {
window.title = tab.name
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
window.title = tab.name
}
}
print("Successfully saved file to: \(url.path)")
} catch {
print("Error saving file: \(error)")
}
}
func saveAsFile(_ tab: Tab) {
let savePanel = NSSavePanel()
savePanel.allowedContentTypes = [.text, .sourceCode, UTType.html, UTType.cSource, UTType.swiftSource, UTType.pythonScript, UTType("public.shell-script")!, UTType("public.markdown")!]
savePanel.nameFieldStringValue = tab.name
guard savePanel.runModal() == .OK, let url = savePanel.url else { return }
do {
try tab.content.write(to: url, atomically: true, encoding: .utf8)
try viewModel.saveTab(tab)
tab.name = url.lastPathComponent
if let window = NSApplication.shared.windows.first {
window.title = tab.name
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
window.title = tab.name
}
}
print("Successfully saved as file to: \(url.path)")
} catch {
print("Error saving as file: \(error)")
}
}
let languageMap: [String: String] = [
"html": "html", "htm": "html",
"c": "c", "h": "c",
"swift": "swift",
"py": "python",
"cpp": "cpp",
"java": "java",
"sh": "bash",
"json": "json",
"md": "markdown", "markdown": "markdown"
]
}
// MARK: - SidebarView
struct SidebarView: View {
let content: String
let language: String
@Binding var selectedTOCItem: String?
var body: some View {
List(generateTableOfContents(), id: \.self, selection: $selectedTOCItem) { item in
Text(item)
.foregroundColor(.gray)
.onTapGesture {
selectedTOCItem = item
if let lineNumber = lineNumber(for: item) {
NotificationCenter.default.post(name: .moveCursorToLine, object: lineNumber)
}
}
}
.frame(width: 200)
.background(Color(nsColor: .windowBackgroundColor).opacity(0.85))
.listStyle(.sidebar)
}
func generateTableOfContents() -> [String] {
if content.isEmpty {
return ["No content"]
}
switch language.lowercased() {
case "markdown":
return content.components(separatedBy: .newlines)
.filter { $0.hasPrefix("#") }
.map { $0.trimmingCharacters(in: .whitespaces).replacingOccurrences(of: "^#+\\s*", with: "", options: .regularExpression) }
case "swift", "c", "cpp", "java", "python":
return content.components(separatedBy: .newlines)
.filter { $0.contains("func") || $0.contains("def") || $0.contains("class") }
.map { line in
let components = line.components(separatedBy: .whitespaces)
return components.last { !$0.isEmpty && !["func", "def", "class"].contains($0) } ?? line
}
default:
return ["No table of contents available for \(language)"]
}
}
func lineNumber(for tocItem: String) -> Int? {
if content.isEmpty {
return nil
}
let lines = content.components(separatedBy: .newlines)
return lines.firstIndex { $0.contains(tocItem) }
}
}
// MARK: - CustomTextEditor
@ -316,13 +234,21 @@ struct CustomTextEditor: NSViewRepresentable {
let textView = NSTextView(frame: .zero, textContainer: textContainer)
textView.isEditable = true
textView.isSelectable = true
textView.font = NSFont.monospacedSystemFont(ofSize: 14, weight: .regular)
textView.backgroundColor = NSColor.textBackgroundColor.withAlphaComponent(0.85)
textView.textColor = NSColor.gray
textView.font = NSFont(name: "SFMono-Regular", size: 12.0) ?? NSFont.monospacedSystemFont(ofSize: 12, weight: .regular)
let appearance = NSAppearance.currentDrawing()
if appearance.name == .darkAqua {
textView.backgroundColor = NSColor(red: 0.1, green: 0.1, blue: 0.1, alpha: 0.85)
textView.textColor = NSColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 0.85)
} else {
textView.backgroundColor = NSColor(red: 1, green: 1, blue: 1, alpha: 0.85)
textView.textColor = NSColor(red: 0, green: 0, blue: 0, alpha: 0.85)
}
textView.delegate = context.coordinator
textView.isHorizontallyResizable = false
textView.isVerticallyResizable = true
textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
textView.maxSize = NSSize(width: .greatestFiniteMagnitude, height: .greatestFiniteMagnitude)
let scrollView = NSScrollView()
scrollView.documentView = textView
@ -331,12 +257,20 @@ struct CustomTextEditor: NSViewRepresentable {
scrollView.autoresizingMask = [.width, .height]
scrollView.translatesAutoresizingMaskIntoConstraints = false
// Add observer for cursor movement
NotificationCenter.default.addObserver(forName: .moveCursorToLine, object: nil, queue: .main) { notification in
if let lineNumber = notification.object as? Int {
textView.scrollToLine(lineNumber)
textView.setSelectedRange(NSRange(location: textView.lineStartIndex(at: lineNumber) ?? 0, length: 0))
NotificationCenter.default.addObserver(forName: .languageChanged, object: nil, queue: .main) { notification in
guard let newLanguage = notification.object as? String else { return }
language = newLanguage
textView.string = text
if let highlightedText = highlightr.highlight(text, as: newLanguage == "markdown" ? "md" : newLanguage) {
textStorage.setAttributedString(highlightedText)
}
textStorage.language = newLanguage == "markdown" ? "md" : newLanguage
}
NotificationCenter.default.addObserver(forName: .moveCursorToLine, object: nil, queue: .main) { notification in
guard let lineNumber = notification.object as? Int else { return }
textView.scrollToLine(lineNumber)
textView.setSelectedRange(NSRange(location: textView.lineStartIndex(at: lineNumber) ?? 0, length: 0))
}
DispatchQueue.main.async {
@ -353,16 +287,15 @@ struct CustomTextEditor: NSViewRepresentable {
}
func updateNSView(_ nsView: NSScrollView, context: Context) {
if let textView = nsView.documentView as? NSTextView, textView.string != text {
textView.string = text
let textStorage = textView.textStorage as! CodeAttributedString
textStorage.beginEditing()
if let highlightedText = highlightr.highlight(text, as: language == "markdown" ? "md" : language) {
textStorage.setAttributedString(highlightedText)
}
textStorage.endEditing()
textStorage.language = language == "markdown" ? "md" : language
guard let textView = nsView.documentView as? NSTextView, textView.string != text else { return }
textView.string = text
let textStorage = textView.textStorage as! CodeAttributedString
textStorage.beginEditing()
if let highlightedText = highlightr.highlight(text, as: language == "markdown" ? "md" : language) {
textStorage.setAttributedString(highlightedText)
}
textStorage.endEditing()
textStorage.language = language == "markdown" ? "md" : language
}
func makeCoordinator() -> Coordinator {
@ -370,16 +303,15 @@ struct CustomTextEditor: NSViewRepresentable {
}
class Coordinator: NSObject, NSTextViewDelegate {
var parent: CustomTextEditor
let parent: CustomTextEditor
init(_ parent: CustomTextEditor) {
self.parent = parent
}
func textDidChange(_ notification: Notification) {
if let textView = notification.object as? NSTextView {
parent.text = textView.string
}
guard let textView = notification.object as? NSTextView else { return }
parent.text = textView.string
}
}
}
@ -390,35 +322,20 @@ extension NSTextView {
guard lineNumber >= 0, lineNumber < string.components(separatedBy: .newlines).count else { return nil }
let lines = string.components(separatedBy: .newlines)
var position = 0
for i in 0...lineNumber {
if i < lines.count {
position += lines[i].count + 1 // +1 for newline
}
for i in 0...lineNumber where i < lines.count {
position += lines[i].count + 1
}
return position > string.count ? nil : position
}
func scrollToLine(_ lineNumber: Int) {
guard let startIndex = lineStartIndex(at: lineNumber) else { return }
let glyphRange = layoutManager?.glyphRange(forCharacterRange: NSRange(location: startIndex, length: 0), actualCharacterRange: nil)
if let glyphRange = glyphRange {
scrollRangeToVisible(glyphRange)
}
guard let glyphRange = layoutManager?.glyphRange(forCharacterRange: NSRange(location: startIndex, length: 0), actualCharacterRange: nil) else { return }
scrollRangeToVisible(glyphRange)
}
}
extension Notification.Name {
static let moveCursorToLine = Notification.Name("moveCursorToLine")
}
// MARK: - HighlightrViewModel
class HighlightrViewModel {
let highlightr: Highlightr = {
if let h = Highlightr() {
h.setTheme(to: "vs2015")
return h
} else {
fatalError("Highlightr initialization failed. Ensure the package is correctly added via Swift Package Manager.")
}
}()
}
static let languageChanged = Notification.Name("languageChanged")
}

View file

@ -1,16 +0,0 @@
import SwiftData
import Foundation // Added to provide UUID type
@Model
class Item: Identifiable {
var id = UUID() // Unique identifier for Identifiable conformance
var name: String
var content: String
var language: String
init(name: String, content: String, language: String) {
self.name = name
self.content = content
self.language = language
}
}

View file

@ -0,0 +1,151 @@
import SwiftUI
import SwiftData
@main
struct Neon_Vision_EditorApp: App {
let container: ModelContainer
@StateObject private var viewModel = ViewModel()
@State private var showCloseAlert = false
@State private var unsavedTabs: [Tab] = []
init() {
do {
container = try ModelContainer(for: Tab.self)
} catch {
fatalError("Failed to create ModelContainer: \(error)")
}
}
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.modelContext, container.mainContext)
.environmentObject(viewModel)
.frame(minWidth: 1000, minHeight: 600)
.alert(isPresented: $showCloseAlert) {
Alert(
title: Text("Save Changes?"),
message: Text("You have unsaved changes. Do you want to save them?"),
primaryButton: .default(Text("Save All")) {
saveAllTabs()
},
secondaryButton: .destructive(Text("Discard")) {
discardUnsaved()
},
tertiaryButton: .cancel()
)
}
}
.defaultSize(width: 1000, height: 600)
.handlesExternalEvents(matching: Set(arrayLiteral: "*"))
.commands {
CommandGroup(replacing: .newItem) {
Button("New Tab") {
viewModel.addTab()
}
.keyboardShortcut("t", modifiers: .command)
}
}
.onChange(of: viewModel.tabs) { _, _ in
unsavedTabs = viewModel.tabs.filter { !$0.content.isEmpty }
}
.onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { _ in
if !unsavedTabs.isEmpty {
showCloseAlert = true
}
}
}
private func saveAllTabs() {
for tab in unsavedTabs {
let savePanel = NSSavePanel()
savePanel.nameFieldStringValue = tab.name
if savePanel.runModal() == .OK, let url = savePanel.url {
do {
try tab.content.write(to: url, atomically: true, encoding: .utf8)
try viewModel.saveTab(tab)
} catch {
print("Error saving tab: \(error)")
}
}
}
unsavedTabs.removeAll()
}
private func discardUnsaved() {
viewModel.discardUnsaved()
unsavedTabs.removeAll()
}
}
// MARK: - ViewModel
@Observable
class ViewModel {
var tabs: [Tab] = []
var selectedTab: Tab?
var modelContext: ModelContext?
func setModelContext(_ context: ModelContext) {
modelContext = context
if tabs.isEmpty {
addTab()
}
}
func addTab() {
let newTab = Tab()
tabs.append(newTab)
selectedTab = newTab
if let window = NSApplication.shared.windows.first {
window.title = newTab.name
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
window.title = newTab.name
}
}
}
func removeTab(_ tab: Tab) {
if let index = tabs.firstIndex(where: { $0.id == tab.id }) {
tabs.remove(at: index)
selectedTab = tabs.last
if let window = NSApplication.shared.windows.first {
window.title = selectedTab?.name ?? "Note"
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
window.title = selectedTab?.name ?? "Note"
}
}
}
}
func saveTab(_ tab: Tab) throws {
guard let context = modelContext else { throw NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "No model context"]) }
do {
try context.save()
if let existingItem = try context.fetch(FetchDescriptor<Tab>(sortBy: [SortDescriptor(\Tab.name)]))
.first(where: { $0.name == tab.name }) {
existingItem.content = tab.content
existingItem.language = tab.language
} else {
context.insert(tab)
}
} catch {
throw error
}
}
func discardUnsaved() {
guard let context = modelContext else { return }
do {
let descriptor = FetchDescriptor<Tab>()
let allTabs = try context.fetch(descriptor)
for tab in allTabs {
context.delete(tab)
}
try context.save()
tabs.removeAll()
addTab()
} catch {
print("Error discarding unsaved tabs: \(error)")
}
}
}

View file

@ -1,12 +0,0 @@
import SwiftUI
import SwiftData
@main
struct Neon_Vision_EditorApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(for: Item.self) // Configure the model container for the Item schema
}
}
}

View file

@ -0,0 +1,17 @@
import Foundation
import SwiftData
// MARK: - Tab
@Model
class Tab {
var id: UUID = UUID()
var name: String
var content: String
var language: String
init(name: String = "Note", content: String = "", language: String = "swift") {
self.name = name
self.content = content
self.language = language
}
}