Make a Sublime Text Plugin – Simple Fuzzy

Motivation

自從習慣在 Vim 裡面各種 Fuzzy Search 之後,發現記憶力越來越差 QQ:開檔案、找 Function Tag、查某一行 Code、乃至於找目前檔案裡的某個關鍵字,通通只需要一點點零散的關鍵字就能夠快速找到並前往(個人推薦 LeaderFfzf.vim)。回到 Sublime Text,找檔名、Tag 還算方便,但唯獨缺少一個「模糊」搜尋 Code 本身的功能,搜尋功能是有,但是需要輸入正確的關鍵字或者用正規表達式來模擬模糊搜尋。

於是乎,只好自己刻了。

偷偷抱怨一下 VS Code 的 C/C++ Extension by Microsoft,查找 Tag 的功能竟然沒有實作模糊匹配。有人在 VS Code 的 Github 上發 Issue,結果開發團隊表示這是 Extension 的問題跟他們無關⋯⋯

Make Our Own Plugin

查了一下 API 發現,Selection Prompt 其實就有內建的 Fuzzy Search 演算法,也就是說只要餵給 API 資料自動就能幫我們做到簡易的 Fuzzy Search 功能。這樣問題就簡化了不少,只要把需要的行全部餵給 Selection Prompt 做為 Input 就搞定了。

Start a Blank Plugin

在開始之前需要新增一個空白 Plugin 在自己的電腦,在選單依序按:Tool、Developer、 New Plugin…,就會跑出一個 Template。

import sublime
import sublime_plugin

class ExampleCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        self.view.insert(edit, 0, "Hello, World!")

將此 Sample 儲存至預設的目錄即可開始開發自製的 Plugin,檔名可以隨意設定例如 Sample.py,Sublime Text 會自動去搜尋目錄底下的所有 Python 檔案去執行。

以我的 MacOS 為例預設是在 /Users/<USERNAME>/Library/Application Support/Sublime Text 3/Packages/User/ 資料夾。

Test the Example Command

