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)
	}
}

実際のところは、ソースコードだったらそれが文字列リテラル内なのか外かで挿入するテキストを変更したりすると使い勝手が向上するような気もしますね。他にも、文字列リテラルがシングルクォーテーションの場合もあるでしょうから、そういったところも配慮すると洗練されてくるのかなと思います。

そういったものを考慮するのって相当大変になると思うので、とりあえず今はそこまで追求しないことにしておきます。