├── .gitignore
├── Extension
├── Extension.entitlements
├── Info.plist
├── SourceEditorCommand.swift
├── SourceEditorExtension.swift
├── Utilities
│ ├── SweetSourceEditorCommand.swift
│ ├── UTI.swift
│ └── XCTextBuffer+Extension.swift
└── XcodeExtensionSample-Bridging-Header.h
├── LICENSE
├── README.md
├── XcodeExtensionSample.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
├── xcshareddata
│ └── xcschemes
│ │ ├── Extension.xcscheme
│ │ ├── XcodeExtensionSample.xcscheme
│ │ └── XcodeExtensionSampleHelper.xcscheme
└── xcuserdata
│ └── tksk_mba.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── XcodeExtensionSample
├── AppDelegate.swift
├── Assets.xcassets
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Base.lproj
│ └── Main.storyboard
├── Info.plist
├── ViewController.swift
└── XcodeExtensionSample.entitlements
└── XcodeExtensionSampleHelper
├── Info.plist
├── XcodeExtensionSampleHelper-Bridging-Header.h
├── XcodeExtensionSampleHelper.entitlements
├── XcodeExtensionSampleHelper.swift
├── XcodeExtensionSampleHelperProtocol.h
└── main.m
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xccheckout
23 | *.xcscmblueprint
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 | *.ipa
28 | *.dSYM.zip
29 | *.dSYM
30 |
31 | ## Playgrounds
32 | timeline.xctimeline
33 | playground.xcworkspace
34 |
35 | # Swift Package Manager
36 | #
37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
38 | # Packages/
39 | # Package.pins
40 | .build/
41 |
42 | # CocoaPods
43 | #
44 | # We recommend against adding the Pods directory to your .gitignore. However
45 | # you should judge for yourself, the pros and cons are mentioned at:
46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
47 | #
48 | # Pods/
49 |
50 | # Carthage
51 | #
52 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
53 | # Carthage/Checkouts
54 |
55 | Carthage/Build
56 |
57 | # fastlane
58 | #
59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
60 | # screenshots whenever they are needed.
61 | # For more information about the recommended setup visit:
62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
63 |
64 | fastlane/report.xml
65 | fastlane/Preview.html
66 | fastlane/screenshots
67 | fastlane/test_output
68 |
--------------------------------------------------------------------------------
/Extension/Extension.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.application-groups
8 |
9 | $(TeamIdentifierPrefix)io.github.takasek.XcodeExtensionSample
10 |
11 | com.apple.security.network.client
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Extension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Extension
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | Sample
17 | CFBundlePackageType
18 | XPC!
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSExtension
26 |
27 | NSExtensionAttributes
28 |
29 | XCSourceEditorCommandDefinitions
30 |
31 |
32 | XCSourceEditorCommandClassName
33 | $(PRODUCT_MODULE_NAME).SourceEditorCommand
34 | XCSourceEditorCommandIdentifier
35 | $(PRODUCT_BUNDLE_IDENTIFIER).SourceEditorCommand
36 | XCSourceEditorCommandName
37 | Source Editor Command
38 |
39 |
40 | XCSourceEditorExtensionPrincipalClass
41 | $(PRODUCT_MODULE_NAME).SourceEditorExtension
42 |
43 | NSExtensionPointIdentifier
44 | com.apple.dt.Xcode.extension.source-editor
45 |
46 | NSHumanReadableCopyright
47 | Copyright © 2017年 takasek. All rights reserved.
48 |
49 |
50 |
--------------------------------------------------------------------------------
/Extension/SourceEditorCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SourceEditorCommand.swift
3 | // Extension
4 | //
5 | // Created by Yoshitaka Seki on 2017/09/07.
6 | // Copyright © 2017年 takasek. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XcodeKit
11 | import Cocoa
12 |
13 | final class PasteboardOutputCommand: SweetSourceEditorCommand {
14 | override class var commandName: String {
15 | return "file UTI -> PasteBoard"
16 | }
17 |
18 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool {
19 | let pasteboard = NSPasteboard.general
20 | pasteboard.declareTypes([.string], owner: nil)
21 | pasteboard.setString(textBuffer.contentUTI, forType: .string)
22 |
23 | return true
24 | }
25 | }
26 |
27 | final class PasteboardInputCommand: SweetSourceEditorCommand {
28 | override class var commandName: String {
29 | return "PasteBoard -> cursor place"
30 | }
31 |
32 | enum Error: MessagedError {
33 | case hasNoText
34 | var message: String {
35 | switch self {
36 | case .hasNoText: return "Pasteboard has no text."
37 | }
38 | }
39 | }
40 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool {
41 | let pasteboard = NSPasteboard.general
42 |
43 | guard let text = pasteboard.pasteboardItems?.first?
44 | .string(forType: NSPasteboard.PasteboardType(
45 | rawValue: "public.utf8-plain-text"
46 | )) else { throw Error.hasNoText }
47 |
48 | try textBuffer.replaceSelection(by: text)
49 |
50 | return true
51 | }
52 | }
53 |
54 | final class OpenAppCommand: SweetSourceEditorCommand {
55 | override class var commandName: String {
56 | return "open Calendar"
57 | }
58 |
59 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool {
60 |
61 | NSWorkspace.shared.launchApplication("Calendar")
62 |
63 | return true
64 | }
65 | }
66 |
67 | final class URLSchemeCommand: SweetSourceEditorCommand {
68 | override class var commandName: String {
69 | return "selected text -> twitter://post"
70 | }
71 |
72 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool {
73 | let text = textBuffer.selectedText(includesUnselectedStartAndEnd: false, trimsIndents: true)
74 |
75 | var c = URLComponents(string: "twitter://post")!
76 | c.queryItems = [
77 | URLQueryItem(name: "message", value: text)
78 | ]
79 | NSWorkspace.shared.open(c.url!)
80 |
81 | return true
82 | }
83 | }
84 |
85 | final class LocalCommandCommand: SweetSourceEditorCommand {
86 | override class var commandName: String {
87 | return "completeBuffer -> uppercased by tr -> completeBuffer"
88 | }
89 |
90 | enum Error: MessagedError {
91 | case commandFailed(String)
92 | var message: String {
93 | switch self {
94 | case .commandFailed(let message): return message
95 | }
96 | }
97 | }
98 |
99 | private func runTask(command: String, arguments: [String], standardInput: Pipe? = nil) throws -> Pipe {
100 | let task = Process(), standardOutput = Pipe(), standardError = Pipe()
101 | task.launchPath = "/usr/bin/env"
102 | task.arguments = [command] + arguments
103 | task.currentDirectoryPath = NSTemporaryDirectory()
104 | task.standardInput = standardInput
105 | task.standardOutput = standardOutput
106 | task.standardError = standardError
107 | task.launch()
108 | task.waitUntilExit()
109 |
110 | guard task.terminationStatus == 0 else {
111 | // 異常終了
112 | let errorOutput = String(data: standardError.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
113 | throw Error.commandFailed(errorOutput)
114 | }
115 | return standardOutput
116 | }
117 |
118 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool {
119 | let tmpFilePath = NSTemporaryDirectory().appending("inputFile")
120 |
121 | try textBuffer.completeBuffer.write(toFile: tmpFilePath, atomically: true, encoding: .utf8)
122 |
123 | let catOutput = try runTask(
124 | command: "cat",
125 | arguments: [tmpFilePath]
126 | )
127 | let trOutput = try runTask(
128 | command: "tr",
129 | arguments: ["[:lower:]", "[:upper:]"],
130 | standardInput: catOutput
131 | )
132 | textBuffer.completeBuffer = String(data: trOutput.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
133 |
134 | return true
135 | }
136 | }
137 |
138 | final class NetworkCommand: SweetSourceEditorCommand {
139 | override class var commandName: String {
140 | return "URLRequest -> cursor place"
141 | }
142 |
143 | enum MyError: MessagedError {
144 | case timedOut
145 | case connectionFailed(NSError?)
146 | var message: String {
147 | switch self {
148 | case .timedOut: return "connection timed out."
149 | case .connectionFailed(let error): return error?.localizedDescription ?? "unknown error."
150 | }
151 | }
152 | }
153 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool {
154 | print(textBuffer.contentUTI)
155 |
156 | enum Result {
157 | case success(String)
158 | case fail(MyError)
159 | }
160 | let urlRequest = URLRequest(url: URL(string: "https://httpbin.org/get")!)
161 |
162 | let semaphore = DispatchSemaphore(value: 0)
163 | var result: Result = .fail(.timedOut)
164 | let task = URLSession.shared.dataTask(with: urlRequest) { data, _, error in
165 | if let res = data.flatMap({ String(data: $0, encoding: .utf8) }) {
166 | result = .success(res)
167 | } else {
168 | result = .fail(.connectionFailed(error as NSError?))
169 | }
170 | semaphore.signal()
171 | }
172 | task.resume()
173 | _ = semaphore.wait(timeout: .now() + 10)
174 |
175 | switch result {
176 | case .fail(let e): throw e
177 | case .success(let r): try textBuffer.replaceSelection(by: r)
178 | }
179 |
180 | return true
181 | }
182 | }
183 |
184 | final class ToDesktopCommand1: SweetSourceEditorCommand {
185 | override class var commandName: String {
186 | return "completeBuffer -> desktop (permission denied)"
187 | }
188 |
189 | // this command doesn't work because of permission. You should pass through XPC.
190 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool {
191 | let dir = NSSearchPathForDirectoriesInDomains(
192 | .desktopDirectory, .userDomainMask, true
193 | ).first!
194 |
195 | try textBuffer.completeBuffer.write(
196 | toFile: dir + "/outputFile",
197 | atomically: true, encoding: .utf8
198 | )
199 |
200 | return true
201 | }
202 | }
203 |
204 | final class ToDesktopCommand2: SweetSourceEditorCommand {
205 | override class var commandName: String {
206 | return "completeBuffer -> (XPC) -> desktop"
207 | }
208 |
209 | enum Error: MessagedError {
210 | case connectionFailed
211 | case executionFailed
212 | var message: String {
213 | switch self {
214 | case .connectionFailed: return "Failed to make connection."
215 | case .executionFailed: return "Execution failed."
216 | }
217 | }
218 | }
219 |
220 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool {
221 | let connection = NSXPCConnection(serviceName: "io.github.takasek.XcodeExtensionSampleHelper")
222 | connection.remoteObjectInterface = NSXPCInterface(with: XcodeExtensionSampleHelperProtocol.self)
223 | defer {
224 | connection.invalidate()
225 | }
226 | guard let helper = connection.remoteObjectProxy as? XcodeExtensionSampleHelperProtocol else {
227 | throw Error.connectionFailed
228 | }
229 | connection.resume()
230 |
231 | let semaphore = DispatchSemaphore(value: 0)
232 | var isSuccess = false
233 |
234 | connection.invalidationHandler = {
235 | print("invalid!")
236 | semaphore.signal()
237 | }
238 |
239 | let dir = NSSearchPathForDirectoriesInDomains(
240 | .desktopDirectory, .userDomainMask, true
241 | ).first!
242 |
243 | // this XPC connection doesn't work. I don't know why...
244 | // see fine example at https://github.com/norio-nomura/SwiftLintForXcode
245 | helper.write(text: textBuffer.completeBuffer, to: dir) { _ in
246 | isSuccess = true
247 | semaphore.signal()
248 | }
249 | _ = semaphore.wait(timeout: .now() + 10)
250 |
251 | if !isSuccess {
252 | throw Error.executionFailed
253 | }
254 |
255 | return true
256 | }
257 | }
258 |
259 | extension UserDefaults {
260 | @objc dynamic var valueFromApp: String? {
261 | return string(forKey: "valueFromApp")
262 | }
263 | }
264 |
265 | final class FileSelectionCommand: SweetSourceEditorCommand {
266 | override class var commandName: String {
267 | return "(App by URLScheme) -> select a file -> (UserDefaults) -> cursor place"
268 | }
269 |
270 | private var _applicationWillTerminate: (() -> Void)?
271 | @objc private func applicationWillTerminate(notification: Notification) {
272 | _applicationWillTerminate?()
273 | }
274 |
275 | enum Error: MessagedError {
276 | case noFileSelected
277 | var message: String {
278 | switch self {
279 | case .noFileSelected: return "No file selected."
280 | }
281 | }
282 | }
283 |
284 | override func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool {
285 | let semaphore = DispatchSemaphore(value: 0)
286 |
287 | var selectedResult: String?
288 |
289 | // The command expects that the app set selected file path to UserDefaults.
290 | let userDefaults = UserDefaults(suiteName: "42U7855PYX.io.github.takasek.XcodeExtensionSample")!
291 | userDefaults.synchronize()
292 | let observation = userDefaults.observe(\UserDefaults.valueFromApp, options:[.old, .new]) { ud, change in
293 | selectedResult = change.newValue?.flatMap { $0 }
294 | semaphore.signal()
295 | }
296 |
297 | // this observation also works.
298 | DistributedNotificationCenter.default().addObserver(
299 | self,
300 | selector: #selector(FileSelectionCommand.applicationWillTerminate(notification:)),
301 | name: Notification.Name("XcodeExtensionSample.applicationWillTerminate"),
302 | object: nil,
303 | suspensionBehavior: .deliverImmediately
304 | )
305 | _applicationWillTerminate = {
306 | // If the app terminated by user unexpectedly, this observation signals the semaphore.
307 | semaphore.signal()
308 | }
309 |
310 | // Open App via URL Scheme
311 | var c = URLComponents(string: "xcextsample://")!
312 | c.queryItems = [
313 | URLQueryItem(name: "title", value: "text")
314 | ]
315 | NSWorkspace.shared.open(c.url!)
316 |
317 | _ = semaphore.wait()
318 |
319 | DistributedNotificationCenter.default().removeObserver(self)
320 | observation.invalidate()
321 |
322 | guard let result = selectedResult else {
323 | // cancel the command.
324 | throw Error.noFileSelected
325 | }
326 |
327 | try textBuffer.replaceSelection(by: result)
328 |
329 | return true
330 | }
331 | }
332 |
--------------------------------------------------------------------------------
/Extension/SourceEditorExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SourceEditorExtension.swift
3 | // Extension
4 | //
5 | // Created by Yoshitaka Seki on 2017/09/07.
6 | // Copyright © 2017年 takasek. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XcodeKit
11 |
12 | class SourceEditorExtension: NSObject, XCSourceEditorExtension {
13 |
14 | func extensionDidFinishLaunching() {
15 | // If your extension needs to do any work at launch, implement this optional method.
16 | print("launched!")
17 | }
18 |
19 | var commandDefinitions: [[XCSourceEditorCommandDefinitionKey: Any]] {
20 | // If your extension needs to return a collection of command definitions that differs from those in its Info.plist, implement this optional property getter.
21 |
22 | return [
23 | PasteboardOutputCommand.commandDefinition,
24 | PasteboardInputCommand.commandDefinition,
25 | OpenAppCommand.commandDefinition,
26 | URLSchemeCommand.commandDefinition,
27 | LocalCommandCommand.commandDefinition,
28 | NetworkCommand.commandDefinition,
29 | ToDesktopCommand1.commandDefinition,
30 | ToDesktopCommand2.commandDefinition,
31 | FileSelectionCommand.commandDefinition,
32 | ]
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Extension/Utilities/SweetSourceEditorCommand.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SweetSourceEditorCommand.swift
3 | // ExtensionSample
4 | //
5 | // Created by Yoshitaka Seki on 2017/08/12.
6 | // Copyright © 2017年 takasek. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XcodeKit
11 |
12 | /// Error with Message to tell the user.
13 | protocol MessagedError: Error {
14 | var message: String { get }
15 | }
16 |
17 | class SweetSourceEditorCommand: NSObject, XCSourceEditorCommand {
18 | /// can override
19 | var validUTIs: [UTI]? {
20 | return nil
21 | }
22 |
23 | /// for XCSourceEditorCommandDefinitionKey.nameKey.
24 | /// can be overridden.
25 | class var commandName: String {
26 | return className()
27 | }
28 |
29 | /// for XCSourceEditorCommandDefinitionKey.identifierKey.
30 | /// can be overridden.
31 | class var commandIdentifier: String {
32 | let bundleIdentifiler = Bundle.main.bundleIdentifier!
33 | return bundleIdentifiler + "." + className()
34 | }
35 |
36 | /// should be overridden
37 | func performImpl(with textBuffer: XCSourceTextBuffer) throws -> Bool {
38 | fatalError("should be implemented")
39 | }
40 |
41 | // MARK: - sweet wrappers
42 |
43 | class var commandDefinition: [XCSourceEditorCommandDefinitionKey: String] {
44 | return [
45 | .nameKey: commandName,
46 | .classNameKey: className(),
47 | .identifierKey: commandIdentifier
48 | ]
49 | }
50 |
51 | enum Error: MessagedError {
52 | case invalidUTI
53 | var message: String {
54 | switch self {
55 | case .invalidUTI: return "invalid UTI"
56 | }
57 | }
58 | }
59 |
60 | func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Swift.Error?) -> Void ) {
61 | enum Closing {
62 | case complete(Swift.Error?)
63 | case cancel
64 | }
65 | var closing: Closing = .complete(nil)
66 | defer {
67 | switch closing {
68 | case .complete(let error):
69 | let returningError: Swift.Error?
70 | if let error = error {
71 | print(error)
72 | if type(of: error) == NSError.self {
73 | returningError = error
74 | } else if let e = error as? MessagedError {
75 | returningError = NSError(domain: "MessagedError", code: 0, userInfo: [NSLocalizedDescriptionKey: e.message])
76 | } else {
77 | returningError = NSError(domain: "Swift.Error", code: 0, userInfo: [NSLocalizedDescriptionKey: "\(error as Any)"])
78 | }
79 | } else {
80 | returningError = nil
81 | }
82 | completionHandler(returningError)
83 | case .cancel:
84 | print("cancelled")
85 | invocation.cancellationHandler()
86 | }
87 | }
88 |
89 | do {
90 | let textBuffer = invocation.buffer
91 |
92 | switch validUTIs?.contains(textBuffer.typedContentUTI) {
93 | case nil:
94 | () // all UTIs can execute the command.
95 | case true?:
96 | () // this UTI can execute the command.
97 | case false?:
98 | throw Error.invalidUTI
99 | }
100 |
101 | guard try performImpl(with: textBuffer) else {
102 | closing = .cancel
103 | return
104 | }
105 |
106 | // complete with no error
107 |
108 | } catch let caughtError {
109 | closing = .complete(caughtError)
110 | return
111 | }
112 | }
113 | }
114 |
115 |
--------------------------------------------------------------------------------
/Extension/Utilities/UTI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UTI.swift
3 | // ExtensionSample
4 | //
5 | // Created by Yoshitaka Seki on 2017/08/12.
6 | // Copyright © 2017年 takasek. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct UTI: Equatable {
12 | let value: String
13 |
14 | static let swiftSource = UTI(value: "public.swift-source")
15 | static let cHeader = UTI(value: "public.c-header")
16 | static let objCSource = UTI(value: "public.objective-c-source")
17 | static let playground = UTI(value: "com.apple.dt.playground")
18 | static let playgroundPage = UTI(value: "com.apple.dt.playgroundpage")
19 | static let storyboard = UTI(value: "com.apple.InterfaceBuilder3.Storyboard.XIB")
20 | static let xib = UTI(value: "com.apple.InterfaceBuilder3.Cocoa.XIB")
21 | static let markdown = UTI(value: "net.daringfireball.markdown")
22 | static let xml = UTI(value: "public.xml")
23 | static let json = UTI(value: "public.json")
24 | static let plist = UTI(value: "com.apple.xml-property-list")
25 | static let entitlement = UTI(value: "com.apple.xcode.entitlements-property-list")
26 |
27 | func conforms(to uti: UTI) -> Bool {
28 | return UTTypeConformsTo(value as CFString, uti.value as CFString)
29 | }
30 |
31 | static func ~= (pattern: UTI, value: UTI) -> Bool {
32 | return value.conforms(to: pattern)
33 | }
34 |
35 | var fileExtension: String? {
36 | return UTTypeCopyPreferredTagWithClass(
37 | value as CFString,
38 | kUTTagClassFilenameExtension
39 | )?.takeRetainedValue() as String?
40 | }
41 | static func == (lhs: UTI, rhs: UTI) -> Bool {
42 | return lhs.value == rhs.value
43 | }
44 | }
45 |
46 |
--------------------------------------------------------------------------------
/Extension/Utilities/XCTextBuffer+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // XCTextBuffer+Extension.swift
3 | // ExtensionSample
4 | //
5 | // Created by Yoshitaka Seki on 2017/08/12.
6 | // Copyright © 2017年 takasek. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import XcodeKit
11 |
12 | extension XCSourceTextBuffer {
13 | var typedContentUTI: UTI {
14 | return UTI(value: contentUTI)
15 | }
16 | func selectedText(includesUnselectedStartAndEnd: Bool, trimsIndents: Bool) -> String {
17 | var texts: [NSString] = []
18 | var minimumIndentLength = Int.max
19 |
20 | selections
21 | .map { $0 as! XCSourceTextRange }
22 | .forEach { selection in
23 | for line in selection.start.line...selection.end.line {
24 | guard line < lines.count else {
25 | // if you select none and place the cursor on last line, it can be out of bound of lines
26 | return
27 | }
28 | var text = self.lines[line] as! NSString
29 |
30 | if !includesUnselectedStartAndEnd {
31 | if line == selection.end.line {
32 | text = text.substring(to: selection.end.column) as NSString
33 | }
34 | if line == selection.start.line {
35 | text = text.substring(from: selection.start.column) as NSString
36 | }
37 | }
38 |
39 | text = text.trimmingCharacters(in: CharacterSet(charactersIn: "\n")) as NSString
40 |
41 | texts.append(text)
42 |
43 | if text.length != 0 {
44 | // race with latest indent length
45 | minimumIndentLength = min(minimumIndentLength, text.indentLength)
46 | }
47 | }
48 | }
49 |
50 | return texts
51 | .map {
52 | if trimsIndents {
53 | return $0.length >= minimumIndentLength
54 | ? $0.substring(from: minimumIndentLength)
55 | : $0 as String
56 | } else {
57 | return $0 as String
58 | }
59 | }
60 | .joined(separator: "\n")
61 | }
62 |
63 | enum Error: MessagedError {
64 | case noSelection
65 | case invalidLine
66 |
67 | var message: String {
68 | switch self {
69 | case .noSelection: return "no selection found."
70 | case .invalidLine: return "line is invalid."
71 | }
72 | }
73 | }
74 |
75 | func insertConsideringLastLineIndents(_ insertion: String, at lineNum: Int) throws {
76 | guard lineNum <= lines.count else {
77 | throw Error.invalidLine
78 | }
79 |
80 | let lastLine = lines[lineNum-1] as! NSString
81 | let prefix = String.init(repeating: " ", count: lastLine.indentLength)
82 |
83 | let fixedInsertion = insertion.components(separatedBy: "\n").map {
84 | if $0.isEmpty {
85 | return String($0)
86 | } else {
87 | return prefix + String($0)
88 | }
89 | }.joined(separator: "\n")
90 |
91 | lines.insert(fixedInsertion, at: lineNum)
92 | }
93 |
94 | func replaceSelection(by insertion: String) throws {
95 | guard let selection = selections.firstObject as? XCSourceTextRange else { throw Error.noSelection }
96 |
97 | var pre = ""
98 | var post = ""
99 | if selection.end.line < lines.count {
100 | pre = (lines[selection.start.line] as! NSString).substring(to: selection.start.column)
101 | post = (lines[selection.end.line] as! NSString).substring(to: selection.end.column)
102 | lines.removeObjects(at: IndexSet(integersIn: selection.start.line...selection.end.line))
103 | } else {
104 | // selection.end.line may exceed lines.count at EOF
105 | }
106 |
107 | lines.insert(pre + insertion + post, at: selection.start.line)
108 | }
109 | }
110 |
111 | private extension NSString {
112 | var indentLength: Int {
113 | var result = 0
114 | for i in 0..
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/XcodeExtensionSample.xcodeproj/xcshareddata/xcschemes/Extension.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
6 |
9 |
10 |
16 |
22 |
23 |
24 |
30 |
36 |
37 |
38 |
39 |
40 |
46 |
47 |
48 |
49 |
55 |
56 |
57 |
58 |
59 |
60 |
72 |
74 |
80 |
81 |
82 |
83 |
84 |
85 |
92 |
94 |
100 |
101 |
102 |
103 |
105 |
106 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/XcodeExtensionSample.xcodeproj/xcshareddata/xcschemes/XcodeExtensionSample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
33 |
34 |
40 |
41 |
42 |
43 |
44 |
45 |
56 |
58 |
64 |
65 |
66 |
67 |
68 |
69 |
75 |
77 |
83 |
84 |
85 |
86 |
88 |
89 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/XcodeExtensionSample.xcodeproj/xcshareddata/xcschemes/XcodeExtensionSampleHelper.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
33 |
34 |
35 |
36 |
47 |
51 |
52 |
53 |
59 |
60 |
61 |
62 |
63 |
64 |
70 |
71 |
77 |
78 |
79 |
80 |
82 |
83 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/XcodeExtensionSample.xcodeproj/xcuserdata/tksk_mba.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Extension.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 1
11 |
12 | XcodeExtensionSample.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 0
16 |
17 | XcodeExtensionSampleHelper.xcscheme_^#shared#^_
18 |
19 | orderHint
20 | 2
21 |
22 |
23 | SuppressBuildableAutocreation
24 |
25 | B64AC1071F64209500155E54
26 |
27 | primary
28 |
29 |
30 |
31 | SuppressBuildableAutocreation
32 |
33 | B624E6E31F6155E00081E582
34 |
35 | primary
36 |
37 |
38 | B624E6FA1F61567E0081E582
39 |
40 | primary
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/XcodeExtensionSample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // XcodeExtensionSample
4 | //
5 | // Created by Yoshitaka Seki on 2017/09/07.
6 | // Copyright © 2017年 takasek. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | @NSApplicationMain
12 | class AppDelegate: NSObject, NSApplicationDelegate {
13 | func applicationWillFinishLaunching(_ aNotification: Notification) {
14 | NSAppleEventManager.shared().setEventHandler(
15 | self,
16 | andSelector: #selector(AppDelegate.handleGetURLEvent(event:replyEvent:)),
17 | forEventClass: AEEventClass(kInternetEventClass),
18 | andEventID: AEEventID(kAEGetURL)
19 | )
20 | }
21 |
22 | private var selectedFileURL: URL?
23 |
24 | @objc func handleGetURLEvent(event: NSAppleEventDescriptor?, replyEvent: NSAppleEventDescriptor?) {
25 | defer {
26 | UserDefaults(suiteName: "42U7855PYX.io.github.takasek.XcodeExtensionSample")?.set(selectedFileURL?.absoluteString, forKey: "valueFromApp")
27 |
28 | DispatchQueue.main.async {
29 | NSApplication.shared.terminate(nil)
30 | }
31 | }
32 | guard
33 | let urlString = event?.paramDescriptor(forKeyword: keyDirectObject)?.stringValue,
34 | let components = URLComponents(string: urlString)
35 | else {
36 | return
37 | }
38 |
39 | guard
40 | let title = components.queryItems?.first(where: { $0.name == "title" })?.value,
41 | let url = self.selectFile(with: title)
42 | else {
43 | return
44 | }
45 |
46 | self.selectedFileURL = url
47 | }
48 |
49 | private func selectFile(with title: String) -> URL? {
50 | let panel = NSOpenPanel()
51 |
52 | panel.prompt = "Select"
53 | panel.canChooseDirectories = true
54 | panel.canCreateDirectories = true
55 | panel.canChooseFiles = true
56 | panel.title = "title"
57 |
58 | let url: URL?
59 | switch panel.runModal() {
60 | case .OK:
61 | url = panel.url
62 | case _:
63 | url = nil
64 | }
65 | return url
66 | }
67 |
68 | func applicationWillTerminate(_ aNotification: Notification) {
69 | DistributedNotificationCenter.default().postNotificationName(
70 | Notification.Name("XcodeExtensionSample.applicationWillTerminate"),
71 | object: nil,
72 | userInfo: selectedFileURL.flatMap { ["url": $0] },
73 | deliverImmediately: true
74 | )
75 | }
76 |
77 | func application(_ application: NSApplication, open urls: [URL]) {
78 | print(urls)
79 | }
80 | }
81 |
82 |
--------------------------------------------------------------------------------
/XcodeExtensionSample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "size" : "16x16",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "size" : "16x16",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "size" : "32x32",
16 | "scale" : "1x"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "size" : "32x32",
21 | "scale" : "2x"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "size" : "128x128",
26 | "scale" : "1x"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "size" : "128x128",
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "size" : "256x256",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "size" : "256x256",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "size" : "512x512",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "size" : "512x512",
51 | "scale" : "2x"
52 | }
53 | ],
54 | "info" : {
55 | "version" : 1,
56 | "author" : "xcode"
57 | }
58 | }
--------------------------------------------------------------------------------
/XcodeExtensionSample/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
672 |
673 |
674 |
675 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 |
684 |
685 |
686 |
687 |
688 |
689 |
690 |
691 |
692 |
693 |
694 |
695 |
696 |
697 |
698 |
699 |
700 |
701 |
702 |
703 |
704 |
705 |
706 |
707 |
708 |
709 |
710 |
711 |
712 |
713 |
714 |
715 |
716 |
717 |
718 |
--------------------------------------------------------------------------------
/XcodeExtensionSample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleURLTypes
22 |
23 |
24 | CFBundleTypeRole
25 | Editor
26 | CFBundleURLSchemes
27 |
28 | xcextsample
29 |
30 |
31 |
32 | CFBundleVersion
33 | 1
34 | LSMinimumSystemVersion
35 | $(MACOSX_DEPLOYMENT_TARGET)
36 | NSHumanReadableCopyright
37 | Copyright © 2017年 takasek. All rights reserved.
38 | NSMainStoryboardFile
39 | Main
40 | NSPrincipalClass
41 | NSApplication
42 |
43 |
44 |
--------------------------------------------------------------------------------
/XcodeExtensionSample/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // XcodeExtensionSample
4 | //
5 | // Created by Yoshitaka Seki on 2017/09/07.
6 | // Copyright © 2017年 takasek. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | class ViewController: NSViewController {
12 |
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 |
16 | // Do any additional setup after loading the view.
17 | }
18 |
19 | override var representedObject: Any? {
20 | didSet {
21 | // Update the view, if already loaded.
22 | }
23 | }
24 |
25 |
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/XcodeExtensionSample/XcodeExtensionSample.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.application-groups
8 |
9 | $(TeamIdentifierPrefix)io.github.takasek.XcodeExtensionSample
10 |
11 | com.apple.security.files.user-selected.read-only
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/XcodeExtensionSampleHelper/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | XcodeExtensionSampleHelper
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | XPC!
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | NSHumanReadableCopyright
24 | Copyright © 2017年 takasek. All rights reserved.
25 | XPCService
26 |
27 | ServiceType
28 | Application
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/XcodeExtensionSampleHelper/XcodeExtensionSampleHelper-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | //
2 | // Use this file to import your target's public headers that you would like to expose to Swift.
3 | //
4 |
5 | #import "XcodeExtensionSampleHelperProtocol.h"
6 |
--------------------------------------------------------------------------------
/XcodeExtensionSampleHelper/XcodeExtensionSampleHelper.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/XcodeExtensionSampleHelper/XcodeExtensionSampleHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // XcodeExtensionSampleHelper.swift
3 | // XcodeExtensionSampleHelper
4 | //
5 | // Created by Yoshitaka Seki on 2017/09/09.
6 | // Copyright © 2017年 takasek. All rights reserved.
7 | //
8 |
9 |
10 | import Foundation
11 |
12 | @objc class XcodeExtensionSampleHelper: NSObject, XcodeExtensionSampleHelperProtocol {
13 | func write(text: String, to directory: String, reply: @escaping HelperResultHandler) {
14 |
15 | try? text.write(
16 | toFile: directory + "/outputFile",
17 | atomically: true, encoding: .utf8
18 | )
19 |
20 | reply(0)
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/XcodeExtensionSampleHelper/XcodeExtensionSampleHelperProtocol.h:
--------------------------------------------------------------------------------
1 | //
2 | // XcodeExtensionSampleHelperProtocol.h
3 | // XcodeExtensionSampleHelper
4 | //
5 | // Created by Yoshitaka Seki on 2017/09/09.
6 | // Copyright © 2017年 takasek. All rights reserved.
7 | //
8 |
9 | @import Foundation;
10 |
11 | typedef void (^HelperResultHandler)(NSInteger status);
12 |
13 | @protocol XcodeExtensionSampleHelperProtocol
14 |
15 | - (void)writeText:(NSString * _Nonnull)text
16 | to:(NSString * _Nonnull)directory
17 | reply:(HelperResultHandler _Nonnull)reply
18 | NS_SWIFT_NAME(write(text:to:reply:));
19 |
20 | @end
21 |
22 |
23 |
--------------------------------------------------------------------------------
/XcodeExtensionSampleHelper/main.m:
--------------------------------------------------------------------------------
1 | //
2 | // main.m
3 | // XcodeExtensionSampleHelper
4 | //
5 | // Created by Yoshitaka Seki on 2017/09/09.
6 | // Copyright © 2017年 takasek. All rights reserved.
7 | //
8 |
9 | #import
10 | #import "XcodeExtensionSampleHelper-Swift.h"
11 |
12 | @interface ServiceDelegate : NSObject
13 | @end
14 |
15 | @implementation ServiceDelegate
16 |
17 | - (BOOL)listener:(NSXPCListener *)listener shouldAcceptNewConnection:(NSXPCConnection *)newConnection {
18 | // This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection.
19 |
20 | // Configure the connection.
21 | // First, set the interface that the exported object implements.
22 | newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(XcodeExtensionSampleHelperProtocol)];
23 |
24 | // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object.
25 | XcodeExtensionSampleHelper *exportedObject = [XcodeExtensionSampleHelper new];
26 | newConnection.exportedObject = exportedObject;
27 |
28 | // Resuming the connection allows the system to deliver more incoming messages.
29 | [newConnection resume];
30 |
31 | // Returning YES from this method tells the system that you have accepted this connection. If you want to reject the connection for some reason, call -invalidate on the connection and return NO.
32 | return YES;
33 | }
34 |
35 | @end
36 |
37 | int main(int argc, const char *argv[])
38 | {
39 | // Create the delegate for the service.
40 | ServiceDelegate *delegate = [ServiceDelegate new];
41 |
42 | // Set up the one NSXPCListener for this service. It will handle all incoming connections.
43 | NSXPCListener *listener = [NSXPCListener serviceListener];
44 | listener.delegate = delegate;
45 |
46 | // Resuming the serviceListener starts this service. This method does not return.
47 | [listener resume];
48 | return 0;
49 | }
50 |
--------------------------------------------------------------------------------