Xcode Source Editor Extension でファイルに応じて動作を変えてみる。
Xcode Source Editor Extension
先日の UUID を挿入する Xcode Source Editor Extension で、開いているファイルの種類に応じて挙動を変えてみました。
先日に こちら で『UUID を自動生成して挿入する Xcode Source Editor Extension』を作成してみましたけど、ソースコードを編集中には二重引用符で括った UUID を挿入するようにしてみました。
ここで記すのと同じ趣旨の Xcode Source Editor Extension を es-kumagai/ESXcodeSourceEditorExtensionUUID にアップしておきます。
方向性
実の元々は Xcode で開いた Markdown に UUID を挿入するのが目的だったので、この前みたいにシンプルに UUID 文字列をそのまま挿入できればよかったのですけど、せっかくなら Swift や Objective-C のコードを書いている時には文字列リテラルとして入れたいなと思って、そうしてみました。
開いているファイルの種類を知る
開いているファイルの種類を知るのは簡単で、symbolメソッド
で受け取れる invocation.buffer
が持っている contentUTIプロパティ
で取得できます。
このプロパティーを参照すると、ファイルの種類が UTI
で表現された String
で取得できるので、これで判定処理を行います。
対象の UTI を知る
今回は、開いているファイルがソースコードかどうかを判定したいので、まずは何を開いたときにどんな UTI が取得できるかを調べてみると、とりあえず自分の関心どころは次の通りでした。
ファイルの種類 | UTI |
---|---|
Swift ソースファイル | public.swift-source |
Objective-C ヘッダーファイル | public.c-header |
Objective-C ソースファイル | public.objective-c-source |
Playground ファイル | com.apple.dt.playground |
Playground Page ファイル | com.apple.dt.playgroundpage |
Markdown ファイル | net.daringfireball.markdown |
XML ファイル | public.xml |
文字列リテラルの表現は文字列ごとに違ってくるので、厳密に実装するのであればもっと細かく判定しないといけないところでしょうけれど、今回はとりあえずそこまでは考慮しないことにします。
それよりも、今回はもっとざっくりと、ソースコードであれば全て『二重引用符で括った UUID 文字列』ということにしようと思います。UTI は幸い、継承関係を持っていて、先ほどの UTI
をみると多くのソースコードが public.source-code
を継承しているので、今回はそれを対象にして見ます。
ただ Playground ファイルはこの UTI を継承していないようで、それ自体は com.apple.dt.playground
になっています。また、同じ Playground のように見えても Playground Page だったりすると、こちらは com.apple.dt.playgroundpage
になっていて、これらは継承関係がないようなので、両方に対応する必要がありそうです。
UTI を判定できるようにする
UTI を継承関係も含めて適切なものを判定するには UTTypeConformsTo関数
を使います。この関数は CoreServices.LaunchServices.UTType
で定義されていて、Foundation
をインポートすれば利用できるようになります。
ただ、この関数は CFString
を使って判定を行う必要があるのと、C 言語的な API なので Swift でそのまま使うとコードの冗長さが目立ってくるので、まずは UTI 文字列を簡単に扱うための UTI
構造体を作っておくことにします。
今回のコードで特に大事になるのは ~=演算子
で、これを使って switch
構文で継承関係も含めたパターンマッチを行います。
struct UTI {
var string: String
}
extension UTI {
func conforms(to uti: UTI) -> Bool {
return UTTypeConformsTo(string as CFString, uti.string as CFString)
}
static func ~= (pattern: UTI, value: UTI) -> Bool {
return value.conforms(to: pattern)
}
}
extension UTI : CustomStringConvertible {
var description: String {
return string
}
}
extension UTI : ExpressibleByStringLiteral {
init(stringLiteral value: String) {
self.init(string: value)
}
init(extendedGraphemeClusterLiteral value: String) {
self.init(string: value)
}
init(unicodeScalarLiteral value: String) {
self.init(string: value)
}
}
UTI を踏まえて UUID 文字列を作る
上のように準備ができたら、ファイルの種類に応じて加工した UUID 文字列を生成して、とりあえず 変数uuidString 変数に格納します。
let uuid = UUID()
let uuidString: String
switch UTI(string: buffer.contentUTI) {
case "public.source-code",
"com.apple.dt.playground",
"com.apple.dt.playgroundpage":
uuidString = "\"\(uuid)\""
default:
uuidString = "\(uuid)"
}
完成へ
そして、あとは performメソッド の中で、それを使って挿入処理をしてあげれば完成です。
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 {
var uuidString: String {
let uuid = UUID()
switch UTI(string: buffer.contentUTI) {
case "public.source-code",
"com.apple.dt.playground",
"com.apple.dt.playgroundpage":
return "\"\(uuid.description)\""
default:
return uuid.description
}
}
let lines = buffer.lines
let text = lines[selection.start.line] as! NSString
let textStartIndex = selection.start.column
let newText = "\(text.substring(to: textStartIndex))\(uuidString)\(text.substring(from: textStartIndex))"
lines[selection.start.line] = newText
}
completionHandler(nil)
}
}
実際のところは、ソースコードだったらそれが文字列リテラル内なのか外かで挿入するテキストを変更したりすると使い勝手が向上するような気もしますね。他にも、文字列リテラルがシングルクォーテーションの場合もあるでしょうから、そういったところも配慮すると洗練されてくるのかなと思います。
そういったものを考慮するのって相当大変になると思うので、とりあえず今はそこまで追求しないことにしておきます。