Xcode8下的文檔注釋插件

前言

蘋(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).

Paste_Image.png
Snip20160930_3.png

2.我們新建一個(gè) target

Paste_Image.png

選擇Xcode Source Editor Extention

Paste_Image.png
Paste_Image.png

選擇 Activate

Paste_Image.png

完成后文件結(jié)構(gòu)如下

Paste_Image.png

3.介紹插件的 info.plist
在 info.plist 里面我們需要關(guān)注是 NSExtention 這個(gè)字段


Paste_Image.png

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è)是文檔注釋.


Paste_Image.png

4.運(yùn)行插件
運(yùn)行前,需要對(duì)項(xiàng)目和 target 都進(jìn)行簽名


Paste_Image.png
Paste_Image.png

編輯 target 的scheme, 在 info 的Executable 里面選擇 Xcode8


Paste_Image.png

運(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)題.

Paste_Image.png
Snip20160930_20.png

我們還可以為每一個(gè)命令添加一個(gè)快捷鍵, 只要在設(shè)置快捷鍵的地方找到我們的命令就好了

Paste_Image.png

5.安裝插件
選擇我們創(chuàng)建的 app, 運(yùn)行, 然后重啟 Xcode 就好了

Snip20160930_29.png

XCSourceEditorExtendsion協(xié)議

一共兩個(gè)方法和一個(gè)結(jié)構(gòu)體


Paste_Image.png

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è)類


Paste_Image.png

當(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è)屬性

Paste_Image.png

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è)元素.

Paste_Image.png

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
    }

ps: 我為何要在蘋(píng)果已經(jīng)提供了文檔注釋功能的情況下還要寫(xiě)這個(gè)插件? 我是不會(huì)告訴你, 那個(gè)功能被我給玩兒崩了.....

Paste_Image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末铸题,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子底瓣,更是在濱河造成了極大的恐慌贪磺,老刑警劉巖螺垢,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扔役,死亡現(xiàn)場(chǎng)離奇詭異笋颤,居然都是意外死亡筑煮,警方通過(guò)查閱死者的電腦和手機(jī)绿满,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén)臂外,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事漏健『炕酰” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵蔫浆,是天一觀的道長(zhǎng)殖属。 經(jīng)常有香客問(wèn)我,道長(zhǎng)瓦盛,這世上最難降的妖魔是什么洗显? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任阔蛉,我火速辦了婚禮莉擒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘雹仿。我一直安慰自己嘱吗,他們只是感情好玄组,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著谒麦,像睡著了一般巧勤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上弄匕,一...
    開(kāi)封第一講書(shū)人閱讀 49,760評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音沽瞭,去河邊找鬼迁匠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛驹溃,可吹牛的內(nèi)容都是我干的城丧。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼豌鹤,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼亡哄!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起布疙,我...
    開(kāi)封第一講書(shū)人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蚊惯,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后灵临,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體截型,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年儒溉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宦焦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖波闹,靈堂內(nèi)的尸體忽然破棺而出酝豪,到底是詐尸還是另有隱情,我是刑警寧澤精堕,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布孵淘,位于F島的核電站,受9級(jí)特大地震影響锄码,放射性物質(zhì)發(fā)生泄漏夺英。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一滋捶、第九天 我趴在偏房一處隱蔽的房頂上張望痛悯。 院中可真熱鬧,春花似錦重窟、人聲如沸载萌。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)扭仁。三九已至,卻和暖如春厅翔,著一層夾襖步出監(jiān)牢的瞬間乖坠,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工刀闷, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留熊泵,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓甸昏,卻偏偏與公主長(zhǎng)得像顽分,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子施蜜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容