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 在:github.com/VernonVan/P…git

建立插件

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



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





这样就建立好了一个可运行的 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 属性则能够动态返回插件的菜单项。
xcode

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!不然会出现下图所示的提示,而后菜单项就会变灰不能再点击:bash


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

3. 保证 TARGETS 组下的两个 Target 用的同一个签名。ide


实现步骤

本 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 供你调试了。



分发

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

打包

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


安装

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


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


结语

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

相关文章
相关标签/搜索