mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
Adjustments to tabbar approach and syntax highlighting of text code
This commit is contained in:
parent
9190efbbda
commit
0faaff61cb
6 changed files with 554 additions and 544 deletions
|
|
@ -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 = "<group>";
|
||||
|
|
@ -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 = "<group>";
|
||||
};
|
||||
98E2A99B2E5C4A98005FFE78 /* Products */ = {
|
||||
98EAE6342E5F15E80050E579 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
98E2A99A2E5C4A98005FFE78 /* Neon Vision Editor.app */,
|
||||
98EAE6332E5F15E80050E579 /* Neon Vision Editor.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
98EAE6532E5F175B0050E579 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
98EAE6592E5F1E890050E579 /* SwiftData.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* 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 */;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Neon Vision Editor/Neon_Vision_EditorApp.swift
Normal file
34
Neon Vision Editor/Neon_Vision_EditorApp.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue