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
を実行します。
すると以下のように出力されます。
selection: <XCSourceTextRange: 0x7fd7a4702600 {{line: 0, column: 0}, {line: 1, column: 4}}>
startLine: 0
endLine: 1
selectedLines: ["Sample\n", "Test\n"]
XCSourceTextRange
の出力では、lineが行番号、columnがその行の何番目を示しているので正しい出力のように見えます。
では次に選択範囲の終端を文字ではなく、行末に変更するとどうなるでしょうか
結果は、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
となります。
完成コード
そのため、選択範囲を正しく抽出したい場合、以下のように書き換える必要があります。
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を開発する方は是非参考にしてみてください。