XCode8起,XCode不在支持插件,Alcatraz不能再使用了。蘋果提供了XCodeKit源碼編輯器拓展,使用它開發出XCode拓展來代替之前的插件。git
環境
macOS 10.12
XCode 8.2.1github
新建macOS Cocoa Application,命名爲MyCommentexpress
新建一個"Xcode Source Editor Extension"的target,命名爲MyCommentExtswift
注意:若是彈出詢問是否激活scheme提示,選擇激活api
在編輯框裏找到Run->Info->Executable選擇Other,而後找到XCode.app數組
以後運行這個拓展Target就會出現灰色Xcode,這個就是可用測試咱們工具的運行實例。xcode
MyCommentExt中Info.plist修改XCSourceEditorCommandIdentifier, XCSourceEditorCommandName內容。這個是命名菜單裏有咱們的命令。app
點運行後,隨便選一個工程打開,開一個源文件查看編輯器
在咱們建立的工程裏有兩個swift文件,SourceEditorExtension.swift, SourceEditorCommand.swift。前者能夠用來自定義命令,而命令在後者裏實現。經過繼承XCSourceEditorCommand,XCode在調用這個命令時將會執行perform方法。咱們能夠在perform裏實現咱們要的功能。下面引用了XCodeCComment的代碼。(稍微修改了一下它的小問題。)ide
// SourceEditorCommand.swift import Foundation import XcodeKit enum CommentStatus { case Plain case Unpair case Pair(range: NSRange) } class SourceEditorCommand: NSObject, XCSourceEditorCommand { func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Swift.Void ) { let buffer = invocation.buffer let selections = buffer.selections selections.forEach { let range = $0 as! XCSourceTextRange print("forEach", range.start, range.end) if range.start.line == range.end.line && range.start.column == range.end.column { let fake: XCSourceTextRange = range let lineText = buffer.lines[fake.start.line] as! String fake.start.column = 0 fake.end.column = lineText.distance(from: lineText.startIndex, to: lineText.endIndex) - 1 if fake.end.column > fake.start.column { let ref = handle(range: fake, inBuffer: buffer) if ref == true { range.end.column += 4 } } } else { let ref = handle(range: range, inBuffer: buffer) if ref == true { range.end.column += 4 } } } completionHandler(nil) } func handle(range: XCSourceTextRange, inBuffer buffer: XCSourceTextBuffer) -> Bool { let selectedText = text(inRange: range, inBuffer: buffer) let status = selectedText.commentStatus switch status { case .Unpair: break case .Plain: insert(position: range.end, length: 1, with: "*/", inBuffer: buffer) insert(position: range.start, length: 0, with: "/*", inBuffer: buffer) return true case .Pair(let commentedRange): let startPair = position(range.start, offsetBy: commentedRange.location, inBuffer: buffer) let endPair = position(range.start, offsetBy: commentedRange.location + commentedRange.length, inBuffer: buffer) replace(position: endPair, length: -("*/".characters.count), with: "", inBuffer: buffer) replace(position: startPair, length: "/*".characters.count, with: "", inBuffer: buffer) } return false } func text(inRange textRange: XCSourceTextRange, inBuffer buffer: XCSourceTextBuffer) -> String { if textRange.start.line == textRange.end.line { let lineText = buffer.lines[textRange.start.line] as! String let from = lineText.index(lineText.startIndex, offsetBy: textRange.start.column) let to = lineText.index(lineText.startIndex, offsetBy: textRange.end.column) return lineText[from...to] } var text = "" for aLine in textRange.start.line...textRange.end.line { let lineText = buffer.lines[aLine] as! String switch aLine { case textRange.start.line: text += lineText.substring(from: lineText.index(lineText.startIndex, offsetBy: textRange.start.column)) case textRange.end.line: text += lineText.substring(to: lineText.index(lineText.startIndex, offsetBy: textRange.end.column + 1)) default: text += lineText } } return text } func position(_ i: XCSourceTextPosition, offsetBy: Int, inBuffer buffer: XCSourceTextBuffer) -> XCSourceTextPosition { var aLine = i.line var aLineColumn = i.column var n = offsetBy repeat { let aLineCount = (buffer.lines[aLine] as! String).characters.count let leftInLine = aLineCount - aLineColumn if leftInLine <= n { n -= leftInLine } else { return XCSourceTextPosition(line: aLine, column: aLineColumn + n) } aLine += 1 aLineColumn = 0 } while aLine < buffer.lines.count return i } func replace(position: XCSourceTextPosition, length: Int, with newElements: String, inBuffer buffer: XCSourceTextBuffer) { var lineText = buffer.lines[position.line] as! String var start = lineText.index(lineText.startIndex, offsetBy: position.column) var end = lineText.index(start, offsetBy: length) if length < 0 { swap(&start, &end) } lineText.replaceSubrange(start..<end, with: newElements) lineText.remove(at: lineText.index(before: lineText.endIndex)) //remove end "\n" buffer.lines[position.line] = lineText } func insert(position: XCSourceTextPosition, length: Int, with newElements: String, inBuffer buffer: XCSourceTextBuffer) { var lineText = buffer.lines[position.line] as! String var start = lineText.index(lineText.startIndex, offsetBy: position.column + length) if start >= lineText.endIndex { start = lineText.index(before: lineText.endIndex) } lineText.insert(contentsOf: newElements.characters, at: start) lineText.remove(at: lineText.index(before: lineText.endIndex)) //remove end "\n" buffer.lines[position.line] = lineText } } extension String { var commentedRange: NSRange? { do { let expression = try NSRegularExpression(pattern: "/\\*[\\s\\S]*\\*/", options: []) let matches = expression.matches(in: self, options: [], range: NSRange(location: 0, length: self.distance(from: self.startIndex, to: self.endIndex))) return matches.first?.range } catch { } return nil } var isUnpairCommented: Bool { if let i = characters.index(of: "*") { if i > startIndex && i < endIndex { if characters[index(before: i)] == "/" || characters[index(after: i)] == "/" { return true } } } return false } var commentStatus: CommentStatus { if let range = commentedRange { return .Pair(range: range) } if isUnpairCommented { return .Unpair } return .Plain } }
perform中invocation.buffer能夠獲取到咱們執行命令時的緩衝文本信息,包括選中的文本內容,每行內容等。buffer.sections是所選文本內容數組,由XCSourceTextRange組成,它標示文本的位置區間,能夠找到所選文本的起始位置。buffer.lines是緩衝文本的每行對應的內容,經過對它進行操做起到操做文本的做用。
再次運行代碼能夠看到咱們的命令生效了
找到Products在Finder,將之拷貝到應用程序文件夾內,再重啓XCode就能夠看到生效了。
另外,在系統的「偏好設置」的「拓展」中也能夠看到拓展。若是咱們從AppStore或第三方下載到拓展,能夠這裏進行啓用和禁用。
從我運行過這個程序後,XCode自身帶有的註釋功能就不可用了。可是若是我調試Run了本工程時,卻又能夠用。中止調試後又不能用了。應該是XCode的bug吧,暫時沒有找到方法解決。知道解決的告訴我一下,謝謝。