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

相關文章
相關標籤/搜索