几天前的 WWDC 发布了不少新特性,其中之一便是 Xcode
官方插件支持。用户可以像安装普通软件一样安装插件,增强它的源代码编辑功能。这也直接导致了原有的第三方插件都不可用。目前开放的接口非常有限,只能对代码进行纯文本处理,不过相信以后会逐渐开放更多功能。
本文向读者展示如何利用 Xcode 8
构建一个编辑器扩展,并实现一个具体的功能。以下项目的所有代码均可以在 Github 上找到。
创建 Mac app 工程
下载最新的 Xcode 8,新建一个项目。如下图所示,从工程模版中选择 Cocoa application
填写工程名称:DemoExtension,去掉 test 相关选项
然后选中工程文件,新建一个 Target
从工程模板中选择 Xcode Source Editor Extension
,填写 Target 名为:DemoXcodeExtension
然后可以看到,Xcode
已经为我们生成了四个文件(Swift
是两个):
两个类均继承自 NSObject
,其中 SourceEditorExtension
服从 XCSourceEditorExtension
协议:
@protocol XCSourceEditorExtension <NSObject>
@optional
/** Invoked when the extension has been launched, which may be some time before the extension actually receives a command (if ever).
\note Make no assumptions about the thread or queue on which this method will be invoked.
*/
- (void)extensionDidFinishLaunching;
@property (readonly, copy) NSArray <NSDictionary <XCSourceEditorCommandDefinitionKey, id> *> *commandDefinitions;
@end
SourceEditorCommand
服从 XCSourceEditorCommand
协议,它仅有一个方法:
@protocol XCSourceEditorCommand <NSObject>
@required
- (void)performCommandWithInvocation:(XCSourceEditorCommandInvocation *)invocation completionHandler:(void (^)(NSError * _Nullable nilOrError))completionHandler;
@end
查看刚刚新建的那个 Target
的 Info.plist
,可以看到比一般的文件多了个 NSExtension 的字段,展开后是一个字典:
NSExtensionPointIdentifier
,一个常量,不要动它NSExtensionAttributes
,是一个字典,包含:XCSourceEditorExtensionPrincipalClass
,它指定了扩展程序的入口类名,在我们这个例子中就是 SourceEditorExtensionXCSourceEditorCommandDefinitions
,它是一个数组,定义了一系列这个扩展程序支持的命令,每个命令包含一个 Identifier, 一个入口类名,及命令名,我们可以将命令名修改为自己喜欢的名字:
这个没有任何功能的 Xcode
扩展已经可以运行了,在运行对象框中选中 DemoXcodeExtension
对象,Command R
运行,它需要附属到一个应用程序上,请注意选择 Xcode 8。运行后,在新起的灰色的 Xcode 8 中打开一个项目,在代码编辑器中,打开 Editor 菜单,不出意外的可以看到我们的新扩展程序的菜单了:
出意外是很正常的,目前有一定概率扩展无法正常加载,多试几次。另外,如果你运行的是 EI Capitan 系统,请在运行此扩展前在终端中输入以下命令并重启电脑:
sudo /usr/libexec/xpccachectl
添加功能
接下来我们正式编码,我们实现一个可以让用户快速选择最小闭合区代码的功能,即:找到最近的成对的包含光标所在位置的 {}
,或 []
,并选中它们中间的代码:
- (void)performCommandWithInvocation:(XCSourceEditorCommandInvocation *)invocation
completionHandler:(void (^)(NSError * _Nullable nilOrError))completionHandler
{
NSInteger bracketStack = 0;
NSInteger braceStack = 0;
XCSourceTextRange *begin = invocation.buffer.selections.firstObject;
XCSourceTextRange *end = invocation.buffer.selections.lastObject;
XCSourceTextRange *range = [[XCSourceTextRange alloc] init];
range.end = (XCSourceTextPosition){invocation.buffer.lines.count - 1, invocation.buffer.lines.lastObject.length};
BOOL found = NO;
for (NSInteger line = begin.start.line; !found && line >= 0; --line) {
NSString *text = invocation.buffer.lines[line];
NSInteger max = line == begin.start.line ? begin.start.column : text.length - 1;
for (NSInteger col = max; !found && col >= 0; --col) {
unichar ch = [text characterAtIndex:col];
switch (ch) {
case '[':
// Found
if (bracketStack == 0) {
range.start = (XCSourceTextPosition){line, col};
found = YES;
}
else {
--bracketStack;
}
break;
case ']':
++bracketStack;
break;
case '{':
// Found
if (braceStack == 0) {
range.start = (XCSourceTextPosition){line, col};
found = YES;
}
else {
--braceStack;
}
break;
case '}':
++braceStack;
break;
default:
break;
}
}
}
found = NO;
braceStack = 0;
bracketStack = 0;
for (NSInteger line = end.end.line; !found && line < invocation.buffer.lines.count; ++line) {
NSString *text = invocation.buffer.lines[line];
NSInteger min = line == end.end.line ? end.end.column : 0;
for (NSInteger col = min; !found && col < text.length; ++col) {
unichar ch = [text characterAtIndex:col];
switch (ch) {
case ']':
// Found
if (bracketStack == 0) {
range.end = (XCSourceTextPosition){line, col + 1};
found = YES;
}
else {
--bracketStack;
}
break;
case '[':
++bracketStack;
break;
case '}':
// Found
if (braceStack == 0) {
range.end = (XCSourceTextPosition){line, col + 1};
found = YES;
}
else {
--braceStack;
}
break;
case '{':
++braceStack;
break;
default:
break;
}
}
}
[invocation.buffer.selections removeAllObjects];
[invocation.buffer.selections addObject:range];
completionHandler(nil);
}
以上方法中包含两个参数:
- invocation,它提供了源代码文件的文本内容相关的信息,对它的修改将直接生效
- completionHandler,这是一个回调 Block,用于通知 Xcode,扩展程序的操作已经完成
为了方便操作,我们可以为扩展命令添加键盘绑定。如下图,在 Xcode 中打开偏好,在 Key Bindings 下找到我们的新菜单命令,添加快捷键:Control Shift S
结果
最终结果如下所示,每次按下 Control Shift S
,Xcode 将选中最小的闭合区域:
此项目为 Demo 展示用,请不要在生产环境中使用