前言
不知道什麼時候,漸漸的從 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
Sublime Text 會自動產生一個 Plugin 範例
先 Ctrl + s 儲存檔案,儲存的名稱可以隨意,這邊我取名為 PHPAnnotation.py
儲存路徑就在 Sublime Text Plugin 預設目錄下
AppData\Roaming\Sublime Text 3\Packages\User
Step 2
接下來更改 Plugin 的名稱,名稱是由 Class 名稱來決定的
規則為 名稱+Command,並且以大駝峰式命名
以範例來說,名稱就叫做 example
這邊我就更換一下 Class 名稱為 PhpAnnotationCommand
Plugin 名稱就是 php_annotation
Ctrl + s 儲存檔案
Step 3
點選 Preferences > Key Bindings
在右側自訂義區加入以下內容,Ctrl + s 儲存檔案後關閉視窗
{
"keys": ["ctrl+shift+c"],
"command": "php_annotation"
}
接下來開一個新的分頁,按下熱鍵(Ctrl + Shift + c)
會看到自動幫我們插入一行 Hello, World!,這代表我們的 Plugin 初步已經成功了
接下來將繼續完成剩餘需求
Step 4
既然這次 Plugin 的主要需求對象是 PHP
就先呼叫 API 來獲取目前設定的語言格式是什麼
如果不是 PHP 的話就不處理
在 run 內加入以下內容
syntax = self.view.settings().get("syntax")
if syntax != "Packages/PHP/PHP.sublime-syntax":
return
Ctrl + s 儲存檔案,接下來一樣到另一個分頁
會發現按下熱鍵後不會跳出 Hello, World!
除非在右下角的語言格式切換成 PHP 才行
Step 5
接下來考慮要怎麼使用這個 Plugin
通常註解的部分都會寫在 Function 的上方
那麼邏輯就把光標移到 Function 上方,按下熱鍵
Plugin 會去讀取光標下一行的所有文字,分析完畢後直接輸出註解
那麼這一步驟要完成的功能就是如何讀取畫面上特定位置的文字
具體邏輯如下
- 調用 sel 取得當前的 selection
- 調用 rowcol 轉換成行跟列的數字
- 調用 text_point 轉換成 point
- 調用 line 將 point 算出起始位置跟結束位置 region
- 調用 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 + `
沒有任何內容被選取的時候
有其他內容被選取的時候
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! 替換成下一行的文字
並且輸出位置改為光標的所在位置
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)