教你如何用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,那你可能做錯了什么。
本文僅用于學習和交流目的,轉載請注明文章譯者、出處以及本文鏈接。
感謝 博文視點 對本期翻譯活動的支持。