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
- SourceEditorCommand.swift
作成されたファイルの役割
まず SourceEditorExtension.swift
では SourceEditorExtensionクラス
が定義されていて、準拠している XCSourceEditorExtensionプロトコル
によって、次の2つの機能を実装できるようになっていました。
機能 | 内容 |
---|---|
extensionDidFinishLaunching() | この Xcode Souce Editor Extension が起動したときに読み込まれるようです。どのスレッドで呼び出されるかは分からないらしい。 |
commandDefinitions | Info.plist の NSExtensionAttributes
にある XCSourceEditorCommandDefinitions
とは違う定義を返したいときに実装するようです。これは XCSourceEditorCommandDefinitionKey
をキーにとる辞書型になっていて、ここで identifierKey
と nameKey
と classNameKey
の値を明示できます。 |
そして SourceEditorCommand.swift
では SourceEditorCommandクラス
が定義されていて、準拠している XCSourceEditorCommandプロトコル
によって、次の1つの機能が実装できるようになっていました。
機能 | 内容 |
---|---|
perform(with:completionHandler:) | コマンドが実行されたときの処理を記述します。このとき (Error?) -> Void 型の Completion Handler が渡ってくるので、処理を終えたときに必ずそれを実行する必要があるようです。Error? には、処理が完了または何もしなかった場合に nil を、失敗した場合にはエラーを引数に添えて呼び出します。 |
これらの機能は、どれも必ずしもメインスレッドで実行されるとは限らないようです。どこかのスレッドに依存する処理が必要な場合は、それを踏まえて実装する必要がありそうです。
機能を実装してみる
それでは Xcode Source Editor Extension に機能を実装してみます。
コマンドを規定する
コマンドを規定するには、ターゲットの Info.plist
に追記する方法と、SourceEditorExtension.swift
の commandDefinitions
で定義する方法とがあるようですけど、今回は前者の方法を使ってみることにします。後者についてはまだ、調べていません。
ターゲットの Info.plist
で定義する方法は簡単で、ほとんどの定義が既に終わっていて、今回の場合は NSExtension
→ NSExtensionAttributes
→ XCSourceEditorCommandDefinitions
に格納されているアイテムの XCSourceEditorCommandName
で、Xcode のメニューに表示したい名称を設定するだけでした。
メニューのルート名について
Xcode Source Editor Extension で追加したメニューは Editor
メニュー内に置かれるのですけど、そのメニュー項目の名前は Info.plist
の Bundle name
になるようです。このとき、少なくとも Xcode 8.1 では Bundle display name
に設定した名前は無視されるようでした。
Bundle name は、既定では $(PRODUCT_NAME)
が設定されているので、メニューのルート名を変更したい場合は、ターゲットの Build Settings
で Product Name
を調整するといいかもしれません。
機能を実装する
肝心の機能は SourceEditorCommand.swift
の performメソッド
で実装します。
今回の実装は、とりあえず単純に、カーソルのある始めの文字位置に 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
に対して insert
や removeObject
を使って行います。
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 {
}
ソースコード操作の要点
これまでの話をまとめると、次のようになります。
- invocation から buffer を取得する。
- buffer.selection を活用して、buffer.lines でソースコードを参照する。
- buffer.lines を書き換えて編集したり buffer.selection を書き換えて選択状況を更新する。
- completionHandler を呼び出して編集操作を終了する。
デバッグしてみる
Xcode Source Editor Extension をデバッグするにあたって、いくつかの準備をする必要がありました。
初回までに必要な設定
スキームの設定
Xcode Source Editor Extension のターゲットをアクティブにして、Run スキーム の Executable
で Xcode を指定します。また Debug executable
のチェックは外しておく必要があるようでした。
プロジェクトの Signing 設定
Xcode Source Editor Extension とそれをホストする Cocoa アプリケーション、両方のターゲットの設定で General
の 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 をビルドして、デバッグできるようになります。