在我的AppCode專案建立過程中,我想念最多的一件事是:能跳轉到記錄控制檯資訊的指定檔案和行。
Xcode不提供這樣的功能,而我不是一個喜歡抱怨的人,所以我決定自己寫個外掛。我用Swift來編寫這個外掛。
想法
如果一個控制檯記錄了fileName.extension:XX 這樣一個名字,轉換成可點選的超連結,這個連結將會開啟指定的檔案並將那行程式碼高亮。
那樣你可以使用自己的記錄機制,只要新增這個簡單的字首,比如:
【程式碼】
1 2 3 |
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的一部分)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
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開啟檔案並高亮那一行。
怎樣把我們的功能注入到那些操作裡去?
簡單調整:
1 2 3 4 5 6 7 |
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 並使用相關物件把儲存標記為控制檯的那個,這個隨後就會排上用場。
1 2 3 4 |
guard let consoleTextView = KZPluginHelper.consoleTextView(), let textStorage = consoleTextView.valueForKey("textStorage") as? NSTextStorage else { return } |
我們怎樣找到一個檔案的路徑,而只有日誌中的相對路徑?
我們可以用shell裡的find命令,這就是你如何用swift語言執行且從一個shell命令中檢索響應。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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命令來檢索工程的完整路徑。
- 新增自定義屬性來儲存字串本身的資訊。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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) } } |
開啟檔案,然後滾到指定的行
開啟一個檔案像呼叫一樣簡單:
1 |
public func application(sender: NSApplication, openFile filename: String) -> Bool |
滾到指定的行需要多一些的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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,那你可能做錯了什麼。