From 0faaff61cb4d71896b033eaad5f4aa666ab209aa Mon Sep 17 00:00:00 2001 From: Rodric Krogh Date: Wed, 27 Aug 2025 13:34:59 +0200 Subject: [PATCH] Adjustments to tabbar approach and syntax highlighting of text code --- Neon Vision Editor.xcodeproj/project.pbxproj | 159 ++-- .../xcshareddata/swiftpm/Package.resolved | 15 - Neon Vision Editor/ContentView.swift | 728 +++++++++++------- Neon Vision Editor/{Tab.swift => Item.swift} | 11 +- Neon Vision Editor/Neon.swift | 151 ---- .../Neon_Vision_EditorApp.swift | 34 + 6 files changed, 554 insertions(+), 544 deletions(-) delete mode 100644 Neon Vision Editor.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved rename Neon Vision Editor/{Tab.swift => Item.swift} (58%) delete mode 100644 Neon Vision Editor/Neon.swift create mode 100644 Neon Vision Editor/Neon_Vision_EditorApp.swift diff --git a/Neon Vision Editor.xcodeproj/project.pbxproj b/Neon Vision Editor.xcodeproj/project.pbxproj index f43a1e9..38a42a1 100644 --- a/Neon Vision Editor.xcodeproj/project.pbxproj +++ b/Neon Vision Editor.xcodeproj/project.pbxproj @@ -3,19 +3,33 @@ archiveVersion = 1; classes = { }; - objectVersion = 77; + objectVersion = 90; objects = { /* Begin PBXBuildFile section */ - 98E2A9AC2E5C5075005FFE78 /* Highlightr in Frameworks */ = {isa = PBXBuildFile; productRef = 98E2A9AB2E5C5075005FFE78 /* Highlightr */; }; + 983EEA302E5F22DA00E19094 /* SwiftData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98EAE6592E5F1E890050E579 /* SwiftData.framework */; }; + 983EEA312E5F22DA00E19094 /* SwiftData.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 98EAE6592E5F1E890050E579 /* SwiftData.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ +/* Begin PBXCopyFilesBuildPhase section */ + 983EEA322E5F22DA00E19094 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + dstPath = ""; + dstSubfolder = Frameworks; + files = ( + 983EEA312E5F22DA00E19094 /* SwiftData.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ - 98E2A99A2E5C4A98005FFE78 /* Neon Vision Editor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Neon Vision Editor.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 98EAE6332E5F15E80050E579 /* Neon Vision Editor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Neon Vision Editor.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 98EAE6592E5F1E890050E579 /* SwiftData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftData.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS26.0.sdk/System/Library/Frameworks/SwiftData.framework; sourceTree = DEVELOPER_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 98E2A99C2E5C4A98005FFE78 /* Neon Vision Editor */ = { + 98EAE6352E5F15E80050E579 /* Neon Vision Editor */ = { isa = PBXFileSystemSynchronizedRootGroup; path = "Neon Vision Editor"; sourceTree = ""; @@ -23,118 +37,114 @@ /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ - 98E2A9972E5C4A98005FFE78 /* Frameworks */ = { + 98EAE6302E5F15E80050E579 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; files = ( - 98E2A9AC2E5C5075005FFE78 /* Highlightr in Frameworks */, + 983EEA302E5F22DA00E19094 /* SwiftData.framework in Frameworks */, ); - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 98E2A9912E5C4A98005FFE78 = { + 98EAE62A2E5F15E80050E579 = { isa = PBXGroup; children = ( - 98E2A99C2E5C4A98005FFE78 /* Neon Vision Editor */, - 98E2A99B2E5C4A98005FFE78 /* Products */, + 98EAE6352E5F15E80050E579 /* Neon Vision Editor */, + 98EAE6532E5F175B0050E579 /* Frameworks */, + 98EAE6342E5F15E80050E579 /* Products */, ); sourceTree = ""; }; - 98E2A99B2E5C4A98005FFE78 /* Products */ = { + 98EAE6342E5F15E80050E579 /* Products */ = { isa = PBXGroup; children = ( - 98E2A99A2E5C4A98005FFE78 /* Neon Vision Editor.app */, + 98EAE6332E5F15E80050E579 /* Neon Vision Editor.app */, ); name = Products; sourceTree = ""; }; + 98EAE6532E5F175B0050E579 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 98EAE6592E5F1E890050E579 /* SwiftData.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 98E2A9992E5C4A98005FFE78 /* Neon Vision Editor */ = { + 98EAE6322E5F15E80050E579 /* Neon Vision Editor */ = { isa = PBXNativeTarget; - buildConfigurationList = 98E2A9A72E5C4A9A005FFE78 /* Build configuration list for PBXNativeTarget "Neon Vision Editor" */; + buildConfigurationList = 98EAE6402E5F15EC0050E579 /* Build configuration list for PBXNativeTarget "Neon Vision Editor" */; buildPhases = ( - 98E2A9962E5C4A98005FFE78 /* Sources */, - 98E2A9972E5C4A98005FFE78 /* Frameworks */, - 98E2A9982E5C4A98005FFE78 /* Resources */, + 98EAE62F2E5F15E80050E579 /* Sources */, + 98EAE6302E5F15E80050E579 /* Frameworks */, + 98EAE6312E5F15E80050E579 /* Resources */, + 983EEA322E5F22DA00E19094 /* Embed Frameworks */, ); buildRules = ( ); - dependencies = ( - ); fileSystemSynchronizedGroups = ( - 98E2A99C2E5C4A98005FFE78 /* Neon Vision Editor */, + 98EAE6352E5F15E80050E579 /* Neon Vision Editor */, ); name = "Neon Vision Editor"; - packageProductDependencies = ( - 98E2A9AB2E5C5075005FFE78 /* Highlightr */, - ); productName = "Neon Vision Editor"; - productReference = 98E2A99A2E5C4A98005FFE78 /* Neon Vision Editor.app */; + productReference = 98EAE6332E5F15E80050E579 /* Neon Vision Editor.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ - 98E2A9922E5C4A98005FFE78 /* Project object */ = { + 98EAE62B2E5F15E80050E579 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 2600; LastUpgradeCheck = 2600; TargetAttributes = { - 98E2A9992E5C4A98005FFE78 = { + 98EAE6322E5F15E80050E579 = { CreatedOnToolsVersion = 26.0; }; }; }; - buildConfigurationList = 98E2A9952E5C4A98005FFE78 /* Build configuration list for PBXProject "Neon Vision Editor" */; + buildConfigurationList = 98EAE62E2E5F15E80050E579 /* Build configuration list for PBXProject "Neon Vision Editor" */; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); - mainGroup = 98E2A9912E5C4A98005FFE78; + mainGroup = 98EAE62A2E5F15E80050E579; minimizedProjectReferenceProxies = 1; - packageReferences = ( - 98E2A9AA2E5C5075005FFE78 /* XCRemoteSwiftPackageReference "Highlightr" */, - ); - preferredProjectObjectVersion = 77; - productRefGroup = 98E2A99B2E5C4A98005FFE78 /* Products */; + preferredProjectObjectVersion = 90; + productRefGroup = 98EAE6342E5F15E80050E579 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( - 98E2A9992E5C4A98005FFE78 /* Neon Vision Editor */, + 98EAE6322E5F15E80050E579 /* Neon Vision Editor */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 98E2A9982E5C4A98005FFE78 /* Resources */ = { + 98EAE6312E5F15E80050E579 /* Resources */ = { isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; files = ( ); - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - 98E2A9962E5C4A98005FFE78 /* Sources */ = { + 98EAE62F2E5F15E80050E579 /* Sources */ = { isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; files = ( ); - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ - 98E2A9A52E5C4A9A005FFE78 /* Debug */ = { + 98EAE63E2E5F15EC0050E579 /* Debug configuration for PBXProject "Neon Vision Editor" */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -169,6 +179,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = CS727NF72U; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -195,7 +206,7 @@ }; name = Debug; }; - 98E2A9A62E5C4A9A005FFE78 /* Release */ = { + 98EAE63F2E5F15EC0050E579 /* Release configuration for PBXProject "Neon Vision Editor" */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -230,6 +241,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = CS727NF72U; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -248,20 +260,22 @@ }; name = Release; }; - 98E2A9A82E5C4A9A005FFE78 /* Debug */ = { + 98EAE6412E5F15EC0050E579 /* Debug configuration for PBXNativeTarget "Neon Vision Editor" */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; - CFBundleIconName = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = CS727NF72U; ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "Neon Vision Editor"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; @@ -269,6 +283,7 @@ "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + INFOPLIST_KEY_UIStatusBarStyle = ""; "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; @@ -280,34 +295,38 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "h3p.Neon-Vision-Editor"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; REGISTER_APP_GROUPS = YES; SDKROOT = auto; STRING_CATALOG_GENERATE_SYMBOLS = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,7"; + TARGETED_DEVICE_FAMILY = "1,2"; XROS_DEPLOYMENT_TARGET = 26.0; }; name = Debug; }; - 98E2A9A92E5C4A9A005FFE78 /* Release */ = { + 98EAE6422E5F15EC0050E579 /* Release configuration for PBXNativeTarget "Neon Vision Editor" */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; - CFBundleIconName = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = CS727NF72U; ENABLE_APP_SANDBOX = YES; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; ENABLE_USER_SELECTED_FILES = readonly; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "Neon Vision Editor"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES; @@ -315,6 +334,7 @@ "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES; "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES; "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES; + INFOPLIST_KEY_UIStatusBarStyle = ""; "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault; "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; @@ -326,16 +346,18 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "h3p.Neon-Vision-Editor"; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; REGISTER_APP_GROUPS = YES; SDKROOT = auto; STRING_CATALOG_GENERATE_SYMBOLS = YES; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,7"; + TARGETED_DEVICE_FAMILY = "1,2"; XROS_DEPLOYMENT_TARGET = 26.0; }; name = Release; @@ -343,44 +365,23 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 98E2A9952E5C4A98005FFE78 /* Build configuration list for PBXProject "Neon Vision Editor" */ = { + 98EAE62E2E5F15E80050E579 /* Build configuration list for PBXProject "Neon Vision Editor" */ = { isa = XCConfigurationList; buildConfigurations = ( - 98E2A9A52E5C4A9A005FFE78 /* Debug */, - 98E2A9A62E5C4A9A005FFE78 /* Release */, + 98EAE63E2E5F15EC0050E579 /* Debug configuration for PBXProject "Neon Vision Editor" */, + 98EAE63F2E5F15EC0050E579 /* Release configuration for PBXProject "Neon Vision Editor" */, ); - defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 98E2A9A72E5C4A9A005FFE78 /* Build configuration list for PBXNativeTarget "Neon Vision Editor" */ = { + 98EAE6402E5F15EC0050E579 /* Build configuration list for PBXNativeTarget "Neon Vision Editor" */ = { isa = XCConfigurationList; buildConfigurations = ( - 98E2A9A82E5C4A9A005FFE78 /* Debug */, - 98E2A9A92E5C4A9A005FFE78 /* Release */, + 98EAE6412E5F15EC0050E579 /* Debug configuration for PBXNativeTarget "Neon Vision Editor" */, + 98EAE6422E5F15EC0050E579 /* Release configuration for PBXNativeTarget "Neon Vision Editor" */, ); - defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ - -/* Begin XCRemoteSwiftPackageReference section */ - 98E2A9AA2E5C5075005FFE78 /* XCRemoteSwiftPackageReference "Highlightr" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/raspu/Highlightr"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.3.0; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 98E2A9AB2E5C5075005FFE78 /* Highlightr */ = { - isa = XCSwiftPackageProductDependency; - package = 98E2A9AA2E5C5075005FFE78 /* XCRemoteSwiftPackageReference "Highlightr" */; - productName = Highlightr; - }; -/* End XCSwiftPackageProductDependency section */ }; - rootObject = 98E2A9922E5C4A98005FFE78 /* Project object */; + rootObject = 98EAE62B2E5F15E80050E579 /* Project object */; } diff --git a/Neon Vision Editor.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Neon Vision Editor.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 00f80b1..0000000 --- a/Neon Vision Editor.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,15 +0,0 @@ -{ - "originHash" : "0a8fce6d3cd251ad12ea6d2f48f55bfdc4fa43f93e39dce01f4b2baa78e6feb8", - "pins" : [ - { - "identity" : "highlightr", - "kind" : "remoteSourceControl", - "location" : "https://github.com/raspu/Highlightr", - "state" : { - "revision" : "05e7fcc63b33925cd0c1faaa205cdd5681e7bbef", - "version" : "2.3.0" - } - } - ], - "version" : 3 -} diff --git a/Neon Vision Editor/ContentView.swift b/Neon Vision Editor/ContentView.swift index 601c071..a99a16a 100644 --- a/Neon Vision Editor/ContentView.swift +++ b/Neon Vision Editor/ContentView.swift @@ -1,341 +1,481 @@ import SwiftUI -import Highlightr -import UniformTypeIdentifiers -import Combine import SwiftData +import UniformTypeIdentifiers +#if os(macOS) import AppKit +#elseif os(iOS) +import UIKit +#endif -// MARK: - ContentView struct ContentView: View { @Environment(\.modelContext) private var modelContext - @EnvironmentObject var viewModel: ViewModel + @Query private var tabs: [Tab] + @State private var selectedTab: Tab? @State private var showSidebar: Bool = true @State private var selectedTOCItem: String? = nil - + #if os(iOS) + @State private var showingDocumentPicker = false + #endif + + private let languages: [String] = ["Swift", "Python", "C", "C++", "Java", "HTML", "Markdown", "JSON", "Bash"] + private let languageMap: [String: String] = [ + "swift": "swift", "py": "python", "c": "c", "cpp": "cpp", "java": "java", + "html": "html", "htm": "html", "md": "markdown", "json": "json", "sh": "bash" + ] + var body: some View { - TabView(selection: $viewModel.selectedTab) { - ForEach(viewModel.tabs, id: \.id) { tab in - VStack { - HStack(spacing: 0) { - if showSidebar { - SidebarView(content: tab.content, language: tab.language, selectedTOCItem: $selectedTOCItem) - } - 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() + NavigationSplitView { + List(tabs, selection: $selectedTab) { tab in + Text(tab.name) + .tag(tab) + .foregroundColor(.gray) + } + .listStyle(.sidebar) + .frame(minWidth: 200) + .background(Color(.windowBackgroundColor).opacity(0.85)) + } detail: { + if let selectedTab = selectedTab { + CustomTextEditor( + text: Binding( + get: { selectedTab.content }, + set: { selectedTab.content = $0 } + ), + language: Binding( + get: { selectedTab.language }, + set: { selectedTab.language = $0 } + ), + isModified: Binding( + get: { selectedTab.isModified }, + set: { selectedTab.isModified = $0 } + ) + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color(.textBackgroundColor).opacity(0.85)) + .toolbar { + ToolbarItemGroup { + Picker("Language", selection: Binding( + get: { selectedTab.language }, + set: { selectedTab.language = $0 } + )) { + ForEach(languages, id: \.self) { lang in + Text(lang).tag(lang.lowercased() as String) } - .padding(.horizontal, 10) - .background(Color(nsColor: .windowBackgroundColor)) - .cornerRadius(8) + } + .labelsHidden() + Button(action: { showSidebar.toggle() }) { + Image(systemName: "sidebar.left") + } + Button(action: { openFile() }) { + Image(systemName: "folder.badge.plus") + } + Button(action: { saveFile(for: selectedTab) }) { + Image(systemName: "floppydisk") + } + Button(action: { saveAsFile(for: selectedTab) }) { + Image(systemName: "square.and.arrow.down") } } } - .tabItem { - Text(tab.name) + .onChange(of: selectedTab.content) { _, _ in + selectedTab.isModified = true } - .tag(tab as Tab?) + .onChange(of: selectedTab.language) { _, _ in + selectedTab.isModified = true + } + .onAppear { + #if os(macOS) + if let window = NSApplication.shared.windows.first { + window.title = selectedTab.name + } + #endif + } + #if os(macOS) + .onReceive(NotificationCenter.default.publisher(for: NSWindow.willCloseNotification)) { _ in + if selectedTab.isModified { + promptToSave(for: selectedTab) + } + } + #elseif os(iOS) + .onDisappear { + if selectedTab.isModified { + promptToSave(for: selectedTab) + } + } + .sheet(isPresented: $showingDocumentPicker) { + DocumentPicker { url in + handleDocumentPickerSelection(url) + } + } + #endif + } else { + Text("Select a tab or create a new one") + .frame(maxWidth: .infinity, maxHeight: .infinity) } } - .tabViewStyle(.automatic) // Enables native macOS tabbing - .onAppear { - viewModel.setModelContext(modelContext) - } } - - 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 } + + func openFile() { + #if os(macOS) + let panel = NSOpenPanel() + panel.allowedContentTypes = [.text, .sourceCode, .swiftSource, .pythonScript, .html, .cSource, .shellScript, .json, UTType("public.markdown") ?? .text] + panel.allowsMultipleSelection = false + panel.canChooseDirectories = false + panel.canChooseFiles = true + + guard panel.runModal() == .OK, let url = panel.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 + let newTab = Tab( + name: url.lastPathComponent, + content: content, + language: languageMap[url.pathExtension.lowercased()] ?? "plaintext" + ) + modelContext.insert(newTab) + selectedTab = newTab if let window = NSApplication.shared.windows.first { - window.title = tab.name - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - window.title = tab.name - } + window.title = newTab.name } } catch { - print("Error opening file: \(error)") + NSAlert(error: error).runModal() } + #elseif os(iOS) + showingDocumentPicker = true + #endif } - - 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)"] + #if os(iOS) + func handleDocumentPickerSelection(_ url: URL) { + do { + let content = try String(contentsOf: url, encoding: .utf8) + let newTab = Tab( + name: url.lastPathComponent, + content: content, + language: languageMap[url.pathExtension.lowercased()] ?? "plaintext" + ) + modelContext.insert(newTab) + selectedTab = newTab + } catch { + let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true) } } - - func lineNumber(for tocItem: String) -> Int? { - if content.isEmpty { - return nil + #endif + + func saveFile(for tab: Tab) { + #if os(macOS) + guard tab.isModified else { return } + let panel = NSSavePanel() + panel.allowedContentTypes = [.text, .sourceCode, .swiftSource, .pythonScript, .html, .cSource, .shellScript, .json, UTType("public.markdown") ?? .text] + panel.nameFieldStringValue = tab.name + + guard panel.runModal() == .OK, let url = panel.url else { return } + do { + try tab.content.write(to: url, atomically: true, encoding: .utf8) + tab.name = url.lastPathComponent + tab.isModified = false + if let window = NSApplication.shared.windows.first { + window.title = tab.name + } + } catch { + NSAlert(error: error).runModal() } - let lines = content.components(separatedBy: .newlines) - return lines.firstIndex { $0.contains(tocItem) } + #elseif os(iOS) + // iOS save logic (simplified, as iOS file handling is complex) + // Implement UIDocumentPickerViewController for export + #endif + } + + func saveAsFile(for tab: Tab) { + #if os(macOS) + let panel = NSSavePanel() + panel.allowedContentTypes = [.text, .sourceCode, .swiftSource, .pythonScript, .html, .cSource, .shellScript, .json, UTType("public.markdown") ?? .text] + panel.nameFieldStringValue = tab.name + + guard panel.runModal() == .OK, let url = panel.url else { return } + do { + try tab.content.write(to: url, atomically: true, encoding: .utf8) + tab.name = url.lastPathComponent + tab.isModified = false + if let window = NSApplication.shared.windows.first { + window.title = tab.name + } + } catch { + NSAlert(error: error).runModal() + } + #elseif os(iOS) + // iOS save-as logic (simplified) + #endif + } + + func promptToSave(for tab: Tab) { + guard tab.isModified else { return } + #if os(macOS) + let alert = NSAlert() + alert.messageText = "Do you want to save changes to \(tab.name)?" + alert.informativeText = "Your changes will be lost if you don't save them." + alert.addButton(withTitle: "Save") + alert.addButton(withTitle: "Cancel") + alert.addButton(withTitle: "Don't Save") + switch alert.runModal() { + case .alertFirstButtonReturn: saveFile(for: tab) + case .alertSecondButtonReturn: break + default: tab.isModified = false + } + #elseif os(iOS) + let alert = UIAlertController(title: "Save Changes?", message: "Do you want to save changes to \(tab.name)?", preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "Save", style: .default) { _ in saveFile(for: tab) }) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + alert.addAction(UIAlertAction(title: "Don't Save", style: .destructive) { _ in tab.isModified = false }) + UIApplication.shared.windows.first?.rootViewController?.present(alert, animated: true) + #endif } } -// MARK: - CustomTextEditor -struct CustomTextEditor: NSViewRepresentable { - @Binding var text: String - @Binding var language: String - let highlightr: Highlightr - - func makeNSView(context: Context) -> NSScrollView { - let textStorage = CodeAttributedString(highlightr: highlightr) - let layoutManager = NSLayoutManager() - textStorage.addLayoutManager(layoutManager) - let textContainer = NSTextContainer() - textContainer.widthTracksTextView = true - textContainer.heightTracksTextView = false - layoutManager.addTextContainer(textContainer) - - let textView = NSTextView(frame: .zero, textContainer: textContainer) - textView.isEditable = true - textView.isSelectable = true - 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: .greatestFiniteMagnitude, height: .greatestFiniteMagnitude) - - let scrollView = NSScrollView() - scrollView.documentView = textView - scrollView.hasVerticalScroller = true - scrollView.hasHorizontalScroller = false - scrollView.autoresizingMask = [.width, .height] - scrollView.translatesAutoresizingMaskIntoConstraints = false - - 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 { - textView.string = text - textStorage.beginEditing() - if let highlightedText = highlightr.highlight(text, as: language == "markdown" ? "md" : language) { - textStorage.setAttributedString(highlightedText) - } - textStorage.endEditing() - textStorage.language = language == "markdown" ? "md" : language - } - - return scrollView +#if os(iOS) +struct DocumentPicker: UIViewControllerRepresentable { + let onSelect: (URL) -> Void + + func makeUIViewController(context: Context) -> UIDocumentPickerViewController { + let picker = UIDocumentPickerViewController(forOpeningContentTypes: [.text, .sourceCode, .swiftSource, .pythonScript, .html, .cSource, .shellScript, .json, UTType("public.markdown") ?? .text]) + picker.delegate = context.coordinator + return picker } - - func updateNSView(_ nsView: NSScrollView, context: Context) { - 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 updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: Context) {} + func makeCoordinator() -> Coordinator { Coordinator(self) } - - class Coordinator: NSObject, NSTextViewDelegate { - let parent: CustomTextEditor - - init(_ parent: CustomTextEditor) { + + class Coordinator: NSObject, UIDocumentPickerDelegate { + let parent: DocumentPicker + + init(_ parent: DocumentPicker) { self.parent = parent } - + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + if let url = urls.first { + parent.onSelect(url) + } + } + } +} +#endif + +struct CustomTextEditor: View { + @Binding var text: String + @Binding var language: String + @Binding var isModified: Bool + @Environment(\.colorScheme) private var colorScheme: SwiftUI.ColorScheme + + var body: some View { + #if os(macOS) + CustomTextViewRepresentable( + text: $text, + language: language, + isModified: $isModified, + colorScheme: colorScheme + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(colorScheme == .dark ? Color.black.opacity(0.85) : Color.white.opacity(0.85)) + #elseif os(iOS) + CustomTextViewRepresentableiOS( + text: $text, + language: language, + isModified: $isModified, + colorScheme: colorScheme + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(colorScheme == .dark ? Color.black.opacity(0.85) : Color.white.opacity(0.85)) + #endif + } +} + +#if os(macOS) +struct CustomTextViewRepresentable: NSViewRepresentable { + @Binding var text: String + let language: String + @Binding var isModified: Bool + let colorScheme: SwiftUI.ColorScheme + + func makeNSView(context: Context) -> NSScrollView { + let textView = NSTextView() + textView.isEditable = true + textView.isRichText = false + textView.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular) + textView.delegate = context.coordinator + textView.string = text + updateHighlighting(textView: textView) + + let scrollView = NSScrollView() + scrollView.documentView = textView + scrollView.hasVerticalScroller = true + scrollView.hasHorizontalScroller = true + return scrollView + } + + func updateNSView(_ nsView: NSScrollView, context: Context) { + if let textView = nsView.documentView as? NSTextView { + if textView.string != text { + textView.string = text + updateHighlighting(textView: textView) + } + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, NSTextViewDelegate { + var parent: CustomTextViewRepresentable + + init(_ parent: CustomTextViewRepresentable) { + self.parent = parent + } + func textDidChange(_ notification: Notification) { guard let textView = notification.object as? NSTextView else { return } parent.text = textView.string + parent.isModified = true + parent.updateHighlighting(textView: textView) } } -} -// MARK: - Extensions -extension NSTextView { - func lineStartIndex(at lineNumber: Int) -> Int? { - 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 where i < lines.count { - position += lines[i].count + 1 + private func updateHighlighting(textView: NSTextView) { + let attributedString = NSMutableAttributedString(string: textView.string) + let range = NSRange(location: 0, length: textView.string.utf16.count) + + // Base attributes + attributedString.addAttribute(.font, value: NSFont.monospacedSystemFont(ofSize: 12, weight: .regular), range: range) + attributedString.addAttribute(.foregroundColor, value: colorScheme == .dark ? NSColor.white : NSColor.black, range: range) + + // Vibrant Light highlighting + if language == "swift" { + // Keywords (pink: 0.983822 0 0.72776 1) + let keywords = ["func", "class", "struct", "let", "var"] + for keyword in keywords { + let regex = try? NSRegularExpression(pattern: "\\b\(keyword)\\b", options: []) + regex?.enumerateMatches(in: textView.string, options: [], range: range) { match, _, _ in + guard let matchRange = match?.range else { return } + attributedString.addAttribute(.foregroundColor, value: NSColor(red: 0.983822, green: 0, blue: 0.72776, alpha: 1), range: matchRange) + attributedString.addAttribute(.font, value: NSFont.monospacedSystemFont(ofSize: 12, weight: .semibold), range: matchRange) + } + } + + // Strings (green: 0 0.743633 0 1) + let stringRegex = try? NSRegularExpression(pattern: "\".*?\"", options: []) + stringRegex?.enumerateMatches(in: textView.string, options: [], range: range) { match, _, _ in + guard let matchRange = match?.range else { return } + attributedString.addAttribute(.foregroundColor, value: NSColor(red: 0, green: 0.743633, blue: 0, alpha: 1), range: matchRange) + } + + // Comments (gray: 0.36526 0.421879 0.475154 1) + let commentRegex = try? NSRegularExpression(pattern: "//.*?\n|/\\*.*?\\*/", options: [.dotMatchesLineSeparators]) + commentRegex?.enumerateMatches(in: textView.string, options: [], range: range) { match, _, _ in + guard let matchRange = match?.range else { return } + attributedString.addAttribute(.foregroundColor, value: NSColor(red: 0.36526, green: 0.421879, blue: 0.475154, alpha: 1), range: matchRange) + } + + // Numbers (blue: 0.11 0 0.81 1) + let numberRegex = try? NSRegularExpression(pattern: "\\b\\d+\\.?\\d*\\b", options: []) + numberRegex?.enumerateMatches(in: textView.string, options: [], range: range) { match, _, _ in + guard let matchRange = match?.range else { return } + attributedString.addAttribute(.foregroundColor, value: NSColor(red: 0.11, green: 0, blue: 0.81, alpha: 1), range: matchRange) + } } - return position > string.count ? nil : position - } - - func scrollToLine(_ lineNumber: Int) { - guard let startIndex = lineStartIndex(at: lineNumber) else { return } - guard let glyphRange = layoutManager?.glyphRange(forCharacterRange: NSRange(location: startIndex, length: 0), actualCharacterRange: nil) else { return } - scrollRangeToVisible(glyphRange) + + textView.textStorage?.setAttributedString(attributedString) } } +#elseif os(iOS) +struct CustomTextViewRepresentableiOS: UIViewRepresentable { + @Binding var text: String + let language: String + @Binding var isModified: Bool + let colorScheme: SwiftUI.ColorScheme -extension Notification.Name { - static let moveCursorToLine = Notification.Name("moveCursorToLine") - static let languageChanged = Notification.Name("languageChanged") -} \ No newline at end of file + func makeUIView(context: Context) -> UITextView { + let textView = UITextView() + textView.isEditable = true + textView.font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular) + textView.delegate = context.coordinator + textView.text = text + updateHighlighting(textView: textView) + return textView + } + + func updateUIView(_ uiView: UITextView, context: Context) { + if uiView.text != text { + uiView.text = text + updateHighlighting(textView: uiView) + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, UITextViewDelegate { + var parent: CustomTextViewRepresentableiOS + + init(_ parent: CustomTextViewRepresentableiOS) { + self.parent = parent + } + + func textViewDidChange(_ textView: UITextView) { + parent.text = textView.text + parent.isModified = true + parent.updateHighlighting(textView: textView) + } + } + + private func updateHighlighting(textView: UITextView) { + let attributedString = NSMutableAttributedString(string: textView.text) + let range = NSRange(location: 0, length: textView.text.utf16.count) + + // Base attributes + attributedString.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: 12, weight: .regular), range: range) + attributedString.addAttribute(.foregroundColor, value: colorScheme == .dark ? UIColor.white : UIColor.black, range: range) + + // Vibrant Light highlighting + if language == "swift" { + // Keywords (pink: 0.983822 0 0.72776 1) + let keywords = ["func", "class", "struct", "let", "var"] + for keyword in keywords { + let regex = try? NSRegularExpression(pattern: "\\b\(keyword)\\b", options: []) + regex?.enumerateMatches(in: textView.text, options: [], range: range) { match, _, _ in + guard let matchRange = match?.range else { return } + attributedString.addAttribute(.foregroundColor, value: UIColor(red: 0.983822, green: 0, blue: 0.72776, alpha: 1), range: matchRange) + attributedString.addAttribute(.font, value: UIFont.monospacedSystemFont(ofSize: 12, weight: .semibold), range: matchRange) + } + } + + // Strings (green: 0 0.743633 0 1) + let stringRegex = try? NSRegularExpression(pattern: "\".*?\"", options: []) + stringRegex?.enumerateMatches(in: textView.text, options: [], range: range) { match, _, _ in + guard let matchRange = match?.range else { return } + attributedString.addAttribute(.foregroundColor, value: UIColor(red: 0, green: 0.743633, blue: 0, alpha: 1), range: matchRange) + } + + // Comments (gray: 0.36526 0.421879 0.475154 1) + let commentRegex = try? NSRegularExpression(pattern: "//.*?\n|/\\*.*?\\*/", options: [.dotMatchesLineSeparators]) + commentRegex?.enumerateMatches(in: textView.text, options: [], range: range) { match, _, _ in + guard let matchRange = match?.range else { return } + attributedString.addAttribute(.foregroundColor, value: UIColor(red: 0.36526, green: 0.421879, blue: 0.475154, alpha: 1), range: matchRange) + } + + // Numbers (blue: 0.11 0 0.81 1) + let numberRegex = try? NSRegularExpression(pattern: "\\b\\d+\\.?\\d*\\b", options: []) + numberRegex?.enumerateMatches(in: textView.text, options: [], range: range) { match, _, _ in + guard let matchRange = match?.range else { return } + attributedString.addAttribute(.foregroundColor, value: UIColor(red: 0.11, green: 0, blue: 0.81, alpha: 1), range: matchRange) + } + } + + textView.attributedText = attributedString + } +} +#endif diff --git a/Neon Vision Editor/Tab.swift b/Neon Vision Editor/Item.swift similarity index 58% rename from Neon Vision Editor/Tab.swift rename to Neon Vision Editor/Item.swift index 86428c9..9302a06 100644 --- a/Neon Vision Editor/Tab.swift +++ b/Neon Vision Editor/Item.swift @@ -1,17 +1,18 @@ import Foundation import SwiftData -// MARK: - Tab @Model -class Tab { +final class Tab { var id: UUID = UUID() var name: String var content: String var language: String - - init(name: String = "Note", content: String = "", language: String = "swift") { + var isModified: Bool + + init(name: String = "Untitled", content: String = "", language: String = "swift") { self.name = name self.content = content self.language = language + self.isModified = false } -} \ No newline at end of file +} diff --git a/Neon Vision Editor/Neon.swift b/Neon Vision Editor/Neon.swift deleted file mode 100644 index ee604a4..0000000 --- a/Neon Vision Editor/Neon.swift +++ /dev/null @@ -1,151 +0,0 @@ -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(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() - 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)") - } - } -} \ No newline at end of file diff --git a/Neon Vision Editor/Neon_Vision_EditorApp.swift b/Neon Vision Editor/Neon_Vision_EditorApp.swift new file mode 100644 index 0000000..8db6c8f --- /dev/null +++ b/Neon Vision Editor/Neon_Vision_EditorApp.swift @@ -0,0 +1,34 @@ +import SwiftUI +import SwiftData + +@main +struct Neon_Vision_EditorApp: App { + let modelContainer: ModelContainer + + init() { + do { + modelContainer = try ModelContainer(for: Tab.self) + } catch { + fatalError("Failed to create ModelContainer: \(error)") + } + } + + var body: some Scene { + WindowGroup { + ContentView() + .environment(\.modelContext, modelContainer.mainContext) + .frame(minWidth: 1000, minHeight: 600) + } + .defaultSize(width: 1000, height: 600) + .commands { + CommandGroup(replacing: .newItem) { + Button("New Tab") { + let context = try? ModelContainer(for: Tab.self).mainContext + let newTab = Tab(name: "Untitled \(Int.random(in: 1...1000))") + context?.insert(newTab) + } + .keyboardShortcut("t", modifiers: .command) + } + } + } +}