教你如何用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,那你可能做错了什么。
本文仅用于学习和交流目的,转载请注明文章译者、出处以及本文链接。
感谢 博文视点 对本期翻译活动的支持。