Xcodeで開いているソースコードのGitHub上の各ブランチを一発で閲覧するプラグインを作ってみた

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

1 はじめに

Xcodeのプラグインの作成方法は、既に、ここDevelopers.IOでも詳しく紹介されいます。


初めてのXcode 5 プラグイン開発 iOS 7 Xcode 5
Xcode 5 プラグイン 開発のテクニック iOS 7 Xcode 5

結構、ややこしいのですが、今回は、極めて簡単にプラグインを作成できるテンプレートがあったので、それを使用させていただきました。

作成したのは、Xcodeで現在編集中のソースコードのGitHub上の物を簡単に閲覧するプラグインです。

今回、サンプルとして作成したプラグインをインストールすると、Xcodeの「View」メニューの一番下に、「GitHub」というメニューが追加されます。

002

そして使用している様子は、次のようの感じです。

(画像をクリックすると動作が確認できます。)

001

編集しているファイルに合致したコードにアクセスできているのが分かっていただけるでしょうか。

上記の例では、作業中のプロジェクトのリモート上のブランチが次のようになっています。

$ git branch -r
 origin/HEAD -> origin/master
 origin/develop
 origin/feature/select-menu
 origin/fix/popup-menu
 origin/master

なので、プラグインは、この情報を読み取って、次のようなポップアップを出しているわけです。

003

コードは、下記にあります。 「とりあえず出来た」っと言うレベルで、突っ込みどころ満載です。プルリクお待ちしております。

