LoginSignup
1

More than 3 years have passed since last update.

Xcode Source Editor Extensionでテキストを複数選択するときに気づきにくい挙動

Posted at

Xcode Source Editor Extensionでテキストの複数選択を取得する場合、
XCSourceTextRangeを使用することで取得可能ですが、実際の使用感とは異なるケースに遭遇したため、以下にまとめました。

環境

  • Xcode10.3
  • Swift5

サンプルコード

SourceEditorCommand.swiftは以下のコードを使用することとします。

import Foundation
import XcodeKit

class SourceEditorCommand: NSObject, XCSourceEditorCommand {

    func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
        let textBuffer = invocation.buffer
        let lines = textBuffer.lines
        let selections = textBuffer.selections

        guard let selection = selections.firstObject as? XCSourceTextRange,
            let _lines = Array(lines) as? [String] else {
            completionHandler(NSError(domain: "Sample", code: 401, userInfo: ["reason": "text is not selected"]))
            return
        }

        let startLine = selection.start.line
        let endLine = selection.end.line

        print("selection: \(selection)")
        print("startLine: \(startLine)")
        print("endLine: \(endLine)")

        let selectedLines = Array(_lines[startLine...endLine])
        print("selectedLines: \(selectedLines)")
        completionHandler(nil)
    }
}

検証

以下の画像のようにSampleからTestの末尾までの2行を選択してSourceEditorCommandを実行します。
Screen Shot 2019-09-06 at 23.19.59.png

すると以下のように出力されます。

selection: <XCSourceTextRange: 0x7fd7a4702600 {{line: 0, column: 0}, {line: 1, column: 4}}>
startLine: 0
endLine: 1
selectedLines: ["Sample\n", "Test\n"]

XCSourceTextRangeの出力では、lineが行番号、columnがその行の何番目を示しているので正しい出力のように見えます。

では次に選択範囲の終端を文字ではなく、行末に変更するとどうなるでしょうか
Screen Shot 2019-09-06 at 23.22.07.png

結果は、selectedLinesArray index is out of rangeのためcrashします

selection: <XCSourceTextRange: 0x7fd2ef813f80 {{line: 0, column: 0}, {line: 2, column: 0}}>
startLine: 0
endLine: 2
Fatal error: Array index is out of range
2019-09-06 23:26:30.600303+0900 Extension[3649:87042] Fatal error: Array index is out of range

原因

終端が文字ではなく、行末の状態で複数行選択しているとselection.end.line実際に選択している次の行の先頭を示します。サンプルの場合だと3行目の0番目を指していることになります。しかし実際に存在するのは["Sample", "Test"]の2行のみのためArray index is out of rangeとなります。

完成コード

そのため、選択範囲を正しく抽出したい場合、以下のように書き換える必要があります。

import Foundation
import XcodeKit

class SourceEditorCommand: NSObject, XCSourceEditorCommand {

    func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void ) -> Void {
        let textBuffer = invocation.buffer
        let lines = textBuffer.lines
        let selections = textBuffer.selections

        guard let selection = selections.firstObject as? XCSourceTextRange,
            let _lines = Array(lines) as? [String] else {
            completionHandler(NSError(domain: "Sample", code: 401, userInfo: ["reason": "text is not selected"]))
            return
        }

        let startLine = selection.start.line

        // 複数行選択かつ、selection.end.columnが0番目を指している場合
        let endLine = (selection.end.column == 0 && (selection.start.line != selection.end.line))
            ? selection.end.line - 1
            : selection.end.line


        print("selection: \(selection)")
        print("startLine: \(startLine)")
        print("endLine: \(endLine)")

        let selectedLines = Array(_lines[startLine...endLine])
        print("selectedLines: \(selectedLines)")
        completionHandler(nil)
    }
}

すると以下のようにendLineが正しく出力されます。

selection: <XCSourceTextRange: 0x7f8589e0abc0 {{line: 0, column: 0}, {line: 2, column: 0}}>
startLine: 0
endLine: 1
selectedLines: ["Sample\n", "Test\n"]

まとめ

上記の対応をすることで、選択範囲の終端が文字or行であっても、対応可能となります。
複数行のテキスト選択を用いたXcode Source Editor Extensionを開発する方は是非参考にしてみてください。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1