教你如何用Swift編寫Xcode插件

jopen 8年前發布 | 10K 次閱讀 Xcode Swift Apple Swift開發

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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!