github サンプルコード(http://github.com/furuya02/OpenGithub)

2 Xcode-Plugin-Template

今回使用させて頂いたのは、Xcode-Plugin-Templateというテンプレートです。

https://github.com/kattrali/Xcode-Plugin-Template

上記でzipファルをダウンロードし、その中のXcodePlugin.xctemplateディレクトリを ~/Library/Developer/Xcode/Templates/File Templates/の下に置くだけです。(Templates及び、File Templatesは、無かったので作成しました。)

配置後、Xcodeを起動すると、new ProjectOS XFile Tremplatesの中にXcode Pulginというテンプレートを確認できます。

004

このテンプレートからプロジェクトを作成して実行するとサンプルとなっているプラグインで、メニューの追加と、メッセージ表示が確認できます。

メニューは、「Edit」の一番下に「Do Action」が追加されます。

006

メニューを選択すると、次のようなポップアップが表示されます。

007

既に、これでプラグインの体裁は出来てしまっているわけですから、超簡単です。

3 デバッグ

プラグインは、Xcodeを再起動しないと読み込めないので、ちょっと動作確認が大変なのですが、この辺、このテンプレートはうまくできています。

このテンプレートでは、コマンド+Rでデバッグ実行すると、編集中のプラグインを読み込んで新しいXcodeが立ち上がります。

なので、2つ立ち上がったXcodeの親子関係をよく理解していないと、訳が分からなくなってしまいます。

下の図は、デバック実行中の状態です。

008

左側が親となっているXcodeで、デバッグ実行中になっています。そして、右のXcodeが、子となるXcodeで、ビルドしたプラグインが動作しているものです。

ブレークポイントなどは、左の親Xcodeで掛けられます。 また、開発したプラグインは、右の子となるXcodeがアクティブになっていないと確認できません。

親のXcodeで「デバッグ中止」とするか、子のXcodeを終了すると、デバッグ終了です。(親Xcodeを終了すると2つとも落ちます) 子のXcodeは、iOS開発中のiPhoneシュミレータみたいな感じですね。

4 プラグインのインストール

プラグインが正常に動作しないと、Xcode自体が起動できなくなったりします。 プラグイン開発前には、プラグインのインストールと、アンインストールの方法をしっかりと把握してから始めた方がいいでしょう。

(1) ディレクトリ

プラグインがインストールされるディレクトリは、次のうちのどちらかです。

  • ~/Library/Application\ Support/Developer/Shared/Xcode/Plug-ins
  • /Library/Application\ Support/Developer/Shared/Xcode/Plug-ins

上記のディレクトリに[プラグイン名].xcpluginという名前でインストールされます。

このファイルを消してしまえば、次から立ち上がるXcodeには、プラグインがインストールされません。 削除は、Xcodeで開発中であれば、Product-Cleanで消しても大丈夫です。

(2) バンドルの許可

初めてのプラグインを認識した時、Xcodeは、インストールを許可するかどうか問い合わせを表示します。

005

ここで、Load Bundle を選択するとプラグインはインストールされますが、Skip Bundleの方を選択してしまうと、2度と問い合わせはありません。

これを再表示させるには、次のコマンドが必要になります。

defaults delete com.apple.dt.Xcode DVTPlugInManagerNonApplePlugIns-Xcode-[Xcodeのバージョン]

5 プラグインの動作

プラグインが動作するためには、何らかのトリガーが必要ですが、それには、次のようなものがあります。

  • Notification通知のトラップ
  • メニュー追加
  • 既存のメソッドのすり替え(Swizzling)

今回作成したプラグインでは、このうち、メニューを追加する方法を選択しました。

下記では、Viewのメニューを捕まえて、セパレータと「GitHub」というメニューを追加しています。

NSMenuItem *menuItem = [[NSApp mainMenu] itemWithTitle:@"View"];
if (menuItem) {
    [[menuItem submenu] addItem:[NSMenuItem separatorItem]];
    NSMenuItem *actionMenuItem = [[NSMenuItem alloc] initWithTitle:@"Github" action:@selector(doAction) keyEquivalent:@"g"];
    [actionMenuItem setKeyEquivalentModifierMask: NSAlternateKeyMask];
    [actionMenuItem setTarget:self];
    [[menuItem submenu] addItem:actionMenuItem];
}

6 情報取得

プラグインとして何らかの機能を追加する時、Xcodeの状態などを取得する必要があります。 今回のプラグインでは、IDEWorkspaceWindowControllerクラスから芋づる式にアクティブとなっているエディタウインドウのファイル名や、プロジェクト名を取り出しています。

// ディスク上のディレクトリとアクテブトなっているファイル名の取得
NSArray *workspaceWindowControllers = [NSClassFromString(@"IDEWorkspaceWindowController") valueForKey:@"workspaceWindowControllers"];
NSString *directory = nil;
NSString *fileName = @"";
for (id controller in workspaceWindowControllers) {
    id window = [controller performSelector:@selector(window)];
    if ( [window isEqual:[NSApp keyWindow]]) {
        id workSpace = [controller valueForKey:@"_workspace"];
        id filePath = [workSpace performSelector:@selector(representingFilePath)];
        NSString *workspacePath = [filePath performSelector:@selector(pathString)];
        directory = [workspacePath stringByDeletingLastPathComponent];

        id editorArea = [controller performSelector:@selector(editorArea)];
        id document = [editorArea performSelector:@selector(primaryEditorDocument)];
        // 2015/02/18 修正しました
        // NSString *fileFullName = [document fileURL];
        NSURL *fileFullName = [document fileURL];
        fileName = [fileFullName lastPathComponent];
    }
}

2015/02/18 上記のコードについて追記

IDEWorkspaceWindowControllerなどの非公開クラスを使用する場合は、isKindOfClassや、respondsToSelectorで確認してから使用する方が良いとのご指摘を頂きました。

for (id controller in workspaceWindowControllers) {
        if (![controller isKindOfClass:NSViewController.class) {
            continue;
        }
        NSViewController *vc = (NSViewController *)controller;
        if (![vc.window isEqual:NSApp.keyWindow]) {
            continue;
        }
        id workSpace = [vc valueForKey:@"_workspace"];
        if (![workSpace respondsToSelector:@selector(representingFilePath)) {
            continue;
        }
        id filePath = [workSpace performSelector:@selector(representingFilePath)];
    ...

また、リモートブランチ名や、サーバのURLは、単純にgitコマンドを使用しています。

// ペースURLとブランチの取得
NSMutableArray *branches = [NSMutableArray array];
NSString *baseUrl = @"";
if(directory!=nil){
    for (NSString *line in [self shell:directory :@"git branch -r"]) {
        NSRange searchResult = [line language="rangeOfString:"];
        if(searchResult.location == NSNotFound){ // HEADの無い行だけ採用
            // 空白、改行を削除
            NSString *tmp = [(NSString*)line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
            // 先頭の「origin/」を削除
            tmp = [tmp substringFromIndex:(NSUInteger)7];
            [branches addObject:tmp];
        }
    }
    for (NSString *line in [self shell:directory :@"git remote -v"]) {
        NSRange searchResult = [line language="rangeOfString:"]; 
        if(searchResult.location != NSNotFound){ // (fetch)のある行だけ採用
            NSString *tmp = [(NSString*)line stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];

            NSCharacterSet *spr = [NSCharacterSet characterSetWithCharactersInString:@" \t"];
            NSArray *arry = [tmp componentsSeparatedByCharactersInSet:spr];
            if (arry.count == 3 ){
                baseUrl = [arry objectAtIndex:1];
            }
        }
    }
}

7 最後に

ちょっとハードルが高いかなと思ったプラグインですが、一通りの仕様を確認すれば、このテンプレートで簡単に作成できそうです。 テンプレートの作者及び、各種のドキュメントを公開してくださっている先駆者に感謝です。

公開したコードには、テキスト選択のNotificationで選択行を取得するコードが含まれているのですが・・・これはGitHub上のコードを表示した後、そこまでスクロールするつもりで実装始めたのですが、力尽きたのでその残骸です・・・

8 参考


初めてのXcode 5 プラグイン開発 iOS 7 Xcode 5
Xcode 5 プラグイン 開発のテクニック iOS 7 Xcode 5
How To Create an Xcode Plugin: Part 1/3
How To Create an Xcode Plugin: Part 2/3
How To Create an Xcode Plugin: Part 3/3
知識ゼロからXcodeのプラグインを開発してみた
gmaesak/ConvertUIColor
エンジニア Blog Home/ エンジニア Blog / Xcodeプラグインの作成方法
いつかに備えてXcode Plugin作成コトハジメ
Extending Xcode 6 with plugins