[Python]開發Sublime Text外掛

前言

不知道什麼時候,漸漸的從 Notepad++ 轉移到了 Sublime Text

最主要的原因應該還是 Sublime Text 提供了多行編輯功能以及實時搜尋高亮功能吧

在各式各樣IDE發展的時代,這種文字編輯器也是越來越少了

上一篇教大家如何用 JavaScript開發Chrome擴充功能

那麼今天就來嘗試一下開發 Sublime Text 的外掛功能吧

環境

Sublime Text 3

Step 1

這次想要實現的外掛功能呢,是自動為 PHP Function 加上註解,註解的格式內容如下

/**
 * 方法名稱
 * function description
 * 
 * @date    今天日期
 * @version    1.0.0
 * @author
 * @param   參數型態    參數名稱    參數預設值
 * @return  
 */

熱鍵就設定 Ctrl + Shift + c

那麼就進入正題吧

開啟 Sublime Text 後點選 Tool > Developer > New Plugin

01

Sublime Text 會自動產生一個 Plugin 範例

02

先 Ctrl + s 儲存檔案,儲存的名稱可以隨意,這邊我取名為 PHPAnnotation.py

儲存路徑就在 Sublime Text Plugin 預設目錄下

AppData\Roaming\Sublime Text 3\Packages\User

03

Step 2

接下來更改 Plugin 的名稱,名稱是由 Class 名稱來決定的

規則為 名稱+Command,並且以大駝峰式命名

以範例來說,名稱就叫做 example

這邊我就更換一下 Class 名稱為 PhpAnnotationCommand

04

Plugin 名稱就是 php_annotation

Ctrl + s 儲存檔案

Step 3

點選 Preferences > Key Bindings

05

在右側自訂義區加入以下內容,Ctrl + s 儲存檔案後關閉視窗

{ 
    "keys": ["ctrl+shift+c"],
    "command": "php_annotation"
}

06

接下來開一個新的分頁,按下熱鍵(Ctrl + Shift + c)

會看到自動幫我們插入一行 Hello, World!,這代表我們的 Plugin 初步已經成功了

07

接下來將繼續完成剩餘需求

Step 4

既然這次 Plugin 的主要需求對象是 PHP

就先呼叫 API 來獲取目前設定的語言格式是什麼

如果不是 PHP 的話就不處理

在 run 內加入以下內容

syntax = self.view.settings().get("syntax")

if syntax != "Packages/PHP/PHP.sublime-syntax":
    return

08

Ctrl + s 儲存檔案,接下來一樣到另一個分頁

會發現按下熱鍵後不會跳出 Hello, World!

除非在右下角的語言格式切換成 PHP 才行

09

Step 5

接下來考慮要怎麼使用這個 Plugin

通常註解的部分都會寫在 Function 的上方

那麼邏輯就把光標移到 Function 上方,按下熱鍵

Plugin 會去讀取光標下一行的所有文字,分析完畢後直接輸出註解

那麼這一步驟要完成的功能就是如何讀取畫面上特定位置的文字

具體邏輯如下

  1. 調用 sel 取得當前的 selection
  2. 調用 rowcol 轉換成行跟列的數字
  3. 調用 text_point 轉換成 point
  4. 調用 line 將 point 算出起始位置跟結束位置 region
  5. 調用 substr 將 region 範圍的文字取出

具體程式碼如下

now       = self.view.sel()[0]
row, _    = self.view.rowcol(now.a)
point     = self.view.text_point(row +1, 0)
next_line = self.view.substr(self.view.line(point))

這邊需要先知曉幾個名詞在 Sublime Text 中的意義

point

int類型,代表的是從文件開頭到特定位置的偏移量

region

代表一段區域,有兩個屬性 a(區域開頭的 point) 跟 b(區域結束的 point)

selection

選擇的區域,可以看成 region 的集合

那麼就可以來解釋為什麼上述的程式碼可以取到光標下一行的內容

self.view.sel()

sel 回傳的是一個陣列,代表的是所有選取的區域

