教你如何用Swift编写Xcode插件

jopen 8年前

GitHub上的源代码

在我的 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,那你可能做错了什么。

本文仅用于学习和交流目的,转载请注明文章译者、出处以及本文链接。

感谢 博文视点 对本期翻译活动的支持。

来自: http://www.cocoachina.com/swift/20151231/14837.html