├── .gitignore ├── Editor.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── Editor ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Editor.entitlements ├── Editor │ ├── Assistant │ │ ├── Editor.Assistant.Language.swift │ │ ├── Editor.Assistant.swift │ │ ├── Editor.Tokenizer.swift │ │ ├── LSP.Types.swift │ │ └── LSP.swift │ ├── Editor.Container.swift │ ├── Editor.Indentation.swift │ ├── Editor.Position.swift │ ├── Editor.Scroll │ │ └── Editor.Scroll.swift │ ├── Editor.Session.swift │ ├── Editor.TextInput │ │ ├── Editor.TextInput.swift │ │ └── Keyboard.swift │ ├── Editor.UI.swift │ └── Editor.swift ├── Info.plist ├── SceneDelegate.swift ├── ViewController.swift └── sample-c.txt ├── LICENSE ├── README.md └── Screenshots └── c.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | .DS_Store 92 | -------------------------------------------------------------------------------- /Editor.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 228DCDC729FC2127000B7616 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDC629FC2127000B7616 /* AppDelegate.swift */; }; 11 | 228DCDC929FC2127000B7616 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDC829FC2127000B7616 /* SceneDelegate.swift */; }; 12 | 228DCDCB29FC2127000B7616 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDCA29FC2127000B7616 /* ViewController.swift */; }; 13 | 228DCDCE29FC2127000B7616 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 228DCDCC29FC2127000B7616 /* Main.storyboard */; }; 14 | 228DCDD029FC2128000B7616 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 228DCDCF29FC2128000B7616 /* Assets.xcassets */; }; 15 | 228DCDD329FC2128000B7616 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 228DCDD129FC2128000B7616 /* LaunchScreen.storyboard */; }; 16 | 228DCDED29FC2268000B7616 /* Editor.Tokenizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDDD29FC2268000B7616 /* Editor.Tokenizer.swift */; }; 17 | 228DCDEE29FC2268000B7616 /* LSP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDDE29FC2268000B7616 /* LSP.swift */; }; 18 | 228DCDEF29FC2268000B7616 /* LSP.Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDDF29FC2268000B7616 /* LSP.Types.swift */; }; 19 | 228DCDF029FC2268000B7616 /* Editor.Assistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDE029FC2268000B7616 /* Editor.Assistant.swift */; }; 20 | 228DCDF129FC2268000B7616 /* Editor.Assistant.Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDE129FC2268000B7616 /* Editor.Assistant.Language.swift */; }; 21 | 228DCDF229FC2268000B7616 /* Editor.Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDE229FC2268000B7616 /* Editor.Session.swift */; }; 22 | 228DCDF329FC2268000B7616 /* Editor.Scroll.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDE429FC2268000B7616 /* Editor.Scroll.swift */; }; 23 | 228DCDF429FC2268000B7616 /* Editor.Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDE529FC2268000B7616 /* Editor.Container.swift */; }; 24 | 228DCDF529FC2268000B7616 /* Editor.TextInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDE729FC2268000B7616 /* Editor.TextInput.swift */; }; 25 | 228DCDF629FC2268000B7616 /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDE829FC2268000B7616 /* Keyboard.swift */; }; 26 | 228DCDF729FC2268000B7616 /* Editor.Indentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDE929FC2268000B7616 /* Editor.Indentation.swift */; }; 27 | 228DCDF829FC2268000B7616 /* Editor.UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDEA29FC2268000B7616 /* Editor.UI.swift */; }; 28 | 228DCDF929FC2268000B7616 /* Editor.Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDEB29FC2268000B7616 /* Editor.Position.swift */; }; 29 | 228DCDFA29FC2268000B7616 /* Editor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228DCDEC29FC2268000B7616 /* Editor.swift */; }; 30 | 228DCDFD29FC2293000B7616 /* BaseComponents in Frameworks */ = {isa = PBXBuildFile; productRef = 228DCDFC29FC2293000B7616 /* BaseComponents */; }; 31 | 228DCE0029FC2503000B7616 /* sample-c.txt in Resources */ = {isa = PBXBuildFile; fileRef = 228DCDFE29FC249F000B7616 /* sample-c.txt */; }; 32 | /* End PBXBuildFile section */ 33 | 34 | /* Begin PBXFileReference section */ 35 | 228DCDC329FC2127000B7616 /* Editor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Editor.app; sourceTree = BUILT_PRODUCTS_DIR; }; 36 | 228DCDC629FC2127000B7616 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 228DCDC829FC2127000B7616 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 38 | 228DCDCA29FC2127000B7616 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 39 | 228DCDCD29FC2127000B7616 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 40 | 228DCDCF29FC2128000B7616 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 41 | 228DCDD229FC2128000B7616 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 42 | 228DCDD429FC2128000B7616 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 43 | 228DCDDA29FC218C000B7616 /* Editor.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Editor.entitlements; sourceTree = ""; }; 44 | 228DCDDD29FC2268000B7616 /* Editor.Tokenizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Editor.Tokenizer.swift; sourceTree = ""; }; 45 | 228DCDDE29FC2268000B7616 /* LSP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LSP.swift; sourceTree = ""; }; 46 | 228DCDDF29FC2268000B7616 /* LSP.Types.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LSP.Types.swift; sourceTree = ""; }; 47 | 228DCDE029FC2268000B7616 /* Editor.Assistant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Editor.Assistant.swift; sourceTree = ""; }; 48 | 228DCDE129FC2268000B7616 /* Editor.Assistant.Language.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Editor.Assistant.Language.swift; sourceTree = ""; }; 49 | 228DCDE229FC2268000B7616 /* Editor.Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Editor.Session.swift; sourceTree = ""; }; 50 | 228DCDE429FC2268000B7616 /* Editor.Scroll.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Editor.Scroll.swift; sourceTree = ""; }; 51 | 228DCDE529FC2268000B7616 /* Editor.Container.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Editor.Container.swift; sourceTree = ""; }; 52 | 228DCDE729FC2268000B7616 /* Editor.TextInput.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Editor.TextInput.swift; sourceTree = ""; }; 53 | 228DCDE829FC2268000B7616 /* Keyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keyboard.swift; sourceTree = ""; }; 54 | 228DCDE929FC2268000B7616 /* Editor.Indentation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Editor.Indentation.swift; sourceTree = ""; }; 55 | 228DCDEA29FC2268000B7616 /* Editor.UI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Editor.UI.swift; sourceTree = ""; }; 56 | 228DCDEB29FC2268000B7616 /* Editor.Position.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Editor.Position.swift; sourceTree = ""; }; 57 | 228DCDEC29FC2268000B7616 /* Editor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Editor.swift; sourceTree = ""; }; 58 | 228DCDFE29FC249F000B7616 /* sample-c.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "sample-c.txt"; sourceTree = ""; }; 59 | /* End PBXFileReference section */ 60 | 61 | /* Begin PBXFrameworksBuildPhase section */ 62 | 228DCDC029FC2127000B7616 /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | 228DCDFD29FC2293000B7616 /* BaseComponents in Frameworks */, 67 | ); 68 | runOnlyForDeploymentPostprocessing = 0; 69 | }; 70 | /* End PBXFrameworksBuildPhase section */ 71 | 72 | /* Begin PBXGroup section */ 73 | 228DCDBA29FC2127000B7616 = { 74 | isa = PBXGroup; 75 | children = ( 76 | 228DCDC529FC2127000B7616 /* Editor */, 77 | 228DCDC429FC2127000B7616 /* Products */, 78 | ); 79 | sourceTree = ""; 80 | }; 81 | 228DCDC429FC2127000B7616 /* Products */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | 228DCDC329FC2127000B7616 /* Editor.app */, 85 | ); 86 | name = Products; 87 | sourceTree = ""; 88 | }; 89 | 228DCDC529FC2127000B7616 /* Editor */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 228DCDDA29FC218C000B7616 /* Editor.entitlements */, 93 | 228DCDC629FC2127000B7616 /* AppDelegate.swift */, 94 | 228DCDC829FC2127000B7616 /* SceneDelegate.swift */, 95 | 228DCDCA29FC2127000B7616 /* ViewController.swift */, 96 | 228DCDDB29FC2268000B7616 /* Editor */, 97 | 228DCDCC29FC2127000B7616 /* Main.storyboard */, 98 | 228DCDCF29FC2128000B7616 /* Assets.xcassets */, 99 | 228DCDD129FC2128000B7616 /* LaunchScreen.storyboard */, 100 | 228DCDD429FC2128000B7616 /* Info.plist */, 101 | 228DCDFE29FC249F000B7616 /* sample-c.txt */, 102 | ); 103 | path = Editor; 104 | sourceTree = ""; 105 | }; 106 | 228DCDDB29FC2268000B7616 /* Editor */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 228DCDDC29FC2268000B7616 /* Assistant */, 110 | 228DCDE229FC2268000B7616 /* Editor.Session.swift */, 111 | 228DCDE329FC2268000B7616 /* Editor.Scroll */, 112 | 228DCDE529FC2268000B7616 /* Editor.Container.swift */, 113 | 228DCDE629FC2268000B7616 /* Editor.TextInput */, 114 | 228DCDE929FC2268000B7616 /* Editor.Indentation.swift */, 115 | 228DCDEA29FC2268000B7616 /* Editor.UI.swift */, 116 | 228DCDEB29FC2268000B7616 /* Editor.Position.swift */, 117 | 228DCDEC29FC2268000B7616 /* Editor.swift */, 118 | ); 119 | path = Editor; 120 | sourceTree = ""; 121 | }; 122 | 228DCDDC29FC2268000B7616 /* Assistant */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 228DCDDD29FC2268000B7616 /* Editor.Tokenizer.swift */, 126 | 228DCDDE29FC2268000B7616 /* LSP.swift */, 127 | 228DCDDF29FC2268000B7616 /* LSP.Types.swift */, 128 | 228DCDE029FC2268000B7616 /* Editor.Assistant.swift */, 129 | 228DCDE129FC2268000B7616 /* Editor.Assistant.Language.swift */, 130 | ); 131 | path = Assistant; 132 | sourceTree = ""; 133 | }; 134 | 228DCDE329FC2268000B7616 /* Editor.Scroll */ = { 135 | isa = PBXGroup; 136 | children = ( 137 | 228DCDE429FC2268000B7616 /* Editor.Scroll.swift */, 138 | ); 139 | path = Editor.Scroll; 140 | sourceTree = ""; 141 | }; 142 | 228DCDE629FC2268000B7616 /* Editor.TextInput */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 228DCDE729FC2268000B7616 /* Editor.TextInput.swift */, 146 | 228DCDE829FC2268000B7616 /* Keyboard.swift */, 147 | ); 148 | path = Editor.TextInput; 149 | sourceTree = ""; 150 | }; 151 | /* End PBXGroup section */ 152 | 153 | /* Begin PBXNativeTarget section */ 154 | 228DCDC229FC2127000B7616 /* Editor */ = { 155 | isa = PBXNativeTarget; 156 | buildConfigurationList = 228DCDD729FC2128000B7616 /* Build configuration list for PBXNativeTarget "Editor" */; 157 | buildPhases = ( 158 | 228DCDBF29FC2127000B7616 /* Sources */, 159 | 228DCDC029FC2127000B7616 /* Frameworks */, 160 | 228DCDC129FC2127000B7616 /* Resources */, 161 | ); 162 | buildRules = ( 163 | ); 164 | dependencies = ( 165 | ); 166 | name = Editor; 167 | packageProductDependencies = ( 168 | 228DCDFC29FC2293000B7616 /* BaseComponents */, 169 | ); 170 | productName = Editor; 171 | productReference = 228DCDC329FC2127000B7616 /* Editor.app */; 172 | productType = "com.apple.product-type.application"; 173 | }; 174 | /* End PBXNativeTarget section */ 175 | 176 | /* Begin PBXProject section */ 177 | 228DCDBB29FC2127000B7616 /* Project object */ = { 178 | isa = PBXProject; 179 | attributes = { 180 | BuildIndependentTargetsInParallel = 1; 181 | LastSwiftUpdateCheck = 1430; 182 | LastUpgradeCheck = 1430; 183 | TargetAttributes = { 184 | 228DCDC229FC2127000B7616 = { 185 | CreatedOnToolsVersion = 14.3; 186 | }; 187 | }; 188 | }; 189 | buildConfigurationList = 228DCDBE29FC2127000B7616 /* Build configuration list for PBXProject "Editor" */; 190 | compatibilityVersion = "Xcode 14.0"; 191 | developmentRegion = en; 192 | hasScannedForEncodings = 0; 193 | knownRegions = ( 194 | en, 195 | Base, 196 | ); 197 | mainGroup = 228DCDBA29FC2127000B7616; 198 | packageReferences = ( 199 | 228DCDFB29FC2293000B7616 /* XCRemoteSwiftPackageReference "BaseComponents" */, 200 | ); 201 | productRefGroup = 228DCDC429FC2127000B7616 /* Products */; 202 | projectDirPath = ""; 203 | projectRoot = ""; 204 | targets = ( 205 | 228DCDC229FC2127000B7616 /* Editor */, 206 | ); 207 | }; 208 | /* End PBXProject section */ 209 | 210 | /* Begin PBXResourcesBuildPhase section */ 211 | 228DCDC129FC2127000B7616 /* Resources */ = { 212 | isa = PBXResourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | 228DCE0029FC2503000B7616 /* sample-c.txt in Resources */, 216 | 228DCDD329FC2128000B7616 /* LaunchScreen.storyboard in Resources */, 217 | 228DCDD029FC2128000B7616 /* Assets.xcassets in Resources */, 218 | 228DCDCE29FC2127000B7616 /* Main.storyboard in Resources */, 219 | ); 220 | runOnlyForDeploymentPostprocessing = 0; 221 | }; 222 | /* End PBXResourcesBuildPhase section */ 223 | 224 | /* Begin PBXSourcesBuildPhase section */ 225 | 228DCDBF29FC2127000B7616 /* Sources */ = { 226 | isa = PBXSourcesBuildPhase; 227 | buildActionMask = 2147483647; 228 | files = ( 229 | 228DCDF429FC2268000B7616 /* Editor.Container.swift in Sources */, 230 | 228DCDCB29FC2127000B7616 /* ViewController.swift in Sources */, 231 | 228DCDED29FC2268000B7616 /* Editor.Tokenizer.swift in Sources */, 232 | 228DCDF529FC2268000B7616 /* Editor.TextInput.swift in Sources */, 233 | 228DCDF329FC2268000B7616 /* Editor.Scroll.swift in Sources */, 234 | 228DCDF629FC2268000B7616 /* Keyboard.swift in Sources */, 235 | 228DCDEF29FC2268000B7616 /* LSP.Types.swift in Sources */, 236 | 228DCDF129FC2268000B7616 /* Editor.Assistant.Language.swift in Sources */, 237 | 228DCDC729FC2127000B7616 /* AppDelegate.swift in Sources */, 238 | 228DCDF829FC2268000B7616 /* Editor.UI.swift in Sources */, 239 | 228DCDF929FC2268000B7616 /* Editor.Position.swift in Sources */, 240 | 228DCDF229FC2268000B7616 /* Editor.Session.swift in Sources */, 241 | 228DCDEE29FC2268000B7616 /* LSP.swift in Sources */, 242 | 228DCDC929FC2127000B7616 /* SceneDelegate.swift in Sources */, 243 | 228DCDF029FC2268000B7616 /* Editor.Assistant.swift in Sources */, 244 | 228DCDF729FC2268000B7616 /* Editor.Indentation.swift in Sources */, 245 | 228DCDFA29FC2268000B7616 /* Editor.swift in Sources */, 246 | ); 247 | runOnlyForDeploymentPostprocessing = 0; 248 | }; 249 | /* End PBXSourcesBuildPhase section */ 250 | 251 | /* Begin PBXVariantGroup section */ 252 | 228DCDCC29FC2127000B7616 /* Main.storyboard */ = { 253 | isa = PBXVariantGroup; 254 | children = ( 255 | 228DCDCD29FC2127000B7616 /* Base */, 256 | ); 257 | name = Main.storyboard; 258 | sourceTree = ""; 259 | }; 260 | 228DCDD129FC2128000B7616 /* LaunchScreen.storyboard */ = { 261 | isa = PBXVariantGroup; 262 | children = ( 263 | 228DCDD229FC2128000B7616 /* Base */, 264 | ); 265 | name = LaunchScreen.storyboard; 266 | sourceTree = ""; 267 | }; 268 | /* End PBXVariantGroup section */ 269 | 270 | /* Begin XCBuildConfiguration section */ 271 | 228DCDD529FC2128000B7616 /* Debug */ = { 272 | isa = XCBuildConfiguration; 273 | buildSettings = { 274 | ALWAYS_SEARCH_USER_PATHS = NO; 275 | CLANG_ANALYZER_NONNULL = YES; 276 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 277 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 278 | CLANG_ENABLE_MODULES = YES; 279 | CLANG_ENABLE_OBJC_ARC = YES; 280 | CLANG_ENABLE_OBJC_WEAK = YES; 281 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 282 | CLANG_WARN_BOOL_CONVERSION = YES; 283 | CLANG_WARN_COMMA = YES; 284 | CLANG_WARN_CONSTANT_CONVERSION = YES; 285 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 286 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 287 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 288 | CLANG_WARN_EMPTY_BODY = YES; 289 | CLANG_WARN_ENUM_CONVERSION = YES; 290 | CLANG_WARN_INFINITE_RECURSION = YES; 291 | CLANG_WARN_INT_CONVERSION = YES; 292 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 293 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 294 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 295 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 296 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 297 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 298 | CLANG_WARN_STRICT_PROTOTYPES = YES; 299 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 300 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 301 | CLANG_WARN_UNREACHABLE_CODE = YES; 302 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 303 | COPY_PHASE_STRIP = NO; 304 | DEBUG_INFORMATION_FORMAT = dwarf; 305 | ENABLE_STRICT_OBJC_MSGSEND = YES; 306 | ENABLE_TESTABILITY = YES; 307 | GCC_C_LANGUAGE_STANDARD = gnu11; 308 | GCC_DYNAMIC_NO_PIC = NO; 309 | GCC_NO_COMMON_BLOCKS = YES; 310 | GCC_OPTIMIZATION_LEVEL = 0; 311 | GCC_PREPROCESSOR_DEFINITIONS = ( 312 | "DEBUG=1", 313 | "$(inherited)", 314 | ); 315 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 316 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 317 | GCC_WARN_UNDECLARED_SELECTOR = YES; 318 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 319 | GCC_WARN_UNUSED_FUNCTION = YES; 320 | GCC_WARN_UNUSED_VARIABLE = YES; 321 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 322 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 323 | MTL_FAST_MATH = YES; 324 | ONLY_ACTIVE_ARCH = YES; 325 | SDKROOT = iphoneos; 326 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 327 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 328 | }; 329 | name = Debug; 330 | }; 331 | 228DCDD629FC2128000B7616 /* Release */ = { 332 | isa = XCBuildConfiguration; 333 | buildSettings = { 334 | ALWAYS_SEARCH_USER_PATHS = NO; 335 | CLANG_ANALYZER_NONNULL = YES; 336 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 337 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 338 | CLANG_ENABLE_MODULES = YES; 339 | CLANG_ENABLE_OBJC_ARC = YES; 340 | CLANG_ENABLE_OBJC_WEAK = YES; 341 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 342 | CLANG_WARN_BOOL_CONVERSION = YES; 343 | CLANG_WARN_COMMA = YES; 344 | CLANG_WARN_CONSTANT_CONVERSION = YES; 345 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 346 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 347 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 348 | CLANG_WARN_EMPTY_BODY = YES; 349 | CLANG_WARN_ENUM_CONVERSION = YES; 350 | CLANG_WARN_INFINITE_RECURSION = YES; 351 | CLANG_WARN_INT_CONVERSION = YES; 352 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 353 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 354 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 355 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 356 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 357 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 358 | CLANG_WARN_STRICT_PROTOTYPES = YES; 359 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 360 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 361 | CLANG_WARN_UNREACHABLE_CODE = YES; 362 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 363 | COPY_PHASE_STRIP = NO; 364 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 365 | ENABLE_NS_ASSERTIONS = NO; 366 | ENABLE_STRICT_OBJC_MSGSEND = YES; 367 | GCC_C_LANGUAGE_STANDARD = gnu11; 368 | GCC_NO_COMMON_BLOCKS = YES; 369 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 370 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 371 | GCC_WARN_UNDECLARED_SELECTOR = YES; 372 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 373 | GCC_WARN_UNUSED_FUNCTION = YES; 374 | GCC_WARN_UNUSED_VARIABLE = YES; 375 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 376 | MTL_ENABLE_DEBUG_INFO = NO; 377 | MTL_FAST_MATH = YES; 378 | SDKROOT = iphoneos; 379 | SWIFT_COMPILATION_MODE = wholemodule; 380 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 381 | VALIDATE_PRODUCT = YES; 382 | }; 383 | name = Release; 384 | }; 385 | 228DCDD829FC2128000B7616 /* Debug */ = { 386 | isa = XCBuildConfiguration; 387 | buildSettings = { 388 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 389 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 390 | CODE_SIGN_ENTITLEMENTS = Editor/Editor.entitlements; 391 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 392 | CODE_SIGN_STYLE = Automatic; 393 | CURRENT_PROJECT_VERSION = 1; 394 | GENERATE_INFOPLIST_FILE = YES; 395 | INFOPLIST_FILE = Editor/Info.plist; 396 | INFOPLIST_KEY_CFBundleDisplayName = Editor; 397 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 398 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 399 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 400 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 401 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 402 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 403 | LD_RUNPATH_SEARCH_PATHS = ( 404 | "$(inherited)", 405 | "@executable_path/Frameworks", 406 | ); 407 | MARKETING_VERSION = 1.0; 408 | PRODUCT_BUNDLE_IDENTIFIER = com.mackh.Editor; 409 | PRODUCT_NAME = "$(TARGET_NAME)"; 410 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 411 | SUPPORTS_MACCATALYST = YES; 412 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 413 | SWIFT_EMIT_LOC_STRINGS = YES; 414 | SWIFT_VERSION = 5.0; 415 | TARGETED_DEVICE_FAMILY = "2,6"; 416 | }; 417 | name = Debug; 418 | }; 419 | 228DCDD929FC2128000B7616 /* Release */ = { 420 | isa = XCBuildConfiguration; 421 | buildSettings = { 422 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 423 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 424 | CODE_SIGN_ENTITLEMENTS = Editor/Editor.entitlements; 425 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 426 | CODE_SIGN_STYLE = Automatic; 427 | CURRENT_PROJECT_VERSION = 1; 428 | GENERATE_INFOPLIST_FILE = YES; 429 | INFOPLIST_FILE = Editor/Info.plist; 430 | INFOPLIST_KEY_CFBundleDisplayName = Editor; 431 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 432 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 433 | INFOPLIST_KEY_UIMainStoryboardFile = Main; 434 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 435 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 436 | IPHONEOS_DEPLOYMENT_TARGET = 15.0; 437 | LD_RUNPATH_SEARCH_PATHS = ( 438 | "$(inherited)", 439 | "@executable_path/Frameworks", 440 | ); 441 | MARKETING_VERSION = 1.0; 442 | PRODUCT_BUNDLE_IDENTIFIER = com.mackh.Editor; 443 | PRODUCT_NAME = "$(TARGET_NAME)"; 444 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; 445 | SUPPORTS_MACCATALYST = YES; 446 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; 447 | SWIFT_EMIT_LOC_STRINGS = YES; 448 | SWIFT_VERSION = 5.0; 449 | TARGETED_DEVICE_FAMILY = "2,6"; 450 | }; 451 | name = Release; 452 | }; 453 | /* End XCBuildConfiguration section */ 454 | 455 | /* Begin XCConfigurationList section */ 456 | 228DCDBE29FC2127000B7616 /* Build configuration list for PBXProject "Editor" */ = { 457 | isa = XCConfigurationList; 458 | buildConfigurations = ( 459 | 228DCDD529FC2128000B7616 /* Debug */, 460 | 228DCDD629FC2128000B7616 /* Release */, 461 | ); 462 | defaultConfigurationIsVisible = 0; 463 | defaultConfigurationName = Release; 464 | }; 465 | 228DCDD729FC2128000B7616 /* Build configuration list for PBXNativeTarget "Editor" */ = { 466 | isa = XCConfigurationList; 467 | buildConfigurations = ( 468 | 228DCDD829FC2128000B7616 /* Debug */, 469 | 228DCDD929FC2128000B7616 /* Release */, 470 | ); 471 | defaultConfigurationIsVisible = 0; 472 | defaultConfigurationName = Release; 473 | }; 474 | /* End XCConfigurationList section */ 475 | 476 | /* Begin XCRemoteSwiftPackageReference section */ 477 | 228DCDFB29FC2293000B7616 /* XCRemoteSwiftPackageReference "BaseComponents" */ = { 478 | isa = XCRemoteSwiftPackageReference; 479 | repositoryURL = "https://github.com/mmackh/BaseComponents"; 480 | requirement = { 481 | branch = master; 482 | kind = branch; 483 | }; 484 | }; 485 | /* End XCRemoteSwiftPackageReference section */ 486 | 487 | /* Begin XCSwiftPackageProductDependency section */ 488 | 228DCDFC29FC2293000B7616 /* BaseComponents */ = { 489 | isa = XCSwiftPackageProductDependency; 490 | package = 228DCDFB29FC2293000B7616 /* XCRemoteSwiftPackageReference "BaseComponents" */; 491 | productName = BaseComponents; 492 | }; 493 | /* End XCSwiftPackageProductDependency section */ 494 | }; 495 | rootObject = 228DCDBB29FC2127000B7616 /* Project object */; 496 | } 497 | -------------------------------------------------------------------------------- /Editor.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Editor.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Editor.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "basecomponents", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/mmackh/BaseComponents", 7 | "state" : { 8 | "branch" : "master", 9 | "revision" : "e01ddac9818a450b645f5a72139e5f508a1be04d" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Editor/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Editor 4 | // 5 | // Created by Maximilian Mackh on 28.04.23. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /Editor/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Editor/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Editor/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Editor/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Editor/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Editor/Editor.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Editor/Editor/Assistant/Editor.Assistant.Language.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Editor.Assistant.Language.swift 3 | // Magma 4 | // 5 | // Created by Maximilian Mackh on 07.03.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Editor.Assistant { 11 | enum Language: String { 12 | case c = "c" 13 | case cpp = "cpp" 14 | case unkown = "" 15 | 16 | static func language(for path: String) -> Language { 17 | .init(rawValue: URL(fileURLWithPath: path).pathExtension) ?? .unkown 18 | } 19 | 20 | var dictionary: Dictionary { 21 | .init(language: self) 22 | } 23 | 24 | class Dictionary { 25 | let language: Language 26 | 27 | lazy var table: [String:String] = { 28 | switch language { 29 | case .c, .cpp: 30 | return [ 31 | "#include": "include", 32 | "#import": "include", 33 | "#define" : "directive", 34 | "#if" : "directive", 35 | "#ifdef" : "directive", 36 | "#ifndef" : "directive", 37 | "#else" : "directive", 38 | "#elif" : "directive", 39 | "#endif" : "directive", 40 | "#error" : "directive", 41 | "#pragma" : "directive", 42 | "extern" : "typdef", 43 | "typedef" : "typdef", 44 | "using" : "typdef", 45 | "struct" : "structure", 46 | "enum" : "structure", 47 | "class" : "structure", 48 | "namespace" : "keyword", 49 | "size_t" : "keyword", 50 | "bool" : "keyword", 51 | "char" : "keyword", 52 | "signed" : "keyword", 53 | "unsigned" : "keyword", 54 | "const" : "keyword", 55 | "volatile" : "keyword", 56 | "byte" : "keyword", 57 | "String" : "keyword", 58 | "int" : "keyword", 59 | "int8_t" : "keyword", 60 | "int16_t" : "keyword", 61 | "int32_t" : "keyword", 62 | "int64_t" : "keyword", 63 | "uint8_t" : "keyword", 64 | "uint16_t" : "keyword", 65 | "uint32_t" : "keyword", 66 | "uint64_t" : "keyword", 67 | "long" : "keyword", 68 | "double" : "keyword", 69 | "float" : "keyword", 70 | "void" : "keyword", 71 | "static" : "keyword", 72 | "true" : "bool", 73 | "false" : "bool", 74 | "if" : "flow", 75 | "else" : "flow", 76 | "for" : "flow", 77 | "while" : "flow", 78 | "do" : "flow", 79 | "return" : "flow", 80 | "continue" : "flow", 81 | "break" : "flow", 82 | "||" : "flow", 83 | "&&" : "flow", 84 | "==" : "flow", 85 | "!=" : "flow", 86 | "<" : "flow", 87 | ">" : "flow", 88 | "<=" : "flow", 89 | ">=" : "flow", 90 | "=" : "assignement", 91 | "|=" : "assignement", 92 | "/=" : "assignement", 93 | "*=" : "assignement", 94 | "&=" : "assignement", 95 | "%=" : "assignement", 96 | "-=" : "assignement", 97 | "+=" : "assignement", 98 | "<<=" : "assignement", 99 | ">>=" : "assignement", 100 | "nil" : "nil", 101 | "null" : "nil", 102 | "NULL" : "nil", 103 | "@mmackh" : "author", 104 | "@akurutepe" : "author", 105 | "sentionic" : "company", 106 | "Sentionic" : "company", 107 | "SENTIONIC" : "company", 108 | "#TODO" : "todo", 109 | ] 110 | case .unkown: 111 | return [:] 112 | } 113 | }() 114 | 115 | init(language: Language) { 116 | self.language = language 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Editor/Editor/Assistant/Editor.Assistant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Editor.Assistant.swift 3 | // Magma 4 | // 5 | // Created by Maximilian Mackh on 21.02.23. 6 | // 7 | 8 | import UIKit 9 | import BaseComponents 10 | 11 | extension Editor { 12 | class Assistant { 13 | class Theme { 14 | lazy var colors: [String: UIColor] = [ 15 | "include" : .init(hex: "#859905"), 16 | "directive" : .init(hex: "#2AA198"), 17 | "comment" : .dynamic(light: .init(hex: "#93a1a1"), dark: .tertiaryLabel), 18 | "keyword" : .init(hex: "#859905"), 19 | "flow" : .label.alpha(0.8), 20 | "constant" : .init(hex: "#cb4b16"), 21 | "string" : .init(hex: "#cb4b16"), 22 | "decimal" : .init(hex: "#d33682"), 23 | "hex" : .init(hex: "#d33682"), 24 | "float" : .init(hex: "#d33682"), 25 | "nil" : .init(hex: "#d33682"), 26 | "label" : .init(hex: "#278BD2"), 27 | "bool" : .init(hex: "#2AA197"), 28 | "typdef" : .init(hex: "#B58901"), 29 | "type" : .init(hex: "#B58901"), 30 | "variable" : .init(hex: "#268bd2"), 31 | "function" : .init(hex: "#6c71c4"), 32 | "author" : .init(hex: "#cb4b16"), 33 | "company" : .label, 34 | "todo" : .label, 35 | "assignement" : .hex("#839596"), 36 | "structure" : .hex("#CD762F"), 37 | "punctuator" : .tertiaryLabel, 38 | ] 39 | 40 | static var solarized: Theme { 41 | .init() 42 | } 43 | } 44 | 45 | struct LSPConfiguration { 46 | let documentPathURL: URL 47 | let rootPath: String 48 | let environmentVariables: [String:String] 49 | } 50 | 51 | struct Suggestion: Hashable, Equatable { 52 | let UUID: String = Foundation.UUID().uuidString 53 | 54 | let label: String 55 | let score: Float 56 | } 57 | 58 | let language: Language 59 | let dictionary: Language.Dictionary 60 | 61 | let theme: Theme 62 | let tokenizer: Editor.Tokenizer 63 | 64 | let lspConfiguration: LSPConfiguration? 65 | var lspClient: LSP.Client? 66 | 67 | var highlightDictionary: [String:String] = [:] 68 | 69 | init?(language: Language, lspConfiguration: LSPConfiguration?, theme: Theme = .solarized) { 70 | self.lspConfiguration = lspConfiguration 71 | 72 | self.language = language 73 | self.dictionary = language.dictionary 74 | 75 | self.theme = theme 76 | self.tokenizer = .init() 77 | 78 | if let lspConfiguration = lspConfiguration { 79 | if [.c, .cpp].contains(language) { 80 | self.lspClient = .init(.clangd, environementVariables: lspConfiguration.environmentVariables) 81 | 82 | self.lspClient?.message(.initialize(rootPath: lspConfiguration.rootPath), responseHandler: { [weak self] response in 83 | guard let self = self else { return } 84 | 85 | self.redoHighlights() 86 | }) 87 | 88 | self.lspClient?.notify(.didOpen(uri: lspConfiguration.documentPathURL)) 89 | } 90 | } 91 | } 92 | 93 | func redoHighlights() { 94 | guard let lspConfiguration else { return } 95 | 96 | self.lspClient?.message(.symbol(uri: lspConfiguration.documentPathURL), responseHandler: { response in 97 | for highlight in ((response as? [String:AnyObject])?["result"] as? [[String:AnyObject]]) ?? [] { 98 | guard let name = highlight["name"] as? String, let symbolRaw = highlight["kind"] as? Int, let symbol = LSP.Symbol(rawValue: symbolRaw) else { continue } 99 | self.highlightDictionary[name] = symbol.string 100 | } 101 | print(self.highlightDictionary) 102 | }) 103 | } 104 | 105 | var documentVersion: Int = 1 106 | 107 | enum Invalidation { 108 | case full(source: String) 109 | case incremental(character: String, position: Position) 110 | } 111 | 112 | func invalidate(invalidation: Invalidation) { 113 | guard let lspConfiguration else { return } 114 | 115 | //LSP.isDebugLogEnabled = true 116 | 117 | switch invalidation { 118 | case .full(source: let source): 119 | self.lspClient?.notify(.didChange(uri: lspConfiguration.documentPathURL, version: documentVersion, text: source, range: nil)) 120 | case .incremental(character: let character, position: let position): 121 | break 122 | //self.lspClient?.notify(.didChange(uri: pathURL, version: documentVersion, text: character, range: .init(start: .init(position: position.offsetBy(row: 0, column: -1)), end: .init(position: position.offsetBy(row: 0, column: character.utf16.count - 1))))) 123 | } 124 | 125 | documentVersion += 1 126 | } 127 | 128 | func suggestions(for position: Editor.Position, completionHandler: @escaping (_ suggestions: [Suggestion])->()) { 129 | //LSP.isDebugLogEnabled = true 130 | guard let lspConfiguration else { return } 131 | 132 | guard let word = position.currentTokenStore?.0, let lspClient = lspClient else { return } 133 | 134 | print("Suggestions for: ", position, word) 135 | 136 | lspClient.message(.textCompletion(uri: lspConfiguration.documentPathURL, line: position.row + 1, column: position.column + 1, triggerKind: 1, triggerCharacter: String(word.last!))) { response in 137 | 138 | var suggestions: [Suggestion] = [] 139 | 140 | for item in (response as? [String:AnyObject])?["result"]?["items"] as? [[String:AnyObject]] ?? [] { 141 | 142 | guard let label: String = item["insertText"] as? String, let score = item["score"] as? Float, let kind = item["kind"] as? Int else { continue } 143 | 144 | // if kind == LSP.CompletionItemKind.keyword.rawValue { 145 | // print("skip", label) 146 | // continue 147 | // } 148 | 149 | suggestions.append(.init(label: label, score: score)) 150 | } 151 | 152 | let matches = suggestions.fuzzySearch(word) { suggestion in 153 | return suggestion.label 154 | } 155 | 156 | DispatchQueue.main.async { 157 | completionHandler(Array(matches)) 158 | } 159 | } 160 | 161 | } 162 | 163 | 164 | func highlight(row: Int, container: Editor.Container, mutableString: NSMutableAttributedString) -> NSMutableAttributedString? { 165 | 166 | tokenizer.scan(string: mutableString.string) { match in 167 | var key: String? 168 | if match.mode == .none { 169 | var lookup = String(match.token) 170 | 171 | if let matchedKey = self.dictionary.table[lookup] { 172 | key = matchedKey 173 | } 174 | if let matchedKey = self.highlightDictionary[lookup] { 175 | key = matchedKey 176 | } 177 | } else if match.mode == .angleBracket { 178 | key = "constant" 179 | } else { 180 | key = match.mode.rawValue 181 | } 182 | 183 | if match.mode == .string { 184 | } 185 | 186 | if let key = key, let color = self.theme.colors[key] { 187 | let range: NSRange = .init(location: match.location, length: match.length) 188 | mutableString.addAttributes([.foregroundColor: color], range: range) 189 | 190 | if key == "flow" || key == "author" || key == "company" || key == "todo" || key == "structure" { 191 | mutableString.addAttributes([.strokeWidth: -3], range: range) 192 | } 193 | } 194 | } 195 | 196 | return mutableString 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /Editor/Editor/Assistant/Editor.Tokenizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Editor.Tokenizer.swift 3 | // Magma 4 | // 5 | // Created by Maximilian Mackh on 06.03.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Editor { 11 | class Tokenizer { 12 | enum Mode: String { 13 | case none = "none" 14 | case decimal = "decimal" 15 | case float = "float" 16 | case hex = "hex" 17 | case inString = "inString" 18 | case string = "string" 19 | case inComment = "inComment" 20 | case comment = "comment" 21 | case inAngleBracket = "inAngleBracket" 22 | case angleBracket = "angleBracket" 23 | case punctuator = "punctuator" 24 | } 25 | 26 | struct Match { 27 | let location: Int 28 | let length: Int 29 | let token: Substring 30 | let mode: Mode 31 | } 32 | 33 | let separators: [Character] 34 | let numericalOperation: [Character] = [.init("+"),"-","*","%","/","="] 35 | 36 | init(separators: [Character] = [.init("."),",",":","(",")","[","]"]) { 37 | self.separators = separators 38 | } 39 | 40 | func scan(string: String, handler: (Match)->()) { 41 | var token: Substring = "" 42 | var mode: Mode = .none 43 | 44 | var column: Int = 0 45 | 46 | var firstMatch: Bool = true 47 | 48 | func match(with string: Substring) { 49 | let length: Int = string.utf16.count 50 | let location: Int = column - length 51 | 52 | handler(.init(location: location, length: length, token: token, mode: mode)) 53 | 54 | firstMatch = false 55 | } 56 | 57 | for character in string { 58 | defer { 59 | column += character.utf16.count 60 | } 61 | 62 | func reset() { 63 | token.removeAll() 64 | mode = .none 65 | } 66 | 67 | let isNumber: Bool = character.isNumber 68 | let isWhitespace: Bool = character.isWhitespace 69 | let isSeparator: Bool = separators.contains(character) 70 | 71 | if mode == .inString { 72 | if character == "\"" { 73 | token.append(character) 74 | mode = .string 75 | continue 76 | } 77 | token.append(character) 78 | continue 79 | } 80 | 81 | if mode == .inComment { 82 | if character.isNewline { 83 | mode = .comment 84 | match(with: token) 85 | reset() 86 | continue 87 | } 88 | token.append(character) 89 | continue 90 | } 91 | 92 | if mode == .inAngleBracket { 93 | if token.count == 2, token == "<=" { 94 | mode = .none 95 | match(with: token) 96 | reset() 97 | continue 98 | } 99 | token.append(character) 100 | if character == ">" { 101 | mode = .angleBracket 102 | } 103 | continue 104 | } 105 | 106 | if mode == .decimal { 107 | if character == "." { 108 | token += "." 109 | mode = .float 110 | continue 111 | } 112 | if character == "x" { 113 | token += "x" 114 | mode = .hex 115 | continue 116 | } 117 | if !isNumber && !isWhitespace && !isSeparator { 118 | match(with: token) 119 | 120 | token.removeAll() 121 | token.append(character) 122 | mode = .none 123 | continue 124 | } 125 | } 126 | 127 | if isWhitespace || isSeparator || mode == .punctuator { 128 | if token.isEmpty { continue } 129 | if mode == .inString { continue } 130 | 131 | if mode == .inComment { 132 | mode = .comment 133 | } 134 | 135 | if mode == .inAngleBracket, token.count == 1 { 136 | mode = .none 137 | } 138 | match(with: token) 139 | reset() 140 | 141 | if character == ";" { 142 | mode = .punctuator 143 | token.append(character) 144 | match(with: token) 145 | reset() 146 | } 147 | 148 | continue 149 | } 150 | 151 | if mode == .float, !isNumber { 152 | match(with: token) 153 | reset() 154 | } 155 | 156 | if isNumber, mode == .none { 157 | if token.isEmpty { 158 | mode = .decimal 159 | } else if numericalOperation.contains(character) { 160 | match(with: token) 161 | reset() 162 | token.append(character) 163 | mode = .decimal 164 | continue 165 | } 166 | } 167 | 168 | if character == "\"" { 169 | if mode == .inString { 170 | mode = .string 171 | } else { 172 | if token.isEmpty { 173 | mode = .inString 174 | } else { 175 | match(with: token) 176 | reset() 177 | mode = .inString 178 | } 179 | } 180 | } 181 | 182 | if token == "/" { 183 | if character == "/" { 184 | mode = .inComment 185 | } 186 | if character == "*" { 187 | mode = .comment 188 | } 189 | } 190 | 191 | if token == "*" { 192 | if character == "/" { 193 | mode = .comment 194 | } 195 | } 196 | 197 | if token == "<" { 198 | if character == "<" { 199 | mode = .none 200 | } else { 201 | mode = .inAngleBracket 202 | } 203 | } 204 | 205 | if token == "!" { 206 | if character != "=" { 207 | match(with: token) 208 | reset() 209 | token.append(character) 210 | continue 211 | } 212 | } 213 | 214 | if token == "&" { 215 | if character != "&" { 216 | match(with: token) 217 | reset() 218 | token.append(character) 219 | continue 220 | } 221 | } 222 | 223 | // pointer, e.g. char* 224 | if mode == .none, character == "*", token.count > 0 { 225 | match(with: token) 226 | reset() 227 | token.append(character) 228 | continue 229 | } 230 | 231 | if (mode == .none || mode == .punctuator), character == ";" { 232 | match(with: token) 233 | reset() 234 | 235 | mode = .punctuator 236 | token.append(";") 237 | continue 238 | } 239 | 240 | if firstMatch { 241 | if character == "*" { 242 | mode = .comment 243 | } 244 | } 245 | 246 | token.append(character) 247 | } 248 | 249 | if !token.isEmpty { 250 | if mode == .inComment { 251 | mode = .comment 252 | } 253 | match(with: token) 254 | } 255 | } 256 | } 257 | } 258 | 259 | -------------------------------------------------------------------------------- /Editor/Editor/Assistant/LSP.Types.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LSP.Types.swift 3 | // Magma 4 | // 5 | // Created by Maximilian Mackh on 12.03.23. 6 | // 7 | 8 | import Foundation 9 | extension LSP { 10 | struct Position { 11 | let line: Int 12 | let character: Int 13 | 14 | init(position: Editor.Position) { 15 | self.line = position.row + 1 16 | self.character = position.column + 1 17 | } 18 | 19 | init(line: Int, character: Int) { 20 | self.line = line 21 | self.character = character 22 | } 23 | 24 | var dictionary: NSDictionary { 25 | [ 26 | "line" : self.line, 27 | "character" : self.character 28 | ] 29 | } 30 | } 31 | 32 | struct Range { 33 | let start: Position 34 | let end: Position 35 | 36 | var dictionary: NSDictionary { 37 | [ 38 | "start" : start.dictionary, 39 | "end" : end.dictionary 40 | ] 41 | } 42 | } 43 | 44 | // https://github.com/ChimeHQ/SwiftLSPClient/blob/main/SwiftLSPClient/Types/SymbolKind.swift 45 | public enum Symbol: Int, CaseIterable, Codable { 46 | case file = 1 47 | case module = 2 48 | case namespace = 3 49 | case package = 4 50 | case `class` = 5 51 | case method = 6 52 | case property = 7 53 | case field = 8 54 | case constructor = 9 55 | case `enum` = 10 56 | case interface = 11 57 | case function = 12 58 | case variable = 13 59 | case constant = 14 60 | case string = 15 61 | case number = 16 62 | case boolean = 17 63 | case array = 18 64 | case object = 19 65 | case key = 20 66 | case null = 21 67 | case enumMember = 22 68 | case `struct` = 23 69 | case event = 24 70 | case `operator` = 25 71 | case typeParameter = 26 72 | 73 | var string: String { 74 | "\(self)" 75 | } 76 | } 77 | 78 | //https://github.com/ChimeHQ/LanguageServerProtocol/blob/4ae3b11542efccc1d3b95c7bb9b6580b27666d2b/Sources/LanguageServerProtocol/LanguageFeatures/Completion.swift#L84 79 | public enum CompletionItemKind: Int, CaseIterable, Codable, Hashable { 80 | case text = 1 81 | case method = 2 82 | case function = 3 83 | case constructor = 4 84 | case field = 5 85 | case variable = 6 86 | case `class` = 7 87 | case interface = 8 88 | case module = 9 89 | case property = 10 90 | case unit = 11 91 | case value = 12 92 | case `enum` = 13 93 | case keyword = 14 94 | case snippet = 15 95 | case color = 16 96 | case file = 17 97 | case reference = 18 98 | case folder = 19 99 | case enumMember = 20 100 | case constant = 21 101 | case `struct` = 22 102 | case event = 23 103 | case `operator` = 24 104 | case typeParameter = 25 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Editor/Editor/Assistant/LSP.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LSP.swift 3 | // Magma 4 | // 5 | // Created by Maximilian Mackh on 05.03.23. 6 | // 7 | 8 | import Foundation 9 | 10 | class LSP { 11 | public static var isDebugLogEnabled: Bool = false 12 | 13 | class Client { 14 | enum Executable { 15 | case clangd 16 | case custom(path: String) 17 | 18 | var path: String { 19 | switch self { 20 | case .clangd: 21 | return "/usr/bin/clangd" 22 | case .custom(let path): 23 | return path 24 | } 25 | } 26 | } 27 | 28 | public let UIDD: String = Foundation.UUID().uuidString 29 | 30 | private let process: LSP.Process 31 | 32 | private var id: Int = 1 33 | private var messageQueue: [Int: Message.Payload] = [:] 34 | private static let payloadSeparator: String = "\r\n\r\n" 35 | 36 | private var buffer: String = "" 37 | private var shouldBuffer: Bool = false 38 | private var expectedContentLength: Int = 0 39 | 40 | init(_ executable: Executable, environementVariables: [String:String]) { 41 | self.process = .init(executable: executable, environmentVariables: environementVariables) 42 | self.process.readHandler = { [unowned self] (data, isError) in 43 | guard let payload = String(data: data, encoding: .utf8) else { return } 44 | 45 | if isError { 46 | if LSP.isDebugLogEnabled == false { return } 47 | print(String(data: data, encoding: .utf8) ?? "Not decodable") 48 | return 49 | } 50 | 51 | let messageComponents: [String] = payload.components(separatedBy: LSP.Client.payloadSeparator) 52 | 53 | if messageComponents.count == 2 { 54 | self.expectedContentLength = Int(messageComponents.first?.components(separatedBy: ": ").last ?? "0")! 55 | let jsonString = messageComponents.last! 56 | 57 | if self.expectedContentLength == jsonString.utf8.count { 58 | buffer = String(jsonString) 59 | self.shouldBuffer = false 60 | } else { 61 | buffer = String(jsonString) 62 | self.shouldBuffer = true 63 | } 64 | } else if shouldBuffer { 65 | self.buffer += payload 66 | self.expectedContentLength -= payload.utf8.count 67 | } else if self.expectedContentLength <= 0 { 68 | self.shouldBuffer = false 69 | } 70 | 71 | let jsonString = String(buffer) 72 | guard let json: NSDictionary = try? JSONSerialization.jsonObject(with: jsonString.data(using: .utf8)!) as? NSDictionary, let responseID: Int = json["id"] as? Int, let payload = self.messageQueue[responseID] else { return } 73 | 74 | payload.responseHandler?(json) 75 | messageQueue[responseID] = nil 76 | } 77 | self.process.terminationHandler = { 78 | print("Bye LSP") 79 | } 80 | } 81 | 82 | @discardableResult 83 | private func write(message: Message, responseHandler: Message.Payload.ResponseHandler?) -> Bool { 84 | guard let payload: Message.Payload = message.packaged(with: id, responseHandler: responseHandler) else { 85 | return false 86 | } 87 | if message.isNotification == false { 88 | self.messageQueue[id] = payload 89 | id += 1 90 | } 91 | self.process._pipe.write(payload.data) 92 | return true 93 | } 94 | 95 | @discardableResult 96 | func notify(_ message: Message) -> Bool { 97 | self.write(message: message, responseHandler: nil) 98 | } 99 | 100 | @discardableResult 101 | func message(_ message: Message, responseHandler: @escaping Message.Payload.ResponseHandler) -> Bool { 102 | self.write(message: message, responseHandler: responseHandler) 103 | } 104 | 105 | func invalidate() { 106 | self.messageQueue.removeAll() 107 | self.process._pipe.close() 108 | } 109 | 110 | deinit { 111 | invalidate() 112 | } 113 | } 114 | 115 | enum Message { 116 | // messages with response 117 | case initialize(rootPath: String) 118 | case textCompletion(uri: URL, line: Int, column: Int, triggerKind: Int, triggerCharacter: String) 119 | case symbol(uri: URL) 120 | 121 | // notifications 122 | case didOpen(uri: URL) 123 | case didChange(uri: URL, version: Int, text: String, range: LSP.Range?) 124 | case didSave(uri: URL) 125 | case didClose(uri: URL) 126 | 127 | var isNotification: Bool { 128 | switch self { 129 | case .didOpen(_), .didChange(_,_,_,_), .didSave(_), .didClose(_): 130 | return true 131 | default: 132 | return false 133 | } 134 | } 135 | 136 | struct Payload { 137 | typealias ResponseHandler = (_ response: Any?)->() 138 | 139 | let message: Message 140 | let data: Data 141 | let responseHandler: ResponseHandler? 142 | } 143 | 144 | func packaged(with id: Int?, responseHandler: Payload.ResponseHandler?) -> Payload? { 145 | let payload: (String, NSDictionary) 146 | switch self { 147 | case .initialize(let rootPath): 148 | payload = ("initialize", ["trace":"off", "processId":ProcessInfo.processInfo.processIdentifier,"rootPath": rootPath]) 149 | case .didOpen(let uri): 150 | payload = ("textDocument/didOpen", ["textDocument":["uri":uri.absoluteString,"languageId":"c","text": try! String(contentsOf: uri)]]) 151 | case .didChange(let uri, let version, let text, let range): 152 | if let range = range { 153 | payload = ("textDocument/didChange", ["textDocument":["uri":uri.absoluteString,"version":version], "contentChanges": [["text": text,"range":range.dictionary]]]) 154 | } else { 155 | payload = ("textDocument/didChange", ["textDocument":["uri":uri.absoluteString,"version":version], "contentChanges": [["text": text]]]) 156 | } 157 | case .didSave(let uri): 158 | payload = ("textDocument/didSave", ["textDocument":["uri":uri.absoluteString,"text": try! String(contentsOf: uri)]]) 159 | case .didClose(let uri): 160 | payload = ("textDocument/didClose", ["textDocument":["uri":uri.absoluteString]]) 161 | case .textCompletion(let uri, let line, let column, let triggerKind, let triggerCharacter): 162 | payload = ("textDocument/completion", ["textDocument":["uri":uri.absoluteString], "position" : ["line":line,"character":column],"context":["triggerKind":triggerKind, "triggerCharacter": triggerCharacter]]) 163 | case .symbol(let uri): 164 | payload = ("textDocument/documentSymbol", ["textDocument":["uri":uri.absoluteString]]) 165 | } 166 | 167 | let json: NSMutableDictionary = ["jsonrpc":"2.0", "method": payload.0, "params" : payload.1] 168 | 169 | if isNotification == false, let id = id { 170 | json["id"] = id 171 | } 172 | 173 | guard let data = try? JSONSerialization.data(withJSONObject:json) else { return nil } 174 | return .init(message: self, data: "Content-Length: \(data.count)\r\n\r\n".data(using: .utf8)! + data, responseHandler: responseHandler) 175 | } 176 | } 177 | } 178 | 179 | extension LSP { 180 | class Process { 181 | var readHandler: ((_ data: Data, _ isError: Bool)->())? { 182 | set { 183 | _pipe.readHandler = newValue 184 | } 185 | get { 186 | _pipe.readHandler 187 | } 188 | } 189 | var terminationHandler: (()->())? { 190 | set { 191 | _process._terminationHandler = newValue 192 | } 193 | get { 194 | nil 195 | } 196 | } 197 | 198 | fileprivate let _process: Foundation.Process 199 | fileprivate let _pipe: LSP.Pipe 200 | 201 | init(executable: LSP.Client.Executable, environmentVariables: [String:String]) { 202 | let pipe: Pipe = .init() 203 | let process: Foundation.Process = .init() 204 | 205 | process.standardInput = pipe.input 206 | process.standardOutput = pipe.output 207 | process.standardError = pipe.error 208 | process._launchPath = executable.path 209 | process.arguments = [] 210 | process.environment = environmentVariables 211 | try? process._run() 212 | 213 | self._process = process 214 | self._pipe = pipe 215 | } 216 | 217 | deinit { 218 | _process.terminate() 219 | } 220 | } 221 | 222 | fileprivate class Pipe { 223 | public let input: Foundation.Pipe 224 | public let output: Foundation.Pipe 225 | public let error: Foundation.Pipe 226 | 227 | public var readHandler: ((_ data: Data, _ isError: Bool)->())? 228 | 229 | private var isClosed: Bool = false 230 | 231 | private let queue: DispatchQueue 232 | 233 | init(queue: DispatchQueue = .init(label: "com.sentionic.magma.stdio")) { 234 | self.input = .init() 235 | self.output = .init() 236 | self.error = .init() 237 | 238 | self.queue = queue 239 | 240 | output.fileHandleForReading.readabilityHandler = { [unowned self] handle in 241 | self.read(data: handle.availableData, isError: false) 242 | } 243 | error.fileHandleForReading.readabilityHandler = { [unowned self] handle in 244 | self.read(data: handle.availableData, isError: true) 245 | } 246 | } 247 | 248 | private func read(data: Data, isError: Bool) { 249 | if isClosed || data.isEmpty { return } 250 | queue.async { [weak self] in 251 | self?.readHandler?(data, isError) 252 | } 253 | } 254 | 255 | public func write(_ data: Data) { 256 | if isClosed { return } 257 | 258 | queue.async { [weak self] in 259 | self?.input.fileHandleForWriting.write(data) 260 | } 261 | } 262 | 263 | public func close() { 264 | if self.isClosed == true { return } 265 | self.isClosed = true 266 | 267 | 268 | [input, output, error].forEach { pipe in 269 | pipe.fileHandleForWriting.closeFile() 270 | pipe.fileHandleForReading.closeFile() 271 | } 272 | } 273 | } 274 | } 275 | 276 | extension Foundation.Process { 277 | var _launchPath: String? { 278 | set { 279 | perform(NSSelectorFromString("setLaunchPath:"), with: newValue) 280 | } 281 | get { 282 | nil 283 | } 284 | } 285 | 286 | func _run() throws { 287 | let error: NSError? = nil 288 | perform(NSSelectorFromString("launchAndReturnError:"), with: error) 289 | if let error = error { 290 | throw error 291 | } 292 | } 293 | 294 | var _terminationHandler: (() -> Void)? { 295 | set { 296 | let handler: @convention(block) (Process) -> Void = { process in 297 | newValue?() 298 | } 299 | let block: AnyObject = unsafeBitCast(handler, to: AnyObject.self) 300 | perform(NSSelectorFromString("setTerminationHandler:"), with: block) 301 | } 302 | get { 303 | nil 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /Editor/Editor/Editor.Container.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Editor.Container.swift 3 | // Magma 4 | // 5 | // Created by Maximilian Mackh on 15.02.23. 6 | // 7 | 8 | import Foundation 9 | import BaseComponents 10 | 11 | extension Editor { 12 | class Container { 13 | enum Delete { 14 | case backward 15 | case forward 16 | } 17 | 18 | enum Update: Codable, Equatable { 19 | case none 20 | case reload(index: Int) 21 | case reloadVisible 22 | case reloadAll 23 | } 24 | 25 | struct Line { 26 | let idx: Int 27 | let length: Int 28 | } 29 | 30 | var strategy: Indentation.Strategy = .init(kind: .spaces, count: 4) 31 | 32 | var lines: [String] = [] 33 | 34 | var selectAllPosition: Position { 35 | let lastIdx: Int = lines.count - 1 36 | let lastLine: String = lines[lastIdx] 37 | let position: Position = .init(row: 0, column: 0).with(selectionTo: .init(row: lastIdx, column: lastLine.count)) 38 | position.visibilityAdjustmentBehaviour = .none 39 | return position 40 | } 41 | 42 | let _onUpdate: (_ update: Update, _ position: Position)->() 43 | 44 | init(onUpdateHandler: @escaping (_ update: Update, _ position: Position)->()) { 45 | self._onUpdate = onUpdateHandler 46 | } 47 | 48 | var session: Session? { 49 | didSet { 50 | lines.removeAll() 51 | lines = (session?.text ?? "").separatedByNewlines() 52 | if lines.count == 0 { 53 | lines.append("") 54 | } 55 | 56 | self.strategy = session?.indentationStrategy ?? .detect(in: Array(lines.prefix(50))) 57 | 58 | self.registerUpdate(.reloadAll, position: session?.position ?? .zero) 59 | } 60 | } 61 | 62 | var string: String { 63 | lines.joined(separator: "\n") 64 | } 65 | 66 | enum Operation: Codable { 67 | case read(position: Position) 68 | case delete(position: Position, length: Int) 69 | case newline(position: Position) 70 | case insert(position: Position, value: String) 71 | case replace(position: Position, value: String) 72 | case indent(position: Position, offset: Int) 73 | case highlight(position: Position) 74 | case move(position: Position) 75 | } 76 | 77 | struct Behaviour: OptionSet { 78 | let rawValue: Int 79 | 80 | static let increaseIndentationAfterOpenBrace: Behaviour = Behaviour(rawValue: 1 << 0) 81 | static let retainIndentationOfPerviousLine: Behaviour = Behaviour(rawValue: 1 << 1) 82 | static let decrementIndentationLevelWithBackspace: Behaviour = Behaviour(rawValue: 1 << 2) 83 | static let closeCurleyBrackets: Behaviour = Behaviour(rawValue: 1 << 3) 84 | static let closeRoundBrackets: Behaviour = Behaviour(rawValue: 1 << 4) 85 | static let closeSquareBrackets: Behaviour = Behaviour(rawValue: 1 << 5) 86 | static let closeQuotationMarks: Behaviour = Behaviour(rawValue: 1 << 6) 87 | static let preventDuplicateCharacterEntry: Behaviour = Behaviour(rawValue: 1 << 7) 88 | 89 | static let `default`: Behaviour = [ 90 | .increaseIndentationAfterOpenBrace, 91 | .retainIndentationOfPerviousLine, 92 | .decrementIndentationLevelWithBackspace, 93 | .closeCurleyBrackets, 94 | .closeRoundBrackets, 95 | .closeSquareBrackets, 96 | .closeQuotationMarks, 97 | .preventDuplicateCharacterEntry, 98 | ] 99 | } 100 | 101 | var behaviour: Behaviour = .default 102 | 103 | enum Initiator: Codable { 104 | case input 105 | case undo 106 | case redo 107 | } 108 | 109 | struct Result: Codable { 110 | let operation: Operation 111 | let undo: [Operation] 112 | 113 | let update: Update 114 | 115 | let updatedPosition: Position 116 | let output: String 117 | } 118 | 119 | @discardableResult 120 | func performOperation(_ operation: Operation, initiator: Initiator = .input) -> Result? { 121 | 122 | switch operation { 123 | case .read(let position): 124 | guard let endPosition: Position = position.currentSelectionEndPosition else { return nil } 125 | 126 | let selection: Position.Selection = .init(position, endPosition) 127 | let selectedRows: [Int] = position.getSelectedIndicies() 128 | 129 | let maxIdx: Int = selectedRows.count - 1 130 | var isStart: Bool = false 131 | var isEnd: Bool = false 132 | 133 | var string: String = "" 134 | 135 | for (idx, row) in selectedRows.enumerated() { 136 | let line: String = lines[row] 137 | isStart = idx == 0 138 | isEnd = idx == maxIdx 139 | 140 | if isStart && isEnd { 141 | let startIndex: String.Index = line.index(utf16Offset: selection.start.column) 142 | let endIndex: String.Index = line.index(utf16Offset: selection.end.column) 143 | string += line[startIndex.. 1 163 | 164 | if position.isSelection || isLengthGreaterThanOne { 165 | if position.isSelection == false { 166 | position.currentSelectionEndPosition = calculateSafePositionOffset(at: position, length: length) 167 | } 168 | return performOperation(.replace(position: position, value: ""), initiator: initiator) 169 | } 170 | 171 | if position.row == 0 && position.column == 0 && length < 0 { return nil } 172 | 173 | let line: String = lines[position.row] 174 | 175 | let splitIndex: String.Index = line.index(utf16Offset: position.column) 176 | 177 | if position.column == 0 && length < 0 { 178 | let suffix: Substring = line.suffix(from: splitIndex) 179 | 180 | let previousRow = position.row - 1 181 | let previousLine: String = lines[previousRow] 182 | 183 | let mergedLine = previousLine + suffix 184 | lines[previousRow] = mergedLine 185 | lines.remove(at: position.row) 186 | 187 | let updatedPosition: Position = .init(row: previousRow, column: previousLine.utf16.count) 188 | 189 | let result: Result = .init(operation: operation, undo: [ 190 | .newline(position: updatedPosition) 191 | ], update: .reloadVisible, updatedPosition: updatedPosition, output: "") 192 | 193 | if initiator == .input { 194 | registerUndo(result) 195 | } 196 | 197 | return result 198 | } else if length > 0 && position.column >= line.utf16.count { 199 | let nextRow = position.row + 1 200 | if nextRow > lines.count - 1 { return nil } 201 | 202 | let nextLine: String = lines[nextRow] 203 | lines[position.row] = line + nextLine 204 | lines.remove(at: nextRow) 205 | 206 | let highlightPosition: Position = position.with(selectionTo: .init(row: nextRow, column: 0)) 207 | 208 | let result: Result = .init(operation: operation, undo: [ 209 | .newline(position: position), 210 | .highlight(position: highlightPosition) 211 | ], update: .reloadVisible, updatedPosition: position, output: "") 212 | 213 | if initiator == .input { 214 | registerUndo(result) 215 | } 216 | } else { 217 | var mutableLine: String = line 218 | 219 | let character: String = String(mutableLine.remove(at: length > 0 ? splitIndex : line.index(before: splitIndex))) 220 | lines[position.row] = mutableLine 221 | 222 | let updatedPosition: Position = .init(row: position.row, column: position.column - (length > 0 ? 0 : character.utf16.count)) 223 | 224 | var undo: [Operation] = [ 225 | .insert(position: updatedPosition, value: character) 226 | ] 227 | if length > 0 { 228 | undo.append(.move(position: position)) 229 | } 230 | let result: Result = .init(operation: operation, undo: undo, update: .reload(index: updatedPosition.row), updatedPosition: updatedPosition, output: "") 231 | 232 | if initiator == .input { 233 | registerUndo(result) 234 | } 235 | 236 | return result 237 | } 238 | break 239 | case .newline(let position): 240 | if position.isSelection { 241 | return performOperation(.replace(position: position, value: "\n"), initiator: initiator) 242 | } 243 | let line: String = lines[position.row] 244 | let splitIndex: String.Index = line.index(utf16Offset: position.column) 245 | let prefix: Substring = line.prefix(upTo: splitIndex) 246 | var suffix: String = String(line.suffix(from: splitIndex)) 247 | 248 | var indentation = line.indentionation(strategy: strategy) 249 | 250 | if initiator == .input { 251 | if prefix.last == "{" && behaviour.contains(.increaseIndentationAfterOpenBrace) { 252 | if suffix.first == "}" { 253 | performOperation(.insert(position: position, value: "\n" + indentation.offset(by: 1).render("") + "\n" + indentation.render(""))) 254 | registerUpdate(.none, position: .init(row: position.row + 1, column: indentation.offset(by: 1).count)) 255 | return nil 256 | } else { 257 | indentation = indentation.offset(by: 1) 258 | suffix = indentation.render(suffix) 259 | } 260 | } else if line.hasSuffix("public:") || line.hasSuffix("private:") { 261 | indentation = indentation.offset(by: 1) 262 | suffix = indentation.render(suffix) 263 | } else if behaviour.contains(.retainIndentationOfPerviousLine) { 264 | suffix = indentation.render(suffix) 265 | } 266 | } 267 | 268 | let updatedPosition: Position = .init(row: position.row + 1, column: indentation.count) 269 | 270 | lines[position.row] = String(prefix) 271 | lines.insert(suffix, at: updatedPosition.row) 272 | 273 | let result: Result = .init(operation: operation, undo: [ 274 | .delete(position: updatedPosition, length: -(1 + indentation.count)) 275 | ], update: .reloadVisible, updatedPosition: updatedPosition, output: "") 276 | 277 | if initiator == .input { 278 | registerUndo(result) 279 | } 280 | 281 | return result 282 | case .insert(let position, let value): 283 | if position.isSelection { 284 | return performOperation(.replace(position: position, value: value), initiator: initiator) 285 | } 286 | 287 | var line: String = lines[position.row] 288 | let insertIdx: String.Index = line.index(utf16Offset: position.column) 289 | 290 | if value.rangeOfCharacter(from: .newlines) != nil { 291 | let prefix: Substring = line.prefix(upTo: insertIdx) 292 | let suffix: Substring = line.suffix(from: insertIdx) 293 | 294 | let lines: [String] = value.separatedByNewlines() 295 | let maxIdx: Int = lines.count - 1 296 | var updatedRow: Int = position.row 297 | var updatedColumn: Int = position.column 298 | 299 | for (idx, line) in lines.enumerated() { 300 | if idx == 0 { 301 | self.lines[position.row] = prefix + line 302 | continue 303 | } else if idx == maxIdx { 304 | self.lines.insert(line + suffix, at: position.row + idx) 305 | updatedColumn = line.utf16.count 306 | } else { 307 | self.lines.insert(line, at: position.row + idx) 308 | } 309 | updatedRow += 1 310 | } 311 | 312 | let updatedPosition: Position = .init(row: updatedRow, column: updatedColumn) 313 | 314 | let result: Result = .init(operation: operation, undo: [ 315 | .delete(position: updatedPosition, length: -value.utf16.count) 316 | ], update: .reloadVisible, updatedPosition: updatedPosition, output: "") 317 | 318 | if initiator == .input { 319 | registerUndo(result) 320 | } 321 | 322 | return result 323 | } else { 324 | var transformedPosition: Position? 325 | var transformedValue: String = value 326 | 327 | let length: Int = line.count 328 | 329 | if initiator == .input { 330 | if position.column == length { 331 | if value == "{", behaviour.contains(.closeCurleyBrackets) { 332 | transformedValue = "{}" 333 | transformedPosition = position.offsetBy(row: 0, column: 1) 334 | } else if value == "(", behaviour.contains(.closeRoundBrackets) { 335 | transformedValue = "()" 336 | transformedPosition = position.offsetBy(row: 0, column: 1) 337 | } else if value == "[", behaviour.contains(.closeSquareBrackets) { 338 | transformedValue = "[]" 339 | transformedPosition = position.offsetBy(row: 0, column: 1) 340 | } else if value == "\"", behaviour.contains(.closeQuotationMarks) { 341 | transformedValue = "\"\"" 342 | transformedPosition = position.offsetBy(row: 0, column: 1) 343 | } 344 | } 345 | 346 | if position.column == length - 1, behaviour.contains(.preventDuplicateCharacterEntry), let lastCharacter = line.last { 347 | if lastCharacter == "}" && value == "}" { 348 | registerUpdate(.none, position: position.offsetBy(row: 0, column: 1)) 349 | return nil 350 | } 351 | if lastCharacter == ")" && value == ")" { 352 | registerUpdate(.none, position: position.offsetBy(row: 0, column: 1)) 353 | return nil 354 | } 355 | if lastCharacter == "]" && value == "]" { 356 | registerUpdate(.none, position: position.offsetBy(row: 0, column: 1)) 357 | return nil 358 | } 359 | if lastCharacter == "\"" && value == "\"" { 360 | registerUpdate(.none, position: position.offsetBy(row: 0, column: 1)) 361 | return nil 362 | } 363 | } 364 | } 365 | 366 | line.insert(contentsOf: transformedValue, at: insertIdx) 367 | lines[position.row] = line 368 | 369 | let updatedPosition: Position = .init(row: position.row, column: position.column + transformedValue.utf16.count) 370 | 371 | if initiator == .input { 372 | let token = seekToken(at: position) 373 | updatedPosition.currentTokenStore = token?.0.isEmpty == true ? nil : token 374 | } 375 | 376 | let result: Result = .init(operation: operation, undo: [ 377 | .delete(position: updatedPosition, length: -transformedValue.utf16.count) 378 | ], update: .reload(index: updatedPosition.row), updatedPosition: updatedPosition, output: "") 379 | 380 | if initiator == .input { 381 | registerUndo(result) 382 | 383 | if let transformedPosition = transformedPosition { 384 | registerUpdate(.none, position: transformedPosition) 385 | } else { 386 | session?.assistant?.invalidate(invalidation: .full(source: string)) 387 | } 388 | } else { 389 | updatedPosition.currentTokenStore = nil 390 | } 391 | 392 | return result 393 | } 394 | case .replace(let position, let value): 395 | if !position.isSelection { return nil } 396 | 397 | guard let endPosition: Position = position.currentSelectionEndPosition else { return nil } 398 | 399 | let selection: Position.Selection = .init(position, endPosition) 400 | let selectedRows: [Int] = position.getSelectedIndicies() 401 | 402 | let maxIdx: Int = selectedRows.count - 1 403 | var isStart: Bool = false 404 | var isEnd: Bool = false 405 | 406 | var string: String = "" 407 | 408 | var prefix: String = "" 409 | var suffix: String = "" 410 | var rowIndiciesToRemove: [Int] = [] 411 | 412 | for (idx, row) in selectedRows.enumerated() { 413 | var line: String = lines[row] 414 | isStart = idx == 0 415 | isEnd = idx == maxIdx 416 | 417 | if isStart && isEnd { 418 | let startIndex: String.Index = line.index(utf16Offset: selection.start.column) 419 | let endIndex: String.Index = line.index(utf16Offset: selection.end.column) 420 | 421 | string += line[startIndex.. 0 { 444 | lines[selectedRows.first!] = prefix + suffix 445 | lines.removeSubrange(rowIndiciesToRemove.first!...rowIndiciesToRemove.last!) 446 | } 447 | 448 | let updatedPosition: Position = selection.start.withoutSelection() 449 | var insertPosition: Position? = nil 450 | 451 | var undo: [Operation] = [] 452 | 453 | if value.count > 0 { 454 | if let result = performOperation(.insert(position: updatedPosition, value: value), initiator: .undo) { 455 | undo.append(contentsOf: result.undo) 456 | insertPosition = result.updatedPosition 457 | } 458 | } 459 | 460 | undo.append(.insert(position: updatedPosition, value: string)) 461 | undo.append(.highlight(position: position)) 462 | 463 | let result: Result = .init(operation: operation, undo: undo, update: .reloadVisible, updatedPosition: insertPosition ?? updatedPosition, output: "") 464 | 465 | if initiator == .input { 466 | registerUndo(result) 467 | } 468 | 469 | return result 470 | case .indent(let position, let offset): 471 | var updatedPosition: Position = position 472 | 473 | var undoSteps: [Operation] = [] 474 | let indicies: [Int] = position.isSelection ? position.getSelectedIndicies() : [position.row] 475 | 476 | let isMultilineSelection = indicies.count > 1 477 | 478 | for (idx, row) in indicies.enumerated() { 479 | let line = lines[row] 480 | let indentation: Indentation = line.indentionation(strategy: strategy) 481 | 482 | if indentation.level == 0 && offset < 0 { continue } 483 | 484 | let updatedIndentation = indentation.offset(by: offset) 485 | self.lines[row] = updatedIndentation.render(line) 486 | 487 | undoSteps.append(.indent(position: .init(row: row, column: 0), offset: -offset)) 488 | 489 | if idx == 0 { 490 | updatedPosition = .init(row: row, column: position.column + (updatedIndentation.level - indentation.level) * indentation.strategy.count) 491 | } else if idx == indicies.count - 1 { 492 | 493 | } 494 | } 495 | 496 | let result: Result = .init(operation: operation, undo: undoSteps, update: .reloadVisible, updatedPosition: updatedPosition, output: "") 497 | 498 | if initiator == .input { 499 | registerUndo(result) 500 | } 501 | return result 502 | case .highlight(let position): 503 | self.registerUpdate(.none, position: position) 504 | case .move(let position): 505 | self.registerUpdate(.none, position: position) 506 | } 507 | 508 | return nil 509 | } 510 | 511 | func registerUpdate(_ update: Update, position: Position, character: Character? = nil) { 512 | if update != .none { 513 | session?.assistant?.invalidate(invalidation: .full(source: self.lines.joined(separator: "\n"))) 514 | } 515 | _onUpdate(update, position) 516 | } 517 | 518 | private func registerUndo(_ result: Result) { 519 | session?.undoStack.append(result) 520 | self.registerUpdate(result.update, position: result.updatedPosition) 521 | } 522 | 523 | func undo() { 524 | guard let result: Result = session?.undoStack.popLast() else { return } 525 | var updatedPosition: Position? 526 | for undo in result.undo { 527 | updatedPosition = performOperation(undo, initiator: .undo)?.updatedPosition 528 | 529 | if case .highlight(let position) = undo { 530 | updatedPosition = position 531 | } 532 | 533 | if case .move(let position) = undo { 534 | updatedPosition = position 535 | } 536 | } 537 | 538 | if let updatedPosition = updatedPosition { 539 | self.registerUpdate(result.update, position: updatedPosition) 540 | } 541 | } 542 | 543 | func redo() { 544 | 545 | } 546 | 547 | func calculateSafePositionOffset(at position: Position, length: Int) -> Position { 548 | let maxRowIdx: Int = lines.count - 1 549 | var row: Int = position.row 550 | var column: Int = position.column 551 | 552 | let isBackwards: Bool = length < 0 553 | 554 | var line: String = lines[row] 555 | var lineCount: Int = isBackwards ? line.utf16.count : line.utf16.count + 1 556 | var length: Int = abs(length) 557 | 558 | while length > 0 { 559 | let isLineEnd: Bool = isBackwards ? column == 0 : column + 1 >= lineCount 560 | 561 | if isLineEnd { 562 | if isBackwards, row == 0 { 563 | break 564 | } 565 | if isBackwards == false, row >= maxRowIdx { 566 | break 567 | } 568 | 569 | length -= 1 570 | row += isBackwards ? -1 : 1 571 | 572 | line = lines[row] 573 | lineCount = line.utf16.count 574 | column = isBackwards ? lineCount : 0 575 | 576 | continue 577 | } 578 | 579 | if isBackwards { 580 | column -= 1 581 | length -= 1 582 | } else { 583 | column += 1 584 | length -= 1 585 | } 586 | } 587 | 588 | return .init(row: row, column: column) 589 | } 590 | 591 | func characterString(at position: Position) -> String? { 592 | if position.column < 0 { return nil } 593 | let line = lines[position.row] 594 | if position.column >= line.utf16.count { return nil } 595 | let index: String.Index = line.index(utf16Offset: position.column) 596 | return String(line[index]) 597 | } 598 | 599 | func line(for row: Int) -> String? { 600 | if row < 0 { return nil } 601 | if row >= lines.count { return nil } 602 | return lines[row] 603 | } 604 | 605 | func seekToken(at position: Position, boundaries: [Character] = [" ",":",".",",",";","!","?","*","&","{","}","(",")","[","]","\"","\'","<",">"]) -> (String,Position)? { 606 | 607 | let line: String = lines[position.row] 608 | 609 | if line.utf16.count <= position.column { return nil } 610 | 611 | var result: String = "" 612 | var utf16Idx: Int = 0 613 | 614 | var startColumn: Int = 0 615 | var endColumn: Int? 616 | 617 | for character in line { 618 | defer { 619 | utf16Idx += character.utf16.count 620 | } 621 | if boundaries.contains(character) { 622 | if utf16Idx >= position.column { 623 | endColumn = utf16Idx 624 | break 625 | } 626 | result.removeAll() 627 | } else { 628 | if result.isEmpty { 629 | startColumn = utf16Idx 630 | } 631 | result.append(character) 632 | } 633 | } 634 | 635 | if result.isEmpty == false && endColumn == nil { 636 | endColumn = line.utf16.count 637 | } 638 | 639 | if result == "" { 640 | return nil 641 | } 642 | 643 | if let endColumn = endColumn { 644 | return (result,.init(row: position.row, column: startColumn).with(selectionTo: .init(row: position.row, column: endColumn))) 645 | } 646 | 647 | return nil 648 | } 649 | } 650 | } 651 | 652 | fileprivate extension String { 653 | func index(utf16Offset: Int) -> String.Index { 654 | if utf16Offset == 0 { 655 | return startIndex 656 | } 657 | if utf16Offset >= utf16.count { 658 | return endIndex 659 | } 660 | return rangeOfComposedCharacterSequence(at: .init(utf16Offset: utf16Offset, in: self)).lowerBound 661 | } 662 | 663 | func characterIndex(utf16Offset: Int) -> (Character, String.Index) { 664 | let index = self.index(utf16Offset: utf16Offset) 665 | return (self[index], index) 666 | } 667 | 668 | func separatedByNewlines() -> [String] { 669 | var lines: [String] = [] 670 | (self as NSString).enumerateLines { line, stop in 671 | lines.append(line) 672 | } 673 | if self.last?.isNewline == true { 674 | lines.append("") 675 | } 676 | return lines 677 | } 678 | } 679 | -------------------------------------------------------------------------------- /Editor/Editor/Editor.Indentation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Editor.Indentation.swift 3 | // Magma 4 | // 5 | // Created by Maximilian Mackh on 15.02.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Editor { 11 | struct Indentation { 12 | struct Strategy: Codable { 13 | let kind: Kind 14 | let count: Int 15 | 16 | static func detect(in lines: [String], fallback: Strategy = .init(kind: .spaces, count: 2)) -> Strategy { 17 | var bestGuess: (Kind?, Int) = (.spaces, 4) 18 | lines.forEach { line in 19 | let indentation = line.measureIndentation() 20 | if indentation.1 > 0 && indentation.1 <= bestGuess.1 { 21 | bestGuess = indentation 22 | } 23 | } 24 | if let kind = bestGuess.0, bestGuess.1 <= fallback.count { 25 | return .init(kind: kind, count: bestGuess.1) 26 | } 27 | return fallback 28 | } 29 | } 30 | 31 | enum Kind: String, Codable { 32 | case spaces = " " 33 | case tab = "\t" 34 | } 35 | 36 | let strategy: Indentation.Strategy 37 | let level: Int 38 | 39 | var count: Int { 40 | level * strategy.count 41 | } 42 | 43 | func offset(by level: Int) -> Indentation { 44 | .init(strategy: strategy, level: max(0, self.level + level)) 45 | } 46 | 47 | func render(_ string: String) -> String { 48 | String(repeating: strategy.kind.rawValue, count: count) + string.removeIndentation() 49 | } 50 | } 51 | } 52 | 53 | extension String { 54 | func measureIndentation() -> (Editor.Indentation.Kind?, Int) { 55 | var kind: Editor.Indentation.Kind? 56 | var count: Int = 0 57 | for character in self.utf8 { 58 | if character == 0x20 { 59 | kind = .spaces 60 | count += 1 61 | continue 62 | } 63 | if character == 0x09 { 64 | kind = .tab 65 | count += 1 66 | continue 67 | } 68 | break 69 | } 70 | return (kind, count) 71 | } 72 | 73 | func removeIndentation() -> String { 74 | String(drop(while: { $0.isWhitespace })) 75 | } 76 | 77 | func indentionation(strategy: Editor.Indentation.Strategy) -> Editor.Indentation { 78 | return .init(strategy: strategy, level: measureIndentation().1 / strategy.count) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Editor/Editor/Editor.Position.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Editor.Position.swift 3 | // Magma 4 | // 5 | // Created by Maximilian Mackh on 15.02.23. 6 | // 7 | 8 | import Foundation 9 | import BaseComponents 10 | 11 | extension Editor { 12 | class Position: Codable, Equatable, CustomDebugStringConvertible, Comparable { 13 | enum DocumentVisibilityAdjustementBehaviour: Codable { 14 | case none 15 | case contentOffset 16 | case followCursor 17 | case scrollToLine 18 | } 19 | 20 | let row: Int 21 | let column: Int 22 | 23 | var currentSelectionEndPosition: Position? 24 | var isSelection: Bool { 25 | currentSelectionEndPosition != nil 26 | } 27 | var isMultilineSelection: Bool { 28 | guard let endPosition = currentSelectionEndPosition else { return false } 29 | let min = min(endPosition.row, row) 30 | let max = max(endPosition.row, row) 31 | return max - min > 0 32 | } 33 | 34 | var visibilityAdjustmentBehaviour: DocumentVisibilityAdjustementBehaviour = .none 35 | 36 | @TransientCodable 37 | var currentTokenStore: (String, Position)? 38 | 39 | var indexPath: IndexPath { 40 | .init(item: row, section: 0) 41 | } 42 | 43 | var selection: Selection? { 44 | guard let endPosition = currentSelectionEndPosition else { return nil } 45 | return .init(.init(row: row, column: column), .init(row: endPosition.row, column: endPosition.column)) 46 | } 47 | 48 | var readableDescription: String { 49 | "Line: \(row + 1) Col: \(column + 1)" 50 | } 51 | 52 | init(row: Int, column: Int) { 53 | self.row = row 54 | self.column = column 55 | } 56 | 57 | static var zero: Position { 58 | .init(row: 0, column: 0) 59 | } 60 | 61 | func offsetBy(row: Int, column: Int) -> Position { 62 | if self.row == 0 && row < 0 { return self } 63 | var targetColumn: Int = self.column + column 64 | if targetColumn < 0 { 65 | targetColumn = 0 66 | } 67 | let position: Position = .init(row: self.row + row, column: targetColumn) 68 | position.visibilityAdjustmentBehaviour = visibilityAdjustmentBehaviour 69 | return position 70 | } 71 | 72 | func with(selectionTo position: Position) -> Position { 73 | let start: Position = .init(row: row, column: column) 74 | start.currentSelectionEndPosition = position 75 | return start 76 | } 77 | 78 | func withoutSelection() -> Position { 79 | .init(row: row, column: column) 80 | } 81 | 82 | func isInSelection(idx: Int) -> Bool { 83 | guard let endPosition = currentSelectionEndPosition else { return false } 84 | let min = min(endPosition.row, row) 85 | let max = max(endPosition.row, row) 86 | return min <= idx && idx <= max 87 | } 88 | 89 | func getSelectedIndexPaths(section: Int = 0) -> [IndexPath] { 90 | getSelectedIndicies().map { idx in 91 | .init(row: idx, section: section) 92 | } 93 | } 94 | 95 | func getSelectedIndicies() -> [Int] { 96 | guard let endPosition = currentSelectionEndPosition else { return [] } 97 | var indicies: [Int] = [] 98 | let min = min(row, endPosition.row) 99 | let max = max(row, endPosition.row) 100 | for i in min...max { 101 | indicies.append(i) 102 | } 103 | return indicies 104 | } 105 | 106 | static func ==(lhs: Position, rhs: Position) -> Bool { 107 | return lhs.row == rhs.row && lhs.column == rhs.column 108 | } 109 | 110 | static func < (lhs: Editor.Position, rhs: Editor.Position) -> Bool { 111 | if lhs.row == rhs.row { 112 | return lhs.column < rhs.column 113 | } 114 | return lhs.row < rhs.row 115 | } 116 | 117 | var debugDescription: String { 118 | "[Position] \(row):\(column)" 119 | } 120 | 121 | struct Selection: CustomDebugStringConvertible { 122 | let start: Position 123 | let end: Position 124 | 125 | init(_ lhs: Position, _ rhs: Position) { 126 | let isReverse: Bool = lhs > rhs 127 | start = isReverse ? rhs : lhs 128 | end = isReverse ? lhs : rhs 129 | } 130 | 131 | var debugDescription: String { 132 | "[Selection] - \(start) to \(end)" 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Editor/Editor/Editor.Scroll/Editor.Scroll.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Editor.Scroll.swift 3 | // Magma 4 | // 5 | // Created by Maximilian Mackh on 24.02.23. 6 | // 7 | 8 | // Experimental replacement to TableView, haven't figured out how to adjust height dynamically, not usesd. 9 | 10 | import UIKit 11 | import BaseComponents 12 | 13 | extension Editor { 14 | class Scroll: UIScrollView { 15 | private class Coordinate: Equatable, Hashable { 16 | var index: Int 17 | var y: CGFloat 18 | var height: CGFloat 19 | 20 | var visibleView: UIView? = nil 21 | 22 | init(index: Int, y: CGFloat, height: CGFloat) { 23 | self.index = index 24 | self.y = y 25 | self.height = height 26 | } 27 | 28 | static func ==(lhs: Coordinate, rhs: Coordinate) -> Bool { 29 | lhs.index == rhs.index 30 | } 31 | 32 | func hash(into hasher: inout Hasher) { 33 | hasher.combine(index) 34 | } 35 | } 36 | 37 | var numberOfRows: Int = 0 38 | var estimatedHeightPerRow: CGFloat = 44.0 39 | var customHeightHandler: ((_ index: Int)->(CGFloat?))? 40 | var additionalYOffset: CGFloat = 0 41 | 42 | private var coordinates: [Coordinate] = [] 43 | private var visibleCoordinates: [Coordinate] = [] 44 | private var customHeightStore: [Int:CGFloat] = [:] 45 | 46 | override var contentOffset: CGPoint { 47 | didSet { 48 | updateVisibleCells() 49 | } 50 | } 51 | 52 | override init(frame: CGRect) { 53 | super.init(frame: frame) 54 | 55 | } 56 | 57 | required init?(coder: NSCoder) { 58 | fatalError("init(coder:) has not been implemented") 59 | } 60 | 61 | override func layoutSubviews() { 62 | super.layoutSubviews() 63 | 64 | updateVisibleCells() 65 | } 66 | 67 | func reloadData() { 68 | 69 | calculateCoordinates() 70 | } 71 | 72 | func setHeight(for index: Int, height: CGFloat) { 73 | customHeightStore[index] = height 74 | } 75 | 76 | private func calculateCoordinates() { 77 | self.coordinates.removeAll() 78 | additionalYOffset = 0 79 | 80 | var yOffsetTracker: CGFloat = 0 81 | 82 | for i in 0..= minYOffset) && y <= maxYOffset { 108 | visible.append(coordinate) 109 | } 110 | } 111 | 112 | if visible == visibleCoordinates { return } 113 | let previous: Set = Set(visible) 114 | let current: Set = Set(visibleCoordinates) 115 | visibleCoordinates = visible 116 | 117 | let added = previous.subtracting(current) 118 | let removed = current.subtracting(previous) 119 | 120 | for removedCoordinate in removed { 121 | removedCoordinate.visibleView?.removeFromSuperview() 122 | } 123 | 124 | for addedCoordinate in added.sorted(by: { $0.index < $1.index }) { 125 | 126 | 127 | let view: UIView = UIView() 128 | view.frame = .init(x: 0, y: addedCoordinate.y, width: bounds.width, height: addedCoordinate.height) 129 | view.backgroundColor = addedCoordinate.index % 2 == 0 ? .blue : .red 130 | addedCoordinate.visibleView = view 131 | addSubview(view) 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Editor/Editor/Editor.Session.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Editor.Session.swift 3 | // Magma 4 | // 5 | // Created by Maximilian Mackh on 15.02.23. 6 | // 7 | 8 | import Foundation 9 | import BaseComponents 10 | 11 | extension Editor { 12 | class Session: Codable { 13 | var UUID: String = Foundation.UUID().uuidString 14 | var createdDate: Date = Date() 15 | 16 | var userInfo: [String: String] = [:] 17 | 18 | var indentationStrategy: Indentation.Strategy? 19 | 20 | var text: String 21 | var position: Position 22 | 23 | @TransientCodable 24 | var assistant: Assistant? 25 | @TransientCodable 26 | var undoStack: [Container.Result] = [] 27 | @TransientCodable 28 | var redoStack: [Container.Result] = [] 29 | @TransientCodable 30 | var temporaryStore: Any? 31 | 32 | init(text: String, position: Position) { 33 | self.text = text 34 | self.position = position 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Editor/Editor/Editor.TextInput/Editor.TextInput.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Editor.TextInput.swift 3 | // Magma 4 | // 5 | // Created by Maximilian Mackh on 19.02.23. 6 | // 7 | 8 | import UIKit 9 | import BaseComponents 10 | 11 | extension Editor { 12 | class TextInput: UIView { 13 | class Field: UITextField, UITextFieldDelegate { 14 | enum Action { 15 | case newline 16 | case indent 17 | case unindent 18 | case undo 19 | case redo 20 | case copy 21 | case cut 22 | case paste 23 | case selectAll 24 | case toggleComment 25 | case escapeKey 26 | } 27 | 28 | var caretHeight: CGFloat = -1 29 | 30 | var textInsertionHandler: ((_ text: String)->())? 31 | var textDeletionHandler: ((_ delete: Keyboard.Event.Delete)->())? 32 | 33 | var textFieldActionHandler: ((_ action: Action)->())? 34 | 35 | var textFieldNavigationHandler: ((_ arrow: Keyboard.Event.Arrow)->())? 36 | 37 | var modifierKeyPressStateDidChange: ((_ modifierKey: Keyboard.Modifier, _ isEnabled: Bool)->())? 38 | 39 | var isShiftKeyPressed: Bool = false { 40 | didSet { 41 | if oldValue != isShiftKeyPressed { 42 | modifierKeyPressStateDidChange?(.shift, isShiftKeyPressed) 43 | } 44 | } 45 | } 46 | var isOptionKeyPressed: Bool = false { 47 | didSet { 48 | if oldValue != isOptionKeyPressed { 49 | modifierKeyPressStateDidChange?(.option, isOptionKeyPressed) 50 | } 51 | } 52 | } 53 | var isCommandKeyPressed: Bool = false { 54 | didSet { 55 | if oldValue != isCommandKeyPressed { 56 | modifierKeyPressStateDidChange?(.command, isCommandKeyPressed) 57 | } 58 | } 59 | } 60 | 61 | lazy var keyboard: Keyboard? = .init(matching: [.keyDown, .keyUp, .flagsChanged]) { [unowned self] event in 62 | if self.isFirstResponder == false { 63 | return event 64 | } 65 | 66 | if event.kind == .keyDown { 67 | if let delete = event.delete { 68 | self.textDeletionHandler?(delete) 69 | return event 70 | } 71 | if let arrow = event.arrow { 72 | self.textFieldNavigationHandler?(arrow) 73 | return event 74 | } 75 | 76 | if event.modifiers == [.command] { 77 | if event.characters == "/" { 78 | self.textFieldActionHandler?(.toggleComment) 79 | } 80 | } 81 | 82 | if event.isEscape { 83 | self.textFieldActionHandler?(.escapeKey) 84 | } 85 | } 86 | 87 | if event.kind == .flagsChanged { 88 | let modifiers: [Keyboard.Modifier] = event.modifiers 89 | isShiftKeyPressed = modifiers.contains(.shift) 90 | isOptionKeyPressed = modifiers.contains(.option) 91 | isCommandKeyPressed = modifiers.contains(.command) 92 | } 93 | 94 | return event 95 | } 96 | 97 | lazy var _keyCommands: [UIKeyCommand] = { 98 | let commands: [(String, UIKeyModifierFlags)] = [ 99 | ("\t", []), 100 | ("\t", [.shift]), 101 | ("[", [.command]), 102 | ("]", [.command]), 103 | ] 104 | let selector: Selector = #selector(keyCommandPressed(_:)) 105 | return commands.map { (command, modifier) in 106 | let keyCommand: UIKeyCommand = .init(input: command, modifierFlags: modifier, action: selector) 107 | keyCommand.wantsPriorityOverSystemBehavior = true 108 | return keyCommand 109 | } 110 | }() 111 | 112 | init() { 113 | super.init(frame: .zero) 114 | 115 | autocorrectionType = .no 116 | autocapitalizationType = .none 117 | 118 | delegate = self 119 | 120 | _ = keyboard 121 | } 122 | 123 | required init?(coder: NSCoder) { 124 | fatalError("init(coder:) has not been implemented") 125 | } 126 | 127 | override func caretRect(for position: UITextPosition) -> CGRect { 128 | if caretHeight > 0 { 129 | return .init(x: 0, y: 0, width: .onePixel * 2, height: caretHeight) 130 | } 131 | return super.caretRect(for: position) 132 | } 133 | 134 | override func layoutSubviews() { 135 | super.layoutSubviews() 136 | 137 | if isFirstResponder { 138 | super.selectAll(nil) 139 | } 140 | } 141 | 142 | override var keyCommands: [UIKeyCommand]? { 143 | _keyCommands 144 | } 145 | 146 | @objc func keyCommandPressed(_ command: UIKeyCommand) { 147 | switch command.input ?? "" { 148 | case "\t": 149 | if command.modifierFlags == .shift { 150 | textFieldActionHandler?(.unindent) 151 | } else { 152 | textFieldActionHandler?(.indent) 153 | } 154 | break 155 | case "]": 156 | textFieldActionHandler?(.indent) 157 | case "[": 158 | textFieldActionHandler?(.unindent) 159 | default: break 160 | } 161 | } 162 | 163 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 164 | if string.isEmpty { 165 | print("delete forward?") 166 | return false 167 | } 168 | 169 | if string == "\n" { 170 | textFieldActionHandler?(.newline) 171 | return false 172 | } 173 | 174 | textInsertionHandler?(string) 175 | return false 176 | } 177 | 178 | override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { 179 | switch action { 180 | case #selector(UIResponderStandardEditActions.copy(_:)), 181 | #selector(UIResponderStandardEditActions.paste(_:)), 182 | #selector(UIResponderStandardEditActions.cut(_:)), 183 | #selector(UIResponderStandardEditActions.selectAll(_:)): 184 | return true 185 | default: 186 | return super.canPerformAction(action, withSender: sender) 187 | } 188 | } 189 | 190 | @objc func undo(_ : Any) { 191 | textFieldActionHandler?(.undo) 192 | } 193 | 194 | @objc func redo(_ : Any) { 195 | textFieldActionHandler?(.redo) 196 | } 197 | 198 | func textFieldShouldClear(_ textField: UITextField) -> Bool { 199 | return true 200 | } 201 | 202 | override func copy(_ sender: Any?) { 203 | textFieldActionHandler?(.copy) 204 | } 205 | 206 | override func cut(_ sender: Any?) { 207 | textFieldActionHandler?(.cut) 208 | } 209 | 210 | override func paste(_ sender: Any?) { 211 | textFieldActionHandler?(.paste) 212 | } 213 | 214 | override func selectAll(_ sender: Any?) { 215 | textFieldActionHandler?(.selectAll) 216 | } 217 | 218 | override func becomeFirstResponder() -> Bool { 219 | keyboard?.isEnabled = true 220 | return super.becomeFirstResponder() 221 | } 222 | 223 | override func resignFirstResponder() -> Bool { 224 | keyboard?.isEnabled = false 225 | return super.resignFirstResponder() 226 | } 227 | } 228 | 229 | var field: Field = .init() 230 | 231 | init() { 232 | super.init(frame: .zero) 233 | field.autoresizingMask = [.flexibleWidth, .flexibleHeight] 234 | addSubview(field) 235 | } 236 | 237 | required init?(coder: NSCoder) { 238 | fatalError("init(coder:) has not been implemented") 239 | } 240 | 241 | override var canBecomeFirstResponder: Bool { 242 | true 243 | } 244 | 245 | @discardableResult 246 | override func becomeFirstResponder() -> Bool { 247 | field.becomeFirstResponder() 248 | } 249 | 250 | @discardableResult 251 | override func resignFirstResponder() -> Bool { 252 | field.resignFirstResponder() 253 | } 254 | 255 | deinit { 256 | } 257 | } 258 | } 259 | 260 | extension Keyboard.Event { 261 | enum Arrow: Int { 262 | case left = 123 263 | case right = 124 264 | case down = 125 265 | case up = 126 266 | } 267 | 268 | enum Delete: Int { 269 | case backward = 51 270 | case forward = 117 271 | } 272 | 273 | var arrow: Arrow? { 274 | .init(rawValue: keyCode) 275 | } 276 | 277 | var delete: Delete? { 278 | .init(rawValue: keyCode) 279 | } 280 | 281 | var isEscape: Bool { 282 | keyCode == 53 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /Editor/Editor/Editor.TextInput/Keyboard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Keyboard.swift 3 | // Magma 4 | // 5 | // Created by Maximilian Mackh on 19.02.23. 6 | // 7 | 8 | import Foundation 9 | 10 | class Keyboard { 11 | struct Mask: OptionSet { 12 | let rawValue: UInt64 13 | static let keyDown = Self(rawValue: 1 << Event.Kind.keyDown.rawValue) 14 | static let keyUp = Self(rawValue: 1 << Event.Kind.keyUp.rawValue) 15 | static let flagsChanged = Self(rawValue: 1 << Event.Kind.flagsChanged.rawValue) 16 | } 17 | 18 | struct Event { 19 | enum Kind: UInt { 20 | case unknown = 0 21 | case keyDown = 10 22 | case keyUp = 11 23 | case flagsChanged = 12 24 | } 25 | 26 | var keyCode: Int { 27 | nsEvent.keyCode ?? 0 28 | } 29 | 30 | var characters: String { 31 | nsEvent.characters ?? "" 32 | } 33 | 34 | var modifiers: [Modifier] { 35 | Modifier.modifiers(for: nsEvent.modifierFlags ?? 0) 36 | } 37 | 38 | var kind: Kind { 39 | .init(rawValue: nsEvent.type ?? 0) ?? .unknown 40 | } 41 | 42 | fileprivate let nsEvent: NSEvent_Private 43 | } 44 | 45 | var isEnabled: Bool = true 46 | 47 | private let nsEvent: AnyClass 48 | private var monitor: Any! 49 | 50 | static private var isAddProtocolSuccessful: Bool = false 51 | 52 | init?(matching mask: Mask, handler: @escaping(_ event: Event)->(Event?)) { 53 | guard let nsEvent = NSClassFromString("NSEvent") else { return nil } 54 | self.nsEvent = nsEvent 55 | 56 | if !Keyboard.isAddProtocolSuccessful && !class_addProtocol(nsEvent, NSEvent_Private.self) { return nil } 57 | Keyboard.isAddProtocolSuccessful = true 58 | 59 | guard let monitor = nsEvent.addLocalMonitorForEvents(matching: mask.rawValue, handler: { [unowned self] nsEvent in 60 | if !self.isEnabled { 61 | return nsEvent 62 | } 63 | return handler(.init(nsEvent: nsEvent))?.nsEvent 64 | }) else { return nil } 65 | self.monitor = monitor as AnyObject 66 | } 67 | 68 | deinit { 69 | guard let monitor = monitor else { return } 70 | nsEvent.removeMonitor?(monitor) 71 | } 72 | } 73 | 74 | extension Keyboard { 75 | struct Modifier: OptionSet { 76 | let rawValue: UInt 77 | 78 | static let capsLock = Self(rawValue: 1 << 16) 79 | static let shift = Self(rawValue: 1 << 17) 80 | static let control = Self(rawValue: 1 << 18) 81 | static let option = Self(rawValue: 1 << 19) 82 | static let command = Self(rawValue: 1 << 20) 83 | 84 | static func modifiers(for flags: UInt) -> [Modifier] { 85 | var modifiers: [Modifier] = [] 86 | if flags & Modifier.capsLock.rawValue > 0 { 87 | modifiers.append(.capsLock) 88 | } 89 | if flags & Modifier.shift.rawValue > 0 { 90 | modifiers.append(.shift) 91 | } 92 | if flags & Modifier.control.rawValue > 0 { 93 | modifiers.append(.control) 94 | } 95 | if flags & Modifier.option.rawValue > 0 { 96 | modifiers.append(.option) 97 | } 98 | if flags & Modifier.command.rawValue > 0 { 99 | modifiers.append(.command) 100 | } 101 | return modifiers 102 | } 103 | } 104 | } 105 | 106 | extension NSObject: NSEvent_Private { } 107 | 108 | @objc private protocol NSEvent_Private { 109 | @objc(addLocalMonitorForEventsMatchingMask:handler:) optional static func addLocalMonitorForEvents(matching mask: CUnsignedLongLong, handler block: @escaping (NSObject) -> AnyObject?) -> Any? 110 | @objc optional static func removeMonitor(_ monitor: Any) 111 | 112 | @objc optional var type: UInt { get } 113 | @objc optional var keyCode: Int { get } 114 | @objc optional var characters: String { get } 115 | @objc optional var modifierFlags: UInt { get } 116 | } 117 | -------------------------------------------------------------------------------- /Editor/Editor/Editor.UI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Editor.UI.swift 3 | // Magma 4 | // 5 | // Created by Maximilian Mackh on 15.02.23. 6 | // 7 | 8 | import UIKit 9 | 10 | extension Editor { 11 | class UI { 12 | let UUID: String = Foundation.UUID().uuidString 13 | 14 | var gutterMinimumCharacterCount: Int = 2 15 | var gutterFont: UIFont = UIFont.monospacedSystemFont(ofSize: 11, weight: .regular) 16 | var gutterInactiveColor: UIColor = .secondaryLabel 17 | 18 | var editorFont: UIFont = UIFont.monospacedSystemFont(ofSize: 14, weight: .regular) 19 | var editorForegroundColor: UIColor = .dynamic(light: .hex("#586E75"), dark: .hex("#94A0A1")) 20 | var editorBackgroundColor: UIColor = .dynamic(light: .hex("#FDF6E3"), dark: .hex("#063642")) 21 | var editorLineSelectionColor: UIColor = .dynamic(light: .hex("#777777").alpha(0.1), dark: .hex("#AAAAAA").alpha(0.2)) 22 | var editorLineSpacing: CGFloat = 5 23 | var editorContentInset: UIEdgeInsets = .init(top: 0, left: 0, bottom: 0, right: 0) 24 | var editorContentOverscroll: CGFloat = 0.3 25 | 26 | var editorLineHighlightColor: UIColor = .dynamic(light: .hex("#CCCCCC").alpha(0.2), dark: .hex("#AAAAAA").alpha(0.1)) 27 | 28 | static var `default`: UI { 29 | .init() 30 | } 31 | 32 | lazy var estimatedLineHeight: CGFloat = { 33 | (editorLineSpacing + editorFont.lineHeight) 34 | }() 35 | 36 | lazy var estimatedEditorCharacterWidth: CGFloat = { 37 | "8".size(withAttributes: [.font : editorFont]).width 38 | }() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Editor/Editor/Editor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Editor.swift 3 | // Magma 4 | // 5 | // Created by Maximilian Mackh on 15.01.23. 6 | // 7 | 8 | import UIKit 9 | import BaseComponents 10 | 11 | class Editor: UIView { 12 | private var _currentSession: Session? 13 | 14 | let document: Document = .init() 15 | 16 | var hints: [Hint] { 17 | set { 18 | document._hintDictionary.removeAll() 19 | for hint in newValue { 20 | document._hintDictionary[hint.position.row] = hint 21 | } 22 | self.document.rebindLines() 23 | } 24 | get { 25 | Array(document._hintDictionary.values) 26 | } 27 | } 28 | 29 | @available(*, unavailable) 30 | init() { 31 | super.init(frame: .zero) 32 | } 33 | 34 | private override init(frame: CGRect) { 35 | super.init(frame: frame) 36 | 37 | build { [unowned self] in 38 | Equal { 39 | self.document 40 | } 41 | } 42 | } 43 | 44 | convenience init(parentViewController: UIViewController) { 45 | self.init(frame: .zero) 46 | 47 | self.document.parentViewController = parentViewController 48 | } 49 | 50 | required init?(coder: NSCoder) { 51 | fatalError("init(coder:) has not been implemented") 52 | } 53 | 54 | override var canBecomeFirstResponder: Bool { 55 | true 56 | } 57 | 58 | override func becomeFirstResponder() -> Bool { 59 | document.becomeFirstResponder() 60 | } 61 | 62 | override func resignFirstResponder() -> Bool { 63 | document.resignFirstResponder() 64 | } 65 | 66 | func resume(session: Session) { 67 | if let session = _currentSession { 68 | if let position = document._currentPosition { 69 | session.position = position 70 | } 71 | session.text = document.container.lines.joined(separator: "\n") 72 | } 73 | 74 | _currentSession = session 75 | 76 | self.document.session = session 77 | _ = document.input.field.becomeFirstResponder() 78 | } 79 | 80 | class Document: UIView, UITableViewDelegate, UITableViewDataSource { 81 | class RowCell: UITableViewCell { 82 | class CodeLabel: UIView { 83 | let paragraphStyle: NSMutableParagraphStyle = { 84 | let paragraphStyle: NSMutableParagraphStyle = .init() 85 | paragraphStyle.alignment = .left 86 | paragraphStyle.allowsDefaultTighteningForTruncation = false 87 | return paragraphStyle 88 | }() 89 | 90 | var font: UIFont! 91 | 92 | var lineBreakMode: NSLineBreakMode = .byWordWrapping { 93 | didSet { 94 | textContainer.lineBreakMode = lineBreakMode 95 | } 96 | } 97 | 98 | let textStorage: NSTextStorage = .init() 99 | let textContainer: NSTextContainer = .init() 100 | let layoutManager: NSLayoutManager = .init() 101 | 102 | private var lineHeightOffset: CGFloat = 0 103 | var index: Int = 0 104 | static weak var selectionPosition: Position? 105 | var selectionColor: UIColor! 106 | var didDrawSelection: Bool = false 107 | 108 | var hint: Hint? 109 | 110 | override init(frame: CGRect) { 111 | super.init(frame: frame) 112 | 113 | backgroundColor = .clear 114 | 115 | textStorage.addLayoutManager(layoutManager) 116 | layoutManager.addTextContainer(textContainer) 117 | 118 | textContainer.lineFragmentPadding = 0 119 | } 120 | 121 | required init?(coder: NSCoder) { 122 | fatalError("init(coder:) has not been implemented") 123 | } 124 | 125 | func update(row: Int, with text: String?, ui: UI, container: Container, assistant: Assistant?) { 126 | lineHeightOffset = (ui.estimatedLineHeight - ui.editorFont.lineHeight) / 2 127 | paragraphStyle.minimumLineHeight = ui.estimatedLineHeight 128 | 129 | selectionColor = ui.editorLineSelectionColor 130 | font = ui.editorFont 131 | 132 | var attributedString: NSMutableAttributedString = .init(string: text ?? "", attributes: [.font: font as Any, .paragraphStyle : paragraphStyle, .foregroundColor : ui.editorForegroundColor, .backgroundColor : selectionColor as Any]) 133 | 134 | 135 | if let assistant = assistant, let highlight = assistant.highlight(row: row, container: container, mutableString: attributedString) { 136 | attributedString = highlight 137 | } 138 | 139 | if let hint { 140 | let length = attributedString.length 141 | 142 | if length > 0 { 143 | let column = length > hint.position.column ? hint.position.column : length - 1 144 | 145 | attributedString.addAttributes([.underlineStyle : NSUnderlineStyle.thick.rawValue, .underlineColor : UIColor.red], range: .init(location: column, length: 1)) 146 | } 147 | } 148 | 149 | textStorage.setAttributedString(attributedString) 150 | 151 | setNeedsDisplay() 152 | } 153 | 154 | override func layoutSubviews() { 155 | super.layoutSubviews() 156 | 157 | textContainer.size = bounds.size 158 | 159 | setNeedsDisplay() 160 | } 161 | 162 | override func draw(_ rect: CGRect) { 163 | super.draw(rect) 164 | 165 | let range: NSRange = NSRange(location: 0, length: textStorage.length) 166 | 167 | if let selectionPosition = CodeLabel.selectionPosition, selectionPosition.isInSelection(idx: index), let endPosition = selectionPosition.currentSelectionEndPosition { 168 | var location: Int = 0 169 | var length: Int = 0 170 | if index == endPosition.row && index == selectionPosition.row { 171 | location = min(selectionPosition.column, endPosition.column) 172 | length = max(selectionPosition.column, endPosition.column) - location 173 | 174 | layoutManager.drawBackground(forGlyphRange: .init(location: location, length: length), at: rect.origin) 175 | } else if index == selectionPosition.row || index == endPosition.row { 176 | let relevantPosition: Position = index == endPosition.row ? endPosition : selectionPosition 177 | let relevantOtherPosition: Position = index == selectionPosition.row ? endPosition : selectionPosition 178 | 179 | let isReverse: Bool = relevantPosition.row > relevantOtherPosition.row 180 | 181 | location = isReverse ? 0 : relevantPosition.column 182 | length = isReverse ? relevantPosition.column : textStorage.length - relevantPosition.column 183 | 184 | if isReverse { 185 | layoutManager.drawBackground(forGlyphRange: .init(location: location, length: length), at: rect.origin) 186 | } else { 187 | let characterRect: CGRect = layoutManager.boundingRect(forGlyphRange: .init(location: location, length: 0), in: textContainer) 188 | let width: CGFloat = bounds.size.width 189 | let height: CGFloat = bounds.size.height 190 | selectionColor.setFill() 191 | UIRectFill(.init(x: characterRect.x, y: characterRect.y, width: width - characterRect.x, height: height)) 192 | if height >= characterRect.size.height * 2 && characterRect.origin.y == 0 { 193 | UIRectFill(.init(x: 0, y: characterRect.size.height, width: width, height: height)) 194 | } 195 | } 196 | } else { 197 | length = max(selectionPosition.column, endPosition.column) 198 | 199 | selectionColor.setFill() 200 | UIRectFill(rect) 201 | } 202 | didDrawSelection = true 203 | } else { 204 | didDrawSelection = false 205 | } 206 | 207 | var origin: CGPoint = rect.origin 208 | origin.y = -lineHeightOffset 209 | layoutManager.drawGlyphs(forGlyphRange: range, at: origin) 210 | } 211 | 212 | func calculateHeight(with availableWidth: CGFloat, ui: UI) -> CGFloat { 213 | textContainer.size = .init(width: availableWidth, height: .infinity) 214 | let usedRect: CGRect = layoutManager.usedRect(for: textContainer) 215 | let rowCount: CGFloat = (usedRect.height / ui.estimatedLineHeight).rounded(.down) 216 | if rowCount < 1.0 { 217 | return ui.estimatedLineHeight 218 | } 219 | return rowCount * ui.estimatedLineHeight 220 | } 221 | 222 | func characterIndex(at point: CGPoint) -> Int { 223 | var fractal: CGFloat = 0 224 | var index = layoutManager.glyphIndex(for: point, in: textContainer, fractionOfDistanceThroughGlyph: &fractal) 225 | 226 | if fractal > 0.4 { 227 | let string = textStorage.string 228 | let character = 229 | string[.init(utf16Offset: index, in: string)] 230 | index += character.utf16.count 231 | } 232 | return index 233 | } 234 | 235 | func boundingRectOfCharacter(at index: Int, ui: UI) -> CGRect { 236 | let count: Int = textStorage.length 237 | if count == 0 { 238 | return .init(origin: .init(x: 0, y: 0), size: .init(width: ui.estimatedEditorCharacterWidth, height: height)) 239 | } 240 | if index >= count { 241 | var rect = boundingRectOfCharacter(at: count - 1, ui: ui) 242 | rect.origin.x += rect.width 243 | return rect 244 | } 245 | return layoutManager.boundingRect(forGlyphRange: .init(location: index, length: 1), in: textContainer) 246 | } 247 | } 248 | 249 | var gutterWidth: CGFloat = 0 250 | let gutterPaddingLeading: CGFloat = 8 251 | let gutterPaddingTrailing: CGFloat = 8 252 | let gutterLabel: UILabel = UILabel().align(.center) 253 | 254 | let content: CodeLabel = .init() 255 | 256 | static let reuseIdentifier: String = "RowCell" 257 | 258 | var characterWidth: CGFloat = 0 259 | 260 | weak var ui: UI? { 261 | didSet { 262 | if let ui = ui, ui.UUID != oldValue?.UUID { 263 | characterWidth = ui.estimatedEditorCharacterWidth 264 | gutterLabel.font = ui.gutterFont 265 | content.font = ui.editorFont 266 | 267 | selectedBackgroundView?.color(.background, ui.editorLineHighlightColor) 268 | } 269 | } 270 | } 271 | 272 | var line: String! 273 | weak var container: Editor.Container! 274 | 275 | var hint: Hint? { 276 | didSet { 277 | if let hint { 278 | hintIndicatorView.isHidden = false 279 | hintIndicatorView.backgroundColor = hint.kind.primaryColor 280 | } else { 281 | hintIndicatorView.isHidden = true 282 | } 283 | } 284 | } 285 | var hintIndicatorView: UIView = UIView().cornerRadius(2) 286 | var onHoverIndicatorView: ((_ isActive: Bool)->())? 287 | 288 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 289 | super.init(style: style, reuseIdentifier: reuseIdentifier) 290 | 291 | color(.background, .clear) 292 | selectedBackgroundView = UIView() 293 | 294 | contentView.build { [unowned self] in 295 | ZSplit { 296 | HSplit { 297 | Padding { 298 | .fixed(self.gutterPaddingLeading) 299 | } 300 | Dynamic { 301 | self.gutterLabel 302 | } size: { 303 | .fixed(self.gutterWidth) 304 | } 305 | Padding { 306 | .fixed(self.gutterPaddingTrailing) 307 | } 308 | Percentage(100) { 309 | self.content 310 | } 311 | } 312 | HSplit { 313 | Padding(5) 314 | Fixed(4) { 315 | let hover: UIHoverGestureRecognizer = .init { gesture in 316 | if gesture.state == .began { 317 | self.onHoverIndicatorView?(true) 318 | } else if gesture.state == .ended || gesture.state == .cancelled { 319 | self.onHoverIndicatorView?(false) 320 | } 321 | } 322 | self.hintIndicatorView.addGestureRecognizer(hover) 323 | return self.hintIndicatorView 324 | } insets: { 325 | .init(vertical: 2) 326 | } 327 | } 328 | } 329 | } 330 | } 331 | 332 | required init?(coder: NSCoder) { 333 | fatalError("init(coder:) has not been implemented") 334 | } 335 | 336 | func bind(line: String, container: Editor.Container, assistant: Assistant?, hint: Hint?, idx: Int, ui: UI) { 337 | self.line = line 338 | self.container = container 339 | 340 | self.hint = hint 341 | 342 | content.hint = hint 343 | content.index = idx 344 | 345 | self.gutterLabel.text = "\(idx + 1)" 346 | content.update(row: idx, with: line, ui: ui, container: container, assistant: assistant) 347 | self.ui = ui 348 | } 349 | 350 | override func setSelected(_ selected: Bool, animated: Bool) { 351 | super.setSelected(selected, animated: animated) 352 | 353 | gutterLabel.alpha = selected ? 1 : 0.3 354 | 355 | content.setNeedsDisplay() 356 | } 357 | 358 | override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize { 359 | guard let ui = ui else { return .zero } 360 | 361 | let collectionViewWidth: CGFloat = superview?.width ?? 0 362 | 363 | self.gutterWidth = 30 364 | let availableContentWidth: CGFloat = collectionViewWidth - gutterPaddingLeading - gutterPaddingTrailing - gutterWidth 365 | 366 | return .init(width: collectionViewWidth, height: content.calculateHeight(with: availableContentWidth, ui: ui)) 367 | } 368 | 369 | override func layoutSubviews() { 370 | super.layoutSubviews() 371 | 372 | selectedBackgroundView?.alpha = CodeLabel.selectionPosition?.currentSelectionEndPosition == nil ? 1 : 0 373 | } 374 | 375 | /// point must be converted to local coordinate system 376 | func column(for point: CGPoint) -> Int { 377 | return content.characterIndex(at: point) 378 | } 379 | 380 | func inputFrame(for position: Position) -> CGRect { 381 | return content.convert(content.boundingRectOfCharacter(at: position.column, ui: ui!), to: superview) 382 | } 383 | } 384 | 385 | weak var parentViewController: UIViewController! 386 | 387 | lazy var tableView: TableView = { 388 | let tableView: TableView = TableView(frame: bounds, style: .plain) 389 | tableView.delegate = self 390 | tableView.dataSource = self 391 | tableView.separatorStyle = .none 392 | tableView.alwaysBounceVertical = true 393 | tableView.allowsMultipleSelection = true 394 | tableView.register(RowCell.self, forCellReuseIdentifier: RowCell.reuseIdentifier) 395 | return tableView 396 | }() 397 | 398 | var text: String { 399 | container.string 400 | } 401 | 402 | var onContentChange: (()->())? 403 | 404 | lazy var container: Editor.Container = Editor.Container(onUpdateHandler: { [weak self] update, position in 405 | guard let self = self else { return } 406 | 407 | switch update { 408 | case .none: 409 | break 410 | case .reload(let index): 411 | UIView.performWithoutAnimation { 412 | let indexPath: IndexPath = .init(row: index, section: 0) 413 | guard let cell = self.tableView.cellForRow(at: indexPath) as? RowCell else { return } 414 | cell.bind(line: self.container.lines[index], container: self.container, assistant: self.session.assistant, hint: self._hintDictionary[index], idx: index, ui: self.ui) 415 | UIView.performWithoutAnimation { 416 | self.tableView.beginUpdates() 417 | self.tableView.endUpdates() 418 | } 419 | } 420 | case .reloadVisible: 421 | self.tableView.reloadData() 422 | case .reloadAll: 423 | self.tableView.reloadData() 424 | } 425 | 426 | if update != .none && update != .reloadAll { 427 | self.onContentChange?() 428 | } 429 | 430 | self.updatePosition(position, using: .virtual) 431 | }) 432 | 433 | var overflowColumnHint: Int = 0 434 | 435 | lazy var input: TextInput = { [unowned self] in 436 | let input: TextInput = .init() 437 | input.field.modifierKeyPressStateDidChange = { key, isEnabled in 438 | if !isEnabled { 439 | self.cursorGesture.cancel() 440 | } 441 | } 442 | input.field.textInsertionHandler = { text in 443 | guard let position = self._currentPosition else { return } 444 | 445 | self.container.performOperation(.insert(position: position, value: text)) 446 | } 447 | input.field.textDeletionHandler = { delete in 448 | guard let position = self._currentPosition else { return } 449 | 450 | self.container.performOperation(.delete(position: position, length: delete == .backward ? -1 : 1)) 451 | } 452 | input.field.textFieldNavigationHandler = { arrow in 453 | guard let position = self._currentPosition else { return } 454 | 455 | let isShiftKeyPressed = self.input.field.isShiftKeyPressed 456 | 457 | switch arrow { 458 | case .up: 459 | if isShiftKeyPressed { 460 | self.selectUp(rows: 1) 461 | } else { 462 | self.moveUp(rows: 1) 463 | } 464 | case .down: 465 | if isShiftKeyPressed{ 466 | self.selectDown(rows: 1) 467 | } else { 468 | self.moveDown(rows: 1) 469 | } 470 | case .left: 471 | if isShiftKeyPressed == false, let selection = position.selection { 472 | self.updatePosition(selection.start, using: .keyboardHorizontalArrow) 473 | return 474 | } 475 | 476 | if position.column > 0, let character: String = self.container.characterString(at: position.offsetBy(row: 0, column: -1)) { 477 | if isShiftKeyPressed { 478 | self.selectLeft(columns: character.utf16.count) 479 | } else { 480 | self.updatePosition(position.offsetBy(row: 0, column: -character.utf16.count), using: .keyboardHorizontalArrow) 481 | } 482 | return 483 | } 484 | if isShiftKeyPressed { 485 | self.selectLeft(columns: 1) 486 | } else { 487 | self.moveLeft(columns: 1) 488 | } 489 | case .right: 490 | if isShiftKeyPressed == false, let selection = position.selection { 491 | self.updatePosition(selection.end, using: .keyboardHorizontalArrow) 492 | return 493 | } 494 | 495 | if let character: String = self.container.characterString(at: position.offsetBy(row: 0, column: 1)) { 496 | if isShiftKeyPressed { 497 | self.selectRight(columns: character.utf16.count) 498 | } else { 499 | self.updatePosition(position.offsetBy(row: 0, column: character.utf16.count), using: .keyboardHorizontalArrow) 500 | } 501 | return 502 | } 503 | if isShiftKeyPressed { 504 | self.selectRight(columns: 1) 505 | } else { 506 | self.moveRight(columns: 1) 507 | } 508 | } 509 | } 510 | input.field.textFieldActionHandler = { action in 511 | guard let position = self._currentPosition else { return } 512 | switch action { 513 | case .newline: 514 | self.container.performOperation(.newline(position: position)) 515 | case .indent: 516 | self.container.performOperation(.indent(position: position, offset: 1)) 517 | case .unindent: 518 | self.container.performOperation(.indent(position: position, offset: -1)) 519 | case .undo: 520 | self.container.undo() 521 | case .redo: 522 | self.container.redo() 523 | case .copy: 524 | UIPasteboard.general.string = self.container.performOperation(.read(position: position))?.output 525 | case .cut: 526 | guard let selection = position.selection else { return } 527 | 528 | let string: String = self.container.performOperation(.read(position: position))?.output ?? "" 529 | UIPasteboard.general.string = string 530 | self.container.performOperation(.delete(position: selection.end, length: -string.utf16.count)) 531 | case .paste: 532 | self.container.performOperation(.insert(position: position, value: UIPasteboard.general.string ?? "")) 533 | case .selectAll: 534 | self.updatePosition(self.container.selectAllPosition, using: .virtual) 535 | case .toggleComment: 536 | let rows: [Int] = position.isSelection ? position.getSelectedIndicies() : [position.row] 537 | 538 | for row in rows { 539 | guard let line: String = self.container.line(for: row), line.isEmpty == false else { continue } 540 | 541 | let shouldComment: Bool = line.hasPrefix("//") == false 542 | 543 | if shouldComment { 544 | self.container.performOperation(.insert(position: .init(row: row, column: 0), value: "//")) 545 | } else { 546 | self.container.performOperation(.delete(position: .init(row: row, column: 2), length: -2)) 547 | } 548 | } 549 | 550 | self.container.registerUpdate(.none, position: position) 551 | case .escapeKey: 552 | position.currentTokenStore = self.container.seekToken(at: position.offsetBy(row: 0, column: -1)) 553 | 554 | self.session.assistant?.suggestions(for: position, completionHandler: { suggestions in 555 | print(suggestions.map({ $0.label }).prefix(10).joined(separator: ", ")) 556 | }) 557 | } 558 | } 559 | return input 560 | }() 561 | 562 | var _currentPosition: Position? = .zero { 563 | didSet { 564 | if let currentPosition = _currentPosition { 565 | 566 | tableView.deselectAll() 567 | 568 | if currentPosition.isSelection { 569 | RowCell.CodeLabel.selectionPosition = currentPosition 570 | 571 | for position in currentPosition.getSelectedIndexPaths() { 572 | tableView.selectRow(at: position, animated: false, scrollPosition: .none) 573 | } 574 | 575 | tableView.scrollTo(position: currentPosition) 576 | 577 | input.alpha = 0.01 578 | 579 | if input.field.isFirstResponder == false { 580 | input.becomeFirstResponder() 581 | } 582 | return 583 | } else { 584 | RowCell.CodeLabel.selectionPosition = nil 585 | 586 | tableView.selectRow(at: .init(row: currentPosition.row, section: 0), animated: false, scrollPosition: .none) 587 | } 588 | } else { 589 | tableView.deselectAll() 590 | } 591 | 592 | input.alpha = 1 593 | 594 | positionTrackerLabel.text = _currentPosition?.readableDescription ?? "" 595 | (positionTrackerLabel.superview as? SplitView)?.invalidateLayout() 596 | 597 | if oldValue == _currentPosition && oldValue?.isSelection == _currentPosition?.isSelection { 598 | return 599 | } 600 | 601 | updateInputPosition(scroll: true) 602 | } 603 | } 604 | 605 | enum Method { 606 | case virtual 607 | case mouse 608 | case keyboardVerticalArrow 609 | case keyboardHorizontalArrow 610 | } 611 | 612 | func updatePosition(_ position: Position, using method: Method) { 613 | if method != .keyboardVerticalArrow { 614 | overflowColumnHint = position.column 615 | } 616 | 617 | if method != .virtual && input.field.isShiftKeyPressed { 618 | print("shiftkeypress") 619 | _currentPosition?.currentSelectionEndPosition = position 620 | _currentPosition = { _currentPosition }() 621 | return 622 | } 623 | 624 | _currentPosition = position 625 | } 626 | 627 | var positionTrackerLabel: UILabel = .init().align(.center).size(using: .monospacedSystemFont(ofSize: 10, weight: .medium)).color(.text, .secondaryLabel) 628 | 629 | var session: Session = .init(text: "", position: .zero) { 630 | didSet { 631 | session.position.visibilityAdjustmentBehaviour = .scrollToLine 632 | container.session = session 633 | } 634 | } 635 | 636 | var ui: UI = .init() { 637 | didSet { 638 | updateUI() 639 | } 640 | } 641 | 642 | var clickTimeInterval: TimeInterval = 0.4 643 | private var clickCountTracker: Int = 1 644 | private var previousCursorClickTimeInterval: TimeInterval = 0 645 | 646 | lazy var cursorGesture: UILongPressPanGestureRecognizer = UILongPressPanGestureRecognizer { [unowned self] gesture in 647 | if gesture.state == .cancelled { return } 648 | 649 | let point = gesture.location(in: self.tableView) 650 | 651 | let isSelecting: Bool = (gesture.state == .changed || gesture.state == .ended) 652 | 653 | guard let indexPath = self.tableView.indexPathForRow(at: .init(x: 0, y: point.y)), let cell = tableView.cellForRow(at: indexPath) as? RowCell else { 654 | // if point is > than available cells, put cursor at very end 655 | if isSelecting || point.y < tableView.contentInset.top { 656 | return 657 | } 658 | 659 | let lastIdx: Int = container.lines.count - 1 660 | if lastIdx < 0 { return } 661 | let lastLine: String = container.lines[lastIdx] 662 | self.updatePosition(.init(row: lastIdx, column: lastLine.utf16.count), using: .mouse) 663 | return 664 | } 665 | 666 | let resolvedPosition: Position = .init(row: indexPath.row, column: cell.column(for: self.tableView.convert(point, to: cell.content))) 667 | 668 | if !isSelecting { 669 | let timeInterval: TimeInterval = Date().timeIntervalSinceReferenceDate 670 | defer { 671 | previousCursorClickTimeInterval = timeInterval 672 | } 673 | if timeInterval - previousCursorClickTimeInterval < self.clickTimeInterval { 674 | clickCountTracker += 1 675 | if clickCountTracker == 2 { 676 | // highlight current token 677 | if let match = self.container.seekToken(at: resolvedPosition) { 678 | self._currentPosition = match.1 679 | gesture.cancel() 680 | } 681 | return 682 | } 683 | if clickCountTracker == 3 { 684 | // highlight current line 685 | self._currentPosition = .init(row: resolvedPosition.row, column: 0).with(selectionTo: .init(row: resolvedPosition.row + 1, column: 0)) 686 | gesture.cancel() 687 | return 688 | } 689 | } else { 690 | clickCountTracker = 1 691 | } 692 | } 693 | 694 | // cell is out of bounds, cancel gesture to prevent further movement 695 | if !isSelecting, !self.tableView.visibility(for: resolvedPosition).isFullyVisible { 696 | resolvedPosition.visibilityAdjustmentBehaviour = .contentOffset 697 | self.updatePosition(resolvedPosition, using: .mouse) 698 | gesture.cancel() 699 | return 700 | } 701 | 702 | if isSelecting { 703 | if resolvedPosition != self._currentPosition, let position = self._currentPosition { 704 | position.currentSelectionEndPosition = resolvedPosition 705 | position.visibilityAdjustmentBehaviour = .contentOffset 706 | self.updatePosition(position, using: .mouse) 707 | } 708 | } else { 709 | resolvedPosition.visibilityAdjustmentBehaviour = .none 710 | self.updatePosition(resolvedPosition, using: .mouse) 711 | } 712 | } 713 | 714 | fileprivate var _hintDictionary: [Int: Hint] = [:] 715 | let hintLabel: HintLabel = .init() 716 | 717 | override init(frame: CGRect) { 718 | super.init(frame: frame) 719 | 720 | addSubview(tableView) 721 | 722 | build { [unowned self] in 723 | Equal() 724 | HSplit { 725 | Equal() 726 | Automatic { 727 | self.positionTrackerLabel 728 | } insets: { 729 | .init(horizontal: 8) 730 | } 731 | Padding(10) 732 | } size: { 733 | .fixed(22) 734 | } 735 | Padding(10) 736 | } 737 | 738 | tableView.addSubview(self.input) 739 | 740 | let cursorHoverGesture = UIHoverGestureRecognizer { gesture in 741 | let state = gesture.state 742 | 743 | if state == .began { 744 | NSCursor.iBeam.set() 745 | } 746 | if state == .ended || state == .cancelled { 747 | NSCursor.arrow.set() 748 | } 749 | } 750 | addGestureRecognizer(cursorHoverGesture) 751 | 752 | cursorGesture.minimumPressDuration = 0 753 | addGestureRecognizer(cursorGesture) 754 | 755 | updateUI() 756 | } 757 | 758 | required init?(coder: NSCoder) { 759 | fatalError("init(coder:) has not been implemented") 760 | } 761 | 762 | override var canBecomeFirstResponder: Bool { 763 | true 764 | } 765 | 766 | override func becomeFirstResponder() -> Bool { 767 | input.becomeFirstResponder() 768 | } 769 | 770 | override func resignFirstResponder() -> Bool { 771 | input.resignFirstResponder() 772 | } 773 | 774 | func updateUI() { 775 | input.field.caretHeight = ui.estimatedLineHeight 776 | 777 | backgroundColor = ui.editorBackgroundColor 778 | tableView.backgroundColor = ui.editorBackgroundColor 779 | 780 | positionTrackerLabel.backgroundColor = ui.editorBackgroundColor 781 | positionTrackerLabel.border(.hairline, width: .onePixel * 2, cornerRadius: 4) 782 | } 783 | 784 | func reloadData() { 785 | tableView.reloadData() 786 | } 787 | 788 | func rebindLines() { 789 | for indexPath in tableView.indexPathsForVisibleRows ?? [] { 790 | guard let cell = tableView.cellForRow(at: indexPath) as? RowCell else { continue } 791 | cell.bind(line: container.lines[indexPath.row], container: container, assistant: session.assistant, hint: _hintDictionary[indexPath.row], idx: indexPath.row, ui: ui) 792 | } 793 | } 794 | 795 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 796 | container.lines.count 797 | } 798 | 799 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 800 | let cell = tableView.dequeueReusableCell(withIdentifier: RowCell.reuseIdentifier, for: indexPath) as! RowCell 801 | let idx: Int = indexPath.row 802 | cell.bind(line: container.lines[idx], container: container, assistant: session.assistant, hint: _hintDictionary[indexPath.row], idx: idx, ui: ui) 803 | return cell 804 | } 805 | 806 | func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { 807 | guard let cell = cell as? RowCell else { return } 808 | 809 | let isSelectedRow: Bool = _currentPosition?.row == indexPath.row 810 | if isSelectedRow { 811 | updateInputPosition(scroll: false, cellHint: cell) 812 | } 813 | 814 | cell.onHoverIndicatorView = { [unowned self, weak cell] isActive in 815 | 816 | if isActive, let hint = cell?.hint { 817 | guard let cell else { return } 818 | 819 | NSCursor.pointingHand.set() 820 | 821 | self.hintLabel.font = UIFont.systemFont(ofSize: 13, weight: .regular) 822 | self.hintLabel.text = hint.text 823 | self.hintLabel.textColor = hint.kind.textColor 824 | self.hintLabel.backgroundColor = hint.kind.secondaryColor 825 | 826 | let indicatorViewFrame = cell.hintIndicatorView.convert(cell.hintIndicatorView.bounds, to: self.tableView) 827 | 828 | let inputFrame: CGRect = self.inputFrame(for: .init(row: indexPath.row, column: self.container.lines[indexPath.row].utf16.count - 1), cellHint: nil) ?? .zero 829 | 830 | let xOffset: CGFloat = inputFrame.x + 14 831 | 832 | let width: CGFloat = self.width - xOffset 833 | let calculatedSize: CGSize = self.hintLabel.sizeThatFits(.init(width: width, height: .infinity)) 834 | self.hintLabel.frame = .init(x: xOffset, y: indicatorViewFrame.y - self.hintLabel.padding/2, width: min(width, calculatedSize.width), height: calculatedSize.height) 835 | self.tableView.addSubview(self.hintLabel) 836 | } else { 837 | NSCursor.iBeam.set() 838 | 839 | self.hintLabel.removeFromSuperview() 840 | } 841 | } 842 | } 843 | 844 | func updateInputPosition(scroll: Bool, cellHint: RowCell? = nil) { 845 | guard let position = _currentPosition else { 846 | input.resignFirstResponder() 847 | print("resign first") 848 | return 849 | } 850 | 851 | if scroll { 852 | tableView.scrollTo(position: position) 853 | } 854 | 855 | guard let frame = self.inputFrame(for: position, cellHint: cellHint) else { return } 856 | 857 | input.frame = frame 858 | 859 | // DispatchQueue.main.async(after: 0.5) { 860 | // self.session.assistant?.suggestions(for: position) { [weak self] suggestions in 861 | // self?.xt.reloadData(suggestions) 862 | // } 863 | // } 864 | // 865 | // if xt.view.window == nil, xt.isBeingPresented == false, position.currentTokenStore != nil { 866 | // xtAnchorView.frame = input.frame 867 | // tableView.addSubview(xtAnchorView) 868 | // 869 | // xt.preferredContentSize = .init(width: 320, height: 200) 870 | // xt.modalPresentationStyle = .popover 871 | // if let presentationController = xt.popoverPresentationController { 872 | // presentationController.sourceView = xtAnchorView 873 | // presentationController.passthroughViews = [self] 874 | // presentationController.permittedArrowDirections = .up 875 | // } 876 | // self.parentViewController.present(self.xt, animated: true) 877 | // } else if xt.view.window != nil, _currentPosition?.currentTokenStore == nil { 878 | // xt.dismiss(animated: false) 879 | // 880 | // xtAnchorView.removeFromSuperview() 881 | // } 882 | 883 | if !input.isFirstResponder { 884 | input.becomeFirstResponder() 885 | } 886 | } 887 | 888 | func inputFrame(for position: Position, cellHint: RowCell?) -> CGRect? { 889 | let indexPath: IndexPath = .init(row: position.row, section: 0) 890 | 891 | guard let cell = cellHint ?? tableView.cellForRow(at: indexPath) as? RowCell else { return nil } 892 | 893 | return cell.inputFrame(for: position) 894 | } 895 | 896 | class CompletionViewController: UIViewController, UICollectionViewDelegate { 897 | 898 | let render: ComponentRender = .init(layout: .list(style: .plain, configuration: { listConfiguration in 899 | listConfiguration.backgroundColor = .clear 900 | listConfiguration.showsSeparators = false 901 | })) 902 | 903 | class Cell: UICollectionViewListCell { 904 | override func bindObject(_ obj: AnyObject) { 905 | guard let suggestion = obj as? Assistant.Suggestion else { return } 906 | 907 | var configuration = defaultContentConfiguration() 908 | configuration.text = suggestion.label 909 | configuration.textProperties.font = .monospacedSystemFont(ofSize: 14, weight: .regular) 910 | contentConfiguration = configuration 911 | } 912 | 913 | override func updateConfiguration(using state: UICellConfigurationState) { 914 | super.updateConfiguration(using: state) 915 | 916 | guard var contentConfiguration = self.contentConfiguration?.updated(for: state) as? UIListContentConfiguration else { return } 917 | contentConfiguration.textProperties.colorTransformer = UIConfigurationColorTransformer { color in 918 | state.isSelected ? .white : .label 919 | } 920 | self.contentConfiguration = contentConfiguration 921 | 922 | if #available(macCatalyst 16.0, *) { 923 | var backgroundConfiguration = defaultBackgroundConfiguration() 924 | backgroundConfiguration.backgroundColorTransformer = .init({ color in 925 | return state.isSelected ? .systemBlue : .clear 926 | }) 927 | self.backgroundConfiguration = backgroundConfiguration 928 | } else { 929 | // Fallback on earlier versions 930 | } 931 | } 932 | } 933 | 934 | override func viewDidLoad() { 935 | super.viewDidLoad() 936 | 937 | view.build { [unowned self] in 938 | Equal { 939 | self.render 940 | } 941 | } 942 | 943 | render.backgroundColor = .clear 944 | render.collectionView.backgroundColor = .clear 945 | render.collectionView.delegate = self 946 | } 947 | 948 | override var canBecomeFirstResponder: Bool { 949 | false 950 | } 951 | 952 | override func becomeFirstResponder() -> Bool { 953 | false 954 | } 955 | 956 | func reloadData(_ suggestions: [Assistant.Suggestion]) { 957 | render.updateSnapshot { builder in 958 | builder.appendSection(using: Cell.self, items: suggestions) 959 | } 960 | } 961 | } 962 | 963 | let xtAnchorView: UIView = UIView() 964 | let xt: CompletionViewController = CompletionViewController() 965 | 966 | var cachedWidth: CGFloat = 0 967 | var isLiveResizing: Bool = false 968 | override func layoutSubviews() { 969 | 970 | let width: CGFloat = self.width 971 | if width == cachedWidth { return } 972 | cachedWidth = width 973 | 974 | if !isLiveResizing { 975 | lazilyLayoutSubviews() 976 | isLiveResizing = true 977 | } 978 | 979 | NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(lazilyLayoutSubviews), object: nil) 980 | perform(#selector(lazilyLayoutSubviews), with: nil, afterDelay: 0.1) 981 | } 982 | 983 | @objc func lazilyLayoutSubviews() { 984 | UIView.performWithoutAnimation { 985 | tableView.estimatedRowHeight = ui.estimatedLineHeight 986 | tableView.frame = bounds 987 | 988 | var contentInsets: UIEdgeInsets = ui.editorContentInset 989 | contentInsets.bottom += bounds.height * ui.editorContentOverscroll 990 | tableView.contentInset = contentInsets 991 | } 992 | 993 | tableView.beginUpdates() 994 | tableView.endUpdates() 995 | updateInputPosition(scroll: false) 996 | 997 | isLiveResizing = false 998 | } 999 | } 1000 | } 1001 | 1002 | extension Editor { 1003 | struct Hint: Codable { 1004 | enum Kind: Codable { 1005 | case error 1006 | case warning 1007 | 1008 | var textColor: UIColor { 1009 | .black 1010 | } 1011 | 1012 | var primaryColor: UIColor { 1013 | switch self { 1014 | case .error: 1015 | return .systemRed 1016 | case .warning: 1017 | return .systemYellow 1018 | } 1019 | } 1020 | 1021 | var secondaryColor: UIColor { 1022 | switch self { 1023 | case .error: 1024 | return .dynamic(light: .hex("#FFBFC0"), dark: .systemRed) 1025 | case .warning: 1026 | return .dynamic(light: .hex("#FFEAAC"), dark: .systemYellow) 1027 | } 1028 | } 1029 | } 1030 | 1031 | let position: Editor.Position 1032 | let text: String 1033 | let kind: Hint.Kind 1034 | } 1035 | 1036 | class HintLabel: UIView { 1037 | private let label: UILabel = .init("") 1038 | 1039 | var padding: CGFloat = 12 1040 | 1041 | var text: String = "" { 1042 | didSet { 1043 | label.text = text 1044 | } 1045 | } 1046 | 1047 | var textColor: UIColor? { 1048 | set { 1049 | label.textColor = newValue 1050 | } 1051 | get { 1052 | label.textColor 1053 | } 1054 | } 1055 | 1056 | var font: UIFont? { 1057 | set { 1058 | label.font = newValue 1059 | } 1060 | get { 1061 | label.font 1062 | } 1063 | } 1064 | 1065 | init() { 1066 | super.init(frame: .zero) 1067 | 1068 | label.autoresizingMask = [.flexibleWidth, .flexibleHeight] 1069 | addSubview(label) 1070 | 1071 | cornerRadius(6) 1072 | } 1073 | 1074 | override func layoutSubviews() { 1075 | super.layoutSubviews() 1076 | 1077 | label.frame = bounds.insetBy(dx: padding / 2, dy: padding / 2) 1078 | } 1079 | 1080 | required init?(coder: NSCoder) { 1081 | fatalError("init(coder:) has not been implemented") 1082 | } 1083 | 1084 | override func sizeThatFits(_ size: CGSize) -> CGSize { 1085 | var size: CGSize = label.sizeThatFits(size) 1086 | size.width += padding 1087 | size.height += padding 1088 | return size 1089 | } 1090 | } 1091 | } 1092 | 1093 | extension Editor.Document { 1094 | private func calculateVerticalMovement(rows: Int, from position: Editor.Position? = nil) -> Editor.Position { 1095 | guard let position = position ?? _currentPosition else { return .zero } 1096 | let isUpMovement: Bool = rows > 0 1097 | let targetIdx: Int = position.row + rows 1098 | var targetPosition: Editor.Position = position 1099 | if position.row + rows < 0 { 1100 | targetPosition = .zero 1101 | self.overflowColumnHint = 0 1102 | } else if let line = self.container.line(for: targetIdx) { 1103 | targetPosition = .init(row: targetIdx, column: line.utf16.count < self.overflowColumnHint ? line.utf16.count : self.overflowColumnHint) 1104 | } else if isUpMovement == true { 1105 | targetPosition = .zero 1106 | } else if isUpMovement == false { 1107 | targetPosition = .init(row: position.row, column: self.container.lines[position.row].utf16.count) 1108 | } 1109 | return targetPosition 1110 | } 1111 | 1112 | func moveUp(rows: Int, method: Method = .keyboardVerticalArrow) { 1113 | let targetPosition: Editor.Position = calculateVerticalMovement(rows: -rows) 1114 | targetPosition.visibilityAdjustmentBehaviour = .contentOffset 1115 | self.updatePosition(targetPosition, using: method) 1116 | } 1117 | 1118 | func moveDown(rows: Int, method: Method = .keyboardVerticalArrow) { 1119 | let targetPosition: Editor.Position = calculateVerticalMovement(rows: rows) 1120 | targetPosition.visibilityAdjustmentBehaviour = .contentOffset 1121 | self.updatePosition(targetPosition, using: method) 1122 | } 1123 | 1124 | private func calculateHorizontalMovement(columns: Int, from position: Editor.Position? = nil) -> Editor.Position { 1125 | guard let position = position ?? _currentPosition else { return .zero } 1126 | return self.container.calculateSafePositionOffset(at: position, length: columns) 1127 | } 1128 | 1129 | func moveLeft(columns: Int, method: Method = .keyboardHorizontalArrow) { 1130 | self.updatePosition(self.calculateHorizontalMovement(columns: -columns), using: method) 1131 | } 1132 | 1133 | func moveRight(columns: Int, method: Method = .keyboardHorizontalArrow) { 1134 | self.updatePosition(self.calculateHorizontalMovement(columns: columns), using: method) 1135 | } 1136 | 1137 | func moveToTop() { 1138 | let position: Editor.Position = .zero 1139 | position.visibilityAdjustmentBehaviour = .scrollToLine 1140 | self.updatePosition(position, using: .virtual) 1141 | } 1142 | 1143 | func moveToBottom() { 1144 | let maxRow: Int = self.container.lines.count - 1 1145 | let line: String = self.container.lines[maxRow] 1146 | let position: Editor.Position = .init(row: maxRow, column: line.count) 1147 | position.visibilityAdjustmentBehaviour = .scrollToLine 1148 | self.updatePosition(position, using: .virtual) 1149 | } 1150 | 1151 | func moveToBeginningOfLine(method: Method = .keyboardHorizontalArrow) { 1152 | guard let position = _currentPosition else { return } 1153 | let updatedPosition: Editor.Position = .init(row: position.row, column: 0) 1154 | self.updatePosition(updatedPosition, using: method) 1155 | } 1156 | 1157 | func moveToEndOfLine(method: Method = .keyboardHorizontalArrow) { 1158 | guard let position = _currentPosition else { return } 1159 | let line: String = self.container.lines[position.row] 1160 | let updatedPosition: Editor.Position = .init(row: position.row, column: line.utf16.count) 1161 | self.updatePosition(updatedPosition, using: method) 1162 | } 1163 | 1164 | func moveToBeginningOfWord(method: Method = .keyboardHorizontalArrow) { 1165 | print("moveToBeginningOfWord") 1166 | } 1167 | 1168 | func moveToEndOfWord(method: Method = .keyboardHorizontalArrow) { 1169 | print("moveToEndOfWord") 1170 | } 1171 | 1172 | func selectToPosition(position: Editor.Position, method: Method = .mouse) { 1173 | guard let position = _currentPosition else { return } 1174 | position.currentSelectionEndPosition = position 1175 | self.updatePosition(position, using: method) 1176 | } 1177 | 1178 | func selectUp(rows: Int, method: Method = .keyboardVerticalArrow) { 1179 | guard let position = _currentPosition else { return } 1180 | self.updatePosition(calculateVerticalMovement(rows: -rows, from: position.currentSelectionEndPosition ?? position), using: method) 1181 | } 1182 | 1183 | func selectDown(rows: Int, method: Method = .keyboardVerticalArrow) { 1184 | guard let position = _currentPosition else { return } 1185 | self.updatePosition(calculateVerticalMovement(rows: rows, from: position.currentSelectionEndPosition ?? position), using: method) 1186 | } 1187 | 1188 | func selectLeft(columns: Int, method: Method = .keyboardHorizontalArrow) { 1189 | guard let position = _currentPosition else { return } 1190 | self.updatePosition(calculateHorizontalMovement(columns: -columns, from: position.currentSelectionEndPosition ?? position), using: method) 1191 | } 1192 | 1193 | func selectRight(columns: Int, method: Method = .keyboardHorizontalArrow) { 1194 | guard let position = _currentPosition else { return } 1195 | self.updatePosition(calculateHorizontalMovement(columns: columns, from: position.currentSelectionEndPosition ?? position), using: method) 1196 | } 1197 | 1198 | 1199 | /* 1200 | selectToTop() 1201 | selectToBottom() 1202 | selectAll() 1203 | selectToBeginningOfLine() 1204 | selectToEndOfLine() 1205 | selectWordsContainingCursors() 1206 | selectToBeginningOfWord() 1207 | selectToEndOfWord() 1208 | scrollToCursorPosition() 1209 | scrollToPosition(position) 1210 | */ 1211 | } 1212 | 1213 | extension Editor { 1214 | class TableView: UITableView { 1215 | struct Visibility { 1216 | let isFullyVisible: Bool 1217 | let cellRect: CGRect 1218 | } 1219 | 1220 | func immediatlyStopScrolling() { 1221 | setContentOffset(contentOffset, animated: false) 1222 | } 1223 | 1224 | func deselectAll() { 1225 | for indexPath in indexPathsForSelectedRows ?? [] { 1226 | deselectRow(at: indexPath, animated: false) 1227 | } 1228 | } 1229 | 1230 | func scrollTo(position: Position) { 1231 | switch position.visibilityAdjustmentBehaviour { 1232 | case .none: 1233 | break 1234 | case .followCursor: 1235 | let visibility: Visibility = visibility(for: position, yOffset: -50) 1236 | if visibility.isFullyVisible { return } 1237 | DispatchQueue.main.async { 1238 | self.contentOffset.y += visibility.cellRect.height 1239 | } 1240 | break 1241 | case .contentOffset: 1242 | let indexPath: IndexPath = (position.currentSelectionEndPosition ?? position).indexPath 1243 | 1244 | let rect: CGRect = convert(rectForRow(at: indexPath), to: superview) 1245 | 1246 | if rect.y - rect.height <= 0 + rect.height + contentInset.top { 1247 | let startOffsetY: CGFloat = safeAreaInsets.top + contentInset.top 1248 | if contentOffset.y + startOffsetY - rect.height <= 0 { 1249 | contentOffset.y = -startOffsetY 1250 | return 1251 | } 1252 | 1253 | contentOffset.y -= rect.height 1254 | } 1255 | 1256 | if rect.y > bounds.height - rect.height { 1257 | contentOffset.y += rect.height 1258 | } 1259 | case .scrollToLine: 1260 | self.scrollToRow(at: position.indexPath, at: .middle, animated: false) 1261 | } 1262 | } 1263 | 1264 | func visibility(for position: Position, yOffset: CGFloat = 0) -> Visibility { 1265 | let rect = rectForRow(at: position.indexPath) 1266 | return .init(isFullyVisible: bounds.offsetBy(dx: 0, dy: yOffset).contains(rect), cellRect: rect) 1267 | } 1268 | 1269 | override func scrollRectToVisible(_ rect: CGRect, animated: Bool) { 1270 | // prevents unwanted automatic scroll 1271 | } 1272 | } 1273 | } 1274 | -------------------------------------------------------------------------------- /Editor/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | UISceneStoryboardFile 19 | Main 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Editor/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Editor 4 | // 5 | // Created by Maximilian Mackh on 28.04.23. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let _ = (scene as? UIWindowScene) else { return } 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | 50 | 51 | } 52 | 53 | -------------------------------------------------------------------------------- /Editor/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Editor 4 | // 5 | // Created by Maximilian Mackh on 28.04.23. 6 | // 7 | 8 | import UIKit 9 | import BaseComponents 10 | 11 | class ViewController: UIViewController { 12 | lazy var editor: Editor = .init(parentViewController: self) 13 | 14 | override func viewDidLoad() { 15 | super.viewDidLoad() 16 | 17 | view.build { [unowned self] in 18 | Equal { 19 | self.editor 20 | } 21 | } 22 | 23 | let session: Editor.Session = .init(text: File(bundleResource: "sample-c", extension: "txt").read(as: String.self) ?? "", position: .zero) 24 | session.assistant = .init(language: .c, lspConfiguration: nil) 25 | editor.resume(session: session) 26 | } 27 | 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Editor/sample-c.txt: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | printf("Hello, World!"); 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Maximilian Mackh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Editor Window 3 |

