Source Editor Extension -- Xcode 格式化 Import 的插件

背景

Xcode 秉承了 Apple 封闭的传统,提供的可自定义的选项比起其他 IDE 来说是比较少的,不过在 Xcode 7 之前(包含 Xcode 7)我们还是可以通过插件实现 Xcode 的自定义,甚至还出现了像 Alcatraz 的专门的插件管理工具,开源社区中也有诸如 VVDocumenter-XcodeCocoaPods 等知名的插件,不过这些便利随着 Xcode 8 的发布成为了过去式。
出于安全性考虑(比如说 Xcode ghost 事件),Apple 从 Xcode 8 开始不再支持第三方的插件。Apple 方面提供了基于 App Extension 的解决方案 -- Xcode Source Editor Extension,这是一个相当简单的方案,能且仅能完成有限的文本编辑辅助,很大部分之前第三方插件能完成的任务都没办法实现了。聊胜于无吧 😑
(本文会介绍 Source Editor Extension 的开发以及分发相关的知识,本文对应的 Demo 在:https://github.com/VernonVan/PPImportArrangerExtension

创建插件

  1. 创建一个 Cocoa App:Source Editor Extension 不能独立存在,必须依附于 Cocoa App。


    Cocoa App
  2. File -> New -> Target -> Xcode Source Editor Extension 添加一个 Target,并激活这个 Target。


    Xcode Source Editor Extension

    激活 target

这样就创建好了一个可运行的 Source Editor Extension,相当的简单。🧐

关键概念

文件结构
  1. SourceEditorExtension 类:遵循 XCSourceEditorExtension 协议的类,XCSourceEditorExtension 协议的头文件如下:
@protocol XCSourceEditorExtension <NSObject>

@optional

- (void)extensionDidFinishLaunching;

@property (readonly, copy) NSArray <NSDictionary <XCSourceEditorCommandDefinitionKey, id> *> *commandDefinitions;

@end

XCSourceEditorExtension 协议只有一个方法和一个属性,extensionDidFinishLaunching 方法是用来在插件加载好后是对插件进行一些准备工作的,根据 WWDC 的说法,各个插件与 Xcode 本身的初始化过程是在不同进程上进行的,同样地,插件的崩溃并不会引起 Xcode 的崩溃。commandDefinitions 属性则可以动态返回插件的菜单项。

SourceEditorCommand 类:遵循 XCSourceEditorCommand 协议的类,实现插件功能的核心类,对应到插件的菜单项,可以一个菜单项对应到一个 Command 类,也可以多个菜单项对应到一个 Command 类,XCSourceEditorCommand 协议头文件定义如下:

@protocol XCSourceEditorCommand <NSObject>

@required

- (void)performCommandWithInvocation:(XCSourceEditorCommandInvocation *)invocation completionHandler:(void (^)(NSError * _Nullable nilOrError))completionHandler;

@end

XCSourceEditorCommandInvocation 类型的参数 invocation 主要是点击的菜单项的标识、当前文本信息(文本字符串数组、选中区间等)以及点击取消按钮的回调事件,completionHandler 参数则是用来通知 Xcode 本插件已经完成了自己的操作,需要保证一定要调用 completionHandler!否则会出现下图所示的提示,然后菜单项就会变灰不能再点击:


插件 busy

菜单项变灰
  1. Info.plist:Info.plist 文件用于静态配置插件对应的菜单项,如下图所示,XCSourceEditorExtensionPrincipalClass 对应到上文说的 XCSourceEditorExtension 类,XCSourceEditorCommandDefinitions 指定菜单项,XCSourceEditorCommandClassName 对应到上文说的 SourceEditorCommand 类,XCSourceEditorCommandIdentifier 是每个具体菜单项的标识,XCSourceEditorCommandName 是菜单项的描述。


    Info.plist
  2. 保证 TARGETS 组下的两个 Target 用的同一个 Team。

实现步骤

本 Demo 要实现的功能就是按照字母顺序重新排列当前文件的所有 Import,强迫症们一定知道我在说什么🤣,先来看一下效果:


效果图

演示效果

可以点击 Editor -> ImportArranger -> Arrange Imports 重新排列所有的 Imports,甚至还可以为其设置快键键。

实现步骤反而没有什么可说的,主要是操作 invocation.buffer.lines 和 invocation.buffer.selections,分别对应的是当前文件的所有行和当前文件的选择区域,都是可变类型的数组,做完自定义的操作后操作数组即可更新当前文件。注意:不管是哪条执行路径,一定要保证调用到 completionHandler。其他需要留意的地方都在代码中的注释中给出:

- (void)performCommandWithInvocation:(XCSourceEditorCommandInvocation *)invocation completionHandler:(void (^)(NSError *_Nullable nilOrError))completionHandler
{
    NSMutableArray<NSString *> *lines = invocation.buffer.lines;
    if (!lines || !lines.count) {
        completionHandler(nil);
        return;
    }

    NSMutableArray<NSString *> *importLines = [[NSMutableArray alloc] init];
    NSInteger firstLine = -1;
    for (NSUInteger index = 0, max = lines.count; index < max; index++) {
        NSString *line = lines[index];
        NSString *pureLine = [line stringByReplacingOccurrencesOfString:@" " withString:@""];       // 去掉多余的空格,以防被空格干扰没检测到 #import
        // 支持 Objective-C、Swift、C 语言的导入方式
        if ([pureLine hasPrefix:@"#import"] || [pureLine hasPrefix:@"import"] || [pureLine hasPrefix:@"@class"]
            || [pureLine hasPrefix:@"@import"] || [pureLine hasPrefix:@"#include"]) {     
            [importLines addObject:line];
            if (firstLine == -1) {
                firstLine = index;      // 记住第一行 #import 所在的行数,用来等下重新插入的位置
            }
        }
    }

    if (!importLines.count) {
        completionHandler(nil);
        return;
    }

    [invocation.buffer.lines removeObjectsInArray:importLines];

    NSArray *noRepeatArray = [[NSSet setWithArray:importLines] allObjects];         // 去掉重复的 #import
    NSMutableArray<NSString *> *sortedImports = [[NSMutableArray alloc] initWithArray:[noRepeatArray sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]];

    // 引用系统文件在前,用户自定义的文件在后
    NSMutableArray *systemImports = [[NSMutableArray alloc] init];
    for (NSString *line in sortedImports) {
        if ([line containsString:@"<"]) {
            [systemImports addObject:line];
        }
    }
    if (systemImports.count) {
        [sortedImports removeObjectsInArray:systemImports];
        [sortedImports insertObjects:systemImports atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, systemImports.count)]];
    }

    if (firstLine >= 0 && firstLine < invocation.buffer.lines.count) {
        // 重新插入排好序的 #import 行
        [invocation.buffer.lines insertObjects:sortedImports atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(firstLine, sortedImports.count)]];
        // 选中所有 #import 行
        [invocation.buffer.selections addObject:[[XCSourceTextRange alloc] initWithStart:XCSourceTextPositionMake(firstLine, 0) end:XCSourceTextPositionMake(firstLine + sortedImports.count, sortedImports.lastObject.length)]];
    }

    completionHandler(nil);
}

