teakun.dev

Xcode source editor extensionを試した記録

試してみたので過程を残しておく。

エディタ上で選択した範囲からその部分がハイライトされるGitHubのリンクを生成するExtensionを作りたかったが、それはできないということが分かって残念。 環境はBigSur11.1でXCode12.3。

ビルドするまで

まずはこのあたりの資料を参考にシンプルなプロジェクトを作ってみる。2017年とちょっと古い資料だけど、基本的な部分は変わっていない。とても参考になった。

これを元にシンプルなプロジェクトができたけど、そのままだとXCodeがクラッシュ、コマンドが認識されないなどの問題が起きた。

XCode12以降でビルドできない問題

extensionを作るためにはXCodeKit.frameworkを使う必要があるけれど、XCode12以降ではそれがembedになっている必要がある。

For compatibility with new security features in macOS 11, Xcode extensions must be built using Xcode 12 and must embed XcodeKit.framework. An Xcode extension rebuilt with these tools remains compatible with older versions of Xcode and macOS.

ということで、Embedの設定をEmbed & Signに変えてやるとよい。

一応他にも同じ問題で困っている人がいるようだった。

Extensionの権限がない問題

上の設定の問題が解消してビルドができるようになったが、自分の環境ではまだ新しく作ったコマンドがXCodeで表示されない問題があった。 どうやら作ったExtensionにmacOS自体の機能拡張の権限が渡っていなかったのが原因。

システム環境設定 -> 機能拡張 -> XCode Source Editorから作ったExtensionのチェックを入れ直して解決。

できたこと

選択した行の取得

エディタで選択している行を取得することはできた。 コードはざっくりこんなところ

    func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
        if invocation.buffer.selections.count <= 0 {
            completionHandler(nil)
            return
        }
        
        guard let targetSelection = invocation.buffer.selections[0] as? XCSourceTextRange else {
            completionHandler(nil)
            return
        }
        
        let startLine = targetSelection.start.line
        let endLine: Int
        if targetSelection.end.column == 0 && (targetSelection.start.line != targetSelection.end.line) {
            endLine = targetSelection.end.line - 1
        } else {
            endLine = targetSelection.end.line
        }
        
        print("start:",startLine)
        print("end:", endLine)

        completionHandler(nil)
    }

行数のカウント(XCSourceTextPosition.line)は0から始まるので注意。エディタの左に出る数字とは1つずれるのでややこしい。

1つハマりどころがあって、複数行選択しているかつ、選択範囲の終端が行末にあるときに、上のコードで言うところの、targetSelection.end.lineが実際に選択している次の行の数になってしまう。 この問題もよくまとまっている記事があって、とても参考になりました。 正直いうとほとんどこの記事を参考に実装した。

クリップボードへの保存

  let pasteboard = NSPasteboard.general
  pasteboard.declareTypes([.string], oner: nil)
  pasteboard.setString(text, forType: .string

ざっくりこんな感じ。シンプル。

できなかったこと

開いているファイルの名前を取得

そもそもサポートされていない。ちょっとこれができないのは厳しい。

gitのリモートリポジトリ、ブランチの取得

Extensionの仕組みとしてはこちらも用意されていない様子。

代案としてshellコマンドをExtension上から実行してリモートリポジトリ、ブランチを取得する方法を考えたが、これも難しかった。 gitコマンドを実行すると、xcrun: error: cannot be used within an App Sandboxとエラーが出てしまう。 これは、App Sandboxという仕組みがmacOSアプリ開発にあって、あらゆる権限が縛られているらしい。確かに自由にシェルのコマンドが打ててしまうと権限管理的にまずいというのは分かる。AppSandboxでは他にもfinderのアクセス、ハードウェアアクセスなどが縛られていた。もっとよく調べたらこれは解決できそうだったけど、先に書いたファイル名を取得できない問題が解決できないのでこれ以上の調査は断念。

コマンドを実行する方法はこの記事がわかりやすかった。この実装自体はコマンドラインツールを作るときにも使いそう?

おわり

Extensionは想像以上に何も作れないということが分かった。もうちょっとAPIを開放してくれたらいいのになあ…

あと、このブログ見出しが見にくくて読みにくいなと自分で思った。ブログのデザインもうちょっといい感じにしたい。