├── 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 | 
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 |
--------------------------------------------------------------------------------