Laravel Goto for Sublime Text
https://github.com/absszero/LaravelGoto
Why
去年下定決心買一套 Sublime Text 之後(USD $75 的樣子),也算是認可 ST 在工作幫我省了不少時間。不過隨著 VS Code 功能逐步豐富,一直有想轉換編輯器的念頭。上個月某個週末夜晚想確認一下 VS Code 目前相關的 Extension 是否已經到位了。便逐步安裝 VS Code 以及相關套件。結果… 跟 ST 還是有段落差。go to definition
在 ST 上能夠針對字串直接轉跳,VS Code 則必須是類或方法的使用才能夠轉跳。
但 ST 也不是所有的文字都能正確轉跳,例如 Laravel Route 定義的部份,能夠轉跳到指定的 Controller,但是 Method 則不行;Blade 檔案的引用由於路徑目錄是透過 .
逗點來區隔(例如:my_dir.
my_view),透過 Goto anything
也起不了作用。Package Control 上面能收尋到最接近的套件是 Laravel OpenFile
。不過路徑必須事先選取起來,並且必須透過 return view('your_view_path')
叫用的檔案才能發揮作用。
功能需求
基於以上的原因,決定自己來做一個 ST 套件,順便練習一下 Python,於是有了 LaravelGoto
套件的實踐。目前最常用的功能包括:
- 開啟 Controller@Method。內建的 Goto 只能開啟 Controller 不能開啟後連帶轉跳到 Method。
- 開啟 View file。內建必須用
/
分隔目錄名稱,然後選取之後使用Goto anthing
搜尋。 - 開啟靜態檔案(js, css…etc)。
找出字串範圍
建立一個 Plugin 並不困難,不過有些功能得設法實現。原本的 Laravel OpenFile
套件必須選取整個字串才能夠發揮作用,但大部分的情況,我希望可以像 Goto definition
,Ctrl + 滑鼠左鍵點擊該字串就可以直達指定的 View file 或者 Controller。藉此參考了 https://github.com/noahcoad/open-url/blob/master/open_url.py#L186
該方法可以根據你目前游標位置,找出字串範圍,或者根據你所選的字串。
# selected 可以透過 self.view.sel()[0] 帶入,代表所選的第一個文字範圍
def get_selection(self, selected):
start = selected.begin()
end = selected.end()
# 當所選範圍開頭跟結果在同一個位置,表示只是指標位置而未做任何選取
if start == end:
# 找出當前位置指標所在的行範圍
line = self.view.line(start)
delimiters = "\"'"
while start > line.a:
# 透過逐字檢查,找到開頭距離游標最接近的單引號或雙引號
if self.view.substr(start - 1) in delimiters:
break
start -= 1
while end < line.b:
# 透過逐字檢查,找到結尾距離游標最接近的單引號或雙引號
if self.view.substr(end) in delimiters:
break
end += 1
# 將開頭與結尾位置作為所選範圍
return sublime.Region(start, end)
範圍轉換為路徑
有了選取範圍之後先轉為文字,然後判斷文字內容來決定路徑類型。
Controller 的部份比較困難的地方是當有兩個一樣的名稱的 Controller,需要透過附加 Namespace 來判定選取的是哪個 Controller(但找出 Namespace 同樣很困難!)
至於 View file,如果包含 ::
表示啟用了 View namespace,所以只取雙冒號之後的路徑名稱
def get_path(self, selected):
selection = self.get_selection(selected)
# 轉換為字串
path = self.substr(selection).strip()
# 如果字串包含 @ 或 Controller 的字樣,判定為 Controller
if ("@" in path or "Controller" in path):
namespace = self.get_namespace(selection)
if namespace:
path = namespace + '\\' + path
else:
# remove Blade Namespace
path = path.split(':')[-1]
return path
搜尋
找到部份檔案路徑之後,由於不同專案結構或使用方式(例如沒有使用 ST 的 Project 功能),很難找到完整的檔案路徑並且開啟。所以這邊是透過 Goto anything
功能檢索可能的檔案路徑。
首先判斷是否是靜態檔案,靜態檔案的特性是路徑副檔名可能是常用的 js
,css
,所以首先定義了網頁開發常用的靜態副檔名,作為檢索的依據。
如果不是靜態檔案,試著判斷是否是控制器路徑。如果是控制器則顯示 Goto anything
功能之後,呼叫 insert
指令逐字把控制器路徑輸入進去。不直接透過 Goto anything
查詢,是由於當我們使用 insert
指令的輸入完控制器檔名後,接著輸入 @
符號可以觸發 Goto symbol
的功能,如此後續輸入可以自動跳到我們所想要的 Method 位置。
最後,如果上述都不符合則判定是 View file,將路徑的 .
轉換成 /
,並且在結尾附加上 .blade.php
,然後呼叫 Goto anything
功能搜尋檔案。
def search(self, path):
args = {
"overlay": "goto",
"show_files": True,
"text": path
}
# 是否是靜態檔案,已經預先定義了靜態檔案的副檔名範圍
if (self.is_static_file(path)):
self.window.run_command("show_overlay", args)
return
is_controller = self.is_controller(path)
if is_controller:
args["text"] = ''
else: # it's a view file
args["text"] = args["text"].replace('.', '/') + '.blade.php'
self.window.run_command("show_overlay", args)
# if it 's Controller path, use insert command to trigger symbol search
if is_controller:
self.window.run_command("insert", {
"characters": path.replace('@', '.php@')
})
return
Namespace
由於控制器可能有檔名重複的情形,所以適當的加上 Namespace 可以更正確的檢索到合適的檔案。但由於複雜的 Namespace 在 Route 方面可能一層包一層,目前只能檢索到最外層的 Namespace。
根據 Laravel 跟 Lumen 各個版本的使用情況,目前有兩種設定 Namespace 的方式:
patterns = [
# Route::namespace('MyNamespace')
re.compile(r"namespace\s*\(\s*(['\"])\s*([^'\"]+)\1"),
# Route::group(['namespace' => 'MyNamespace'])
re.compile(r"['\"]namespace['\"]\s*=>\s*(['\"])([^'\"]+)\1"),
]
接下來要透過 ST 語法解析內建的 selector 找出每一個 Route 函式包含的範圍,並且確保當前所選的控制器路由,是否包含在所選的函式範圍當中。如果是,則帶出可能的 Namespace。
def get_namespace(self, selected):
# PHP 語法測試範例 https://github.com/sublimehq/Packages/blob/7c40526e68e07e0e4a6681bb2d04359115ec1331/PHP/syntax_test_php.php
# 找出所有的函式範圍
functions = self.view.find_by_selector('meta.function-call')
for function in functions:
# 如果函式範圍不包含所選的位置則跳過
if (not function.contains(selected)):
continue
# 一般函式的第一行應該就會包含 Namespace
region = self.view.line(function)
block = self.substr(region).strip()
# 用正規表達式檢索批配出 Namespace
for pattern in patterns:
matched = pattern.search(block)
if (matched):
return matched.group(2)
return
總結
由於 Python 是執行在 Sublime Text 的內部,所有也得透過內部的 Console 查看程式編寫的情況。另外這年頭寫程式一定要來點單元測試,這部份也在後來添加了上去。完成作品已經提交 Package Control,不過要排到審查上線,大概還有許多時間。
後續開發套件的經驗也拿來修改 SublimeLinter-contrib-php-cs-fixer 套件,以符合新版 SublimeLinter 的改版需求。基本上還是解決工作的需求,不過 ST 仍然有些功能即使已經使用多年仍然一知半解。希望找個時間細部摸索一下,期望開發的時間能做更有效的應用。