Xcode Extensions
A brave new world
Xcode 8 now supports an official extension API. The first extension type supported is the Source Editor extension (though probably not the last). The flip-side is that Xcode 8 adopts System Integrity Protection. That means it is no longer possible to inject code into the Xcode process. Alcatraz is closed for business.
Basics of XcodeKit Source Extensions
Source Extensions have several capabilities and are organized around commands. Each extension can provide a list of commands in its plist file or via an override of XCSourceEditorExtension
. Each command specifies what XCSourceEditorCommand
subclass implements the command, the name on the menu, and a unique identifier. The same class can implement several commands by switching on the identifier.
Once the user invokes a command Xcode will call perform(with:completionHandler:)
to allow your command to asynchronously do its work.
The first parameter is an XCSourceEditorCommandInvocation
. That invocation has the contents of the editor's text buffer (either as a single string or as an array of lines). It also has the selections, and each selection has a start
and end
that tell you the line and column (which can be used to index into the text buffer's lines
array). If nothing is selected, the single XCSourceTextRange
in the array will have the same start
and end
point representing the insertion point.
More Awesome
I would post screenshots to follow along but the NDA prohibits screenshots of pre-release software so you'll have to make do with the text.
First create a new Cocoa (Mac) application; this is just a container app. Then go to the Project Settings and add a new Target. Select Xcode Source Extension. You'll get a prompt about switching to the extension's scheme, say yes. Edit that new scheme and under Executable select Xcode-beta.app
as the executable.
Anytime you run your extension a new copy of Xcode will launch. It has a helpful black and white icon to let you know it's a separate process hosting your extension.The default template includes a command (pre-configured in the plist) and extension. You can edit the name of the command in the plist to give it a better name in the Editor menu. If you choose to provide the commands in code you must supply them all; Xcode does not appear to merge them with the commands provided in the plist.
Now in the command you can paste something like this:
func perform(with invocation: XCSourceEditorCommandInvocation,
completionHandler: (NSError?) -> Void ) -> Void {
let buffer = invocation.buffer
if let insertionPoint = buffer.selections[0] as? XCSourceTextRange {
let currentLine = insertionPoint.start.line
buffer.lines.insert("// More Awesome!", at: currentLine)
}
completionHandler(nil)
}
This code is assuming there are no selections so the only text range in the selections
property is the one representing the insertion point. I chose to make changes by mutating the lines
array, but you can also access var completeBuffer: String
. Any changes made to completeBuffer
or lines
is immediately reflected in the other. You can determine the type of text the buffer contains by checking buffer.contentUTI
, which for Swift will be public.swift-source
When you're done make sure you call completionHandler
to let Xcode know you finished.
If you want to support cancellation, set invocation.cancellationHandler = { ... }
. Make sure you handle this in a thread-safe way (perhaps by using the OSAtomic
functions to set a cancellation flag). This will let your extension know it should abandon processing immediately, though you must still call the completion handler.
Batteries Not Included
If you're running this on OS X 10.11 like I am, you'll need to run sudo /usr/libexec/xpccachectl
and reboot before Xcode will even attempt to load your extensions. It has something to do with new SDKs being installed and the XPC service in El Capitan not expecting it.
In the first beta the extensions are a bit wonky. Xcode often launches two copies of the extension and when you open a source file in Xcode Heart of Darkness Edition your editor command will just be greyed out for no obvious reason. Sometimes clicking on and off a source file will make it start working, other times stopping the extension and running again will make it work. I already spoke to an Apple engineer in the labs and they are aware of the problems and plan on fixing them soon.
At a more fundamental level, the current XcodeKit interface does not provide any information about the project, the compilation context, SourceKit/Clang, etc. If you want to do something like a refactoring tool or determine the kind of token that is currently selected you'll have to interface with SourceKit on your own. That would be possible but the current interface doesn't provide the path of the file so it is basically impossible to do anything that requires compiler help. Without any context SourceKit will only be able to give you a really basic parsing of the text. This is one area I hope to see further improvements in the future.
Conclusion
XcodeKit and Source Editor Extensions are a welcome development. I'm glad the Xcode team has started down this road I'm looking forward to further extension points within Xcode.
** Update:** Please dupe my radar Support Xcode Template Extensions to handle Swift GYB files. I framed it as being for Swift GYB files because that can help lift it in the priority list but it isn't specific to GYB files at all; I basically want Xcode to allow extensions to register a UTI then call the extension anytime files with that UTI are edited. The extension can then generate one or more output files to be included in the project. This is basically just making the CoreData model generation stuff in Xcode 8 public API. It means you can do all kinds of fun things like parse a simple struct definition with SourceKit to generate models with automatic serialization and conformance to various protocols. The possibilities are endless and every dupe of this radar will count as a vote!
This blog represents my own personal opinion and is not endorsed by my employer.