4 | 5 | ## Editor 6 | 7 | Attempt at writing a code editor in UIKit for the Mac from scratch. There are many missing core features and there are occasional crashes. Writing a text editor is a humbling experience that I've spent many months on. No external dependencies, although there are inspirations from all over. Also, there's no SPM for now. 8 | 9 | ## How to Use 10 | 11 | The central idea behind Editor is to use Sessions for state, like cursor position, text content, the undo and redo stack as well as the indentation strategy. You can attach an optional assistant to a session for highlighting, code completion, etc. Hints are used to show errors or warnings. 12 | 13 | ```swift 14 | import UIKit 15 | import BaseComponents 16 | 17 | class ViewController: UIViewController { 18 | lazy var editor: Editor = .init(parentViewController: self) 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | 23 | view.build { [unowned self] in 24 | Equal { 25 | self.editor 26 | } 27 | } 28 | 29 | let session: Editor.Session = .init(text: File(bundleResource: "sample-c", extension: "txt").read(as: String.self) ?? "", position: .zero) 30 | session.assistant = .init(language: .c, lspConfiguration: nil) 31 | editor.resume(session: session) 32 | } 33 | } 34 | ``` 35 | 36 | ### Todo 37 | 38 | - [ ] Code Completion (LSP?, libc?, clangd?, ...) 39 | - [ ] Highlighting (TreeSitter?, LSP?, ...) 40 | - [ ] Improve indentations (strategy, multiple lines, etc.) 41 | - [ ] Highlight matching [] {} () 42 | - [ ] Redo 43 | - [ ] ... 44 | -------------------------------------------------------------------------------- /Screenshots/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmackh/Editor/19eb7d00a94ea699e5ccb58a392d5907117850e7/Screenshots/c.png --------------------------------------------------------------------------------