Xcode Source Editor Extension を作成してみる

Xcode Source Editor Extension

Xcode でソースファイルを編集中に UUID を簡単に生成できると都合が良かったので、そんな Xcode Source Editor Extension を試作してみることにしました。


Xcode でソースファイルを編集中に、個人的な事情でテキスト内に UUID を簡単に挿入できると都合が良かったので、Xcode 8 の新機能 Xcode Source Editor Extension を使って実現してみることにしました。

Xcode Source Editor Extension を使うと、ソースコードの編集に関する機能を自由に実装して Xcode のメニューから呼び出せるようになります。

ここで記すのと同じ趣旨の Xcode Source Editor Extension を es-kumagai/ESXcodeSourceEditorExtensionUUID にアップしておきます。

ターゲットの作成

まずは Xcode で Cocoa Application プロジェクトを作成します。

体裁的には、ここで作成したアプリケーションに Xcode Source Editor Extension をホストして頒布するみたいになるようです。プロジェクト名はホストするアプリケーションの名前として、目に付くところで使われます。

そしてプロジェクトの設定画面で、ターゲット Xcode Source Editor Extension を追加します。

自動で作成されたファイル

これで Xcode Source Editor Extension がプロジェクトについかされて、次の2つのファイルが生成されます。

作成されたファイルの役割

まず SourceEditorExtension.swift では SourceEditorExtensionクラス が定義されていて、準拠している XCSourceEditorExtensionプロトコル によって、次の2つの機能を実装できるようになっていました。

機能 内容
extensionDidFinishLaunching() この Xcode Souce Editor Extension が起動したときに読み込まれるようです。どのスレッドで呼び出されるかは分からないらしい。
commandDefinitions Info.plist の NSExtensionAttributes にある XCSourceEditorCommandDefinitions とは違う定義を返したいときに実装するようです。これは XCSourceEditorCommandDefinitionKey をキーにとる辞書型になっていて、ここで identifierKeynameKeyclassNameKey の値を明示できます。

そして SourceEditorCommand.swift では SourceEditorCommandクラス が定義されていて、準拠している XCSourceEditorCommandプロトコル によって、次の1つの機能が実装できるようになっていました。

機能 内容
perform(with:completionHandler:) コマンドが実行されたときの処理を記述します。このとき (Error?) -> Void 型の Completion Handler が渡ってくるので、処理を終えたときに必ずそれを実行する必要があるようです。Error? には、処理が完了または何もしなかった場合に nil を、失敗した場合にはエラーを引数に添えて呼び出します。

これらの機能は、どれも必ずしもメインスレッドで実行されるとは限らないようです。どこかのスレッドに依存する処理が必要な場合は、それを踏まえて実装する必要がありそうです。

機能を実装してみる

それでは Xcode Source Editor Extension に機能を実装してみます。

コマンドを規定する

コマンドを規定するには、ターゲットの Info.plist に追記する方法と、SourceEditorExtension.swiftcommandDefinitions で定義する方法とがあるようですけど、今回は前者の方法を使ってみることにします。後者についてはまだ、調べていません。

ターゲットの Info.plist で定義する方法は簡単で、ほとんどの定義が既に終わっていて、今回の場合は NSExtensionNSExtensionAttributesXCSourceEditorCommandDefinitions に格納されているアイテムの XCSourceEditorCommandName で、Xcode のメニューに表示したい名称を設定するだけでした。

メニューのルート名について

Xcode Source Editor Extension で追加したメニューは Editor メニュー内に置かれるのですけど、そのメニュー項目の名前は Info.plistBundle name になるようです。このとき、少なくとも Xcode 8.1 では Bundle display name に設定した名前は無視されるようでした。

Bundle name は、既定では $(PRODUCT_NAME) が設定されているので、メニューのルート名を変更したい場合は、ターゲットの Build SettingsProduct Name を調整するといいかもしれません。

機能を実装する

肝心の機能は SourceEditorCommand.swiftperformメソッド で実装します。

今回の実装は、とりあえず単純に、カーソルのある始めの文字位置に UUID を新規作成して挿入するようにしてみます。複数選択されている場合や、範囲選択されている場合は考慮していません。

import Foundation
import XcodeKit

class SourceEditorCommand: NSObject, XCSourceEditorCommand {
	
	func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
		
		let buffer = invocation.buffer
		
		if let selection = buffer.selections.firstObject as? XCSourceTextRange {
			
			let lines = buffer.lines
			
			let text = lines[selection.start.line] as! NSString
			let textStartIndex = selection.start.column

			let newText = "\(text.substring(to: textStartIndex))\(UUID())\(text.substring(from: textStartIndex))"

			lines[selection.start.line] = newText
		}
		
		completionHandler(nil)
	}
}

今回の要所を、ざっくりと記しておきます。

Completion Handler

最後に必ず、引数で渡されてきた completionHandler を呼び出す必要があります。

completionHandler(nil)

処理が正常に終了した場合(正常に、何も処理をしなかった場合も含む)には Completion Handler の引数に nil を渡します。エラーが発生した場合は、何らかのエラーを表現するインスタンスを指定しますが、基本的には NSError で作ってあげれば良さそうです。