現在你可以在 Console Ctrl-` 裡面輸入以下指令測試一下範例 Command。

view.run_command('example')

你會發現目前文件的游標所在行,最前面插入了一個 Hello, World! 字串,就代表外掛載入成功。Command 名稱的轉換方式就是去掉尾巴的 Command 之後將 CamelCase 轉成 snake_case 即是。

Make It an Independent Plugin

User 資料夾裡面的 Python 檔案雖然會被 Sublime Text 載入使用,但卻不會被視為一個單獨的 Plugin。為了方便管理並且讓 Package Control 抓到我們自製的 Plugin,需要往上一層並新增一個 Plugin 資料夾,然後把檔案移到這邊。

例如我想要製作一個名為 Simple Fuzzy 的外掛,於是新增了 /Users/<USERNAME>/Library/Application Support/Sublime Text 3/Packages/SimpleFuzzy/ 目錄並且在裡面加入了剛剛的 Sample 檔案,然後重新命名為 SimpleFuzzy.py

現在在 Command Palette 中找到 Package Control: List Packages 並輸入 SimpleFuzzy,就可以找到我們剛剛新建的 Plugin。

Add a Sublime Text Command

為了呼叫功能需要定義一個自製的 Command。

Sublime Text API 根據不同的使用時機提供了 3 種 Command 介面,三者的主要差異是可以在 self 拿到的 Attribute 不同:

  • 跟目前文件互動的 Text Command
  • 跟目前視窗互動的 Window Command
  • 跟應用程式互動的 Application Command

以我的需求選擇使用了 Text Command(用於搜尋目前文件)及 Window Command (用於搜尋 Project 資料夾)。

加入第一個簡單的 fuzzy_current_file Command:

class FuzzyCurrentFileCommand(sublime_plugin.TextCommand):
    def run(self, edit, pos):
        self.view.sel().clear()
        self.view.sel().add(sublime.Region(pos))
        self.view.show_at_center(sublime.Region(pos))

    def input(self, args):
        if "pos" not in args:
            return EditorLineInputHandler(self.view)
        else:
            return None

做的事情很簡單,就是讓使用者輸入他想找的行然後跳過去。這裡只有跳過去的程式碼,彈出的搜尋框是用 InputHandler 實作。

執行的順序是 input(如果有的話)然後 runrun 方法可以取得 InputHandler 回傳的資料(此例中的 pos),而 edit 參數是Text Command 內建,若有修改文件內容的需要會用到此參數。

Adapt List-Item Selection Prompt

由於彈出的輸入框已經內建 Fuzzy Search 功能,因此只要將文件內容逐行全部餵給 InputHandler 即可。

class EditorLineInputHandler(sublime_plugin.ListInputHandler):
    def __init__(self, view):
        self.view = view

    def name(self):
        return "pos"

    def placeholder(self):
        return "Search content line..."

    def list_items(self):
        regions = self.view.find_all('.+\n')
        lines = [self.view.substr(region).strip().replace('\t', '') for region in regions]
        positions = [r.begin() for r in regions]
        return [
            sublime.ListInputItem(
                text=line_str,
                value=pos,
            ) for pos, line_str in zip(positions, lines)
            if len(line_str)
        ]

這裡解釋一下幾個重要的 Method:

  • name():返回此 InputHandler 處理的參數命名
  • list_itmes():返回一個字串 List 或者更客製化的 ListInputItem List

簡單的把空白列移除之後回傳給 InputHandler 即可。使用者找到所需要的行之後按下 Enter 會回傳 posFuzzyCurrentFileCommand,然後就可以跳到指定的位置。

咦,無法叫出 Input Prompt?因為欲使用 InputHandler 還必須多做一件事,就是在 .sublime-commands 檔案中為我們的 Command 新增一個 Entry。在同一層資料夾新增一個 Default.sublime-commands 檔案然後輸入:

[
    { "caption": "SimpleFuzzy: Current File…", "command": "fuzzy_current_file" },
]

就可以在 Command Palette 中找到自定義 Command:SimpleFuzzy: Current File…。按下 Enter 沒意外的話會彈出一個視窗並且可以模糊尋找目前文件的任意行,並且透過再次按下 Enter 跳轉。

更多的使用方法請參考使用手冊 [1],其他使用教學則可以參考 [2]、[3]。


此小工具外掛的開發到此告一個段落之後,就可以準備部署到 Sublime Text 官方 Package Control 的目錄中,如此一來就能讓全世界的使用者下載來使用。

Deploy for Package Control

部署流程大致如下:

  1. 首先必須開一個自己的雲端空間維護這個 Project,可以使用 Github 或 BitBucket 的 Public Repository
  2. Fork 官方 Package Control 的 Repository 並且在 Repository 列表中新增一個自己的 Plugin 條目
  3. 發 Pull Request 請求將自己的 Plugin 加入官方的 master 主 Branch

更詳細的步驟可以參考官方公布的一份準則文件,裡面還額外要求一些每個 Plugin 開發者都必須遵守的規則,例如:

  • 必須先到 Package Control 搜尋頁檢視是否已經有類似或者重複的功能
  • 挑選一個簡單明瞭、易於辨識的名稱
  • 移除所有非必要檔案
  • ⋯⋯等

如果是用雲端版本控制服務,在想要發佈新版本的時候需要新增特定格式的 Tag 以標示版號,例如 1.0.0 之類。

最後,經過一番討論及修改之後,就可以等到 Merged 啦!撒花🎉 自製的 SimpleFuzzy 出現在 Package Control 首頁啦~

Sublime Text Package Control - Simple Fuzzy

歡迎大家下載來玩~

Installation

在 Command Palette 中找到 Package Control: Install Package 並輸入 SimpleFuzzy,就可以找到這個 Plugin 下載來用啦~

Source Code

目前功能實作的方法頗陽春,所以遇到很肥的 Project Folder 的時候會卡一下 XD 之後有空再看要如何改善~

ukyouz/SublimeText-SimpleFuzzy

 

References

  1. Documentation API Reference | Sublime Text
  2. Sublime Text Community Documentation
  3. How to Create a Sublime Text 2 Plugin | envato-tuts+