如果沒有任何選取的內容,那麼就是獲取光標所在的位置

這時候 now.a 與 now.b 的值是一樣的

可以透過修改 Plugin 來驗證一下

開啟 Console ,快捷鍵是 Ctrl + `

沒有任何內容被選取的時候

11

有其他內容被選取的時候

12

self.view.rowcol(now.a)

rowcol 回傳的是藉由 point 計算出 row 跟 col 的值,可以當作是 Y 與 X 的軸點

self.view.text_point(row +1, 0)

text_point 與 rowcol 剛好相反,是藉由 row 與 col 計算出 point 的值

所以把 row +1 和 col = 0 帶入計算出來的就是光標下一行開頭的 point

self.view.line(point)

line 回傳的是一個 region,內容是 point 所在的該行的開頭 point 與 結束 point

self.view.substr(self.view.line(point))

substr 回傳的是某個區域內的內容

所以上述的流程就能獲取到光標下一行的所有文字

再把輸出的部分修改一下,把 Hello, World! 替換成下一行的文字

並且輸出位置改為光標的所在位置

10

Ctrl + s 儲存檔案後就可以去試試看效果了

Step 6

上一步已經取到需要的文字了

那麼接下來就來寫一段驗證是否為 PHP Function 的正規表達式

([\s]*).*?function[\s]+(.+?)\((.*)\).+

開頭的 ([\s]*) 主要目的在於獲取排版的 空格 或是 Tab

接下來的 .*? 是要忽略一些修飾子,例如 private static

再來是一定要存在的 function 關鍵字

[\s]+ 是要忽略 function 與 function name 中間的 空格 或是 Tab

(.+?) 是要獲取 function name

\((.*)\) 是要獲取參數列表,會使用 .* 而不使用 .*? 原因在於使用 .*? 遇到 $param = array() 會造成獲取不完全

最後的 .+ 為忽略後面所有的文字

不了解正規表達式的讀者也不用擔心,用 split 切割也能分析出需要的部分,只是比較麻煩

以上的方式只適合單行的 Function,有些開發者會把參數斷行,至於這種狀況要怎麼解決就交給各位讀者去思考了

至於參數部分的話,就用一個簡單的自動機去處理

暫且不用管參數格式是否正確,因為那真的太麻煩了....

接下來就實際寫兩個方法吧

#分析方法,分析失敗回傳None
def analysis_function(self, function):
    search  = re.search('([\s]*).*?function[\s]+(.+?)\((.*)\).*?$', function, re.IGNORECASE)
        
    if not search:
        return None

    params = search.group(3).strip()

    if params != '':
        params = self.analysis_param(params)
    else:
        params = []

    return [search.group(1), search.group(2), params]

# 分析參數,不管參數格式是否正確
# 回傳格式為多維列表,每個列表子項代表一個參數
# 0 => 參數類型 1 => 參數名稱 2 => 參數預設值
def analysis_param(self, params):
    result = []
    item   = []
    temp   = ''
    status = 0
    flag   = False
    count  = 0
    string = ''
    for c in params:
        if status == 0:
            if item:
                result.append(item)
                item = []
            if c == '$' or c == '&':
                status = 1
                item.append(temp)
                temp = c
            elif c == ' ' and temp != '':
                status = 1
                item.append(temp)
                temp = ''
            elif c != ' ':
                temp += c
        elif status == 1:
            if c == '=':
                status = 2
                item.append(temp)
                temp = ''
            elif c == ',':
                status = 0
                item.append(temp)
                temp = ''
            elif c != ' ':
                temp += c

        elif status == 2:
            if flag == True:
                if c == string and temp.endswith('\\') == False:
                    item.append(temp)
                    temp = ''
                    flag = False
                else:
                    temp += c
            elif (c == '\'' or c == '"') and count == 0:
                string = c
                flag = True
            elif c == '(':
                count += 1
                temp += c
            elif c == ')':
                count -= 1
                temp += c
            elif c == ',' and count == 0:
                status = 0
                if temp != '':
                    item.append(temp)
                temp = ''
            elif (count == 0 and c != ' ') or count > 0:
                temp += c
    
    if temp != '':
        item.append(temp)

    if item:
        result.append(item)

    return result

Ctrl + s 儲存檔案

Step 7

最難的部分已經完成了,剩下的就是輸出註解內容了

沒什麼需要注意的地方,一樣寫個方法

def print_annotation(self, edit, point, info):
    insert_point = self.view.text_point(point, 0)
    annotation   = ''

    annotation  += info[0] + '/**' + '\n'
    annotation  += info[0] + ' * ' + info[1] + '\n'
    annotation  += info[0] + ' * function description' + '\n'
    annotation  += info[0] + ' * ' + '\n'
    annotation  += info[0] + ' * @date    ' + time.strftime("%Y/%m/%d") + '\n'
    annotation  += info[0] + ' * @version    1.0.0' + '\n'
    annotation  += info[0] + ' * @author    ' + '\n'
    
    for item in info[2]:
        annotation += info[0] + ' * @param    '

        if item[0] != '':
            annotation += item[0] + '    '

        annotation += item[1] + '    '

        if len(item) > 2:
            annotation += item[2]

        annotation += '\n';
    
    annotation  += info[0] + ' * @return' + '\n'
    annotation  += info[0] + ' */'

    self.view.insert(edit, insert_point, annotation)

Ctrl + s 儲存檔案

Step 8

最後一部把 run 方法中加入上面寫的三個方法後這個 Plugin 就大功告成啦

完整程式碼

import sublime
import sublime_plugin
import re
import time

class PhpAnnotationCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        syntax = self.view.settings().get("syntax")

        if syntax != "Packages/PHP/PHP.sublime-syntax":
            return

        now       = self.view.sel()[0]
        row, _    = self.view.rowcol(now.a)
        point     = self.view.text_point(row +1, 0)
        next_line = self.view.substr(self.view.line(point))
        info      = self.analysis_function(next_line)

        if info == None:
            return

        self.print_annotation(edit, row, info)

    def analysis_function(self, function):
        search  = re.search('([\s]*).*?function[\s]+(.+?)\((.*)\).*?$', function, re.IGNORECASE)
        
        if not search:
            return None
            
        params = search.group(3).strip()

        if params != '':
            params = self.analysis_param(params)
        else:
            params = []

        return [search.group(1), search.group(2), params]

    def analysis_param(self, params):
        result = []
        item   = []
        temp   = ''
        status = 0
        flag   = False
        count  = 0
        string = ''
        for c in params:
            if status == 0:
                if item:
                    result.append(item)
                    item = []
                
                if c == '$' or c == '&':
                    status = 1
                    item.append(temp)
                    temp = c
                elif c == ' ' and temp != '':
                    status = 1
                    item.append(temp)
                    temp = ''
                elif c != ' ':
                    temp += c
            elif status == 1:
                if c == '=':
                    status = 2
                    item.append(temp)
                    temp = ''
                elif c == ',':
                    status = 0
                    item.append(temp)
                    temp = ''
                elif c != ' ':
                    temp += c

            elif status == 2:
                if flag == True:
                    if c == string and temp.endswith('\\') == False:
                        item.append(temp)
                        temp = ''
                        flag = False
                    else:
                        temp += c
                elif (c == '\'' or c == '"') and count == 0:
                    string = c
                    flag = True
                elif c == '(':
                    count += 1
                    temp += c
                elif c == ')':
                    count -= 1
                    temp += c
                elif c == ',' and count == 0:
                    status = 0
                    if temp != '':
                        item.append(temp)
                    temp = ''
                elif (count == 0 and c != ' ') or count > 0:
                    temp += c
        
        if temp != '':
            item.append(temp)

        if item:
            result.append(item)

        return result

    def print_annotation(self, edit, point, info):
        insert_point = self.view.text_point(point, 0)
        annotation   = ''

        annotation  += info[0] + '/**' + '\n'
        annotation  += info[0] + ' * ' + info[1] + '\n'
        annotation  += info[0] + ' * function description' + '\n'
        annotation  += info[0] + ' * ' + '\n'
        annotation  += info[0] + ' * @date    ' + time.strftime("%Y/%m/%d") + '\n'
        annotation  += info[0] + ' * @version    1.0.0' + '\n'
        annotation  += info[0] + ' * @author    ' + '\n'
        
        for item in info[2]:
            annotation += info[0] + ' * @param    '

            if item[0] != '':
                annotation += item[0] + '    '

            annotation += item[1] + '    '

            if len(item) > 2:
                annotation += item[2]

            annotation += '\n';
        
        annotation  += info[0] + ' * @return' + '\n'
        annotation  += info[0] + ' */'

        self.view.insert(edit, insert_point, annotation)

11

參考資料

Sublime Text API

Read more

[LeetCode] #12 Integer to Roman 解題

題目連結 題型解說 這是一題難度為普通的題目 需要設計一個方法,此方法會傳入一個整數 num 要求是把整數轉換成羅馬字母,轉換清單如下 I => 1 V => 5 X => 10 L => 50 C => 100 D => 500 M => 1000 但羅馬字母有一些特殊規則 4 並非 IIII 而是 IV,9 並非 VIIII 而是 IX 這規則同樣可以套用到 40 90 400 900 解題思路 既然知道特殊規則是一樣的,變得是使用的符號,那麼先從 num 取個位數開始 轉換完成後,把 num 除上 10,消除個位數,

By Michael

[LeetCode] #11 Container With Most Water 解題

題目連結 題型解說 這是一題難度為中等的題目 需要設計一個方法,此方法會傳入一個數字陣列 height 陣列中的元素代表每一個柱子的高度 現在需要計算出,該陣列中以某兩隻柱子為邊界,最多可以裝多少水 以範例來說 height = [1,8,6,2,5,4,8,3,7] 最多可以裝 7 * 7 = 49 單位的水 解題思路 計算面積就是底乘上高 底的計算方式為 「右邊柱子的 index」 減去 「左邊柱子的 index」 高就取最短的那一根柱子高度 拿題目給的例子來當範例 建立三個變數 result、left、right left、right 代表左右兩邊的 index result 代表目前最大容量,初始值 0 第一步,找出最短的柱子高度,

By Michael

[LeetCode] #941 Valid Mountain Array 解題

題目連結 題型解說 這是一題難度為簡單的題目 需要設計一個方法,此方法會傳入一個數字陣列 arr 判斷陣列中的元素是不是由低到高再從高到低(山形)的排序,且不連續一個以上數字 比如說 [1,2,3,2] 就是一個山形陣列,但 [1,2,2,3,2] 不是,因為有兩個 2 [1,2,3,4,5] 和 [5,4,3,2,1] 也不算是山形陣列,前者只有往上沒有往下,後者相反 解題思路 準備一個數字變數(temp)和布林變數(asc),跑一次迴圈,有可能遇到如下狀況 1. 某個數字與前一個數字相同,這時候直接回傳 false

By Michael

[LeetCode] #944 Delete Columns to Make Sorted 解題

題目連結 題型解說 這是一題難度為簡單的題目 需要設計一個方法,此方法會傳入一個字串陣列 strs 這個陣列中每個字串的長度都相同,字串內容都是小寫英文 需要檢查每個元素的第 N 個字元是不是由小至大排列,並回傳有幾個錯誤排列 比如傳入的陣列長這樣 ["cba","daf","ghi"] 取第一個字元 = cdg 取第二個字元 = bah 取第三個字元 = afi 其中第二組的結果(bah)並不是由小至大排列,故回傳 1 解題思路 這一題就用兩個迴圈各別把字元取出來,並比較是否比上一個字元大(Java 中的字元可以直接比較),如果不是就將結果+1 程式碼 Java class Solution { public int minDeletionSize(String[] strs) { int result = 0; for (int i = 0,

By Michael