XCSourceEditorCommandInvocation

Source Code Editor に関する情報は、引数に XCSourceEditorCommandInvocation のインスタンスで渡されてきます。

このインスタンスが持っている XCSourceTextBuffer型bufferプロパティ を使って、ソースコードの内容を編集していくことになります。

let buffer: XCSourceTextBuffer = invocation.buffer

XCSourceTextBuffer.lines

まず、この中にある linesプロパティ が、ソースコードの各行を保存しています。

型は NSMutableArray になっていて、各要素は Any型 で取得することになりますけど、内部は NSString になっています。配列自体は編集可能 (Mutable) で、中身それぞれ自体は編集不可 (Immutable) なので、書き換えるときは行単位で差し替えることになります。

let lines: NSMutableArray = buffer.lines

行単位での追加や削除の機能も、この lines に対して insertremoveObject を使って行います。

lines.insert(text, at: lineNumber)

lines.removeObject(at: lineNumber)

XCSourceTextBuffer.selections

現在のカーソルがある位置や選択されているテキストの範囲を取得するには XCSourceTextBuffer が持っている selections を参照します。

これも NSMutableArray になっていて、選択範囲を編集することが可能です。さらに配列の値は XCSourceTextRange型 で、現在位置を示す lineプロパティ と現在の文字位置を示す columnプロパティ を読み書きできます。

if let selection = buffer.selections.firstObject as? XCSourceTextRange {

}

ソースコード操作の要点

これまでの話をまとめると、次のようになります。

デバッグしてみる

Xcode Source Editor Extension をデバッグするにあたって、いくつかの準備をする必要がありました。

初回までに必要な設定

スキームの設定

Xcode Source Editor Extension のターゲットをアクティブにして、Run スキームExecutableXcode を指定します。また Debug executable のチェックは外しておく必要があるようでした。

プロジェクトの Signing 設定

Xcode Source Editor Extension とそれをホストする Cocoa アプリケーション、両方のターゲットの設定で GeneralSigningEnable Development Signingボタン を押して、署名を Automatically manage signing な状態にしておく必要があるようです。

設定は、ターゲット設定の General で行います。

このときに指定する Term は、必ずしも Apple Developer Program の有料年間購読をしている必要はなさそうでした。

インストール済みの場合は無効化

作成している Xcode Source Editor Extension を正式にインストールしてある場合は、それを無効化しないとデバッグ実行時にも優先的にそれが利用されてしまうことがあるようでした。

そのため、作成中の方が実行されない場合は、macOS のシステム環境設定にある 機能拡張 で、該当する Xcode Source Editor Extension からチェックを外す必要があるかもしれません。

ここで目的の Xcode Server Editor Extension からチェックを外してあげると、現在作成中のものがデバッグ実行できるようになる様子でした。ただ、いったんデバッグ実行をすると、その時点で、正式にインストールされている Extension が再び有効化されるようだったので、特に気をつけておきたいところかもしれません。

デバッグする

これまでに記したようにして Xcode Source Editor Extension をデバッグできる環境が整っていると、通常どおりに Run 実行してあげるだけで、自動的に Xcode が起動して、作成中の Extension を使える状態になります。

Xcode Source Editor Extension が有効になると、Xcode の Editor メニューに、作成中のプロダクト名のメニューが追加されます。そこのサブメニューとして、今回は Info.plist に登録してあるメニュー項目が表示されるので、これを選択することで動作確認を行えます。

おまけ

Xcode Source Editor Extension のインストール方法

作成した Xcode Source Editor Extension をインストールするには、アプリをリリースするときと同様にターゲットを Archive して、Developer ID-signed Application としてエクスポートします。

そうすると、ホストアプリケーションが出来上がっているので、これを実行することで、その中に組み込まれている Xcode Source Editor Extension が macOS システムにインストールされます。そうしたら、あとは macOS の システム環境設定 から有効化できます。

Empty プロジェクトから作成した場合

今回は Xcode Source Editor Extension のプロジェクトを作るにあたって、最初に Cocoa アプリケーションを作成しましたが、もし最初に Empty プロジェクトを作った場合の対応方法についても記しておきます。

要所としては、作成する Xcode Source Editor Extension をホストする Cocoa アプリケーションが必要になるというところです。

ホストアプリケーションを実装

まず、プロジェクトに Cocoa Application ターゲットを追加します。ターゲット名は、インストールする際に実行するアプリケーション名でも使われます。

そして、作成した Cocoa アプリケーションのターゲット設定で、作成する Xcode Source Editor Extension ターゲットを Embedded Binaries に登録します。

さらに Xcode Source Editor Extension ターゲットの Build Settings で、Product Bundle Identifier が、先ほど追加したホストアプリケーションのサブドメインになるように調整します。

ホストアプリケーションがビルドされるようにする

最後に、Xcode Source Editor Extension のスキーム設定を調整して、ビルド時にホストアプリケーションがビルドされるようにします。

こうすることで、Empty プロジェクトから始めた場合でも、Xcode Source Editor Extension をビルドして、デバッグできるようになります。