教你如何用Swift編寫Xcode插件
-
本文由CocoaChina譯者@ztdj121 翻譯
-
作者: Krzysztof
在我的 AppCode 項目創建過程中,我想念最多的一件事是:能跳轉到記錄控制臺信息的指定文件和行。
Xcode不提供這樣的功能,而我不是一個喜歡抱怨的人,所以我決定自己寫個插件。 我用Swift來編寫這個插件。
想法
如果一個控制臺記錄了fileName.extension:XX 這樣一個名字,轉換成可點擊的超鏈接,這個鏈接將會打開指定的文件并將那行代碼高亮。
那樣你可以使用自己的記錄機制,只要添加這個簡單的前綴,比如:
【代碼】
func logMessage(message: String, filename: String = __FILE__, line: Int = __LINE__, funct: String = __FUNCTION__) {
print("\((filename as NSString).lastPathComponent):\(line) \(funct):\r\(message)")
} 或者可以使用 CocoaLumberjack ,你要想一些好的日志,可以用我的自定義格式。
Swift版本(Objective-C版本是 KZBootstrap 的一部分)
import Foundation
import CocoaLumberjack.DDDispatchQueueLogFormatter
class KZFormatter: DDDispatchQueueLogFormatter {
lazy var formatter: NSDateFormatter = {
let dateFormatter = NSDateFormatter()
dateFormatter.formatterBehavior = .Behavior10_4
dateFormatter.dateFormat = "HH:mm:ss.SSS"
return dateFormatter
}()
override func formatLogMessage(logMessage: DDLogMessage!) -> String {
let dateAndTime = formatter.stringFromDate(logMessage.timestamp)
var logLevel: String
let logFlag = logMessage.flag
if logFlag.contains(.Error) {
logLevel = "ERR"
} else if logFlag.contains(.Warning){
logLevel = "WRN"
} else if logFlag.contains(.Info) {
logLevel = "INF"
} else if logFlag.contains(.Debug) {
logLevel = "DBG"
} else if logFlag.contains(.Verbose) {
logLevel = "VRB"
} else {
logLevel = "???"
}
let formattedLog = "\(dateAndTime) |\(logLevel)| \((logMessage.file as NSString).lastPathComponent):\(logMessage.line): ( \(logMessage.function) ): \(logMessage.message)"
return formattedLog;
}
} 實現—主要部分
要實現那些需求我們需要做到兩點:
1、控制臺NSTextStorage fixAttributesInRange--這樣我們可以在找到正則表達式日志的時候隨時更改屬性。
2、NSTextView mouseDown--這樣在控制臺的鏈接里點擊鼠標的時候,我們可以強迫Xcode打開文件并高亮那一行。
怎樣把我們的功能注入到那些操作里去?
簡單調整:
static func swizzleMethods() {
let original = class_getInstanceMethod(NSClassFromString("NSTextStorage"), Selector("fixAttributesInRange:"))
method_exchangeImplementations(original, class_getInstanceMethod(NSClassFromString("NSTextStorage"), Selector("kz_fixAttributesInRange:")))
let original2 = class_getInstanceMethod(NSClassFromString("NSTextView"), Selector("mouseDown:"))
method_exchangeImplementations(original2, class_getInstanceMethod(NSClassFromString("NSTextView"), Selector("kz_mouseDown:")))
} 我們如何確定一個NSTextStorage 是控制臺實際的那個?
我們可以觀察IDEControlGroupDidChangeNotification ,找到IDEConsoleTextView 并使用相關對象把存儲標記為控制臺的那個,這個隨后就會排上用場。
guard let consoleTextView = KZPluginHelper.consoleTextView(),
let textStorage = consoleTextView.valueForKey("textStorage") as? NSTextStorage else {
return
}
textStorage.kz_isUsedInXcodeConsole = true 我們怎樣找到一個文件的路徑,而只有日志中的相對路徑?
我們可以用shell里的find命令,這就是你如何用swift語言運行且從一個shell命令中檢索響應。
static func runShellCommand(command: String) -> String? {
let pipe = NSPipe()
let task = NSTask()
task.launchPath = "/bin/sh"
task.arguments = ["-c", String(format: "%@", command)]
task.standardOutput = pipe
let file = pipe.fileHandleForReading
task.launch()
guard let result = NSString(data: file.readDataToEndOfFile(), encoding: NSUTF8StringEncoding)?.stringByTrimmingCharactersInSet(NSCharacterSet.newlineCharacterSet()) else {
return nil
}
return result as String
} 把鏈接放到日志中
-
使用模式匹配來找到日志里的事件。
-
使用shell里的find命令來檢索工程的完整路徑。
-
添加自定義屬性來存儲字符串本身的信息。
private func injectLinksIntoLogs() {
let text = string as NSString
guard let path = KZPluginHelper.workspacePath() else {
return
}
let matches = pattern.matchesInString(string, options: .ReportProgress, range: editedRange)
for result in matches where result.numberOfRanges == 4 {
let fullRange = result.rangeAtIndex(0)
let fileNameRange = result.rangeAtIndex(1)
let extensionRange = result.rangeAtIndex(2)
let lineRange = result.rangeAtIndex(3)
guard let result = KZPluginHelper.runShellCommand("find \"\(path)\" -name \"\(text.substringWithRange(fileNameRange)).\(text.substringWithRange(extensionRange))\" | head -n 1") else {
continue
}
addAttribute(NSLinkAttributeName, value: "", range: fullRange)
addAttribute(KZLinkedConsole.Strings.linkedPath, value: result, range: fullRange)
addAttribute(KZLinkedConsole.Strings.linkedLine, value: text.substringWithRange(lineRange), range: fullRange)
addAttribute(NSBackgroundColorAttributeName, value: NSColor.whiteColor(), range: fullRange)
}
} 打開文件,然后滾到指定的行
打開一個文件像調用一樣簡單:
public func application(sender: NSApplication, openFile filename: String) -> Bool
滾到指定的行需要多一些的代碼:
private func scrollTextView(textView: NSTextView, toLine line: Int) {
guard let text = (textView.string as NSString?) else {
return
}
var currentLine = 1
var index = 0
for (; index < text.length; currentLine++) {
let lineRange = text.lineRangeForRange(NSMakeRange(index, 0))
index = NSMaxRange(lineRange)
if currentLine == line {
textView.scrollRangeToVisible(lineRange)
textView.setSelectedRange(lineRange)
break
}
}
} 現在處理NSString比String簡單很多,否則我還得介紹和Range的轉換。
歸因
寫這個插件比較簡單,因為我能看別人寫的插件,主要和控制臺有關,如果他們不是開源的,寫這個插件會比較麻煩。
安裝
用Alcatraz工具然后查找 KZLinkedConsole, 或者你可以只 編譯工程 ,它就可以自動安裝了。
總結
這是我第一次嘗試寫Xcode插件,必須說在Xcode工作時調試Xcode是很有趣的一件事。
我個人認為這個插件非常有用,因為我們經常有很多日志,能直接跳轉到記錄錯誤的那行是非常節省時間的。
一定要下載GitHub上的源代碼,用Swift語言處理私有API是很有趣的。KVC(鍵值編碼機制)可使它更簡單地檢索值,而不用引入Objective-C綁定。
如果你正在用cmd+shift+f,那你可能做錯了什么。
本文僅用于學習和交流目的,轉載請注明文章譯者、出處以及本文鏈接。
感謝 博文視點 對本期翻譯活動的支持。