选择这个插件作为当前 Scheme,选择 Xcode 运行,然后就会弹出一个黑色的 Xcode 供你调试了。


image.png

调试插件

分发

插件开发测试完成之后,最重要的当然是将插件分发出去,供他人使用。Apple 在 WWDC 说到 Xcode Source Editor Extension 是可以上架 Mac App Store 的,不过受限于 Source Editor Extension 功能实在太少,目前也没有在 Mac App Store 上看到很火的插件。更多是直接把 .app 文件上传到 Github 上供人下载(这里有人整理了一些不错的插件:https://github.com/theswiftdev/awesome-xcode-extensions),具体步骤如下:

打包

测试完成后,找到 Products 下面的 .app 文件,注意需要保证上文中说的两个签名是一致的。然后就可以把这个 .app 上传到个人网站或者 Github 上供人下载使用了。


.app 文件

安装

当我们下载好了一个 .app 格式的插件之后,将 .app 文件拖到应用程序(Applications)文件夹中,双击这个 .app 文件,然后在 系统偏好设置-> 扩展 -> Xcode Source Editor Extension 勾选该插件,最后重启 Xcode 就可以在 Editor 菜单中找到该插件了。


勾选插件

还可以在 Xcode 中为插件的菜单项设置快捷键。


设置快键键

结语

至少现有的 Xcode Source Editor Extension 还是比较受限的,接口少的可怜,可想象的空间不是很多,大部分之前第三方插件能做的事情都没办法完成了🤷‍♀️。还是默默希望 Apple 能以更加开放的姿态,提供更多的接口给开发者,Xcode 没办法满足所有人的喜好,起码,能让喜欢折腾的人把它变得更好 :-D

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容