WWDC2016之初识Xcode Source Editor Extension

以往Xcode插件开发在没有官方支持的情况下,基本处于处于刀耕火种的状态。虽然后来有了Xcode-Plugin-Template和各种dump好的头文件,我们仍然需要在没有文档的情况下做各种猜测和hook。关于插件的开发,可以看看这篇文章。幸运的是苹果终于注意到了这一需求,并在WWDC2016上发布这一新特性–Xcode Source Editor Extension

简介

Xcode Source Editor Extension,从字面意思理解就是Xcode源代码编辑器的一个扩展,事实上它的命名的确很好地反映了它的功能和定位。那么基于这项技术我们究竟能做什么呢?WWDC session 414给我们的答案是:

Add commands to the source editor

Edit text

Change selections

给Xcode的代码编辑器扩展一些命令(在Editor菜单下增加额外的菜单),通过这些命令对源代码进行编辑(包括整个文件和选中的部分,这里的selection我起初不理解,后面做Demo就会理解了)。

相比于以往的插件开发,Xcode Source Editor Extension具有以下特点:

  • 稳定。extension运行在其独立的进程中,插件的崩溃不会再引起Xcode的崩溃了。
  • 安全。extension拥有自己的sandBox,而且不会直接去操作Xcode工程,而是由Xcode将text以invocation的方式传递。
  • 快速。每个extension在Xcode启动后会即会被异步地以XPC的方式加载。
  • 简单。苹果提供了官方支持。

此外,作为App Extension的一种,可在App Store发布,优秀的插件也可以为开发者增加收入。下面我们就跟着demo来探究如何实现一个Xcode Source Editor Extension

工程创建

我将创建一个为全部选中行添加注释的demo。根据官方指导,创建一个插件工程非常容易。首先我们创建一个Cocoa Application命名为”XTExtension”,接着在Application Extension中选中Xcode Source Editor Extension,命名为XTComment
extension template
此时工程的XTComment目录下有两个文件SourceEditorExtension.swiftSourceEditorCommand.swfitSourceEditorExtension用来管理extension的生命周期相关,SourceEditorCommand则用来处理具体的命令。Extension的加载和处理流程如下:
flow
Xcode的启动时候会记载所有的extension,并保证它们的生命周期。当用户点击选择命令按钮后,Xcode会以invocation的形式通知到extension,extension做出相应的处理并把处理结果回调给Xcode。SourceEditorExtension.swift的方法extensionDidFinishLaunching在extension被加载时调用,可以在这里做一些初始化的工作。

1
2
3
4
func extensionDidFinishLaunching() {
// If your extension needs to do any work at launch, implement this optional method.
print("Extension launched...")
}

添加菜单

打开Info.plist,展开NSExtensionXCSourceEditorCommandDefinitions,命令菜单的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<key>NSExtensionAttributes</key>
<dict>
<key>XCSourceEditorCommandDefinitions</key>
<array>
<dict>
<key>XCSourceEditorCommandClassName</key>
<string>$(PRODUCT_MODULE_NAME).SourceEditorCommand</string>
<key>XCSourceEditorCommandIdentifier</key>
<string>com.xt.APPlugins.AutoComment.SourceEditorCommand</string>
<key>XCSourceEditorCommandName</key>
<string>Source Editor Command</string>
</dict>
</array>
<key>XCSourceEditorExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).SourceEditorExtension</string>
</dict>

其中:

  • XCSourceEditorCommandClassName指向命令的处理类,该类实现XCSourceEditorCommand
  • XCSourceEditorCommandIdentifier是命令的唯一标识,通常我们会对一组命令设置同一个XCSourceEditorCommand,在invocation中获取此标识做区分处理。
  • XCSourceEditorCommandName是命令在菜单栏展示的菜单名称。

此外,菜单项是动态加载的,可以实现SourceEditorExtensioncommandDefinitions:实现,但是这会覆盖掉Info.plist中的定义。

1
2
3
4
5
6
var commandDefinitions: [[XCSourceEditorCommandDefinitionKey: AnyObject]] {
// If your extension needs to return a collection of command definitions that differs from those in its Info.plist, implement this optional property getter.
return [[.classNameKey: "SourceEditorCommand",
.identifierKey: "CustomIdentifier",
.nameKey: "CustomeName"]]
}

Extension的菜单会默认加载在Show Code Coverage下。编译运行,选取Xcode-Beta作为Lanuching App,点击Editor,然而Show Code Coverage下空空如也,什么也没有。作为新主题,可查的资料实在太少,一度以为因为Xcode是Beta版的问题并打算放弃了。很偶然的一个机会在Xcode 8.0 beta Release Notes看到了这个问题的答案,WTF!

To use Xcode Extensions on macOS 10.11 El Capitan, you must first launch Xcode and let it install additional system components. Then, in Terminal, run sudo /usr/libexec/xpccachectl.
Once the operation completes, restart your Mac. (26106213)

业务实现

打开SourceEditorCommand.swift,实现perform(with:completionHandler:)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: (NSError?) -> Void ) -> Void {
var lineIndexes = [Int]()
for range in invocation.buffer.selections {
guard let range = range as? XCSourceTextRange else {
continue
}
let start = range.start
let end = range.end
for i in start.line...end.line {
lineIndexes.append(i)
}
}
lineIndexes.reverse()
for lineNumber in lineIndexes {
guard let line = invocation.buffer.lines[lineNumber] as? NSString else {
continue
}
let commentLine = NSString(format: "// %@", line)
invocation.buffer.lines.insert(commentLine, at: lineNumber)
}
// Implement your command here, invoking the completion handler when done. Pass it nil on success, and an NSError on failure.
completionHandler(nil)
}

invocationbuffer提供了比较丰富的信息,包括文件的lines, selections, indentationWidth等等,具体作用可以查看WWDC视频或者苹果的文档查看,在此就不一一介绍了。
feature show
此外,需要注意的一点是,苹果希望文本的编辑是快速流畅的,如果你的处理的文本时间过长苹果就会为用户提供一个取消的入口,这会触发invocation的cancellationHandler,所以在这里终止你的耗时的工作,否则你的extension可能不能正常地工作。最后,附上苹果的extension开发建议。

Start up quickly

Use GCD and follow standard asynchronous patterns

Don’t replace the whole buffer if you don’t have to

Handle cancellation quickly

小结

Xcode Source Editor Extension提供了一种官方Xcode插件开发方法,从上文可以看出,其所涉及的类和函数都很少,非常容易上手。同时,从目前来看,她的能力还相对较弱,一些复杂的如FuzzyAutocompletePlugin还无法实现。但我相信很快会有优秀的插件来丰富Xcode功能和提升开发效率的。

Demo

参考