前言
蘋(píng)果在 Xcode8 以后已經(jīng)不支持第三方插件了,但是他又開(kāi)放了一個(gè)專門(mén)制作插件的東西: Xcode Source Editor Extention,所以現(xiàn)在只能這個(gè)來(lái)制作 Xcode 插件了.
創(chuàng)建Xcode Source Editor Extention
1.新建一個(gè)項(xiàng)目
首先我們先新建一個(gè) Mac 下的 app, 因?yàn)槲覀兪莿?chuàng)建 Extention ,所以我們不關(guān)心這個(gè) app 具體實(shí)現(xiàn).
2.我們新建一個(gè) target
選擇Xcode Source Editor Extention
選擇 Activate
完成后文件結(jié)構(gòu)如下
3.介紹插件的 info.plist
在 info.plist 里面我們需要關(guān)注是 NSExtention 這個(gè)字段
XCSourceEditorExtensionPrincipalClass: 這個(gè)字段表示的是插件的類名, 他必須與遵守了XCSourceEditorExtendsion 協(xié)議的類一致,而且每個(gè)插件僅有一個(gè)這樣的類
XCSourceEditorCommandDefinitions: 這個(gè)數(shù)組表示的是插件里面有幾個(gè)命令,每一個(gè)元素是一個(gè)命令. 如果有多個(gè)命令的話,直接往這個(gè)數(shù)組里面添加元素就好.
XCSourceEditorCommandClassName: 命令所對(duì)應(yīng)的類名,這個(gè)類需要遵守 XCSourceEditorCommand 協(xié)議
XCSourceEditorCommandIdentifier: 命令的標(biāo)識(shí)
XCSourceEditorCommandName: 這個(gè)是命令在 Xcode 里顯示的名字,可以隨便寫(xiě),無(wú)所謂
我在項(xiàng)目里創(chuàng)建了兩個(gè)命令,一個(gè)普通的注釋,一個(gè)是文檔注釋.
4.運(yùn)行插件
運(yùn)行前,需要對(duì)項(xiàng)目和 target 都進(jìn)行簽名
編輯 target 的scheme, 在 info 的Executable 里面選擇 Xcode8
運(yùn)行后會(huì)出現(xiàn)一個(gè)灰色圖標(biāo)的 Xcode , 這個(gè)就是測(cè)試用的 Xcode 了,我們可以在這個(gè)Xcode 里面的 Editor選項(xiàng)卡里的最后找到我們的插件(必須選中某個(gè)文件才會(huì)出現(xiàn)). 如果沒(méi)有找到或者無(wú)法點(diǎn)擊插件, 重新運(yùn)行就好了. 注意不要用灰色圖標(biāo)的 Xcode 再次運(yùn)行新, 可能會(huì)出現(xiàn)為知的錯(cuò)誤和問(wèn)題.
我們還可以為每一個(gè)命令添加一個(gè)快捷鍵, 只要在設(shè)置快捷鍵的地方找到我們的命令就好了
5.安裝插件
選擇我們創(chuàng)建的 app, 運(yùn)行, 然后重啟 Xcode 就好了
XCSourceEditorExtendsion協(xié)議
一共兩個(gè)方法和一個(gè)結(jié)構(gòu)體
extensionDidFinishLaunching: 插件加載后會(huì)調(diào)用,此時(shí)命令還沒(méi)有被執(zhí)行,也不會(huì)獲取任何信息
commandDefinition: 這個(gè)方法會(huì)返回一個(gè)數(shù)組,也就是 info.plist 里面的XCSourceEditorCommandDefinitions字段對(duì)應(yīng)的那個(gè)數(shù)組
XCSourceEditorCommand 協(xié)議
一個(gè)方法和一個(gè)類
當(dāng)我們執(zhí)行我們的命令時(shí), 系統(tǒng)會(huì)把當(dāng)前我們選中的文件里面的所有文本信息包裹到 invocation 里傳給我們
completionHandler: 這個(gè) block 應(yīng)該在我們做完處理后或者是發(fā)生錯(cuò)誤時(shí)調(diào)用. 如果把一個(gè)錯(cuò)誤傳給這個(gè) block, 那么系統(tǒng)就會(huì)有一個(gè)提示.
invocation 是一個(gè) XCSourceEditorCommandInvocation 的實(shí)例, 它buffer 屬性里有我們需要處理的數(shù)據(jù); cancellationHandler屬性是當(dāng)用戶取消了正在執(zhí)行的命令會(huì)調(diào)用的 block.
XCSourceTextBuffer
我們要操作的所有文本數(shù)據(jù)都在這里,我主要介紹 lines 和 selections 這個(gè)兩個(gè)屬性
lines
系統(tǒng)會(huì)把當(dāng)前文件的每一行字符串存進(jìn)這個(gè)數(shù)組, 我們可以通過(guò)下標(biāo)取到對(duì)應(yīng)的那一行字符串, 然后就可以進(jìn)行操作了
selections
這個(gè)數(shù)組里存的是我們選中的行, 但是它并不是把每一行字符串存進(jìn)數(shù)組, 而是專門(mén)存了XCSourceTextRange類型的元素. 一般情況下, 這個(gè)數(shù)組里只有一個(gè)元素.
XCSourceTextRange中的 start 屬性是我們選中的開(kāi)始索引, 包括行索引(line)和列索引(column).
XCSourceTextRange中的 end 屬性是我們選中的結(jié)束索引.
當(dāng)我們拿到開(kāi)始的行索引和結(jié)束的行索引后, 我們就可以去 lines 里面取到對(duì)應(yīng)的那一行的字符串了.
至此, 關(guān)于蘋(píng)果文檔里東西已經(jīng)介紹完了, 剩下的就是在遵守了XCSourceEditorCommand協(xié)議的類里去寫(xiě)邏輯代碼, 進(jìn)行相關(guān)的操作, 我就不啰嗦了, 大家可以去 github 上看我的源碼
關(guān)于選擇多行并給每一個(gè)方法或?qū)傩蕴砑游臋n注釋的思路
添加文檔注釋, 實(shí)際就是給 lines 這個(gè)數(shù)組插入數(shù)據(jù), 但是因?yàn)閿?shù)組在插入數(shù)據(jù)后, 索引會(huì)變化, 所以不能夠用selections里面的那個(gè)行索引來(lái)取值.
我的辦法是給一個(gè)中間變量, addLineCount, 來(lái)記錄我們插入了多少行數(shù)據(jù), 然后我們從selections中去到的行索引加上這個(gè)值就是真實(shí)的索引了.
func perform(with invocation: XCSourceEditorCommandInvocation, completionHandler: @escaping (Error?) -> Void) {
let lines = invocation.buffer.lines
let selections = invocation.buffer.selections
for selection in selections {
if let textRange = selection as? XCSourceTextRange,
textRange.start.line != lines.count {
var lineIndex = 0
if textRange.start.line == textRange.end.line {
lineIndex = textRange.start.line + addLineCount
var line = lines[lineIndex] as! String
if line == "\n" {
lines.removeObject(at: lineIndex)
}
line = lines[lineIndex] as! String
if line.hasVarOrLet() {
insertVarOrLetDoc(at: lineIndex, withLine: line, inLines: lines)
}else if line.hasFuncMethod() {
insertFuncDoc(at: lineIndex, withLine: line, inLines: lines)
}else if line.hasProperty() {
insertPropertyDoc(at: lineIndex, withLine: line, inLines: lines)
}else if line.hasMethod() {
insertMethodDoc(at: lineIndex, withLine: line, inLines: lines)
}
}else {
for index in textRange.start.line...textRange.end.line {
lineIndex = index + addLineCount
let line = lines[lineIndex] as! String
if line.hasVarOrLet() {
insertVarOrLetDoc(at: lineIndex, withLine: line, inLines: lines)
}else if line.hasFuncMethod() {
insertFuncDoc(at: lineIndex, withLine: line, inLines: lines)
}else if line.hasProperty() {
insertPropertyDoc(at: lineIndex, withLine: line, inLines: lines)
}else if line.hasMethod() {
insertMethodDoc(at: lineIndex, withLine: line, inLines: lines)
}
}
}
}
}
completionHandler(nil)
}
每添加一行數(shù)據(jù), 就讓這個(gè)addLineCount加一
func insertFuncDoc(at index: Int, withLine line: String, inLines lines: NSMutableArray) {
var charIndex = line.startIndex
while line[charIndex] == " " {
charIndex = line.index(after: charIndex)
}
let spaceStr = line.substring(to: charIndex)
let prefixDoc = spaceStr + threeCommentStr
if line.funcStrHasReturnValue() {
if hasDoc(at: index-1, withPrefix: prefixDoc + returnsStr, inLines: lines) {
return
}
let returnValueStr = prefixDoc + returnsStr + "<#return value description#>"
lines.insert(returnValueStr, at: index)
lines.insert(prefixDoc, at: index)
addLineCount += 2
}
let paramNames = line.parserFuncStrParameter()
if paramNames.count > 0 {
for paramName in paramNames.reversed() {
if hasDoc(at: index-1, withPrefix: prefixDoc + parameterStr + paramName, inLines: lines) {
return
}
let paramDoc = prefixDoc + parameterStr + paramName + ": " + "<#\(paramName) description#>"
lines.insert(paramDoc, at: index)
}
lines.insert(prefixDoc, at: index)
addLineCount += (paramNames.count + 1)
}
if hasDoc(at: index-1, withPrefix: prefixDoc, inLines: lines) {
return
}
let funcDoc = prefixDoc + spaceChar + descriptionStr
lines.insert(funcDoc, at: index)
addLineCount += 1
}
已經(jīng)添加過(guò)文檔注釋的, 不會(huì)重復(fù)添加
func hasDoc(at index: Int, withPrefix prefix: String, inLines lines: NSMutableArray) -> Bool {
if let line = (lines[index] as? String), line.hasPrefix(prefix) {
return true
}
return false
}