├── Assets └── banner.png ├── EditKit ├── AlignAroundEqualsCommand.swift ├── BeautifyJSONCommand.swift ├── EditKit.entitlements ├── EditorController.swift ├── Extensions │ ├── Collection+Additions.swift │ ├── Data+Additions.swift │ ├── Error+Additions.swift │ ├── String+Additions.swift │ ├── StringIndex+Additions.swift │ ├── StringProtocol+Additions.swift │ └── XCSourceTextBuffer+Additions.swift ├── FormatAsSingleLineCommand.swift ├── FormatCodeForSharingCommand.swift ├── Info.plist ├── InitializerFromSelectionCommand.swift ├── SelectionConversionCommand.swift ├── SourceEditorCommand.swift ├── SourceEditorExtension.swift ├── StripTrailingWhitespaceCommand.swift ├── Third Party │ ├── AutoMarkCommand.swift │ ├── CreateTypeDefinitionCommand.swift │ ├── FormatAsMultiLine.swift │ ├── JSON to Codable │ │ ├── Array+Utils.swift │ │ ├── ConvertJSONToCodableCommand.swift │ │ ├── Copyable.swift │ │ ├── Data+Utils.swift │ │ ├── DateFormatter+Types.swift │ │ ├── Dictionary+Utils.swift │ │ ├── GeneratorType.swift │ │ ├── KotlinGenerator.swift │ │ ├── KotlinTypeHandler.swift │ │ ├── Node.swift │ │ ├── NodeViewModel.swift │ │ ├── Primitive.swift │ │ ├── String+Utils.swift │ │ ├── StructGenerator.swift │ │ ├── SwiftGenerator.swift │ │ ├── SwiftTypeHandler.swift │ │ ├── Tree+Debug.swift │ │ └── Tree.swift │ ├── SearchInCommand.swift │ ├── Sort Lines and Imports │ │ ├── CountableClosedRange+Extension.swift │ │ ├── ImportSorter.swift │ │ ├── Sort.swift │ │ ├── SortSelectedLinesByAlphabetically.swift │ │ ├── SortSelectedLinesByLenght.swift │ │ ├── SortSelectedRangeList.swift │ │ └── String+Extension.swift │ └── SwiftUI View Operations │ │ ├── FormatLines.swift │ │ ├── Parser │ │ ├── Extensions │ │ │ ├── Extension.swift │ │ │ ├── ExtensionChar.swift │ │ │ └── ExtensionString.swift │ │ ├── FormatError.swift │ │ ├── Indent.swift │ │ ├── Parser.swift │ │ ├── Pref.swift │ │ ├── Preferences.swift │ │ └── SwiftParser.swift │ │ ├── ToggleBraceLine.swift │ │ ├── ToggleBraceLines.swift │ │ └── XcodeLines.swift ├── WrapInIfDefCommand.swift └── WrapInLocalizedStringCommand.swift ├── EditKitPro.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── xcshareddata │ └── xcschemes │ │ ├── EditKit.xcscheme │ │ └── EditKitPro.xcscheme └── xcuserdata │ └── aryamansharda.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ └── xcschememanagement.plist ├── EditKitPro ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Group 26 (1)-1024.png │ │ ├── Group 26 (1)-128.png │ │ ├── Group 26 (1)-16.png │ │ ├── Group 26 (1)-256.png │ │ ├── Group 26 (1)-32.png │ │ ├── Group 26 (1)-512.png │ │ └── Group 26 (1)-64.png │ ├── BackgroundColor.colorset │ │ └── Contents.json │ ├── Contents.json │ └── Logo.imageset │ │ ├── Contents.json │ │ └── Group 26 (1).png ├── EditKitPro.entitlements ├── EditKitProApp.swift ├── LandingPageView.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── RoadmapView.swift └── StandardButtonStyle.swift ├── EditKitProTests └── EditKitProTests.swift ├── EditKitProUITests ├── EditKitProUITests.swift └── EditKitProUITestsLaunchTests.swift ├── LICENSE.md └── README.md /Assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryamansharda/EditKitPro/8320a5820994132a42574f522b1094d8f1930a75/Assets/banner.png -------------------------------------------------------------------------------- /EditKit/AlignAroundEqualsCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlignAroundEqualsCommand.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 12/17/22. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | 11 | final class AlignAroundEqualsCommand { 12 | static func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 13 | // Ensure a selection is provided 14 | guard let selection = invocation.buffer.selections.firstObject as? XCSourceTextRange else { 15 | completionHandler(GenericError.default.nsError) 16 | return 17 | } 18 | 19 | // Keep an array of changed line indices. 20 | var changedLineIndexes = [Int]() 21 | 22 | // Loop through each line in the buffer and find the furthest out equal sign 23 | var maximumEqualCharacterIndex = 0 24 | for lineIndex in selection.start.line...selection.end.line { 25 | guard let originalLine = invocation.buffer.lines[lineIndex] as? String, 26 | let index = originalLine.distance(of: "=") else { 27 | continue 28 | } 29 | 30 | maximumEqualCharacterIndex = max(maximumEqualCharacterIndex, index + 1) 31 | } 32 | 33 | // Loop through all lines, split around equal, and reformat + trimming along the way 34 | for lineIndex in selection.start.line...selection.end.line { 35 | guard let originalLine = invocation.buffer.lines[lineIndex] as? String else { 36 | // Input was not a String 37 | completionHandler(GenericError.default.nsError) 38 | return 39 | } 40 | 41 | let components = originalLine.components(separatedBy: "=") 42 | guard components.count == 2 else { 43 | continue 44 | } 45 | 46 | var newLine = String() 47 | 48 | // Grab the text to the left of the equals sign and pads it with extra spaces to be 49 | // in line with the furthest equal sign identified 50 | if let lhs = components.first { 51 | let extraPaddingAmount = maximumEqualCharacterIndex - lhs.count 52 | newLine = lhs + String(repeating: " ", count: extraPaddingAmount) 53 | newLine += "= " 54 | } 55 | 56 | // Grab the text to the right of the equals sign and append it 57 | if let rhs = components.last?.trimmingCharacters(in: .whitespacesAndNewlines) { 58 | newLine += rhs 59 | } 60 | 61 | // Only update lines that have changed. 62 | if originalLine != newLine { 63 | changedLineIndexes.append(lineIndex) 64 | invocation.buffer.lines[lineIndex] = newLine 65 | } 66 | } 67 | 68 | // Select all lines that were replaced. 69 | let updatedSelections: [XCSourceTextRange] = changedLineIndexes.map { lineIndex in 70 | let lineSelection = XCSourceTextRange() 71 | lineSelection.start = XCSourceTextPosition(line: lineIndex, column: 0) 72 | lineSelection.end = XCSourceTextPosition(line: lineIndex + 1, column: 0) 73 | return lineSelection 74 | } 75 | 76 | // Set selections then return with no error. 77 | invocation.buffer.selections.setArray(updatedSelections) 78 | completionHandler(nil) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /EditKit/BeautifyJSONCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BeautifyJSONCommand.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 1/16/23. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | 11 | final class BeautifyJSONCommand: NSObject, XCSourceEditorCommand { 12 | func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 13 | // Checks that a valid selection exists 14 | guard let selection = invocation.buffer.selections.firstObject as? XCSourceTextRange else { 15 | completionHandler(NSError(domain: "BeautifyJSON", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid Selection"])) 16 | return 17 | } 18 | 19 | let startIndex = selection.start.line 20 | let endIndex = selection.end.line 21 | let selectedRange = NSRange(location: startIndex, length: 1 + endIndex - startIndex) 22 | 23 | // Grabs the lines included in the current selection 24 | guard let selectedLines = invocation.buffer.lines.subarray(with: selectedRange) as? [String] else { 25 | completionHandler(NSError(domain: "BeautifyJSON", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid Selection"])) 26 | return 27 | } 28 | 29 | // Reduces them down into one String with the formatting stripped away 30 | let rawJSON = selectedLines.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines) 31 | 32 | // Converts it to Data and then back to a String to quickly format the JSON 33 | guard let beautifiedJSON = rawJSON.data(using: .utf8)?.prettyPrintedJSONString else { 34 | completionHandler(NSError(domain: "BeautifyJSON", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid Selection"])) 35 | return 36 | } 37 | 38 | // Clears out the existing selection 39 | invocation.buffer.lines.replaceObjects(in: selectedRange, withObjectsFrom: []) 40 | 41 | // And inserts the beautified JSON at the line matching the start of the original selection 42 | invocation.buffer.lines.insert(beautifiedJSON, at: startIndex) 43 | 44 | // Set beautified JSON as output 45 | completionHandler(nil) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /EditKit/EditKit.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /EditKit/EditorController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditorController.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 12/14/22. 6 | // 7 | 8 | import XcodeKit 9 | import AppKit 10 | 11 | enum GenericError: Error, LocalizedError { 12 | case `default` 13 | 14 | var errorDescription: String? { 15 | switch self { 16 | case .default: 17 | return "Something went wrong. Please check your selection and try again." 18 | } 19 | } 20 | } 21 | 22 | struct EditorController { 23 | enum EditorCommandIdentifier: String { 24 | case alignAroundEquals = "EditKitPro.EditKit.AlignAroundEquals" 25 | case autoCreateExtensionMarks = "EditKitPro.EditKit.AutoCreateExtensionMarks" 26 | case beautifyJSON = "EditKitPro.EditKit.BeautifyJSON" 27 | case convertJSONtoCodable = "EditKitPro.EditKit.ConvertJSONToCodable" 28 | case convertSelectionToUppercase = "EditKitPro.EditKit.ConvertToUppercase" 29 | case convertSelectionToLowercase = "EditKitPro.EditKit.ConvertToLowercase" 30 | case convertSelectionToSnakeCase = "EditKitPro.EditKit.ConvertToSnakeCase" 31 | case convertSelectionToCamelCase = "EditKitPro.EditKit.ConvertToCamelCase" 32 | case convertSelectionToPascalCase = "EditKitPro.EditKit.ConvertToPascalCase" 33 | case createTypeDefinition = "EditKitPro.EditKit.CreateTypeDefinition" 34 | case formatAsMultiLine = "EditKitPro.EditKit.FormatAsMultiLine" 35 | case formatAsSingleLine = "EditKitPro.EditKit.FormatAsSingleLine" 36 | case formatCodeForSharing = "EditKitPro.EditKit.FormatCodeForSharing" 37 | case searchOnGitHub = "EditKitPro.EditKit.SearchOnPlatform.GitHub" 38 | case searchOnGoogle = "EditKitPro.EditKit.SearchOnPlatform.Google" 39 | case searchOnStackOverflow = "EditKitPro.EditKit.SearchOnPlatform.StackOverflow" 40 | case sortImports = "EditKitPro.EditKit.SortImports" 41 | case sortLinesAlphabeticallyAscending = "EditKitPro.EditKit.SortLinesAlphabeticallyAscending" 42 | case sortLinesAlphabeticallyDescending = "EditKitPro.EditKit.SortLinesAlphabeticallyDescending" 43 | case disableOuterView = "EditKitPro.EditKit.DisableOuterView" 44 | case disableView = "EditKitPro.EditKit.DisableView" 45 | case deleteOuterView = "EditKitPro.EditKit.DeleteOuterView" 46 | case deleteView = "EditKitPro.EditKit.DeleteView" 47 | case sortLinesByLength = "EditKitPro.EditKit.SortLinesByLength" 48 | case stripTrailingWhitespaceInFile = "EditKitPro.EditKit.StripTrailingWhitespaceInFile" 49 | case wrapInIfDef = "EditKitPro.EditKit.WrapInIfDef" 50 | case wrapInLocalizedString = "EditKitPro.EditKit.WrapInLocalizedString" 51 | case initFromSelcectedProperties = "EditKitPro.EditKit.InitializerFromSelection" 52 | } 53 | 54 | static func handle(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 55 | 56 | // Fail fast if there is no text selected at all or there is no text in the file 57 | guard let commandIdentifier = EditorCommandIdentifier(rawValue: invocation.commandIdentifier) else { return } 58 | 59 | switch commandIdentifier { 60 | case .sortImports: 61 | ImportSorter().perform(with: invocation, completionHandler: completionHandler) 62 | 63 | case .sortLinesByLength: 64 | SortSelectedLinesByLength.perform(with: invocation, completionHandler: completionHandler) 65 | 66 | case .sortLinesAlphabeticallyAscending: 67 | SortSelectedLinesByAlphabeticallyAscending().perform(with: invocation, completionHandler: completionHandler) 68 | 69 | case .sortLinesAlphabeticallyDescending: 70 | SortSelectedLinesByAlphabeticallyDescending().perform(with: invocation, completionHandler: completionHandler) 71 | 72 | case .stripTrailingWhitespaceInFile: 73 | StripTrailingWhitespaceCommand.perform(with: invocation, completionHandler: completionHandler) 74 | 75 | case .alignAroundEquals: 76 | AlignAroundEqualsCommand.perform(with: invocation, completionHandler: completionHandler) 77 | 78 | case .formatCodeForSharing: 79 | // Note: this doesn't work on Slack regardless 80 | FormatCodeForSharingCommand.perform(with: invocation, completionHandler: completionHandler) 81 | 82 | case .formatAsMultiLine: 83 | // This only works on single lines 84 | FormatAsMultiLine().perform(with: invocation, completionHandler: completionHandler) 85 | 86 | case .formatAsSingleLine: 87 | // This only works on single lines 88 | FormatAsSingleLineCommand.perform(with: invocation, completionHandler: completionHandler) 89 | 90 | case .autoCreateExtensionMarks: 91 | AutoMarkCommand().perform(with: invocation, completionHandler: completionHandler) 92 | 93 | case .wrapInIfDef: 94 | WrapInIfDefCommand.perform(with: invocation, completionHandler: completionHandler) 95 | 96 | case .wrapInLocalizedString: 97 | WrapInLocalizedStringCommand.perform(with: invocation, completionHandler: completionHandler) 98 | 99 | case .searchOnGoogle, .searchOnStackOverflow, .searchOnGitHub: 100 | SearchOnPlatform().perform(with: invocation, completionHandler: completionHandler) 101 | 102 | case .convertJSONtoCodable: 103 | ConvertJSONToCodableCommand().perform(with: invocation, completionHandler: completionHandler) 104 | 105 | case .beautifyJSON: 106 | BeautifyJSONCommand().perform(with: invocation, completionHandler: completionHandler) 107 | 108 | case .createTypeDefinition: 109 | CreateTypeDefinitionCommand().perform(with: invocation, completionHandler: completionHandler) 110 | 111 | case .disableView: 112 | ToggleBraceLines().perform(with: invocation, completionHandler: completionHandler) 113 | 114 | case .disableOuterView: 115 | ToggleBraceLine().perform(with: invocation, completionHandler: completionHandler) 116 | 117 | case .deleteOuterView: 118 | RemoveBraceLine().perform(with: invocation, completionHandler: completionHandler) 119 | 120 | case .deleteView: 121 | RemoveBraceLines().perform(with: invocation, completionHandler: completionHandler) 122 | case .initFromSelcectedProperties: 123 | InitializerFromSelectionCommand.perform(with: invocation, completionHandler: completionHandler) 124 | case .convertSelectionToUppercase: 125 | SelectionConversionCommand.perform(with: invocation, operation: .uppercase, completionHandler: completionHandler) 126 | case .convertSelectionToLowercase: 127 | SelectionConversionCommand.perform(with: invocation, operation: .lowercase, completionHandler: completionHandler) 128 | case .convertSelectionToSnakeCase: 129 | SelectionConversionCommand.perform(with: invocation, operation: .snakeCase, completionHandler: completionHandler) 130 | case .convertSelectionToCamelCase: 131 | SelectionConversionCommand.perform(with: invocation, operation: .camelCase, completionHandler: completionHandler) 132 | case .convertSelectionToPascalCase: 133 | SelectionConversionCommand.perform(with: invocation, operation: .pascalCase, completionHandler: completionHandler) 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /EditKit/Extensions/Collection+Additions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Collection+Additions.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 2/25/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Collection { 11 | func distance(to index: Index) -> Int { distance(from: startIndex, to: index) } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /EditKit/Extensions/Data+Additions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+Additions.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 2/25/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Data { 11 | var prettyPrintedJSONString: NSString? { 12 | guard let object = try? JSONSerialization.jsonObject(with: self, options: []), 13 | let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]), 14 | let prettyPrintedString = NSString(data: data, encoding: String.Encoding.utf8.rawValue) else { return nil } 15 | 16 | return prettyPrintedString 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /EditKit/Extensions/Error+Additions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 2/25/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Error where Self: LocalizedError { 11 | var nsError: NSError { 12 | let userInfo: [String : Any] = [ 13 | NSLocalizedFailureReasonErrorKey : errorDescription ?? String() 14 | ] 15 | 16 | return NSError(domain: "EditKit", code: 0, userInfo: userInfo) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /EditKit/Extensions/String+Additions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Additions.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 2/25/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | 12 | func substring(from: Int, to: Int? = nil) -> String { 13 | let lineLetters = self.map { String($0) } 14 | let lineLettersSlice = lineLetters[from..<(to ?? count)] 15 | return lineLettersSlice.joined() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /EditKit/Extensions/StringIndex+Additions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringIndex+Additions.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 2/25/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String.Index { 11 | func distance(in string: S) -> Int { string.distance(to: self) } 12 | } 13 | -------------------------------------------------------------------------------- /EditKit/Extensions/StringProtocol+Additions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringProtocol+Additions.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 2/25/23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension StringProtocol { 11 | func distance(of element: Element) -> Int? { firstIndex(of: element)?.distance(in: self) } 12 | func distance(of string: S) -> Int? { range(of: string)?.lowerBound.distance(in: self) } 13 | } 14 | -------------------------------------------------------------------------------- /EditKit/Extensions/XCSourceTextBuffer+Additions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCSourceTextBuffer+Additions.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 2/25/23. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | 11 | extension XCSourceTextBuffer { 12 | func substring(by index: Int, from: Int, to: Int? = nil) -> String { 13 | var index = index 14 | if index == lines.count { 15 | index -= 1 16 | } 17 | let line = lines[index] as! String 18 | return line.substring(from: from, to: to) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /EditKit/FormatAsSingleLineCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormatAsSingleLineCommand.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 4/6/23. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | 11 | final class FormatAsSingleLineCommand { 12 | static func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 13 | // Verifies a selection exists 14 | guard let selections = invocation.buffer.selections as? [XCSourceTextRange], let selection = selections.first else { 15 | completionHandler(GenericError.default.nsError) 16 | return 17 | } 18 | 19 | let startIndex = selection.start.line 20 | let endIndex = selection.end.line 21 | let selectedRange = NSRange(location: startIndex, length: 1 + endIndex - startIndex) 22 | 23 | // Grabs the currently selected lines 24 | guard let selectedLines = invocation.buffer.lines.subarray(with: selectedRange) as? [String] else { 25 | completionHandler(GenericError.default.nsError) 26 | return 27 | } 28 | 29 | // Flatten into a single string and replace new lines with spaces 30 | let trimmedInputLines = selectedLines.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } 31 | 32 | // Concatenate the lines with a space separator, excluding the first two and last lines 33 | let compositeLine = trimmedInputLines.enumerated().reduce(into: "") { result, enumeratedLine in 34 | let (index, line) = enumeratedLine 35 | let isFirstTwoLines = index < 2 36 | let isLastLine = index == trimmedInputLines.count - 1 37 | 38 | result += (isFirstTwoLines || isLastLine) ? line : " " + line 39 | } 40 | 41 | // We want to preserve the leading spacing (indendation), but want to trim the trailing 42 | let leadingWhitespace = selectedLines.first?.prefix(while: { $0.isWhitespace }) ?? "" 43 | 44 | // Clears out the existing selection 45 | invocation.buffer.lines.replaceObjects(in: selectedRange, withObjectsFrom: []) 46 | 47 | // And inserts the reformated line at the selection's starting position 48 | invocation.buffer.lines.insert(leadingWhitespace + compositeLine, at: startIndex) 49 | 50 | completionHandler(nil) 51 | } 52 | 53 | private static func removeTrailingWhitespace(_ line: String) -> String { 54 | let leadingWhitespace = line.prefix(while: { $0.isWhitespace }) 55 | let trailingText = line.trimmingCharacters(in: .whitespaces) 56 | return leadingWhitespace + trailingText 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /EditKit/FormatCodeForSharingCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormatCodeForSharing.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 12/17/22. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | import AppKit 11 | 12 | final class FormatCodeForSharingCommand { 13 | static func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 14 | guard let selections = invocation.buffer.selections as? [XCSourceTextRange], let selection = selections.first else { 15 | completionHandler(GenericError.default.nsError) 16 | return 17 | } 18 | 19 | let startIndex = selection.start.line 20 | let endIndex = selection.end.line 21 | let selectedRange = NSRange(location: startIndex, length: 1 + endIndex - startIndex) 22 | 23 | // Grabs the lines included in the current selection 24 | guard let selectedLines = invocation.buffer.lines.subarray(with: selectedRange) as? [String] else { 25 | completionHandler(GenericError.default.nsError) 26 | return 27 | } 28 | 29 | // Reduces them down into one String with the formatting stripped away 30 | let text = selectedLines.joined() 31 | let pasteboardString = "```\n\(text)```" 32 | let pasteboard = NSPasteboard.general 33 | pasteboard.declareTypes([.string], owner: nil) 34 | pasteboard.setString(pasteboardString, forType: .string) 35 | 36 | completionHandler(nil) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /EditKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionAttributes 8 | 9 | XCSourceEditorCommandDefinitions 10 | 11 | 12 | XCSourceEditorCommandClassName 13 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 14 | XCSourceEditorCommandIdentifier 15 | EditKitPro.EditKit.AlignAroundEquals 16 | XCSourceEditorCommandName 17 | Align around = 18 | 19 | 20 | XCSourceEditorCommandClassName 21 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 22 | XCSourceEditorCommandIdentifier 23 | EditKitPro.EditKit.AutoCreateExtensionMarks 24 | XCSourceEditorCommandName 25 | Auto MARK Extensions 26 | 27 | 28 | XCSourceEditorCommandClassName 29 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 30 | XCSourceEditorCommandIdentifier 31 | EditKitPro.EditKit.BeautifyJSON 32 | XCSourceEditorCommandName 33 | Beautify JSON 34 | 35 | 36 | XCSourceEditorCommandClassName 37 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 38 | XCSourceEditorCommandIdentifier 39 | EditKitPro.EditKit.ConvertJSONToCodable 40 | XCSourceEditorCommandName 41 | Convert JSON to Codable 42 | 43 | 44 | XCSourceEditorCommandClassName 45 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 46 | XCSourceEditorCommandIdentifier 47 | EditKitPro.EditKit.ConvertToUppercase 48 | XCSourceEditorCommandName 49 | Convert selection to uppercase 50 | 51 | 52 | XCSourceEditorCommandClassName 53 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 54 | XCSourceEditorCommandIdentifier 55 | EditKitPro.EditKit.ConvertToLowercase 56 | XCSourceEditorCommandName 57 | Convert selection to lowercase 58 | 59 | 60 | XCSourceEditorCommandClassName 61 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 62 | XCSourceEditorCommandIdentifier 63 | EditKitPro.EditKit.ConvertToCamelCase 64 | XCSourceEditorCommandName 65 | Convert selection to camelCase 66 | 67 | 68 | XCSourceEditorCommandClassName 69 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 70 | XCSourceEditorCommandIdentifier 71 | EditKitPro.EditKit.ConvertToPascalCase 72 | XCSourceEditorCommandName 73 | Convert selection to PascalCase 74 | 75 | 76 | XCSourceEditorCommandClassName 77 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 78 | XCSourceEditorCommandIdentifier 79 | EditKitPro.EditKit.ConvertToSnakeCase 80 | XCSourceEditorCommandName 81 | Convert selection to snake_case 82 | 83 | 84 | XCSourceEditorCommandClassName 85 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 86 | XCSourceEditorCommandIdentifier 87 | EditKitPro.EditKit.FormatCodeForSharing 88 | XCSourceEditorCommandName 89 | Copy as Markdown 90 | 91 | 92 | XCSourceEditorCommandClassName 93 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 94 | XCSourceEditorCommandIdentifier 95 | EditKitPro.EditKit.InitializerFromSelection 96 | XCSourceEditorCommandName 97 | Create initializer from selection 98 | 99 | 100 | XCSourceEditorCommandClassName 101 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 102 | XCSourceEditorCommandIdentifier 103 | EditKitPro.EditKit.CreateTypeDefinition 104 | XCSourceEditorCommandName 105 | Create type defintion 106 | 107 | 108 | XCSourceEditorCommandClassName 109 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 110 | XCSourceEditorCommandIdentifier 111 | EditKitPro.EditKit.FormatAsSingleLine 112 | XCSourceEditorCommandName 113 | Format as single line 114 | 115 | 116 | XCSourceEditorCommandClassName 117 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 118 | XCSourceEditorCommandIdentifier 119 | EditKitPro.EditKit.FormatAsMultiLine 120 | XCSourceEditorCommandName 121 | Format as multi-line 122 | 123 | 124 | XCSourceEditorCommandClassName 125 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 126 | XCSourceEditorCommandIdentifier 127 | EditKitPro.EditKit.SearchOnPlatform.GitHub 128 | XCSourceEditorCommandName 129 | Search selection on GitHub 130 | 131 | 132 | XCSourceEditorCommandClassName 133 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 134 | XCSourceEditorCommandIdentifier 135 | EditKitPro.EditKit.SearchOnPlatform.Google 136 | XCSourceEditorCommandName 137 | Search selection on Google 138 | 139 | 140 | XCSourceEditorCommandClassName 141 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 142 | XCSourceEditorCommandIdentifier 143 | EditKitPro.EditKit.SearchOnPlatform.StackOverflow 144 | XCSourceEditorCommandName 145 | Search selection on StackOverflow 146 | 147 | 148 | XCSourceEditorCommandClassName 149 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 150 | XCSourceEditorCommandIdentifier 151 | EditKitPro.EditKit.SortImports 152 | XCSourceEditorCommandName 153 | Sort imports 154 | 155 | 156 | XCSourceEditorCommandClassName 157 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 158 | XCSourceEditorCommandIdentifier 159 | EditKitPro.EditKit.SortLinesAlphabeticallyAscending 160 | XCSourceEditorCommandName 161 | Sort lines alphabetically (ascending) 162 | 163 | 164 | XCSourceEditorCommandClassName 165 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 166 | XCSourceEditorCommandIdentifier 167 | EditKitPro.EditKit.SortLinesAlphabeticallyDescending 168 | XCSourceEditorCommandName 169 | Sort lines alphabetically (descending) 170 | 171 | 172 | XCSourceEditorCommandClassName 173 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 174 | XCSourceEditorCommandIdentifier 175 | EditKitPro.EditKit.SortLinesByLength 176 | XCSourceEditorCommandName 177 | Sort lines by length 178 | 179 | 180 | XCSourceEditorCommandClassName 181 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 182 | XCSourceEditorCommandIdentifier 183 | EditKitPro.EditKit.StripTrailingWhitespaceInFile 184 | XCSourceEditorCommandName 185 | Strip trailing whitespace in file 186 | 187 | 188 | XCSourceEditorCommandClassName 189 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 190 | XCSourceEditorCommandIdentifier 191 | EditKitPro.EditKit.WrapInIfDef 192 | XCSourceEditorCommandName 193 | Wrap in #ifdef 194 | 195 | 196 | XCSourceEditorCommandClassName 197 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 198 | XCSourceEditorCommandIdentifier 199 | EditKitPro.EditKit.WrapInLocalizedString 200 | XCSourceEditorCommandName 201 | Wrap in NSLocalizedString 202 | 203 | 204 | XCSourceEditorCommandClassName 205 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 206 | XCSourceEditorCommandIdentifier 207 | EditKitPro.EditKit.DisableOuterView 208 | XCSourceEditorCommandName 209 | SwiftUI -> Disable outer view 210 | 211 | 212 | XCSourceEditorCommandClassName 213 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 214 | XCSourceEditorCommandIdentifier 215 | EditKitPro.EditKit.DeleteOuterView 216 | XCSourceEditorCommandName 217 | SwiftUI -> Delete outer view 218 | 219 | 220 | XCSourceEditorCommandClassName 221 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 222 | XCSourceEditorCommandIdentifier 223 | EditKitPro.EditKit.DisableView 224 | XCSourceEditorCommandName 225 | SwiftUI -> Disable view 226 | 227 | 228 | XCSourceEditorCommandClassName 229 | $(PRODUCT_MODULE_NAME).SourceEditorCommand 230 | XCSourceEditorCommandIdentifier 231 | EditKitPro.EditKit.DeleteView 232 | XCSourceEditorCommandName 233 | SwiftUI -> Delete view 234 | 235 | 236 | XCSourceEditorExtensionPrincipalClass 237 | $(PRODUCT_MODULE_NAME).SourceEditorExtension 238 | 239 | NSExtensionPointIdentifier 240 | com.apple.dt.Xcode.extension.source-editor 241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /EditKit/InitializerFromSelectionCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InitializerFromSelectionCommand.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 3/1/23. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | 11 | final class InitializerFromSelectionCommand { 12 | static func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 13 | // Verifies a selection exists 14 | guard let selections = invocation.buffer.selections as? [XCSourceTextRange], let selection = selections.first else { 15 | completionHandler(GenericError.default.nsError) 16 | return 17 | } 18 | 19 | let startIndex = selection.start.line 20 | let endIndex = selection.end.line 21 | let selectedRange = NSRange(location: startIndex, length: 1 + endIndex - startIndex) 22 | 23 | // Grabs the currently selected lines 24 | guard let selectedLines = invocation.buffer.lines.subarray(with: selectedRange) as? [String] else { 25 | completionHandler(GenericError.default.nsError) 26 | return 27 | } 28 | 29 | var initParameters = [(variableName: String, variableType: String)]() 30 | for line in selectedLines { 31 | let trimmedInputLine = line.trimmingCharacters(in: .whitespacesAndNewlines) 32 | 33 | // Skip over empty lines 34 | guard !trimmedInputLine.isBlank else { continue } 35 | // Skip over any assignments 36 | guard !(trimmedInputLine.contains("let ") && trimmedInputLine.contains("=")) else { continue } 37 | 38 | // The delimiter changes depending on whether an assignment occurs or not 39 | let delimiter: String 40 | if line.contains("=") && line.contains(":") { 41 | delimiter = ":" 42 | } else if line.contains("=") { 43 | delimiter = "=" 44 | } else { 45 | delimiter = ":" 46 | } 47 | 48 | let components = line.components(separatedBy: delimiter) 49 | 50 | // We break the line around the delimiter and grab the first value to the left and right of it 51 | // Ex. var foo: String would be split into "var foo" and " String" 52 | // By trimming the whitespace, splitting on the " ", and taking the last and first elements from the resulting arrays, 53 | // We can easily extract the variable name and type 54 | if let leadingComponents = components.first?.trimmingCharacters(in: .whitespacesAndNewlines), 55 | let trailingComponents = components.last?.trimmingCharacters(in: .whitespacesAndNewlines), 56 | // Find first and last where not empty 57 | let variableName = leadingComponents.components(separatedBy: " ").last(where: { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }), 58 | let variableType = trailingComponents.components(separatedBy: " ").first(where: { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }) { 59 | 60 | // Removes the force unwrap, if present 61 | let variableTypeWithoutForcedUnwraps = variableType.replacingOccurrences(of: "!", with: "") 62 | initParameters.append((variableName: variableName, variableType: variableTypeWithoutForcedUnwraps)) 63 | } 64 | } 65 | 66 | var body = String() 67 | var initializerBody = [String]() 68 | 69 | // Creates the body of the initializer with the appropriate padding 70 | let initBodyPadding = "\t\t" 71 | for parameter in initParameters { 72 | body += "\n\(initBodyPadding)self.\(parameter.variableName) = \(parameter.variableName)" 73 | initializerBody.append("\(parameter.variableName): \(parameter.variableType)") 74 | } 75 | 76 | // Creates the init's method signature from the list of variables collected ealier 77 | let padding = "\n\t" 78 | let initializer = "\(padding)init(\(initializerBody.joined(separator: ", "))) {\(body)\(padding)}" 79 | 80 | // Inserts the new initializer on the line following the last selected line 81 | invocation.buffer.lines.insert(initializer, at: endIndex + 1) 82 | 83 | completionHandler(nil) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /EditKit/SelectionConversionCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectionConversionCommand.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 3/19/23. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | 11 | final class SelectionConversionCommand { 12 | enum Operation { 13 | case lowercase 14 | case uppercase 15 | case snakeCase 16 | case pascalCase 17 | case camelCase 18 | } 19 | 20 | static func perform(with invocation: XCSourceEditorCommandInvocation, operation: Operation, completionHandler: (Error?) -> Void) { 21 | // Verifies a selection exists 22 | guard let selections = invocation.buffer.selections as? [XCSourceTextRange], let selection = selections.first else { 23 | completionHandler(GenericError.default.nsError) 24 | return 25 | } 26 | 27 | let startIndex = selection.start.line 28 | let endIndex = selection.end.line 29 | let selectedRange = NSRange(location: startIndex, length: 1 + endIndex - startIndex) 30 | 31 | // Grabs the currently selected lines 32 | guard let selectedLines = invocation.buffer.lines.subarray(with: selectedRange) as? [String] else { 33 | completionHandler(GenericError.default.nsError) 34 | return 35 | } 36 | 37 | for (index, selectedLine) in selectedLines.enumerated() { 38 | var modifiedLine = selectedLine 39 | 40 | let isFirstLineOfSelection = index == 0 //&& selection.start.column != 0 41 | let isLastLineOfSelection = index == selectedLines.count - 1 //&& selection.end.column != 0 42 | 43 | if isFirstLineOfSelection && isLastLineOfSelection { 44 | let endOfSelection = selectedLine.prefix(selection.end.column) 45 | let trailingUnselected = selectedLine.suffix(from: selectedLine.index(selectedLine.startIndex, offsetBy: selection.end.column)) 46 | 47 | let leadingUnselected = selectedLine.prefix(selection.start.column) 48 | let transformedSelection = applyOperation(operation: operation, on: String(endOfSelection.dropFirst(selection.start.column))) 49 | 50 | modifiedLine = leadingUnselected + transformedSelection + trailingUnselected 51 | 52 | } else if isFirstLineOfSelection { 53 | // Handles the case that the first line is a partial selection 54 | let selectedSubstring = String(selectedLine.dropFirst(selection.start.column)) 55 | let unselectedSubstring = selectedLine.prefix(selection.start.column) 56 | 57 | modifiedLine = String(unselectedSubstring + applyOperation(operation: operation, on: selectedSubstring)) 58 | 59 | } else if isLastLineOfSelection { 60 | // Handles the case that the last line is a partial selection 61 | let selectedSubstring = String(selectedLine.prefix(selection.end.column)) 62 | let unselectedSubstring = selectedLine.dropFirst(selection.end.column) 63 | 64 | modifiedLine = String(applyOperation(operation: operation, on: selectedSubstring) + unselectedSubstring) 65 | 66 | } else { 67 | modifiedLine = applyOperation(operation: operation, on: selectedLine) 68 | } 69 | 70 | invocation.buffer.lines[index + startIndex] = modifiedLine 71 | } 72 | 73 | completionHandler(nil) 74 | } 75 | 76 | static func applyOperation(operation: Operation, on input: String) -> String { 77 | switch operation { 78 | case .pascalCase: 79 | return toPascalCase(input) 80 | case .camelCase: 81 | return toCamelCase(input) 82 | case .snakeCase: 83 | return toSnakeCase(input) 84 | case .uppercase: 85 | return input.uppercased() 86 | case .lowercase: 87 | return input.lowercased() 88 | } 89 | } 90 | 91 | // Courtesy of ChatGPT :) 92 | static func toCamelCase(_ input: String) -> String { 93 | let words = input.components(separatedBy: CharacterSet.alphanumerics.inverted) 94 | let firstWord = words.first ?? "" 95 | let camelCaseWords = words.dropFirst().map { $0.capitalized } 96 | let camelCase = ([firstWord] + camelCaseWords).joined() 97 | return camelCase.prefix(1).lowercased() + camelCase.dropFirst() 98 | } 99 | 100 | static func toPascalCase(_ input: String) -> String { 101 | let words = input.components(separatedBy: CharacterSet.alphanumerics.inverted) 102 | let pascalCaseWords = words.map { word -> String in 103 | guard !word.isEmpty else { return "" } 104 | let firstChar = String(word.prefix(1)).capitalized 105 | let remainingChars = String(word.dropFirst()) 106 | return firstChar + remainingChars 107 | } 108 | let pascalCase = pascalCaseWords.joined() 109 | return pascalCase 110 | } 111 | 112 | static func toSnakeCase(_ inputString: String) -> String { 113 | var result = "" 114 | var isBeginningOfWord = true 115 | 116 | for character in inputString { 117 | if character.isLetter || character.isNumber { 118 | if character.isUppercase { 119 | if !isBeginningOfWord { 120 | result.append("_") 121 | } 122 | result.append(String(character).lowercased()) 123 | } else { 124 | result.append(character) 125 | } 126 | isBeginningOfWord = false 127 | } else { 128 | if !isBeginningOfWord { 129 | result.append("_") 130 | } 131 | isBeginningOfWord = true 132 | } 133 | } 134 | 135 | // Remove trailing underscore, if any 136 | if result.last == "_" { 137 | result.removeLast() 138 | } 139 | 140 | return result 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /EditKit/SourceEditorCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SourceEditorCommand.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 12/14/22. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | 11 | class SourceEditorCommand: NSObject, XCSourceEditorCommand { 12 | func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void { 13 | EditorController.handle(with: invocation, completionHandler: completionHandler) 14 | completionHandler(nil) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /EditKit/SourceEditorExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SourceEditorExtension.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 12/14/22. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | 11 | class SourceEditorExtension: NSObject, XCSourceEditorExtension {} 12 | -------------------------------------------------------------------------------- /EditKit/StripTrailingWhitespaceCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StripTrailingWhitespaceCommand.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 12/17/22. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | 11 | final class StripTrailingWhitespaceCommand { 12 | static func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 13 | // Keep an array of changed line indices. 14 | var changedLineIndexes = [Int]() 15 | 16 | // Loop through each line in the buffer, searching for trailing whitespace and replace only 17 | // the trailing whitespace. 18 | for lineIndex in 0 ..< invocation.buffer.lines.count { 19 | let originalLine = invocation.buffer.lines[lineIndex] as! String 20 | let newLine = originalLine.replacingOccurrences(of: "[ \t]+|[ \t]+$", with: "", options: []) 21 | 22 | // Only update lines that have changed. 23 | if originalLine != newLine { 24 | changedLineIndexes.append(lineIndex) 25 | invocation.buffer.lines[lineIndex] = newLine 26 | } 27 | } 28 | 29 | // Select all lines that were replaced. 30 | let updatedSelections: [XCSourceTextRange] = changedLineIndexes.map { lineIndex in 31 | let lineSelection = XCSourceTextRange() 32 | lineSelection.start = XCSourceTextPosition(line: lineIndex, column: 0) 33 | lineSelection.end = XCSourceTextPosition(line: lineIndex + 1, column: 0) 34 | return lineSelection 35 | } 36 | 37 | // Set selections then return with no error. 38 | invocation.buffer.selections.setArray(updatedSelections) 39 | completionHandler(nil) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /EditKit/Third Party/AutoMarkCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AutoMarkCommand.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 1/16/23. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | 11 | class AutoMarkCommand: NSObject, XCSourceEditorCommand { 12 | func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 13 | var lineClassMark: Int? 14 | var linePropertieMark: Int? 15 | var lineIBOutletMark: Int? 16 | var lineViewDidLoadMark: Int? 17 | var lineInitializersMark: Int? 18 | var lineIBActionMark: Int? 19 | var linePrivateMethodMark: Int? 20 | var linePublicMethodMark: Int? 21 | var lineExtensionMark: Int? 22 | 23 | func checkForExistingMarks() { 24 | let allLines = invocation.buffer.lines.map { $0 as! String }.joined() 25 | 26 | if allLines.contains("MARK: - IBOutlets") { 27 | lineIBOutletMark = 0 28 | } 29 | 30 | if allLines.contains("MARK: - Properties") { 31 | linePropertieMark = 0 32 | } 33 | 34 | if allLines.contains("MARK: - Initializers") { 35 | lineInitializersMark = 0 36 | } 37 | 38 | if allLines.contains("MARK: - IBOutlets") { 39 | lineIBOutletMark = 0 40 | } 41 | 42 | if allLines.contains("MARK: - View Lifecycle") { 43 | lineViewDidLoadMark = 0 44 | } 45 | 46 | if allLines.contains("MARK: - IBActions") { 47 | lineIBActionMark = 0 48 | } 49 | 50 | if allLines.contains("MARK: - Private Methods") { 51 | linePrivateMethodMark = 0 52 | } 53 | 54 | if allLines.contains("MARK: - Public Methods") { 55 | linePublicMethodMark = 0 56 | } 57 | 58 | if allLines.contains("MARK: - Extensions") { 59 | lineExtensionMark = 0 60 | } 61 | 62 | } 63 | 64 | checkForExistingMarks() 65 | 66 | // Find the main entity first 67 | for i in 0.. Any? { 14 | if index < count { 15 | return self[index] 16 | } 17 | 18 | return nil 19 | } 20 | } 21 | 22 | fileprivate extension Sequence { 23 | func findFirstOccurence(_ block: (Iterator.Element) -> Bool) -> Iterator.Element? { 24 | for x in self where block(x) { 25 | return x 26 | } 27 | 28 | return nil 29 | } 30 | 31 | func some(_ block: (Iterator.Element) -> Bool) -> Bool { 32 | return findFirstOccurence(block) != nil 33 | } 34 | 35 | func all(_ block: (Iterator.Element) -> Bool) -> Bool { 36 | return findFirstOccurence { !block($0) } == nil 37 | } 38 | } 39 | 40 | private enum UIKitClass: String { 41 | case view = "UIView" 42 | case button = "UIButton" 43 | case viewController = "UIViewController" 44 | case tableViewController = "UITableViewController" 45 | case tableView = "UITableView" 46 | case tableViewCell = "UITableViewCell" 47 | case collectionView = "UICollectionView" 48 | case collectionViewCell = "UICollectionViewCell" 49 | 50 | var detectedEndings: [String] { 51 | switch self { 52 | case .view: 53 | return ["View", "view"] 54 | case .button: 55 | return ["Button", "button"] 56 | case .viewController: 57 | return ["ViewController"] 58 | case .tableViewController: 59 | return ["TableViewController"] 60 | case .tableView: 61 | return ["TableView"] 62 | case .tableViewCell: 63 | return ["Cell"] 64 | case .collectionView: 65 | return ["CollectionView"] 66 | case .collectionViewCell: 67 | return ["CollectionViewCell", "CollectionCell"] 68 | } 69 | } 70 | 71 | static func detectSuperclass(forTypeName name: String) -> UIKitClass? { 72 | // check tableViewCell after collectionViewCell to give it a chance to be detected 73 | let candidates: [UIKitClass] = [.view, .button, .tableViewController, .viewController, .tableView, 74 | .collectionViewCell, .collectionView, .tableViewCell] 75 | return candidates.findFirstOccurence { 76 | return $0.detectedEndings.findFirstOccurence { name.hasSuffix($0) } != nil 77 | } 78 | } 79 | } 80 | 81 | private enum Type { 82 | 83 | case classType(parentType: UIKitClass?) 84 | case structType 85 | case enumType 86 | case protocolType 87 | 88 | static func propableType(forFileName name: String) -> Type { 89 | 90 | if let detectedClass = UIKitClass.detectSuperclass(forTypeName: name) { 91 | return .classType(parentType: detectedClass) 92 | } 93 | 94 | if name.hasSuffix("Protocol") || name.hasSuffix("able") { 95 | return .protocolType 96 | } 97 | 98 | return .classType(parentType: nil) 99 | } 100 | 101 | func declarationCode(forTypeName name: String, tabWidth: Int) -> String? { 102 | let s: String? = { 103 | switch self { 104 | case .classType(parentType: let type): 105 | return "class \(name)" + (type.map { ": \($0.rawValue)" } ?? "") 106 | case .protocolType: 107 | return "protocol \(name) " 108 | default: 109 | return nil 110 | } 111 | }() 112 | return s.map { "\n" + $0 + " {\n\(String(repeating: " ", count: tabWidth))\n}" } 113 | } 114 | } 115 | 116 | struct EditorHelper { 117 | static func setCursor(atLine line: Int, column: Int, buffer: XCSourceTextBuffer) { 118 | let range = XCSourceTextRange() 119 | let position = XCSourceTextPosition(line: line, column: column) 120 | range.start = position 121 | range.end = position 122 | buffer.selections.setArray([range]) 123 | } 124 | } 125 | 126 | 127 | class CreateTypeDefinitionCommand: NSObject, XCSourceEditorCommand { 128 | private func trimEmptyLinesAtTheEnd(_ invocation: XCSourceEditorCommandInvocation) { 129 | while (invocation.buffer.lines.lastObject as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) == "" { 130 | invocation.buffer.lines.removeLastObject() 131 | } 132 | } 133 | 134 | private func lineHasDeclaration(_ line: String) -> Bool { 135 | let line = line.trimmingCharacters(in: .whitespacesAndNewlines) 136 | let declarations = ["class", "struct", "enum", "protocol", "extension", "func", "var", "let"] 137 | return declarations.some { line.hasPrefix($0) } 138 | } 139 | 140 | private func lineHasUIKitImport(_ line: String) -> Bool { 141 | return line.trimmingCharacters(in: .whitespacesAndNewlines) == "import UIKit" 142 | } 143 | 144 | // "// Classname.swift" -> "Classname" 145 | private func fileName(fromFileNameComment comment: String) -> String? { 146 | 147 | let comment = comment.trimmingCharacters(in: .whitespacesAndNewlines) 148 | 149 | let commentPrefix = "//" 150 | guard comment.hasPrefix(commentPrefix) else { return nil } 151 | 152 | let swiftExtensionSuffix = ".swift" 153 | guard comment.hasSuffix(swiftExtensionSuffix) else { return nil } 154 | 155 | let startIndex = comment.index(comment.startIndex, offsetBy: commentPrefix.count) 156 | let endIndex = comment.index(comment.endIndex, offsetBy: -swiftExtensionSuffix.count) 157 | 158 | return comment[startIndex.. Void) { 182 | guard invocation.buffer.contentUTI == "public.swift-source" else { 183 | completionHandler(CreateTypeDefintionError.invalidSourceType.nsError) 184 | return 185 | } 186 | 187 | guard let secondLine = invocation.buffer.lines.safeObject(atIndex: 1) as? String else { 188 | completionHandler(CreateTypeDefintionError.noHeaderComments.nsError) 189 | return 190 | } 191 | 192 | guard let fileName = fileName(fromFileNameComment: secondLine) else { 193 | completionHandler(CreateTypeDefintionError.unableToExtractFileName.nsError) 194 | return 195 | } 196 | 197 | guard fileName.rangeOfCharacter(from: CharacterSet.letters.inverted) == nil else { return } 198 | guard invocation.buffer.lines.all({ !lineHasDeclaration($0 as? String ?? "") }) else { return } 199 | 200 | let type = Type.propableType(forFileName: fileName) 201 | guard let code = type.declarationCode(forTypeName: fileName, tabWidth: invocation.buffer.tabWidth) else { 202 | completionHandler(CreateTypeDefintionError.unableToGenerateTypeDefinition.nsError) 203 | return 204 | } 205 | 206 | trimEmptyLinesAtTheEnd(invocation) 207 | 208 | switch type { 209 | case .classType(parentType: let parentType) where parentType != nil: 210 | if invocation.buffer.lines.all({ !lineHasUIKitImport($0 as? String ?? "") }) { 211 | invocation.buffer.lines.add("import UIKit\n") 212 | } 213 | default: 214 | break 215 | } 216 | 217 | invocation.buffer.lines.add(code) 218 | EditorHelper.setCursor(atLine: invocation.buffer.lines.count - 2, column: invocation.buffer.tabWidth, buffer: invocation.buffer) 219 | 220 | completionHandler(nil) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /EditKit/Third Party/FormatAsMultiLine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormatAsMultiLine.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 1/16/23. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | 11 | fileprivate enum SelectionKind { 12 | case parameters 13 | case array 14 | } 15 | 16 | fileprivate extension XCSourceEditorCommand { 17 | /// Get the lines of an entire files as an array of `String`s. 18 | func getLines(from buffer: XCSourceTextBuffer) -> [String] { 19 | guard let lines = buffer.lines as? [String] else { return [] } 20 | return lines 21 | } 22 | 23 | /// Get a single string from a range. 24 | func getText(from range: XCSourceTextRange, buffer: XCSourceTextBuffer) -> String { 25 | let allLines = getLines(from: buffer) 26 | let lines = allLines[range.start.line ... range.end.line] 27 | let text = lines.map { String($0) }.joined() 28 | return text 29 | } 30 | } 31 | 32 | final class FormatAsMultiLine: NSObject, XCSourceEditorCommand { 33 | /// The `Format Selected Code` command. 34 | 35 | enum FormatAsMultliLineError: Error, LocalizedError { 36 | case noSelection 37 | case unbalancedBrackets 38 | 39 | var errorDescription: String? { 40 | switch self { 41 | case .noSelection: 42 | return "Something went wrong. Please check your selection and try again." 43 | case .unbalancedBrackets: 44 | return "The number of opening and closing brackets ( or { are not equal." 45 | } 46 | } 47 | } 48 | 49 | func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 50 | /// Get the selection first. 51 | guard 52 | let selection = invocation.buffer.selections.firstObject, 53 | let range = selection as? XCSourceTextRange 54 | else { 55 | completionHandler(FormatAsMultliLineError.noSelection.nsError) 56 | return 57 | } 58 | 59 | /// It's possible that the user selected the last "extra" line too. 60 | if range.start.line > invocation.buffer.lines.count - 1 { range.start.line -= 1 } 61 | if range.end.line > invocation.buffer.lines.count - 1 { range.end.line -= 1 } 62 | 63 | /// Store the current lines of the entire file. 64 | let oldLines = getLines(from: invocation.buffer) 65 | 66 | /// The width of a single tab, usually ` `. 67 | let tab: String 68 | 69 | /// The selection's starting tab. 70 | /// Example: 71 | // **input** ` init()` 72 | // **output** ` ` 73 | let startTab: Substring 74 | 75 | let startColumn: Int 76 | 77 | if invocation.buffer.usesTabsForIndentation { 78 | tab = "\u{0009}" /// Tab character. 79 | 80 | startTab = oldLines[range.start.line] 81 | .prefix { $0 == "\u{0009}" } 82 | 83 | startColumn = startTab.count 84 | 85 | } else { 86 | startTab = oldLines[range.start.line] 87 | .prefix { $0 == " " } 88 | 89 | tab = String(repeating: " ", count: invocation.buffer.indentationWidth) 90 | 91 | startColumn = startTab.count / invocation.buffer.indentationWidth 92 | } 93 | 94 | /// The tab that prefixes each parameter/array element. 95 | let contentTab = startTab + tab 96 | 97 | /// The entire text of the file. 98 | let text = getText(from: range, buffer: invocation.buffer) 99 | 100 | /// Get the opening and closing indices if the selected text contains parameters. 101 | let openingParenthesisIndex = text.firstIndex(of: "(") 102 | let closingParenthesisIndex = text.lastIndex(of: ")") 103 | 104 | /// Get the opening and closing array element if the selected text is an array. 105 | let openingArrayIndex = text.firstIndex(of: "[") 106 | let closingArrayIndex = text.lastIndex(of: "]") 107 | 108 | /// Determine if the selection was an array or a set of parameters. 109 | /// Only use the opening brace for comparison. 110 | var selectionKind: SelectionKind 111 | switch (openingParenthesisIndex, openingArrayIndex) { 112 | case let (.some(openingParenthesisIndex), .some(openingArrayIndex)): 113 | if openingParenthesisIndex < openingArrayIndex { 114 | selectionKind = .parameters 115 | } else { 116 | selectionKind = .array 117 | } 118 | case (.some, .none): 119 | selectionKind = .parameters 120 | case (.none, .some): 121 | selectionKind = .array 122 | default: 123 | completionHandler(FormatAsMultliLineError.noSelection.nsError) 124 | return 125 | } 126 | 127 | /// Determine if the selection was an array or a set of parameters. 128 | let openingBracesIndex: String.Index? = selectionKind == .parameters 129 | ? openingParenthesisIndex 130 | : openingArrayIndex 131 | 132 | let closingBracesIndex: String.Index? = selectionKind == .parameters 133 | ? closingParenthesisIndex 134 | : closingArrayIndex 135 | 136 | /// Make sure there's an opening and closing index. 137 | guard let openingBracesIndex = openingBracesIndex, let closingBracesIndex = closingBracesIndex else { 138 | completionHandler(FormatAsMultliLineError.unbalancedBrackets.nsError) 139 | return 140 | } 141 | 142 | /// Skip the opening `(` or `[`. 143 | let openingContentIndex = text.index(after: openingBracesIndex) 144 | let closingContentIndex = closingBracesIndex 145 | 146 | /// The text inside the braces. 147 | let contentsString = text[openingContentIndex ..< closingContentIndex] 148 | let contents = contentsString 149 | .components(separatedBy: ",") 150 | 151 | /// Format the content by adding spaces and commas. 152 | let contentsFormatted: [String] = contents.enumerated() 153 | .map { index, element in 154 | let line = element.trimmingCharacters(in: .whitespacesAndNewlines) 155 | if index == contents.indices.last { 156 | return contentTab + line 157 | } else { 158 | return contentTab + line + "," 159 | } 160 | } 161 | 162 | /// The string that comes before the selection. 163 | let openingString = text[.. [1, 2, 4, 6] 8 | */ 9 | var uniqueElements: [Element] { 10 | return Dictionary(grouping: self, by: { $0 }).compactMap { $1.count == 1 ? $0 : nil } 11 | } 12 | 13 | /** 14 | Returns all common elements in an array. 15 | 16 | [1, 2, 3, 3, 4, 5, 5, 5, 6] -> [3, 5] 17 | */ 18 | var commonElements: [Element] { 19 | return self.filter { !uniqueElements.contains($0) } 20 | } 21 | 22 | func contains(optional: Element?) -> Bool { 23 | guard let element = optional else { return false } 24 | return contains(element) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/ConvertJSONToCodableCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SourceEditorCommand.swift 3 | // Json2SwiftExtension 4 | // 5 | // Created by Husnain Ali on 23/02/22. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | import AppKit 11 | 12 | class ConvertJSONToCodableCommand: NSObject, XCSourceEditorCommand { 13 | 14 | func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 15 | guard let selection = invocation.buffer.selections.firstObject as? XCSourceTextRange else { 16 | completionHandler(NSError.invalidSelection) 17 | return 18 | } 19 | 20 | guard let copiedString = NSPasteboard.general.pasteboardItems?.first?.string(forType: .string) else { 21 | completionHandler(NSError.emptyClipboard) 22 | return 23 | } 24 | 25 | guard let data = copiedString.data(using: .utf8), 26 | let jsonArray = data.serialized() else { 27 | completionHandler(NSError.malformedJSON) 28 | return 29 | } 30 | 31 | Tree.build(.swift, name: String(placeholder: "Root"),from: jsonArray) 32 | invocation.buffer.lines.insert(Tree.write(), at: selection.start.line) 33 | 34 | completionHandler(nil) 35 | } 36 | } 37 | 38 | extension NSError { 39 | static var emptyClipboard: NSError { 40 | NSError( 41 | domain: "", 42 | code: 1, 43 | userInfo: failureReasonInfo( 44 | title: "Empty Clipboard", 45 | message: "The clipboard appears empty." 46 | ) 47 | ) 48 | } 49 | 50 | static var invalidSelection: NSError { 51 | NSError( 52 | domain: "", 53 | code: 1, 54 | userInfo: failureReasonInfo( 55 | title: "Selection Invalid", 56 | message: "The selection is invalid." 57 | ) 58 | ) 59 | } 60 | 61 | static var malformedJSON: NSError { 62 | NSError( 63 | domain: "", 64 | code: 1, 65 | userInfo: failureReasonInfo( 66 | title: "Malformed JSON", 67 | message: "The copied JSON appears malformed." 68 | ) 69 | ) 70 | } 71 | } 72 | 73 | private extension NSError { 74 | static func failureReasonInfo(title: String, message: String, comment: String = "") -> [String: Any] { 75 | [ 76 | NSLocalizedFailureReasonErrorKey: NSLocalizedString( 77 | title, 78 | value: message, 79 | comment: comment 80 | ) 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/Copyable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | protocol Copyable { 4 | init(instance: Self) 5 | } 6 | 7 | extension Copyable { 8 | func copy() -> Self { 9 | return .init(instance: self) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/Data+Utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public extension Data { 4 | /** 5 | Serializes the JSON data from a file. 6 | 7 | Will first check for a single key value pair object. 8 | If that fails it will look for an array of key value pairs and take use the first object. 9 | */ 10 | func serialized() -> [String: Any]? { 11 | var object: Any? 12 | 13 | do { 14 | object = try JSONSerialization.jsonObject(with: self, options : .allowFragments) 15 | } catch { 16 | print("Error writing file: \(error.localizedDescription)") 17 | } 18 | 19 | if let json = object as? [String: Any] ?? (object as? [[String: Any]])?.first { 20 | return json 21 | } 22 | 23 | return nil 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/DateFormatter+Types.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension DateFormatter { 4 | static let iso8601: DateFormatter = { 5 | let formatter = DateFormatter() 6 | formatter.timeZone = TimeZone(abbreviation: "UTC") 7 | formatter.locale = Locale(identifier: "en_US_POSIX") 8 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSZ" 9 | return formatter 10 | }() 11 | 12 | static let dateAndTime: DateFormatter = { 13 | let formatter = DateFormatter() 14 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" 15 | return formatter 16 | }() 17 | 18 | static let dateOnly: DateFormatter = { 19 | let formatter = DateFormatter() 20 | formatter.dateFormat = "yyyy-MM-dd" 21 | return formatter 22 | }() 23 | 24 | static let yearOnly: DateFormatter = { 25 | let formatter = DateFormatter() 26 | formatter.dateFormat = "yyyy" 27 | return formatter 28 | }() 29 | 30 | static let commentsDate: DateFormatter = { 31 | let formatter = DateFormatter() 32 | formatter.dateFormat = "MM/dd/yy" 33 | return formatter 34 | }() 35 | } 36 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/Dictionary+Utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Dictionary where Key == String { 4 | /** 5 | Returns a dictionary object if it is the value for the given key. 6 | */ 7 | func value(from key: String) -> [String: Any]? { 8 | guard let dictionary = self[key] as? [String: Any] ?? (self[key] as? [[String: Any]])?.first else { return nil } 9 | return dictionary 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/GeneratorType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum GeneratorType { 4 | case swift 5 | case kotlin 6 | 7 | func detectType(of value: Any) -> Primitive? { 8 | switch self { 9 | case .swift: 10 | return SwiftTypeHandler.detectType(of: value) 11 | case .kotlin: 12 | return KotlinTypeHandler.detectType(of: value) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/KotlinGenerator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct KotlinGenerator { 4 | static func generate(with viewModel: ModelInfo) -> String { 5 | var kotlin = "" 6 | kotlin += kotlinParcelable(with: viewModel) 7 | return kotlin 8 | } 9 | 10 | private static func kotlinParcelable(with viewModel: ModelInfo) -> String { 11 | var kotlin = "data class \(viewModel.name)(\n" 12 | kotlin += kotlinValues(from: viewModel.properties) 13 | kotlin += ")\n" 14 | return kotlin 15 | } 16 | 17 | private static func kotlinValues(from properties: [String: String]) -> String { 18 | var kotlin = "" 19 | for key in properties.keys.sorted() { 20 | guard let value = properties[key] else { continue } 21 | kotlin += " val \(update(key: key)): \(value)," 22 | 23 | if key != properties.keys.sorted().last { 24 | kotlin += "\n" 25 | } 26 | } 27 | kotlin += "\n" 28 | return kotlin 29 | } 30 | } 31 | 32 | private extension KotlinGenerator { 33 | static func update(key: String) -> String { 34 | let characterSet = Set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLKMNOPQRSTUVWXYZ1234567890_") 35 | let newKey = key.filter { characterSet.contains($0) } 36 | 37 | if keywords.contains(newKey) { 38 | return "`\(newKey)`" 39 | } 40 | 41 | return newKey 42 | } 43 | 44 | static var keywords: [String] { 45 | "as class break continue do else for fun false if in interface super return object package null is try throw true this typeof typealias when while val var".components(separatedBy: " ") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/KotlinTypeHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct KotlinTypeHandler { 4 | static func detectType(of value: Any) -> Primitive? { 5 | if value is [[String: Any]] { 6 | return .custom(.list) 7 | } 8 | 9 | if let array = value as? Array { 10 | guard let item = array.first else { return nil } 11 | guard let type = detectType(of: item) else { return nil } 12 | return .list("List<\(type.description)>") 13 | } 14 | 15 | if value is [String: Any] { 16 | return .custom(.object) 17 | } 18 | 19 | if let x = value as? NSNumber { 20 | if x === NSNumber(value: true) || x === NSNumber(value: false) { 21 | return .bool 22 | } 23 | } 24 | 25 | if value is Double { 26 | return .double 27 | } 28 | 29 | if value is Int { 30 | return .int 31 | } 32 | 33 | if value is String { 34 | return .string 35 | } 36 | 37 | return .any 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/Node.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class Node: Codable, Copyable { 4 | /// This is the exact key from the json. 5 | var key: String 6 | 7 | /// Value type 8 | var valueType: String? 9 | 10 | /** 11 | Name of node. 12 | Capitalized representaion of the json key. 13 | */ 14 | var name: String 15 | 16 | /// The node's parent 17 | var parent: Node? 18 | 19 | /// Array of children nodes 20 | var children: [Node] = [] 21 | 22 | /** 23 | Generated model associated with Node. 24 | Nodes without children will not contain a generated model. 25 | */ 26 | var model: String? 27 | 28 | /** 29 | The key's level in the JSON data. 30 | */ 31 | var level: Int { 32 | var level: Int = 0 33 | 34 | ascend { _ in 35 | level += 1 36 | } 37 | 38 | return level 39 | } 40 | 41 | init(name: String) { 42 | self.key = "" 43 | self.name = name 44 | } 45 | 46 | init(key: String, value: Any? = nil, generatorType: GeneratorType) { 47 | self.key = key 48 | self.name = key.camelCased().capitalize().singular(from: value) 49 | self.setType(with: value, generatorType: generatorType) 50 | } 51 | 52 | required init(instance: Node) { 53 | self.key = instance.key 54 | self.valueType = instance.valueType 55 | 56 | name = instance.name 57 | children = instance.children 58 | } 59 | } 60 | 61 | extension Node { 62 | /** 63 | Appends a new child node to the current node. 64 | 65 | - Parameter child: The child node that will be appended 66 | */ 67 | func add(child: Node) { 68 | children.append(child) 69 | child.parent = self 70 | } 71 | 72 | /** 73 | Removes a new child node from the current node when such node is a common node. 74 | 75 | - Parameter child: The child node that will be removed 76 | */ 77 | func remove(common child: Node) { 78 | guard let index = children.firstIndex(of: child) else { return } 79 | children[index].children.removeAll() 80 | } 81 | 82 | /** 83 | Filters out any nodes that are the same and retains one. 84 | */ 85 | func filter() { 86 | children = Array(Set(children)) 87 | } 88 | 89 | /** 90 | Generates the model for the node. 91 | A model should only exist on nodes that have children. 92 | */ 93 | func generateModel(for type: GeneratorType) { 94 | let generatedStruct = StructGenerator.generate(with: self) 95 | 96 | switch type { 97 | case .swift: 98 | model = SwiftGenerator.generate(with: generatedStruct) 99 | case .kotlin: 100 | model = KotlinGenerator.generate(with: generatedStruct) 101 | } 102 | } 103 | 104 | func updateType() { 105 | guard let type = valueType else { return } 106 | 107 | if type.contains("[") { 108 | valueType = name.appendBrackets() 109 | } else { 110 | valueType = name 111 | } 112 | } 113 | } 114 | 115 | private extension Node { 116 | /** 117 | Ascends from the current node through all of the parents. 118 | Handler will be executed each time a parent node is reached. 119 | */ 120 | func ascend(handler: @escaping (Node) -> ()) { 121 | var node = self 122 | 123 | while node.parent != nil { 124 | guard let parent = node.parent else { continue } 125 | node = parent 126 | handler(node) 127 | } 128 | } 129 | 130 | func setType(with value: Any?, generatorType: GeneratorType) { 131 | guard let value = value, let type = generatorType.detectType(of: value) else { return } 132 | 133 | switch type { 134 | case .array(let type): 135 | valueType = type 136 | case .custom(let type): 137 | switch type { 138 | case .object: 139 | valueType = name 140 | case .array: 141 | valueType = name.objectArray(from: value) 142 | case .list: 143 | valueType = name.objectList(from: value) 144 | } 145 | default: 146 | valueType = type.description 147 | } 148 | } 149 | } 150 | 151 | extension Node: Equatable { 152 | static func == (lhs: Node, rhs: Node) -> Bool { 153 | return lhs.name == rhs.name && lhs.children == rhs.children 154 | } 155 | } 156 | 157 | extension Node: Hashable { 158 | func hash(into hasher: inout Hasher) { 159 | hasher.combine(name) 160 | hasher.combine(children) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/NodeViewModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct NodeViewModel: Codable { 4 | public let name: String 5 | public let model: String 6 | } 7 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/Primitive.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum CustomType { 4 | case object 5 | case array 6 | case list 7 | } 8 | 9 | enum Primitive { 10 | case array(String), custom(CustomType), list(String) 11 | case bool, date, double, int, string, url, any 12 | 13 | var description: String { 14 | switch self { 15 | case .array: return "Array" 16 | case .bool: return "Bool" 17 | case .date: return "Date" 18 | case .double: return "Double" 19 | case .int: return "Int" 20 | case .list: return "List" 21 | case .string: return "String" 22 | case .url: return "URL" 23 | case .any: return "Any" 24 | case .custom: return "Custom" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/String+Utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | /// Adds brackets around String 5 | func appendBrackets() -> String { 6 | return "[\(self)]" 7 | } 8 | 9 | /// Adds brackets around String 10 | func appendCarrots() -> String { 11 | return "<\(self)>" 12 | } 13 | 14 | /** 15 | Capitalized the first letter in a string. Doesn't set any letters lowercase. 16 | 17 | While String has a capitalized representation, the resulting String 18 | will lowercase letters in the string that are capitalized. 19 | */ 20 | func capitalize() -> String { 21 | return prefix(1).uppercased() + self.dropFirst() 22 | } 23 | 24 | /** 25 | Converts snake_case to camelCase 26 | 27 | foo_bar -> fooBar 28 | */ 29 | func camelCased() -> String { 30 | guard contains("_") else { return self } 31 | return self 32 | .split(separator: "_") 33 | .enumerated() 34 | .map { $0.offset > 0 ? $0.element.capitalized : $0.element.lowercased() } 35 | .joined() 36 | } 37 | 38 | /** 39 | Appends brackets to singular objects in an array. 40 | 41 | Foos -> [Foo] 42 | */ 43 | func objectArray(from json: Any?) -> String? { 44 | guard json is [[String: Any]] else { return self } 45 | return singular(from: json).appendBrackets() 46 | } 47 | 48 | /** 49 | Appends brackets to singular objects in an array. 50 | 51 | Foos -> [Foo] 52 | */ 53 | func objectList(from json: Any?) -> String? { 54 | guard json is [[String: Any]] else { return self } 55 | return "List\(singular(from: json).appendCarrots())" 56 | } 57 | 58 | /** 59 | Returns the json key singular if the value is an array of objects. 60 | 61 | "Foos": [ 62 | { 63 | "bar": "quuz", 64 | "baz": "norf" 65 | } 66 | ] 67 | 68 | Result: Foo 69 | 70 | "Foos": { 71 | "bar": "quuz", 72 | "baz": "norf" 73 | } 74 | 75 | Result: Foos 76 | 77 | */ 78 | func singular(from value: Any? = nil) -> String { 79 | guard value is [[String: Any]] else { return self } 80 | 81 | if suffix(3) == "ies" { 82 | return dropLast(3) + "y" 83 | } 84 | 85 | if last == "s" { 86 | return dropLast().description 87 | } 88 | 89 | return self 90 | } 91 | 92 | /** 93 | Creates an editor placeholder. 94 | */ 95 | init(placeholder: String) { 96 | self = "<#\(placeholder)#>" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/StructGenerator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | typealias ModelInfo = (name: String, properties: [String: String]) 4 | 5 | struct StructGenerator { 6 | static var currentNode: String? 7 | 8 | static func generate(with parent: Node) -> ModelInfo { 9 | var properties: [String: String] = [:] 10 | 11 | parent.children.forEach { node in 12 | properties[node.key] = node.valueType 13 | } 14 | 15 | currentNode = parent.name 16 | 17 | return ModelInfo(name: parent.name, properties: properties) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/SwiftGenerator.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct SwiftGenerator { 4 | static func generate(with viewModel: ModelInfo) -> String { 5 | var swift = "" 6 | swift += swiftObject(with: viewModel) 7 | return swift 8 | } 9 | 10 | private static func swiftObject(with viewModel: ModelInfo) -> String { 11 | var swift = "struct \(viewModel.name): Codable {\n" 12 | swift += swiftFor(properties: viewModel.properties) 13 | swift += "}\n" 14 | return swift 15 | } 16 | 17 | private static func swiftFor(properties: [String: String]) -> String { 18 | var swift = "" 19 | swift += swiftCodingKeys(from: properties.keys.sorted()) 20 | swift += swiftVariables(from: properties) 21 | swift += swiftInit(from: properties) 22 | return swift 23 | } 24 | 25 | private static func swiftCodingKeys(from keys: [String]) -> String { 26 | var swift = " enum CodingKeys: String, CodingKey, CaseIterable {\n" 27 | keys.forEach { key in 28 | let newKey = update(key: key) 29 | if newKey != key { 30 | swift += " case \(newKey) = \"\(key)\"\n" 31 | } else { 32 | swift += " case \(key)\n" 33 | } 34 | } 35 | swift += " }\n\n" 36 | return swift 37 | } 38 | 39 | private static func swiftVariables(from properties: [String: String]) -> String { 40 | var swift = "" 41 | for key in properties.keys.sorted() { 42 | guard let value = properties[key] else { continue } 43 | swift += " let \(update(key: key)): \(value)\n" 44 | } 45 | swift += "\n" 46 | return swift 47 | } 48 | 49 | private static func swiftInit(from properties: [String: String]) -> String { 50 | var swift = "" 51 | swift += " init(" 52 | for (index, key) in properties.keys.sorted().enumerated() { 53 | guard let value = properties[key] else { continue } 54 | if index != properties.keys.count - 1 { 55 | swift += "\(update(key: key)): \(value), " 56 | } else { 57 | swift += "\(update(key: key)): \(value)" 58 | } 59 | } 60 | swift += ") {\n" 61 | properties.keys.sorted().forEach { key in 62 | swift += " self.\(update(key: key)) = \(update(key: key))\n" 63 | } 64 | swift += " }\n" 65 | return swift 66 | } 67 | } 68 | 69 | private extension SwiftGenerator { 70 | static func update(key: String) -> String { 71 | let characterSet = Set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLKMNOPQRSTUVWXYZ1234567890_") 72 | var newKey = key.filter { characterSet.contains($0) } 73 | 74 | if let char = newKey.first, let _ = Int(String(char)) { 75 | newKey = newKey.camelCased() 76 | return "_\(newKey)" 77 | } 78 | 79 | if keywords.contains(newKey) { 80 | return "`\(newKey)`" 81 | } 82 | 83 | if newKey.contains("_") { 84 | return newKey.camelCased() 85 | } 86 | 87 | return newKey 88 | } 89 | 90 | static var keywords: [String] { 91 | "Any as associatedtype break case catch class continue default defer deinit do else enum extension fallthrough false fileprivate for func guard if import in init inout internal is let nil open operator private protocol public repeat rethrows return Self self static struct subscript super switch Type throw throws true try typealias var where while".components(separatedBy: " ") 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/SwiftTypeHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct SwiftTypeHandler { 4 | static func detectType(of value: Any) -> Primitive? { 5 | if value is [[String: Any]] { 6 | return .custom(.array) 7 | } 8 | 9 | if let array = value as? Array { 10 | guard let item = array.first else { return nil } 11 | guard let type = detectType(of: item) else { return nil } 12 | return .array("[\(type.description)]") 13 | } 14 | 15 | if value is [String: Any] { 16 | return .custom(.object) 17 | } 18 | 19 | if let x = value as? NSNumber { 20 | if x === NSNumber(value: true) || x === NSNumber(value: false) { 21 | return .bool 22 | } 23 | } 24 | 25 | if value is Int { 26 | return .int 27 | } 28 | 29 | if value is Double { 30 | return .double 31 | } 32 | 33 | if let string = value as? String { 34 | if isURL(string) { 35 | return .url 36 | } 37 | 38 | if isDate(string) { 39 | return .date 40 | } 41 | 42 | return .string 43 | } 44 | 45 | return .any 46 | } 47 | 48 | static func isURL(_ string: String) -> Bool { 49 | NSPredicate( 50 | format:"SELF MATCHES %@", 51 | argumentArray: ["((https|http)://)((\\w|-)+)(([.]|[/])((\\w|-)+))+"] 52 | ) 53 | .evaluate(with: string) 54 | } 55 | 56 | static func isDate(_ string: String) -> Bool { 57 | if DateFormatter.iso8601.date(from: string) != nil { 58 | return true 59 | } 60 | 61 | if DateFormatter.dateAndTime.date(from: string.replacingOccurrences(of: "+00:00", with: "")) != nil { 62 | return true 63 | } 64 | 65 | if DateFormatter.dateOnly.date(from: string) != nil { 66 | return true 67 | } 68 | 69 | return false 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/Tree+Debug.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Tree { 4 | static var description: String { 5 | guard let rootNode = rootNode else { return "" } 6 | var structure = "" 7 | structure += rootNode.key != "" ? "- \(rootNode.name), 🔑 = \(rootNode.key)" : "- \(rootNode.name)" 8 | structure += rootNode.valueType != nil ? " 💰 = \(rootNode.valueType!)\n" : "\n" 9 | structure += structureForChildren(on: rootNode) 10 | return structure 11 | } 12 | } 13 | 14 | private extension Tree { 15 | static func structureForChildren(on node: Node) -> String { 16 | var structure = "" 17 | node.children.sorted(by: { $0.name < $1.name }).forEach { child in 18 | structure += String(repeating: "\t", count: child.level) 19 | structure += child.key != "" ? "- \(child.name), 🔑 = \(child.key)" : "- \(child.name)" 20 | structure += child.valueType != nil ? " 💰 = \(child.valueType!)\n" : "\n" 21 | structure += structureForChildren(on: child) 22 | } 23 | return structure 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /EditKit/Third Party/JSON to Codable/Tree.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Tree { 4 | static var rootNode: Node? 5 | 6 | private static var type: GeneratorType = .swift 7 | private static var parents: [Node] = [] 8 | 9 | public static func build(_ type: GeneratorType, name: String = "Root", from json: [String: Any]) { 10 | self.type = type 11 | parents = [] 12 | 13 | let rootNode = Node(name: name) 14 | addChildren(to: rootNode, with: json) 15 | self.rootNode = rootNode 16 | 17 | createNodes() 18 | } 19 | 20 | public static func write() -> String { 21 | var model = "" 22 | 23 | parents.forEach { 24 | $0.generateModel(for: type) 25 | 26 | if let nodeModel = $0.model { 27 | if $0 != parents.first { 28 | model += "\n" 29 | } 30 | 31 | model += nodeModel 32 | } 33 | } 34 | 35 | return model 36 | } 37 | 38 | public static func forEach(_ handler: @escaping (NodeViewModel) -> ()) { 39 | parents.forEach { 40 | $0.generateModel(for: type) 41 | 42 | if let model = $0.model { 43 | handler(NodeViewModel(name: $0.name, model: model)) 44 | } 45 | } 46 | } 47 | } 48 | 49 | private extension Tree { 50 | /** 51 | Adds children to a parent node. 52 | Runs recursively if a child node should contain children. 53 | 54 | - Parameters: 55 | - node: The node to which the children will be added 56 | - json: The json object related to the node 57 | */ 58 | static func addChildren(to node: Node, with json: [String: Any]) { 59 | parents.append(node) 60 | 61 | json.keys.forEach { 62 | let newNode = Node(key: $0, value: json[$0], generatorType: type) 63 | node.add(child: newNode) 64 | 65 | if let json = json.value(from: $0) { 66 | addChildren(to: newNode, with: json) 67 | } 68 | } 69 | } 70 | 71 | /** 72 | Updates the names of every node that has the same name but different children. 73 | Will not change names of root node children as these are named after json files. 74 | 75 | "foo": { 76 | "bal": { 77 | "bar": "quuz", 78 | "baz": "norf", 79 | "baq": "duif" 80 | } 81 | } 82 | 83 | "faa": { 84 | "baf": "quuz", 85 | "bao": "norf", 86 | "bal": { 87 | "bar": "quuz", 88 | "baz": "norf" 89 | } 90 | } 91 | 92 | Resulting Tree: 93 | 94 | - Foo 95 | - FooBal 96 | - Faa 97 | - Baf 98 | - Bao 99 | - FaaBal 100 | 101 | */ 102 | static func createNodes() { 103 | let dictionary = Dictionary(grouping: parents, by: { $0.name }) 104 | 105 | dictionary.keys.forEach { 106 | if let nodes = dictionary[$0], nodes.count > 1 { 107 | nodes.uniqueElements 108 | .filter { $0.parent != nil && $0.parent != rootNode } 109 | .forEach { 110 | $0.name = "\($0.parent!.name)\($0.name)" 111 | $0.updateType() 112 | } 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /EditKit/Third Party/SearchInCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchInCommand.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 12/17/22. 6 | // 7 | 8 | import Foundation 9 | import Cocoa 10 | import XcodeKit 11 | 12 | enum SearchEngine { 13 | case google, stackOverflow, github 14 | 15 | init(identifier: String) { 16 | guard let searchType = identifier.components(separatedBy: ".").last else { 17 | self = .google 18 | return 19 | } 20 | 21 | switch searchType { 22 | case "Google": 23 | self = .google 24 | case "StackOverflow": 25 | self = .stackOverflow 26 | case "GitHub": 27 | self = .github 28 | default: 29 | self = .google 30 | } 31 | } 32 | 33 | var urlPrefix: String { 34 | switch self { 35 | case .google: 36 | return "https://www.google.com/search?q=" 37 | case .stackOverflow: 38 | return "https://stackoverflow.com/search?q=" 39 | case .github: 40 | return "https://github.com/search?q=" 41 | } 42 | } 43 | 44 | func url(with keyword: String) -> String { 45 | urlPrefix + keyword.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlHostAllowed)! 46 | } 47 | } 48 | 49 | final class SearchOnPlatform: NSObject, XCSourceEditorCommand { 50 | func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 51 | guard let selection = invocation.buffer.selections.firstObject as? XCSourceTextRange else { 52 | completionHandler(GenericError.default.nsError) 53 | return 54 | } 55 | 56 | let startIndex = selection.start.line 57 | let endIndex = selection.end.line 58 | 59 | let selectedRange = NSRange(location: startIndex, length: 1 + endIndex - startIndex) 60 | 61 | guard let selectedLines = invocation.buffer.lines.subarray(with: selectedRange) as? [String] else { 62 | completionHandler(GenericError.default.nsError) 63 | return 64 | } 65 | 66 | let keyword = selectedLines.joined(separator: "\n").trimmingCharacters(in: .whitespacesAndNewlines) 67 | let engine = SearchEngine(identifier: invocation.commandIdentifier) 68 | 69 | openInSearchEngine(urlString: engine.url(with: keyword)) 70 | 71 | completionHandler(nil) 72 | } 73 | 74 | private func openInSearchEngine(urlString: String) { 75 | guard let url = URL(string: urlString) else { 76 | // Invalid URL 77 | return 78 | } 79 | 80 | NSWorkspace.shared.open(url) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /EditKit/Third Party/Sort Lines and Imports/CountableClosedRange+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountableClosedRange+Extension.swift 3 | // Lazy Xcode 4 | // 5 | // Created by aniltaskiran on 26.05.2020. 6 | // Copyright © 2020 Anıl. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension CountableClosedRange where Bound == Int { 12 | func saneRange(for elementsCount: Int) -> CountableClosedRange { 13 | let lowerBound = Swift.max(self.lowerBound, 0) 14 | let upperBound = Swift.min(self.upperBound, Swift.max(0, elementsCount - 1)) 15 | return lowerBound...upperBound 16 | } 17 | 18 | func omittingFirstAndLastNewLine(in lines: NSMutableArray) -> CountableClosedRange { 19 | let saneRange = self.saneRange(for: lines.count) 20 | 21 | guard lines.count > 2, 22 | let first = lines[saneRange.lowerBound] as? String, 23 | let last = lines[saneRange.upperBound] as? String else { 24 | return self 25 | } 26 | 27 | let lowerBound = first.trimmingCharacters(in: .newlines).isEmpty ? saneRange.lowerBound + 1 : saneRange.lowerBound 28 | let upperBound = last.trimmingCharacters(in: .newlines).isEmpty ? saneRange.upperBound - 1 : saneRange.upperBound 29 | 30 | return lowerBound...upperBound 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /EditKit/Third Party/Sort Lines and Imports/ImportSorter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SourceEditorCommand.swift 3 | // SorterExtension 4 | // 5 | // Created by aniltaskiran on 24.05.2020. 6 | // Copyright © 2020 Anıl. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XcodeKit 11 | 12 | class ImportSorter: NSObject, XCSourceEditorCommand { 13 | func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 14 | // Implement your command here, invoking the completion handler when done. Pass it nil on success, and an NSError on failure. 15 | let bridgedLines = invocation.buffer.lines.compactMap { $0 as? String } 16 | 17 | let importFrameworks = bridgedLines.enumerated().compactMap({ 18 | $0.element.isImportLine ? $0.element.removeImportPrefix.removeNewLine : nil 19 | }).sorted() 20 | 21 | let importIndex = bridgedLines.enumerated().compactMap({ 22 | return $0.element.isImportLine ? $0.offset : nil 23 | }).sorted() 24 | 25 | guard importIndex.count == importFrameworks.count && invocation.buffer.lines.count > importIndex.count else { 26 | completionHandler(GenericError.default.nsError) 27 | return 28 | } 29 | 30 | importFrameworks.enumerated().forEach({ invocation.buffer.lines[importIndex[$0]] = "import \($1)" }) 31 | completionHandler(nil) 32 | } 33 | } 34 | 35 | struct Line: Comparable { 36 | static func < (lhs: Line, rhs: Line) -> Bool { 37 | lhs.element < rhs.element 38 | } 39 | 40 | let index: Int 41 | let element: String 42 | } 43 | -------------------------------------------------------------------------------- /EditKit/Third Party/Sort Lines and Imports/Sort.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sort.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 1/19/23. 6 | // 7 | 8 | import Foundation 9 | enum Sort { 10 | case alphabeticallyAscending 11 | case alphabeticallyDescending 12 | case length 13 | 14 | func orderStyle(_ lhs: String, _ rhs: String) -> Bool { 15 | switch self { 16 | case .alphabeticallyAscending: return lhs < rhs 17 | case .alphabeticallyDescending: return lhs > rhs 18 | case .length: return lhs.count < rhs.count 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /EditKit/Third Party/Sort Lines and Imports/SortSelectedLinesByAlphabetically.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SortSelectedLinesByAlphabetically.swift 3 | // Lines Sorter 4 | // 5 | // Created by aniltaskiran on 24.05.2020. 6 | // Copyright © 2020 Anıl. All rights reserved. 7 | // 8 | 9 | import XcodeKit 10 | 11 | class SortSelectedLinesByAlphabeticallyAscending: NSObject, XCSourceEditorCommand { 12 | func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 13 | SortSelectedRange().sort(buffer: invocation.buffer, by: .alphabeticallyAscending, completionHandler: completionHandler) 14 | } 15 | } 16 | 17 | class SortSelectedLinesByAlphabeticallyDescending: NSObject, XCSourceEditorCommand { 18 | func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 19 | SortSelectedRange().sort(buffer: invocation.buffer, by: .alphabeticallyDescending, completionHandler: completionHandler) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /EditKit/Third Party/Sort Lines and Imports/SortSelectedLinesByLenght.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SortSelectedLinesByLenght.swift 3 | // Lines Sorter 4 | // 5 | // Created by aniltaskiran on 24.05.2020. 6 | // Copyright © 2020 Anıl. All rights reserved. 7 | // 8 | 9 | import XcodeKit 10 | 11 | class SortSelectedLinesByLength { 12 | static func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 13 | SortSelectedRange().sort(buffer: invocation.buffer, by: .length, completionHandler: completionHandler) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /EditKit/Third Party/Sort Lines and Imports/SortSelectedRangeList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SortSelectedRangeList.swift 3 | // Lines Sorter 4 | // 5 | // Created by aniltaskiran on 24.05.2020. 6 | // Copyright © 2020 Anıl. All rights reserved. 7 | // 8 | 9 | import XcodeKit 10 | 11 | struct SortSelectedRange { 12 | enum SortSelectedRangeError: Error, LocalizedError { 13 | case genericError 14 | 15 | var errorDescription: String? { 16 | return "Please verify your selection and try again." 17 | } 18 | } 19 | 20 | func sort(buffer: XCSourceTextBuffer, by sort: Sort, completionHandler: (Error?) -> Void) { 21 | // At least something is selected 22 | guard let firstSelection = buffer.selections.firstObject as? XCSourceTextRange, 23 | let lastSelection = buffer.selections.lastObject as? XCSourceTextRange, 24 | firstSelection.start.line < lastSelection.end.line else { 25 | completionHandler(SortSelectedRangeError.genericError.nsError) 26 | return 27 | } 28 | 29 | let range = (firstSelection.start.line...lastSelection.end.line).saneRange(for: buffer.lines.count) 30 | var lines = range.compactMap({ buffer.lines[$0] as? String }).sorted(by: sort.orderStyle) 31 | 32 | // Handles the situation where the user is sorting elements of a list 33 | var commaCount = 0 34 | 35 | // If the number of trailing commas is one less than the number of lines, then we are looking 36 | // at a selection of array elements 37 | lines.forEach { if $0.trimmingCharacters(in: .newlines).last == "," { commaCount += 1 }} 38 | let selectionIsList = commaCount == lines.count - 1 39 | 40 | // If we're in an array, ensure all rows, but the last have a trailing comma 41 | if selectionIsList { 42 | lines = lines.map { 43 | if let lastCharacter = $0.trimmingCharacters(in: .newlines).last, lastCharacter == "," { 44 | return $0 45 | } else { 46 | return $0.trimmingCharacters(in: .newlines) + "," 47 | } 48 | } 49 | 50 | if let lastLine = lines.last, lastLine.trimmingCharacters(in: .newlines).last == "," { 51 | lines[lines.count - 1] = String(lastLine.trimmingCharacters(in: .newlines).dropLast(1)) 52 | } 53 | } 54 | 55 | guard lines.count == range.count else { 56 | completionHandler(SortSelectedRangeError.genericError.nsError) 57 | return 58 | } 59 | 60 | let totalLineCount = buffer.lines.count 61 | range.enumerated().forEach { 62 | if $1 > totalLineCount { return } 63 | buffer.lines[$1] = lines[$0] 64 | } 65 | 66 | let lastSelectedLine = buffer.lines[range.upperBound] as? String 67 | firstSelection.start.column = 0 68 | lastSelection.end.column = lastSelectedLine?.count ?? 0 69 | 70 | EditorHelper.setCursor(atLine: lastSelection.end.line, column: lastSelection.end.column, buffer: buffer) 71 | completionHandler(nil) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /EditKit/Third Party/Sort Lines and Imports/String+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extension.swift 3 | // SorterExtension 4 | // 5 | // Created by aniltaskiran on 24.05.2020. 6 | // Copyright © 2020 Anıl. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension String { 12 | var isBlank: Bool { 13 | return trimmingCharacters(in: .whitespacesAndNewlines).isEmpty 14 | } 15 | 16 | var isImportLine: Bool { 17 | hasPrefix("import ") 18 | } 19 | 20 | var removeImportPrefix: String { 21 | replacingOccurrences(of: "import ", with: "") 22 | } 23 | 24 | var removeNewLine: String { 25 | trimmingCharacters(in: .whitespacesAndNewlines) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /EditKit/Third Party/SwiftUI View Operations/FormatLines.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormatLines.swift 3 | // SwiftUI Tools 4 | // 5 | // Created by Dave Carlton on 8/9/21. 6 | // 7 | 8 | import XcodeKit 9 | import OSLog 10 | 11 | #warning("This class does not appear to be in use.") 12 | class FormatLines: XcodeLines { 13 | 14 | override func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 15 | do { 16 | try performSetup(invocation: invocation) 17 | var hasSelection = false 18 | 19 | for i in 0 ..< selections.count { 20 | if let selection = selections[i] as? XCSourceTextRange { 21 | hasSelection = true 22 | for j in selection.start.line...selection.end.line { 23 | updateLine(lines: lines, newLines: newLines, index: j) 24 | } 25 | } 26 | } 27 | if !hasSelection { 28 | for i in 0 ..< lines.count { 29 | updateLine(lines: lines, newLines: newLines, index: i) 30 | } 31 | } 32 | 33 | completionHandler(nil) 34 | } catch { 35 | completionHandler(error as NSError) 36 | } 37 | } 38 | 39 | func updateLine(lines: NSMutableArray, newLines: [String], index: Int) { 40 | guard index < newLines.count, index < lines.count else { 41 | return 42 | } 43 | if let line = lines[index] as? String { 44 | let newLine = newLines[index] + "\n" 45 | if newLine != line { 46 | lines[index] = newLine 47 | } 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /EditKit/Third Party/SwiftUI View Operations/Parser/Extensions/Extension.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | 5 | typealias StringObj = (string: String, index: String.Index) 6 | 7 | func findParentheses(from start: String.Index, reFormat: Bool = true) throws -> StringObj { 8 | return try findBlock(type: .parentheses, from: start, reFormat: reFormat) 9 | } 10 | 11 | func findSquare(from start: String.Index, reFormat: Bool = true) throws -> StringObj { 12 | return try findBlock(type: .square, from: start, reFormat: reFormat) 13 | } 14 | 15 | func findBlock(type: IndentType, from start: String.Index, reFormat: Bool) throws -> StringObj { 16 | var target = index(after: start) 17 | var result = String(type.rawValue) 18 | while target < endIndex { 19 | let next = self[target] 20 | 21 | if next == "\"" { 22 | let quote = try findQuote(from: target) 23 | target = quote.index 24 | result += quote.string 25 | continue 26 | } else if next == "(" { 27 | let block = try findParentheses(from: target, reFormat: false) 28 | target = block.index 29 | result += block.string 30 | continue 31 | } else if next == "[" { 32 | let block = try findSquare(from: target, reFormat: false) 33 | target = block.index 34 | result += block.string 35 | continue 36 | } else { 37 | result.append(next) 38 | } 39 | target = index(after: target) 40 | if next == type.stopSymbol() { 41 | break 42 | } 43 | } 44 | // MARK: no need to new obj 45 | if reFormat { 46 | let obj = try SwiftParser(string: result).format() 47 | return (obj, target) 48 | } else { 49 | return (result, target) 50 | } 51 | } 52 | 53 | func findQuote(from start: String.Index) throws -> StringObj { 54 | var escape = false 55 | var target = index(after: start) 56 | var result = "\"" 57 | while target < endIndex { 58 | let next = self[target] 59 | if next == "\n" { 60 | throw FormatError.stringError 61 | } 62 | 63 | if escape && next == "(" { 64 | let block = try findParentheses(from: target) 65 | target = block.index 66 | result += block.string 67 | 68 | escape = false 69 | continue 70 | } else { 71 | result.append(next) 72 | } 73 | 74 | target = index(after: target) 75 | if !escape && next == "\"" { 76 | return (result, target) 77 | } 78 | if next == "\\" { 79 | escape = !escape 80 | } else { 81 | escape = false 82 | } 83 | } 84 | return (result, index(before: endIndex)) 85 | } 86 | 87 | func findTernary(from target: String.Index) -> StringObj? { 88 | let start = nextNonSpaceIndex(index(after: target)) 89 | guard let first = findStatement(from: start) else { 90 | return nil 91 | } 92 | let middle = nextNonSpaceIndex(first.index) 93 | guard middle < endIndex, self[middle] == ":" else { 94 | return nil 95 | } 96 | let end = nextNonSpaceIndex(index(after: middle)) 97 | guard let second = findObject(from: end) else { 98 | return nil 99 | } 100 | return ("? " + first.string + " : " + second.string, second.index) 101 | } 102 | 103 | func findStatement(from start: String.Index) -> StringObj? { 104 | guard let obj1 = findObject(from: start) else { 105 | return nil 106 | } 107 | let operIndex = nextNonSpaceIndex(obj1.index) 108 | guard operIndex < endIndex, self[operIndex].isOperator() else { 109 | return obj1 110 | } 111 | let list = operatorList[self[operIndex]] 112 | for compare in list! { 113 | if isNext(string: compare, strIndex: operIndex) { 114 | let operEnd = index(operIndex, offsetBy: compare.count) 115 | let obj2Index = nextNonSpaceIndex(operEnd) 116 | if let obj2 = findObject(from: obj2Index) { 117 | return (string: obj1.string + " " + compare + " " + obj2.string, index: obj2.index) 118 | } else { 119 | return obj1 120 | } 121 | } 122 | } 123 | return obj1 124 | } 125 | 126 | func findObject(from start: String.Index) -> StringObj? { 127 | guard start < endIndex else { 128 | return nil 129 | } 130 | var target = start 131 | var result = "" 132 | 133 | if self[target] == "-" { 134 | target = index(after: target) 135 | result = "-" 136 | } 137 | 138 | let list: [Character] = ["?", "!", "."] 139 | while target < endIndex { 140 | let next = self[target] 141 | if next.isAZ() || list.contains(next) { // MARK: check complex case 142 | result.append(next) 143 | target = index(after: target) 144 | } else if next == "[" { 145 | guard let block = try? findSquare(from: target) else { 146 | return nil 147 | } 148 | target = block.index 149 | result += block.string 150 | } else if next == "(" { 151 | guard let block = try? findParentheses(from: target) else { 152 | return nil 153 | } 154 | target = block.index 155 | result += block.string 156 | } else if next == "\"" { 157 | guard let quote = try? findQuote(from: target) else { 158 | return nil 159 | } 160 | target = quote.index 161 | result += quote.string 162 | } else { 163 | break 164 | } 165 | } 166 | if result.isEmpty { 167 | return nil 168 | } 169 | return (result, target) 170 | } 171 | 172 | func findGeneric(from start: String.Index) throws -> StringObj? { 173 | var target = index(after: start) 174 | var count = 1 175 | var result = "<" 176 | while target < endIndex { 177 | let next = self[target] 178 | switch next { 179 | case " ": 180 | result.keepSpace() 181 | case "A" ... "z", "0" ... "9", "[", "]", ".", "?", ":", "&": 182 | result.append(next) 183 | case ",": 184 | result.append(", ") 185 | target = nextNonSpaceIndex(index(after: target)) 186 | continue 187 | case "<": 188 | count += 1 189 | result.append(next) 190 | case ">": 191 | count -= 1 192 | result.append(next) 193 | if count == 0 { 194 | return (result, index(after: target)) 195 | } else if count < 0 { 196 | return nil 197 | } 198 | case "\"": 199 | let quote = try findQuote(from: target) 200 | target = quote.index 201 | result += quote.string 202 | continue 203 | case "(": 204 | let block = try findParentheses(from: target) 205 | target = block.index 206 | result += block.string 207 | continue 208 | case "-": 209 | if isNext(string: "->", strIndex: target) { 210 | result.keepSpace() 211 | result.append("-> ") 212 | target = index(target, offsetBy: 2) 213 | continue 214 | } 215 | return nil 216 | default: 217 | return nil 218 | } 219 | 220 | target = index(after: target) 221 | } 222 | return nil 223 | } 224 | 225 | // MARK: remove duplicate in Parser.swift 226 | func isNext(string target: String, strIndex: String.Index) -> Bool { 227 | if let stopIndex = self.index(strIndex, offsetBy: target.count, limitedBy: endIndex), 228 | let _ = self.range(of: target, options: [], range: strIndex ..< stopIndex) { 229 | return true 230 | } 231 | return false 232 | } 233 | 234 | } 235 | -------------------------------------------------------------------------------- /EditKit/Third Party/SwiftUI View Operations/Parser/Extensions/ExtensionChar.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Character { 4 | 5 | func isAZ() -> Bool { 6 | if self >= "a" && self <= "z" { 7 | return true 8 | } else if self >= "A" && self <= "Z" { 9 | return true 10 | } else if self >= "0" && self <= "9" { 11 | return true 12 | } 13 | return false 14 | } 15 | 16 | func isOperator() -> Bool { 17 | return self == "+" || self == "-" || self == "*" || self == "/" || self == "%" 18 | } 19 | 20 | func isUpperBlock() -> Bool { 21 | return self == "{" || self == "[" || self == "(" 22 | } 23 | 24 | func isLowerBlock() -> Bool { 25 | return self == "}" || self == "]" || self == ")" 26 | } 27 | 28 | func isSpace() -> Bool { 29 | return self == " " || self == "\t" 30 | } 31 | 32 | func isBlank() -> Bool { 33 | return isSpace() || self == "\n" 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /EditKit/Third Party/SwiftUI View Operations/Parser/Extensions/ExtensionString.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | 5 | var last: Character { 6 | return last ?? "\0" as Character 7 | } 8 | 9 | func lastWord() -> String { 10 | if !isEmpty { 11 | let end = lastNonBlankIndex(endIndex) 12 | if end != startIndex || !self[end].isBlank() { 13 | let start = lastIndex(from: end) { $0.isBlank() } 14 | if self[start].isBlank() { 15 | return String(self[index(after: start) ... end]) 16 | } 17 | return String(self[start ... end]) 18 | } 19 | } 20 | return "" 21 | } 22 | 23 | func trim() -> String { 24 | return trimmingCharacters(in: .whitespaces) 25 | } 26 | 27 | mutating func keepSpace() { 28 | if !last.isBlank() { 29 | append(" ") 30 | } 31 | } 32 | 33 | func nextIndex(from start: String.Index, checker: (Character) -> Bool) -> String.Index { 34 | var target = start 35 | while target < endIndex { 36 | if checker(self[target]) { 37 | break 38 | } 39 | target = index(after: target) 40 | } 41 | return target 42 | } 43 | 44 | func nextNonSpaceIndex(_ index: String.Index) -> String.Index { 45 | return nextIndex(from: index) { !$0.isSpace() } 46 | } 47 | 48 | func lastIndex(from: String.Index, checker: (Character) -> Bool) -> String.Index { 49 | var target = from 50 | while target > startIndex { 51 | target = index(before: target) 52 | if checker(self[target]) { 53 | break 54 | } 55 | } 56 | return target 57 | } 58 | 59 | func lastNonSpaceIndex(_ start: String.Index) -> String.Index { 60 | return lastIndex(from: start) { !$0.isSpace() } 61 | } 62 | 63 | func lastNonSpaceChar(_ start: String.Index) -> Character { 64 | return self[lastNonSpaceIndex(start)] 65 | } 66 | 67 | func lastNonBlankIndex(_ start: String.Index) -> String.Index { 68 | return lastIndex(from: start) { !$0.isBlank() } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /EditKit/Third Party/SwiftUI View Operations/Parser/FormatError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum FormatError: Error { 4 | case stringError 5 | } 6 | -------------------------------------------------------------------------------- /EditKit/Third Party/SwiftUI View Operations/Parser/Indent.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum IndentType: Character { 4 | case parentheses = "(", square = "[", curly = "{", ifelse = "f" 5 | 6 | func stopSymbol() -> Character { 7 | switch self { 8 | case .parentheses: 9 | return ")" 10 | case .square: 11 | return "]" 12 | case .curly: 13 | return "}" 14 | case .ifelse: 15 | return "{" 16 | } 17 | } 18 | 19 | } 20 | 21 | class Indent { 22 | static var char: String = "" { 23 | didSet { 24 | size = char.count 25 | } 26 | } 27 | static var size: Int = 0 28 | static var paraAlign = false 29 | var count: Int // general indent count 30 | var line: Int // count number of line 31 | var extra: Int // from extra indent 32 | var indentAdd: Bool // same line flag, if same line add only one indent 33 | var extraAdd: Bool 34 | var isLeading: Bool 35 | var leading: Int // leading for smart align 36 | var inSwitch: Bool // is in switch block 37 | var inEnum: Bool // is in enum block 38 | var inCase: Bool // is case statement 39 | var block: IndentType 40 | 41 | init() { 42 | count = 0 43 | extra = 0 44 | line = 0 45 | indentAdd = false 46 | extraAdd = false 47 | isLeading = false 48 | leading = 0 49 | inSwitch = false 50 | inEnum = false 51 | inCase = false 52 | block = .curly 53 | } 54 | 55 | init(with indent: Indent, offset: Int, type: IndentType?) { 56 | self.block = type ?? .curly 57 | self.count = indent.count 58 | self.extra = indent.extra 59 | self.line = 0 60 | self.isLeading = indent.isLeading 61 | self.leading = indent.leading 62 | self.inSwitch = false 63 | self.inEnum = false 64 | self.inCase = false 65 | self.indentAdd = false 66 | self.extraAdd = false 67 | 68 | if (block != .parentheses || !Indent.paraAlign) && !indent.indentAdd { 69 | self.count += 1 70 | self.indentAdd = true 71 | } else if indent.indentAdd { 72 | self.indentAdd = true 73 | if indent.count > 0 { 74 | indent.count -= 1 75 | } 76 | } else { 77 | self.indentAdd = false 78 | } 79 | if !indent.extraAdd { 80 | // if block != .curly { 81 | self.count += indent.extra 82 | // } 83 | self.extraAdd = true 84 | } else { 85 | self.extraAdd = false 86 | } 87 | 88 | if Indent.paraAlign { 89 | if block != .curly { 90 | self.leading = max(offset - count * Indent.size - 1, 0) 91 | } 92 | } else { 93 | self.leading = 0 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /EditKit/Third Party/SwiftUI View Operations/Parser/Parser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension SwiftParser { 4 | 5 | func isNext(char: Character, skipBlank: Bool = false) -> Bool { 6 | var next = string.index(after: strIndex) 7 | 8 | while next < string.endIndex { 9 | if skipBlank && string[next].isBlank() { 10 | next = string.index(after: next) 11 | continue 12 | } 13 | 14 | return string[next] == char 15 | } 16 | return false 17 | } 18 | 19 | func isPrevious(str: String) -> Bool { 20 | 21 | if let start = string.index(strIndex, offsetBy: -str.count, limitedBy: string.startIndex) { 22 | if let _ = string.range(of: str, options: [], range: start ..< strIndex) { 23 | return true 24 | } 25 | } 26 | return false 27 | } 28 | 29 | func isBetween(words: (start: String, end: String)...) -> Bool { 30 | // MARK: check word, not position 31 | if strIndex < string.endIndex { 32 | let startIndex = string.nextNonSpaceIndex(strIndex) 33 | for word in words { 34 | if let endIndex = string.index(startIndex, offsetBy: word.end.count, limitedBy: string.endIndex), 35 | let _ = string.range(of: word.end, options: [], range: startIndex ..< endIndex) { 36 | if retString.lastWord() == word.start { // MARK: cache last word 37 | return true 38 | } 39 | } 40 | } 41 | } 42 | return false 43 | } 44 | 45 | func isNext(word: String) -> Bool { 46 | let index = string.nextNonSpaceIndex(strIndex) 47 | if let endIndex = string.index(index, offsetBy: word.count, limitedBy: string.endIndex), 48 | let _ = string.range(of: word, options: [], range: index ..< endIndex) { 49 | return true 50 | } 51 | return false 52 | } 53 | 54 | // MARK: move to Entension.swift 55 | func isNext(string target: String) -> Bool { 56 | if let endIndex = string.index(strIndex, offsetBy: target.count, limitedBy: string.endIndex), 57 | let _ = string.range(of: target, options: [], range: strIndex ..< endIndex) { 58 | return true 59 | } 60 | return false 61 | } 62 | 63 | func space(with word: String) -> String.Index { 64 | if retString.last != "(" { 65 | retString.keepSpace() 66 | } 67 | retString += word + " " 68 | return string.nextNonSpaceIndex(string.index(strIndex, offsetBy: word.count)) 69 | } 70 | 71 | func space(with words: [String]) -> String.Index? { 72 | for word in words { 73 | if isNext(string: word) { 74 | return space(with: word) 75 | } 76 | } 77 | return nil 78 | } 79 | 80 | func trim() { 81 | if retString.last.isSpace() { 82 | retString = retString.trim() 83 | } 84 | } 85 | 86 | func trimWithIndent(addExtra: Bool = true) { 87 | trim() 88 | if retString.last == "\n" { 89 | addIndent(addExtra: addExtra) 90 | } 91 | } 92 | 93 | func addIndent(addExtra: Bool = true) { 94 | var checkInCase = false 95 | if indent.inSwitch { 96 | if isNext(word: "case") { 97 | checkInCase = true 98 | indent.inCase = true 99 | indent.count -= 1 100 | } else if isNext(word: "default") || isNext(word: "@unknown") { 101 | indent.extra -= 1 102 | } 103 | } 104 | if isNext(word: "switch") { 105 | isNextSwitch = true 106 | } else if isNext(word: "enum") { 107 | isNextEnum = true 108 | } 109 | let count = indent.count + (addExtra ? indent.extra : 0) 110 | if count > 0 { 111 | retString += String(repeating: Indent.char, count: count) 112 | } 113 | indent.extra = 0 114 | if indent.isLeading && indent.leading > 0 { 115 | retString += String(repeating: " ", count: indent.leading) 116 | } 117 | if checkInCase { 118 | indent.isLeading = true 119 | indent.leading += 1 120 | } 121 | } 122 | 123 | func add(with words: [String]) -> String.Index? { 124 | for word in words { 125 | if isNext(string: word) { 126 | return add(string: word) 127 | } 128 | } 129 | return nil 130 | } 131 | 132 | func add(string target: String) -> String.Index { 133 | retString += target 134 | return string.index(strIndex, offsetBy: target.count) 135 | } 136 | 137 | func add(char: Character) -> String.Index { 138 | retString.append(char) 139 | return string.index(after: strIndex) 140 | } 141 | 142 | func addToNext(_ start: String.Index, stopWord: String) -> String.Index { 143 | if let result = string.range(of: stopWord, options: [], range: start ..< string.endIndex) { 144 | retString += string[start ..< result.upperBound] 145 | return result.upperBound 146 | } 147 | retString += string[start ..< string.endIndex] 148 | return string.endIndex 149 | } 150 | 151 | func addLine() -> String.Index { 152 | let start = strIndex 153 | var findNewLine = false 154 | 155 | if let result = string.range(of: "\n", options: [], range: start ..< string.endIndex) { 156 | findNewLine = true 157 | strIndex = result.lowerBound 158 | } else { 159 | strIndex = string.endIndex 160 | } 161 | retString += string[start ..< strIndex] 162 | if findNewLine { 163 | strIndex = checkLine("\n", checkLast: false) 164 | } 165 | return strIndex 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /EditKit/Third Party/SwiftUI View Operations/Parser/Pref.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | enum Pref: String { 4 | case paraAlign 5 | case autoRemoveChar 6 | 7 | static func getUserDefaults() -> UserDefaults { 8 | return UserDefaults.init(suiteName: "com.jintin.swimat.config")! 9 | } 10 | 11 | static func isParaAlign() -> Bool { 12 | if let _ = getUserDefaults().object(forKey: Pref.paraAlign.rawValue) { 13 | return getUserDefaults().bool(forKey: Pref.paraAlign.rawValue) 14 | } 15 | return true 16 | } 17 | 18 | static func setParaAlign(isAlign: Bool) { 19 | getUserDefaults().set(isAlign, forKey: Pref.paraAlign.rawValue) 20 | getUserDefaults().synchronize() 21 | } 22 | 23 | static func isAutoRemoveChar() -> Bool { 24 | if let _ = getUserDefaults().object(forKey: Pref.autoRemoveChar.rawValue) { 25 | return getUserDefaults().bool(forKey: Pref.autoRemoveChar.rawValue) 26 | } 27 | return true 28 | } 29 | 30 | static func setAutoRemoveChar(isAlign: Bool) { 31 | getUserDefaults().set(isAlign, forKey: Pref.autoRemoveChar.rawValue) 32 | getUserDefaults().synchronize() 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /EditKit/Third Party/SwiftUI View Operations/Parser/Preferences.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class Preferences: Codable { 4 | static let parameterAlignment = "areParametersAligned" 5 | static let removeSemicolons = "areSemicolonsRemoved" 6 | 7 | private static let sharedUserDefaults = { 8 | UserDefaults(suiteName: "com.jintin.swimat.configuration")! 9 | }() 10 | 11 | static var areParametersAligned: Bool { 12 | get { 13 | return getBool(key: parameterAlignment) 14 | } 15 | set { 16 | setBool(key: parameterAlignment, value: newValue) 17 | } 18 | } 19 | var areParametersAligned = false 20 | 21 | static var areSemicolonsRemoved: Bool { 22 | get { 23 | return getBool(key: removeSemicolons) 24 | } 25 | set { 26 | setBool(key: removeSemicolons, value: newValue) 27 | } 28 | } 29 | var areSemicolonsRemoved = false 30 | 31 | static func getBool(key: String) -> Bool { 32 | return sharedUserDefaults.bool(forKey: key) 33 | } 34 | 35 | static func setBool(key: String, value: Bool) { 36 | sharedUserDefaults.set(value, forKey: key) 37 | sharedUserDefaults.synchronize() 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /EditKit/Third Party/SwiftUI View Operations/Parser/SwiftParser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let operatorList: [Character: [String]] = 4 | [ 5 | "+": ["+=<", "+=", "+++=", "+++", "+"], 6 | "-": ["->", "-=", "-<<"], 7 | "*": ["*=", "*"], 8 | "/": ["/=", "/"], 9 | "~": ["~=", "~~>", "~>"], 10 | "%": ["%=", "%"], 11 | "^": ["^="], 12 | "&": ["&&=", "&&&", "&&", "&=", "&+", "&-", "&*", "&/", "&%"], 13 | "<": ["<<<", "<<=", "<<", "<=", "<~~", "<~", "<--", "<-<", "<-", "<^>", "<|>", "<*>", "<||?", "<||", "<|?", "<|", "<"], 14 | ">": [">>>", ">>=", ">>-", ">>", ">=", ">->", ">"], 15 | "|": ["|||", "||=", "||", "|=", "|"], 16 | "!": ["!==", "!="], 17 | "=": ["===", "==", "="], 18 | ".": ["...", "..<", "."], 19 | "#": ["#>", "#"] 20 | ] 21 | 22 | fileprivate let negativeCheckSigns: [Character] = 23 | ["+", "-", "*", "/", "&", "|", "^", "<", ">", ":", "(", "[", "{", "=", ",", ".", "?"] 24 | fileprivate let negativeCheckKeys = 25 | ["case", "return", "if", "for", "while", "in"] 26 | fileprivate let numbers: [Character] = 27 | ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] 28 | 29 | class SwiftParser { 30 | let string: String 31 | var retString = "" 32 | var strIndex: String.Index 33 | var indent = Indent() 34 | var indentStack = [Indent]() 35 | var newlineIndex: Int = 0 36 | var isNextSwitch: Bool = false 37 | var isNextEnum: Bool = false 38 | var autoRemoveChar: Bool = false 39 | 40 | init(string: String, preferences: Preferences? = nil) { 41 | self.string = string 42 | self.strIndex = string.startIndex 43 | 44 | if let preferences = preferences { 45 | // Use the preferences given (for example, when testing) 46 | Indent.paraAlign = preferences.areParametersAligned 47 | autoRemoveChar = preferences.areSemicolonsRemoved 48 | } else { 49 | // Fallback on user-defined preferences 50 | Indent.paraAlign = Preferences.areParametersAligned 51 | autoRemoveChar = Preferences.areSemicolonsRemoved 52 | return 53 | } 54 | } 55 | 56 | func format() throws -> String { 57 | while strIndex < string.endIndex { 58 | let char = string[strIndex] 59 | strIndex = try check(char: char) 60 | } 61 | removeUnnecessaryChar() 62 | return retString.trim() 63 | } 64 | 65 | func check(char: Character) throws -> String.Index { 66 | switch char { 67 | case "+", "*", "%", ">", "|", "=", "~", "^", "!", "&": 68 | if let index = space(with: operatorList[char]!) { 69 | return index 70 | } 71 | return add(char: char) 72 | case ".": 73 | if let index = add(with: operatorList[char]!) { 74 | return index 75 | } 76 | return add(char: char) 77 | case "-": 78 | return checkMinus(char: char) 79 | case "/": 80 | return checkSlash(char: char) 81 | case "<": 82 | return try checkLess(char: char) 83 | case "?": 84 | return try checkQuestion(char: char) 85 | case ":": 86 | return checkColon(char: char) 87 | case "#": 88 | return checkHash(char: char) 89 | case "\"": 90 | return try checkQuote(char: char) 91 | case "\n": 92 | return checkLineBreak(char: char) 93 | case " ", "\t": 94 | return checkSpace(char: char) 95 | case ",": 96 | return checkComma(char: char) 97 | case "{", "[", "(": 98 | return checkUpperBlock(char: char) 99 | case "}", "]", ")": 100 | return checkLowerBlock(char: char) 101 | default: 102 | return checkDefault(char: char) 103 | } 104 | } 105 | 106 | func checkMinus(char: Character) -> String.Index { 107 | if let index = space(with: operatorList[char]!) { 108 | return index 109 | } else { 110 | var noSpace = false 111 | if !retString.isEmpty { 112 | // check scientific notation 113 | if strIndex != string.endIndex { 114 | if retString.last == "e" && numbers.contains(string[string.index(after: strIndex)]) { 115 | noSpace = true 116 | } 117 | } 118 | // check negative 119 | let last = retString.lastNonSpaceChar(retString.endIndex) 120 | if last.isAZ() { 121 | if negativeCheckKeys.contains(retString.lastWord()) { 122 | noSpace = true 123 | } 124 | } else { 125 | if negativeCheckSigns.contains(last) { 126 | noSpace = true 127 | } 128 | } 129 | } 130 | if noSpace { 131 | return add(char: char) 132 | } 133 | return space(with: "-") 134 | } 135 | } 136 | 137 | func checkSlash(char: Character) -> String.Index { 138 | if isNext(char: "/") { 139 | return addLine() 140 | } else if isNext(char: "*") { 141 | return addToNext(strIndex, stopWord: "*/") 142 | } 143 | return space(with: operatorList[char]!)! 144 | } 145 | 146 | func checkLess(char: Character) throws -> String.Index { 147 | if isNext(char: "#") { 148 | return add(string: "<#") 149 | } 150 | if let result = try string.findGeneric(from: strIndex), !isNext(char: " ") { 151 | retString += result.string 152 | return result.index 153 | } 154 | return space(with: operatorList[char]!)! 155 | } 156 | 157 | func checkQuestion(char: Character) throws -> String.Index { 158 | if isNext(char: "?") { 159 | // MARK: check double optional or nil check 160 | return add(string: "??") 161 | } else if let ternary = string.findTernary(from: strIndex) { 162 | retString.keepSpace() 163 | retString += ternary.string 164 | return ternary.index 165 | } else { 166 | return add(char: char) 167 | } 168 | } 169 | 170 | func checkColon(char: Character) -> String.Index { 171 | _ = checkInCase() 172 | trimWithIndent() 173 | retString += ": " 174 | return string.nextNonSpaceIndex(string.index(after: strIndex)) 175 | } 176 | 177 | func checkHash(char: Character) -> String.Index { 178 | if isNext(string: "#if") { 179 | indent.count += 1 180 | return addLine() // MARK: bypass like '#if swift(>=3)' 181 | } else if isNext(string: "#else") { 182 | indent.count -= 1 183 | trimWithIndent() 184 | indent.count += 1 185 | return addLine() // bypass like '#if swift(>=3)' 186 | } else if isNext(string: "#endif") { 187 | indent.count -= 1 188 | trimWithIndent() 189 | return addLine() // bypass like '#if swift(>=3)' 190 | } else if isNext(char: "!") { // shebang 191 | return addLine() 192 | } 193 | if let index = checkHashQuote(index: strIndex, count: 0) { 194 | return index 195 | } 196 | if let index = add(with: operatorList[char]!) { 197 | return index 198 | } 199 | return add(char: char) 200 | } 201 | 202 | func checkHashQuote(index: String.Index, count: Int) -> String.Index? { 203 | switch string[index] { 204 | case "#": 205 | return checkHashQuote(index: string.index(after: index), count: count + 1) 206 | case "\"": 207 | return addToNext(strIndex, stopWord: "\"" + String(repeating: "#", count: count)) 208 | default: 209 | return nil 210 | } 211 | } 212 | 213 | func checkQuote(char: Character) throws -> String.Index { 214 | if isNext(string: "\"\"\"") { 215 | strIndex = add(string: "\"\"\"") 216 | return addToNext(strIndex, stopWord: "\"\"\"") 217 | } 218 | let quote = try string.findQuote(from: strIndex) 219 | retString += quote.string 220 | return quote.index 221 | } 222 | 223 | func checkLineBreak(char: Character) -> String.Index { 224 | removeUnnecessaryChar() 225 | indent.line += 1 226 | return checkLine(char) 227 | } 228 | 229 | func checkSpace(char: Character) -> String.Index { 230 | if retString.lastWord() == "if" { 231 | let leading = retString.count - newlineIndex 232 | let newIndent = Indent(with: indent, offset: leading, type: IndentType(rawValue: "f")) 233 | indentStack.append(indent) 234 | indent = newIndent 235 | } 236 | retString.keepSpace() 237 | return string.index(after: strIndex) 238 | } 239 | 240 | func checkComma(char: Character) -> String.Index { 241 | trimWithIndent() 242 | retString += ", " 243 | return string.nextNonSpaceIndex(string.index(after: strIndex)) 244 | } 245 | 246 | func checkUpperBlock(char: Character) -> String.Index { 247 | if char == "{" && indent.block == .ifelse { 248 | if let last = indentStack.popLast() { 249 | indent = last 250 | if indent.indentAdd { 251 | indent.indentAdd = false 252 | } 253 | } 254 | } 255 | let offset = retString.count - newlineIndex 256 | let newIndent = Indent(with: indent, offset: offset, type: IndentType(rawValue: char)) 257 | indentStack.append(indent) 258 | indent = newIndent 259 | if indent.block == .curly { 260 | if isNextSwitch { 261 | indent.inSwitch = true 262 | isNextSwitch = false 263 | } 264 | if isNextEnum { 265 | indent.inEnum = true 266 | isNextEnum = false 267 | } 268 | indent.count -= 1 269 | trimWithIndent() 270 | indent.count += 1 271 | if !retString.last.isUpperBlock() { 272 | retString.keepSpace() 273 | } 274 | 275 | retString += "{ " 276 | return string.nextNonSpaceIndex(string.index(after: strIndex)) 277 | } else { 278 | if Indent.paraAlign && char == "(" && isNext(char: "\n") { 279 | indent.count += 1 280 | } 281 | retString.append(char) 282 | return string.nextNonSpaceIndex(string.index(after: strIndex)) 283 | } 284 | } 285 | 286 | func checkLowerBlock(char: Character) -> String.Index { 287 | var addIndentBack = false 288 | if let last = indentStack.popLast() { 289 | indent = last 290 | if indent.indentAdd { 291 | indent.indentAdd = false 292 | addIndentBack = true 293 | } 294 | } else { 295 | indent = Indent() 296 | } 297 | 298 | if char == "}" { 299 | if isNext(char: ".", skipBlank: true) { 300 | trimWithIndent() 301 | } else { 302 | trimWithIndent(addExtra: false) 303 | } 304 | if addIndentBack { 305 | indent.count += 1 306 | } 307 | retString.keepSpace() 308 | let next = string.index(after: strIndex) 309 | if next < string.endIndex && string[next].isAZ() { 310 | retString += "} " 311 | } else { 312 | retString += "}" 313 | } 314 | return next 315 | } 316 | if addIndentBack { 317 | indent.count += 1 318 | } 319 | trimWithIndent() 320 | return add(char: char) 321 | } 322 | 323 | func removeUnnecessaryChar() { 324 | if autoRemoveChar && retString.last == ";" { 325 | retString = String(retString[.. Bool { 330 | if indent.inCase { 331 | indent.inCase = false 332 | indent.leading -= 1 333 | indent.isLeading = false 334 | indent.count += 1 335 | return true 336 | } 337 | return false 338 | } 339 | 340 | func checkLine(_ char: Character, checkLast: Bool = true) -> String.Index { 341 | trim() 342 | newlineIndex = retString.count - 1 343 | if checkLast { 344 | checkLineEndExtra() 345 | } else { 346 | indent.extra = 0 347 | } 348 | indent.indentAdd = false 349 | indent.extraAdd = false 350 | strIndex = add(char: char) 351 | if !isNext(string: "//") { 352 | if isBetween(words: ("if", "let"), ("guard", "let")) { 353 | indent.extra = 1 354 | } else if isPrevious(str: "case") { 355 | indent.extra = 1 356 | } else if isNext(word: "else") { 357 | if retString.lastWord() != "}" { 358 | indent.extra = 1 359 | } 360 | } 361 | addIndent() 362 | } 363 | return string.nextNonSpaceIndex(strIndex) 364 | } 365 | 366 | func checkDefault(char: Character) -> String.Index { 367 | strIndex = add(char: char) 368 | while strIndex < string.endIndex { 369 | let next = string[strIndex] 370 | if next.isAZ() { 371 | strIndex = add(char: next) 372 | } else { 373 | break 374 | } 375 | } 376 | return strIndex 377 | } 378 | 379 | func checkLineChar(char: Character) -> Int? { 380 | switch char { 381 | case "+", "-", "*", "=", ".", "&", "|": 382 | return 1 383 | case ":": 384 | if self.checkInCase() { 385 | return 0 386 | } 387 | if !self.indent.inSwitch { 388 | return 1 389 | } 390 | case ",": 391 | if self.indent.inEnum { 392 | return 0 393 | } 394 | if self.indent.line == 1 && (self.indent.block == .parentheses || self.indent.block == .square) { 395 | self.indent.isLeading = true 396 | } 397 | if self.indent.block == .curly { 398 | return 1 399 | } 400 | default: 401 | break 402 | } 403 | return nil 404 | } 405 | 406 | func checkLineEndExtra() { 407 | guard indent.block != .ifelse else { 408 | return 409 | } 410 | 411 | if let result = checkLineChar(char: retString.last) { 412 | indent.extra = result 413 | return 414 | } 415 | if strIndex < string.endIndex { 416 | let next = string.nextNonSpaceIndex(string.index(after: strIndex)) 417 | if next < string.endIndex { 418 | if let result = checkLineChar(char: string[next]) { 419 | indent.extra = result 420 | } else if string[next] == "?" { 421 | indent.extra = 1 422 | } else { 423 | indent.extra = 0 424 | } 425 | } 426 | // MARK: check next if ? : 427 | } 428 | } 429 | 430 | } 431 | -------------------------------------------------------------------------------- /EditKit/Third Party/SwiftUI View Operations/ToggleBraceLine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SourceEditorCommand.swift 3 | // SwiftUI Tools 4 | // 5 | // Created by Dave Carlton on 8/8/21. 6 | // 7 | 8 | import XcodeKit 9 | 10 | class ToggleBraceLine: XcodeLines { 11 | 12 | override func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 13 | do { 14 | try performSetup(invocation: invocation) 15 | for i in 0 ..< selections.count { 16 | if i < selections.count, let selection = selections[i] as? XCSourceTextRange { 17 | // Look at current line for "{", found has matching "}" line 18 | let found = hasOpenBrace(index: selection.start.line) 19 | if found != 0 { 20 | toggleComment(index: selection.start.line) 21 | toggleComment(index: found + selection.start.line) 22 | } 23 | } else { 24 | completionHandler(XcodeLinesError.invalidLineSelection.nsError) 25 | return 26 | } 27 | } 28 | 29 | invocation.buffer.selections.removeAllObjects() 30 | completionHandler(nil) 31 | } catch { 32 | completionHandler(GenericError.default.nsError) 33 | } 34 | } 35 | } 36 | 37 | 38 | class RemoveBraceLine: XcodeLines { 39 | 40 | override func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 41 | do { 42 | try performSetup(invocation: invocation) 43 | for i in 0 ..< selections.count { 44 | if i < selections.count, let selection = selections[i] as? XCSourceTextRange { 45 | // Look at current line for "{", found has matching "}" line 46 | let found = hasOpenBrace(index: selection.start.line) 47 | if found != 0 { 48 | removeLine(index: found + selection.start.line) 49 | removeLine(index: selection.start.line) 50 | } 51 | } else { 52 | completionHandler(XcodeLinesError.invalidLineSelection.nsError) 53 | return 54 | } 55 | } 56 | 57 | invocation.buffer.selections.removeAllObjects() 58 | completionHandler(nil) 59 | } catch { 60 | completionHandler(GenericError.default.nsError) 61 | } 62 | } 63 | } 64 | 65 | 66 | -------------------------------------------------------------------------------- /EditKit/Third Party/SwiftUI View Operations/ToggleBraceLines.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToggleBraceLines.swift 3 | // SwiftUI Tools 4 | // 5 | // Created by Dave Carlton on 8/9/21. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | import OSLog 11 | 12 | class ToggleBraceLines: XcodeLines { 13 | 14 | override func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 15 | do { 16 | try performSetup(invocation: invocation) 17 | 18 | for i in 0 ..< selections.count { 19 | if i < selections.count, let selection = selections[i] as? XCSourceTextRange { 20 | // Look at current line for "{", found has matching "}" line 21 | let found = hasOpenBrace(index: selection.start.line) 22 | if found != 0 { 23 | for i in selection.start.line ... found + selection.start.line { 24 | toggleComment(index: i) 25 | } 26 | } 27 | } else { 28 | completionHandler(XcodeLinesError.invalidLineSelection.nsError) 29 | return 30 | } 31 | } 32 | 33 | invocation.buffer.selections.removeAllObjects() 34 | completionHandler(nil) 35 | } catch { 36 | completionHandler(GenericError.default.nsError) 37 | } 38 | } 39 | } 40 | 41 | class RemoveBraceLines: XcodeLines { 42 | 43 | override func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 44 | do { 45 | try performSetup(invocation: invocation) 46 | for i in 0 ..< selections.count { 47 | if i < selections.count, let selection = selections[i] as? XCSourceTextRange { 48 | // Look at current line for "{", found has matching "}" line 49 | let found = hasOpenBrace(index: selection.start.line) 50 | if found != 0 { 51 | for i in (selection.start.line ... found + selection.start.line).reversed() { 52 | removeLine(index: i) 53 | } 54 | } 55 | } else { 56 | completionHandler(XcodeLinesError.invalidLineSelection.nsError) 57 | return 58 | } 59 | } 60 | 61 | invocation.buffer.selections.removeAllObjects() 62 | completionHandler(nil) 63 | } catch { 64 | completionHandler(GenericError.default.nsError) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /EditKit/Third Party/SwiftUI View Operations/XcodeLines.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcodeLines.swift 3 | // SwiftUI Tools 4 | // 5 | // Created by Dave Carlton on 8/9/21. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | import AppKit 11 | import OSLog 12 | 13 | let supportUTIs = [ 14 | "com.apple.dt.playground", 15 | "public.swift-source", 16 | "com.apple.dt.playgroundpage"] 17 | 18 | class XcodeLines: NSObject, XCSourceEditorCommand { 19 | 20 | enum XcodeLinesError: Error, LocalizedError { 21 | case incompatibleFileType 22 | case invalidLineSelection 23 | 24 | var errorDescription: String? { 25 | switch self { 26 | case .incompatibleFileType: 27 | return "Incomaptible file type found (only Swift & Playgrounds supported)." 28 | case .invalidLineSelection: 29 | return "Please verify line selections and try again." 30 | } 31 | } 32 | } 33 | 34 | var newLines: [String] = [] 35 | var invocation: XCSourceEditorCommandInvocation? 36 | var lines: NSMutableArray = [] 37 | var selections: NSMutableArray = [] 38 | var log: Logger = Logger() 39 | 40 | func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 41 | completionHandler(nil) 42 | return 43 | } 44 | 45 | func performSetup(invocation: XCSourceEditorCommandInvocation) throws { 46 | self.invocation = invocation 47 | 48 | let uti = invocation.buffer.contentUTI 49 | 50 | guard supportUTIs.contains(uti) else { 51 | throw XcodeLinesError.incompatibleFileType 52 | } 53 | 54 | if invocation.buffer.usesTabsForIndentation { 55 | Indent.char = "\t" 56 | } else { 57 | Indent.char = String(repeating: " ", count: invocation.buffer.indentationWidth) 58 | } 59 | 60 | let parser = SwiftParser(string: invocation.buffer.completeBuffer) 61 | newLines = try parser.format().components(separatedBy: "\n") 62 | lines = invocation.buffer.lines 63 | selections = invocation.buffer.selections 64 | } 65 | 66 | func findBalanced(lines: ArraySlice) -> Int { 67 | var b: Int = 0 68 | var c: Int = 0 69 | for line in lines { 70 | if line.contains("{") { b += 1 } 71 | if line.contains("}") { b -= 1 } 72 | if b == 0 { 73 | return c 74 | } 75 | c += 1 76 | } 77 | return 0 78 | } 79 | 80 | func updateLine(index: Int) { 81 | guard index < newLines.count, index < lines.count else { 82 | return 83 | } 84 | if let line = lines[index] as? String { 85 | let newLine = newLines[index] + "\n" 86 | if newLine != line { 87 | lines[index] = newLine 88 | } 89 | } 90 | } 91 | 92 | func toggleComment(index: Int) { 93 | var newLine:String 94 | 95 | guard index < newLines.count, index < lines.count else { 96 | return 97 | } 98 | 99 | if let line = lines[index] as? String { 100 | if line.hasPrefix("//") { 101 | let r = line.range(of: "//") 102 | newLine = String(line.suffix(from: r!.upperBound)) 103 | } else { 104 | newLine = "//" + newLines[index] + "\n" 105 | } 106 | if newLine != line { 107 | lines[index] = newLine 108 | } 109 | } 110 | } 111 | 112 | func removeLine(index: Int) { 113 | guard index < newLines.count, index < lines.count else { 114 | return 115 | } 116 | 117 | invocation?.buffer.lines.removeObject(at: index) 118 | 119 | } 120 | 121 | func hasOpenBrace(index: Int) -> Int { 122 | var found = 0 123 | let currentLine = newLines[index] 124 | if currentLine.contains("{") { 125 | let rangeLines = newLines.suffix(from: index) 126 | found = findBalanced(lines: rangeLines) 127 | } 128 | return found 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /EditKit/WrapInIfDefCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WrapInIfDefCommand.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 12/17/22. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | 11 | final class WrapInIfDefCommand { 12 | static func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 13 | // Verifies a selection exists 14 | guard let selections = invocation.buffer.selections as? [XCSourceTextRange], let selection = selections.first else { 15 | completionHandler(GenericError.default.nsError) 16 | return 17 | } 18 | 19 | let startIndex = selection.start.line 20 | let endIndex = selection.end.line 21 | let selectedRange = NSRange(location: startIndex, length: 1 + endIndex - startIndex) 22 | 23 | // Grabs the currently selected lines 24 | let selectedLines = invocation.buffer.lines.subarray(with: selectedRange) 25 | 26 | // Wraps the selection in an #ifdef and uses the selection for both parts of the conditional body 27 | invocation.buffer.lines.insert("#if swift(>=5.5)", at: startIndex) 28 | 29 | for string in selectedLines.reversed() { 30 | invocation.buffer.lines.insert(string, at: startIndex + 1) 31 | } 32 | 33 | invocation.buffer.lines.insert("#else", at: startIndex + selectedLines.count + 1) 34 | invocation.buffer.lines.insert("#endif", at: endIndex + selectedLines.count + 3) 35 | 36 | completionHandler(nil) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /EditKit/WrapInLocalizedStringCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WrapInLocalizedString.swift 3 | // EditKit 4 | // 5 | // Created by Aryaman Sharda on 1/15/23. 6 | // 7 | 8 | import Foundation 9 | import XcodeKit 10 | 11 | final class WrapInLocalizedStringCommand { 12 | static func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 13 | // Ensure a selection is provided 14 | guard let selection = invocation.buffer.selections.firstObject as? XCSourceTextRange else { 15 | completionHandler(GenericError.default.nsError) 16 | return 17 | } 18 | 19 | // Keep an array of changed line indices. 20 | var changedLineIndexes = [Int]() 21 | 22 | for lineIndex in selection.start.line...selection.end.line { 23 | guard let originalLine = invocation.buffer.lines[lineIndex] as? String else { 24 | continue 25 | } 26 | 27 | do { 28 | // Find and capture text in quotes 29 | let regex = try NSRegularExpression(pattern: "\"(.*?)\"") 30 | let matches = regex.matches(in: originalLine, options: [], range: NSRange(location: 0, length: originalLine.utf16.count)) 31 | 32 | var modifiedLine = originalLine 33 | for match in matches { 34 | // Extract the substring matching the capture group 35 | if let substringRange = Range(match.range, in: originalLine) { 36 | let quotedText = String(originalLine[substringRange]) 37 | let localizedStringTemplate = "NSLocalizedString(<#T##String#>, value: \(quotedText), comment: \(quotedText))" 38 | modifiedLine = modifiedLine.replacingOccurrences(of: quotedText, with: localizedStringTemplate) 39 | } 40 | } 41 | 42 | // Only update lines that have changed. 43 | if originalLine != modifiedLine { 44 | changedLineIndexes.append(lineIndex) 45 | invocation.buffer.lines[lineIndex] = modifiedLine 46 | } 47 | } catch { 48 | // Regex was bad! 49 | completionHandler(GenericError.default.nsError) 50 | } 51 | } 52 | 53 | // Select all lines that were replaced. 54 | let updatedSelections: [XCSourceTextRange] = changedLineIndexes.map { lineIndex in 55 | let lineSelection = XCSourceTextRange() 56 | lineSelection.start = XCSourceTextPosition(line: lineIndex, column: 0) 57 | lineSelection.end = XCSourceTextPosition(line: lineIndex + 1, column: 0) 58 | return lineSelection 59 | } 60 | 61 | // Set selections then return with no error. 62 | invocation.buffer.selections.setArray(updatedSelections) 63 | completionHandler(nil) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /EditKitPro.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /EditKitPro.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /EditKitPro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "roadmap", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/AvdLee/Roadmap.git", 7 | "state" : { 8 | "branch" : "main", 9 | "revision" : "d3eb0cd195883b8d32f923174b268cdc177f312f" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /EditKitPro.xcodeproj/xcshareddata/xcschemes/EditKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 47 | 48 | 58 | 62 | 63 | 64 | 70 | 71 | 72 | 73 | 81 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /EditKitPro.xcodeproj/xcshareddata/xcschemes/EditKitPro.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 40 | 41 | 42 | 45 | 51 | 52 | 53 | 54 | 55 | 65 | 67 | 73 | 74 | 75 | 76 | 82 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /EditKitPro.xcodeproj/xcuserdata/aryamansharda.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 9 | 21 | 22 | 36 | 37 | 51 | 52 | 53 | 54 | 55 | 57 | 69 | 70 | 71 | 73 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /EditKitPro.xcodeproj/xcuserdata/aryamansharda.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | EditKit.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 1 11 | 12 | EditKitPro.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 93B282FC294AEDD200364206 21 | 22 | primary 23 | 24 | 25 | 93B2830D294AEDD200364206 26 | 27 | primary 28 | 29 | 30 | 93B28317294AEDD200364206 31 | 32 | primary 33 | 34 | 35 | 93B2832E294AEDDD00364206 36 | 37 | primary 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /EditKitPro/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 | -------------------------------------------------------------------------------- /EditKitPro/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 26 (1)-16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Group 26 (1)-32.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Group 26 (1)-32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Group 26 (1)-64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Group 26 (1)-128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Group 26 (1)-256.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Group 26 (1)-256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Group 26 (1)-512.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Group 26 (1)-512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Group 26 (1)-1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /EditKitPro/Assets.xcassets/AppIcon.appiconset/Group 26 (1)-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryamansharda/EditKitPro/8320a5820994132a42574f522b1094d8f1930a75/EditKitPro/Assets.xcassets/AppIcon.appiconset/Group 26 (1)-1024.png -------------------------------------------------------------------------------- /EditKitPro/Assets.xcassets/AppIcon.appiconset/Group 26 (1)-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryamansharda/EditKitPro/8320a5820994132a42574f522b1094d8f1930a75/EditKitPro/Assets.xcassets/AppIcon.appiconset/Group 26 (1)-128.png -------------------------------------------------------------------------------- /EditKitPro/Assets.xcassets/AppIcon.appiconset/Group 26 (1)-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryamansharda/EditKitPro/8320a5820994132a42574f522b1094d8f1930a75/EditKitPro/Assets.xcassets/AppIcon.appiconset/Group 26 (1)-16.png -------------------------------------------------------------------------------- /EditKitPro/Assets.xcassets/AppIcon.appiconset/Group 26 (1)-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryamansharda/EditKitPro/8320a5820994132a42574f522b1094d8f1930a75/EditKitPro/Assets.xcassets/AppIcon.appiconset/Group 26 (1)-256.png -------------------------------------------------------------------------------- /EditKitPro/Assets.xcassets/AppIcon.appiconset/Group 26 (1)-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryamansharda/EditKitPro/8320a5820994132a42574f522b1094d8f1930a75/EditKitPro/Assets.xcassets/AppIcon.appiconset/Group 26 (1)-32.png -------------------------------------------------------------------------------- /EditKitPro/Assets.xcassets/AppIcon.appiconset/Group 26 (1)-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryamansharda/EditKitPro/8320a5820994132a42574f522b1094d8f1930a75/EditKitPro/Assets.xcassets/AppIcon.appiconset/Group 26 (1)-512.png -------------------------------------------------------------------------------- /EditKitPro/Assets.xcassets/AppIcon.appiconset/Group 26 (1)-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryamansharda/EditKitPro/8320a5820994132a42574f522b1094d8f1930a75/EditKitPro/Assets.xcassets/AppIcon.appiconset/Group 26 (1)-64.png -------------------------------------------------------------------------------- /EditKitPro/Assets.xcassets/BackgroundColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.204", 9 | "green" : "0.169", 10 | "red" : "0.165" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /EditKitPro/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /EditKitPro/Assets.xcassets/Logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Group 26 (1).png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EditKitPro/Assets.xcassets/Logo.imageset/Group 26 (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aryamansharda/EditKitPro/8320a5820994132a42574f522b1094d8f1930a75/EditKitPro/Assets.xcassets/Logo.imageset/Group 26 (1).png -------------------------------------------------------------------------------- /EditKitPro/EditKitPro.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /EditKitPro/EditKitProApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditKitProApp.swift 3 | // EditKitPro 4 | // 5 | // Created by Aryaman Sharda on 12/14/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct EditKitProApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | LandingPageView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /EditKitPro/LandingPageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // EditKitPro 4 | // 5 | // Created by Aryaman Sharda on 12/14/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LandingPageView: View { 11 | 12 | @State var isPopover = false 13 | 14 | var body: some View { 15 | ZStack { 16 | Color("BackgroundColor").edgesIgnoringSafeArea(.all) 17 | VStack(alignment: .center) { 18 | Image("Logo") 19 | .resizable() 20 | .scaledToFit() 21 | .frame(width: 200, height: 200) 22 | 23 | titleView 24 | 25 | VStack(spacing: 8) { 26 | voteOnFeaturesButton 27 | contributeButton 28 | 29 | Divider() 30 | .foregroundColor(.white) 31 | .frame(width: 200) 32 | .padding() 33 | 34 | watchTutorialButton 35 | readDocumentationButton 36 | messageDeveloperButton 37 | } 38 | .padding(.vertical, 16) 39 | } 40 | .foregroundColor(.white) 41 | } 42 | } 43 | } 44 | 45 | extension LandingPageView { 46 | var titleView: some View { 47 | VStack(alignment: .center) { 48 | HStack(alignment: .firstTextBaseline) { 49 | Text("EditKit Pro") 50 | .font(.title) 51 | .fontWeight(.bold) 52 | Text("v1.0") 53 | .font(.title3) 54 | .fontWeight(.regular) 55 | } 56 | 57 | Text("Multi-tool for Xcode") 58 | .font(.title3) 59 | .fontWeight(.regular) 60 | } 61 | } 62 | 63 | var voteOnFeaturesButton: some View { 64 | Button(action: { self.isPopover.toggle() }) { 65 | HStack { 66 | Image(systemName: "gift") 67 | .shadow(radius: 2.0) 68 | Text("Want To Vote On New Features?") 69 | .font(.title3) 70 | .fontWeight(.medium) 71 | .multilineTextAlignment(.center) 72 | .fixedSize(horizontal: false, vertical: true) 73 | } 74 | }.popover(isPresented: self.$isPopover, arrowEdge: .bottom) { 75 | RoadmapContainerView() 76 | } 77 | .modifier(StandardButtonStyle(bodyColor: .green)) 78 | } 79 | 80 | var contributeButton: some View { 81 | Button(action: { 82 | NSWorkspace.shared.open(URL(string: "https://github.com/aryamansharda/EditKitPro")!) 83 | }) { 84 | HStack { 85 | Image(systemName: "swift") 86 | .shadow(radius: 2.0) 87 | Text("Want To Contribute?") 88 | .font(.title3) 89 | .fontWeight(.medium) 90 | .multilineTextAlignment(.center) 91 | .fixedSize(horizontal: false, vertical: true) 92 | } 93 | } 94 | .modifier(StandardButtonStyle(bodyColor: .green)) 95 | } 96 | 97 | var watchTutorialButton: some View { 98 | Button { 99 | NSWorkspace.shared.open(URL(string: "https://www.youtube.com/watch?v=ZM4VHOvPdQU&t=5s&ab_channel=AryamanSharda")!) 100 | } label: { 101 | HStack { 102 | Image(systemName: "play") 103 | Text("Watch Tutorial") 104 | .font(.title3) 105 | .fontWeight(.medium) 106 | } 107 | } 108 | .modifier(StandardButtonStyle(bodyColor: .blue)) 109 | } 110 | 111 | var readDocumentationButton: some View { 112 | Button { 113 | NSWorkspace.shared.open(URL(string: "https://digitalbunker.dev/editkit-pro/")!) 114 | } label: { 115 | HStack { 116 | Image(systemName: "book") 117 | Text("Read Documentation") 118 | .font(.title3) 119 | .fontWeight(.medium) 120 | } 121 | } 122 | .modifier(StandardButtonStyle(bodyColor: .blue)) 123 | } 124 | 125 | var messageDeveloperButton: some View { 126 | Button { 127 | NSWorkspace.shared.open(URL(string: "mailto:aryaman@digitalbunker.dev")!) 128 | } label: { 129 | HStack(spacing: 8) { 130 | Image(systemName: "message") 131 | VStack { 132 | Text("Message Me") 133 | .font(.title3) 134 | .fontWeight(.medium) 135 | } 136 | } 137 | } 138 | .modifier(StandardButtonStyle(bodyColor: .blue)) 139 | } 140 | } 141 | 142 | struct Previews_LandingPageView_Previews: PreviewProvider { 143 | static var previews: some View { 144 | LandingPageView() 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /EditKitPro/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /EditKitPro/RoadmapView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoadmapView.swift 3 | // EditKitPro 4 | // 5 | // Created by Aryaman Sharda on 2/23/23. 6 | // 7 | 8 | import SwiftUI 9 | import Roadmap 10 | 11 | struct RoadmapContainerView: View { 12 | private let configuration = RoadmapConfiguration( 13 | roadmapJSONURL: URL(string: "https://simplejsoncms.com/api/w1wxyqgoqv")!, 14 | namespace: "roadmap", 15 | style: RoadmapTemplate.playful.style 16 | ) 17 | 18 | var body: some View { 19 | RoadmapView(configuration: configuration) 20 | .frame(width: 800, height: 600) 21 | } 22 | } 23 | 24 | struct RoadmapView_Previews: PreviewProvider { 25 | static var previews: some View { 26 | RoadmapContainerView() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /EditKitPro/StandardButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StandardButtonStyle.swift 3 | // EditKitPro 4 | // 5 | // Created by Aryaman Sharda on 2/25/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StandardButtonStyle: ViewModifier { 11 | let bodyColor: Color 12 | 13 | func body(content: Content) -> some View { 14 | content 15 | .buttonStyle(.plain) 16 | .frame(width: 250) 17 | .padding(.all, 16) 18 | .contentShape(Rectangle()) 19 | .background( 20 | RoundedRectangle(cornerRadius: 50, style: .continuous).fill(bodyColor.opacity(0.5)) 21 | ) 22 | .overlay( 23 | RoundedRectangle(cornerRadius: 50, style: .continuous) 24 | .strokeBorder(bodyColor, lineWidth: 1) 25 | ) 26 | .focusable(false) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /EditKitProTests/EditKitProTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditKitProTests.swift 3 | // EditKitProTests 4 | // 5 | // Created by Aryaman Sharda on 12/14/22. 6 | // 7 | 8 | import XCTest 9 | @testable import EditKitPro 10 | 11 | final class EditKitProTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /EditKitProUITests/EditKitProUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditKitProUITests.swift 3 | // EditKitProUITests 4 | // 5 | // Created by Aryaman Sharda on 12/14/22. 6 | // 7 | 8 | import XCTest 9 | 10 | final class EditKitProUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /EditKitProUITests/EditKitProUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditKitProUITestsLaunchTests.swift 3 | // EditKitProUITests 4 | // 5 | // Created by Aryaman Sharda on 12/14/22. 6 | // 7 | 8 | import XCTest 9 | 10 | final class EditKitProUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aryaman Sharda 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 | ![](Assets/banner.png) 2 | 3 | # EditKitPro 4 | EditKit Pro provides a suite of tools to help you write better, cleaner, and more efficient code. Whether you need to quickly format your code, create Codable models, generate mock data, or move around in SwiftUI more efficiently, EditKit Pro has you covered. 5 | 6 | This is an open-source Xcode Editor Extension with a variety of mini-tools for iOS / macOS Developers. 7 | 8 | Demos of EditKit can be found on the [blog post](https://digitalbunker.dev/editkit-pro/) and this [YouTube Video](https://www.youtube.com/watch?v=ZM4VHOvPdQU&t=6s&ab_channel=AryamanSharda). 9 | 10 | ## Features 11 | The current version of EditKit supports the following features: 12 | 13 | - Align around equals 14 | - Auto `MARK` extensions 15 | - Beautify JSON 16 | - Convert JSON to Codable 17 | - Copy as Markdown 18 | - Create type definition 19 | - Format as multi-line 20 | - Format as single-line 21 | - Convert selection to snakecase 22 | - Convert selection to camelcase 23 | - Convert selection to Pascal case 24 | - Search selection on GitHub, Google, StackOverflow 25 | - Sort imports 26 | - Sort lines alphabetically (ascending and descending) 27 | - Sort lines by length 28 | - Strip trailing whitespaces 29 | - Wrap in `#ifdef` 30 | - Wrap in `NSLocalizedString` 31 | - SwiftUI -> Disable outer view 32 | - SwiftUI -> Delete outer view 33 | - SwiftUI -> Disable outer view 34 | - SwiftUI -> Delete view 35 | 36 | ## Installation 37 | The most convenient way of installing the current release is through the [App Store](https://apps.apple.com/us/app/editkit-pro/id1659984546?mt=12). Once installed, you'll need to open `System Preferences -> Extensions -> Enable EditKit Pro`. 38 | 39 | If EditKit Pro is not visible in Extensions, this may be due to multiple conflicting Xcode installations. 40 | 41 | Alternatively, you can clone this Xcode project: 42 | 43 | 1. Once downloaded, open the .xcodeproj. 44 | 2. Before running, make sure to change the Team to your Personal Development Team for both the main app target and the `EditKit` extension. The extension will not appear in Xcode unless it is signed correctly. 45 | 3. Select the `EditKit` extension and hit Run. 46 | 4. You should see a debug version of Xcode appear. Pick any project or create a new one. 47 | 5. Navigate to a source code file. Now, in the Editor dropdown menu, you should now see an entry for `EditKit`. 48 | 49 | ## Requirements 50 | Please make sure you **only have one valid** installation of Xcode on your machine and have a valid Apple Developer account as signing the extension will be required in order to run it locally. 51 | 52 | ## Contributing 53 | All contributions are welcome. Just fork the repo and make a pull request. 54 | 55 | 1. In order to add new functionality to `EditKit`, create a new entry in `EditorCommandIdentifier` and a assign a unqiue key for your new command. 56 | 2. Then, in the `EditKit` extension's `Info.plist`, add an entry in `XCSourceEditorCommandDefinitions` for your new command. 57 | 3. In `EditorController.swift`, add a case to the `handle` function for your new command. 58 | 4. Now, you implement your new functionality by creating a new `XCSourceEditorCommand` class (i.e. `BeautifyJSONCommand`) or creating a class that operates on the `XCSourceEditorCommandInvocation` provided by the Xcode Editor Extension (i.e. `AlignAroundEqualsCommand` 59 | 60 | ``` 61 | class AlignAroundEqualsCommand { 62 | static func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 63 | ... 64 | } 65 | } 66 | ``` 67 | 68 | or 69 | 70 | ``` 71 | class BeautifyJSONCommand: NSObject, XCSourceEditorCommand { 72 | func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (Error?) -> Void) { 73 | .... 74 | } 75 | } 76 | ``` 77 | 78 | All files in the `Third Party` folder are modified versions of the open source libraries mentioned below. 79 | 80 | ## Open Source Dependencies 81 | EditKit would not have been possible without the help and inspiration from these open source libraries: 82 | 83 | - [XShared](https://github.com/Otbivnoe/XShared/) 84 | - [Xcode Source Editor Extension](https://github.com/cellular/xcodeextensionmark-swift/) 85 | - [alanzeino](https://github.com/alanzeino/source-editor-extension/) 86 | - [DeclareType](https://github.com/timaktimak/DeclareType) 87 | - [Sorter](https://github.com/aniltaskiran/LazyXcode/) 88 | - [Multiliner](https://github.com/aheze/Multiliner/) 89 | - [swiftuitools](https://github.com/tgunr/swiftuitools/) 90 | - [Finch](https://github.com/NicholasBellucci/Finch/) 91 | 92 | Note: Many of their original implementations have modified to support Swift 5.7+ and to fix bugs. 93 | 94 | ## Contact 95 | If you have any questions, feel free to message me at [aryaman@digitalbunker.dev](mailto:aryaman@digitalbunker.dev) or on [Twitter](https://twitter.com/aryamansharda). 96 | --------------------------------------------------------------------------------