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

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

## 環境
- Xcode10.3
- Swift5

## サンプルコード
`SourceEditorCommand.swift`は以下のコードを使用することとします。

```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`を実行します。
<img width="537" alt="Screen Shot 2019-09-06 at 23.19.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116582/6645a90c-6b74-4fcc-7c3d-d4fc29510384.png">



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

```
selection: <XCSourceTextRange: 0x7fd7a4702600 {{line: 0, column: 0}, {line: 1, column: 4}}>
startLine: 0
endLine: 1
selectedLines: ["Sample\n", "Test\n"]
```
`XCSourceTextRange`の出力では、lineが行番号、columnがその行の何番目を示しているので正しい出力のように見えます。


では次に選択範囲の終端を文字ではなく、**行末**に変更するとどうなるでしょうか
<img width="531" alt="Screen Shot 2019-09-06 at 23.22.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116582/e6f73a09-06a6-f362-ec13-bd144ae92437.png">

結果は、`selectedLines`で`Array 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`となります。

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

```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

        // 複数行選択かつ、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を開発する方は是非参考にしてみてください。