Sublime Text Plugin – Define Parser

前言

繼上次的 Simple Fuzzy,這次基於 C-define Parser 又製作了一個實用 Sublime Text 外掛 Define Parser。主要想要解決的痛點就是在 Sublime Text 裡面看 C 程式碼時無法區分當下程式碼是否被 #if/#elif 區塊隱藏。

先看完成效果圖:

本外掛是基於 C-define Parser 修改的開發,而 Sublime Text 外掛的製作基礎可以參考 Simple Fuzzy 一文。

Plugin Implemenatation

這次想要達到以下效果:

  1. 打開 C 原始碼資料夾自動 Build Define Database
  2. 利用產生的 Define 資料庫標示未使用區域,實作上 Highlight 為 Comment 區
  3. 可加入快捷鍵快速查看游標處的展開值
  4. 可新增 Compile Configuration File 帶入額外的 Define 數值定義

目前 Build Database 的實作方式,比起 Language Server Protocol、或正港的 Compiler,畢竟是純 Python 實現,速度並不快,但由於 Define 數值在 C 語法中鮮少更改,在 Configuration 固定的情況下,Build Database 其實多半只需要製作一次,最後發現對使用體驗影響不大,實現效果出奇優異,反而配置上更彈性,也省去介接其他應用的麻煩。

Build Define Database

為了在使用者打開 Sublime Text 的時候自動 Build Database,加入了 Event Listener:

class EvtListener(sublime_plugin.EventListener):
    def on_new_window_async(self, window):
        active_folder = _get_folder(window)
        if active_folder is None:
            return
        _init_parser(window)

為避免浪費 CPU 資源,先呼叫 _get_folder過濾目標資料夾。

def _init_parser(window):
    # ...
    p = C_DefineParser.Parser()

    predefines = _get_configs_from_file(
        window, _get_setting(window, DP_SETTING_COMPILE_FILE)
    )
    for d in predefines:
        logger.debug("  predefine: %s", d)
        p.insert_define(d[0], token=d[1])

    def async_proc():
        p.read_folder_h(active_folder)
        # ...
        logger.info("done_parser: %s", active_folder)

    sublime.status_message("building define database, please wait...")
    sublime.set_timeout_async(async_proc, 0)

為避免 GUI 卡住,使用 sublime.set_timeout_async 做了簡單的 async 異步執行,並且在讀取資料夾 .h 檔之前先讀入 Configuration 預處理。

Code Highlighting

重頭戲就是要針對 C 語法相關檔案標記非使用區域,使用 Parser 提供的 p.read_file_lines 將有效行數排除之後,剩下的即是,利用 view.add_regions 可以新增 Highlight 群組,標記為 Comment。

def _mark_inactive_code(view):
    window = view.window()
    p = _get_parser(window)
    filename = view.file_name()

    _, ext = os.path.splitext(filename)
    if ext not in _get_setting(window, DP_SETTING_SUPPORT_EXT):
        return

    fileio = io.StringIO(view.substr(sublime.Region(0, view.size())))
    fileio.name = filename
    num_lines = len(fileio.readlines())
    inactive_lines = set(range(1, 1 + num_lines))

    fileio.seek(0)
    for _, lineno in p.read_file_lines(
        fileio,
        reserve_whitespace=True,
        ignore_header_guard=True,
        include_block_comment=True,
    ):
        inactive_lines.remove(lineno)
    inactive_lines -= set(p.filelines.get(filename, []))

    regions = [
        sublime.Region(view.text_point(line - 1, 0), view.text_point(line, 0))
        for line in inactive_lines
    ]
    view.add_regions(
        REGION_INACTIVE_NAME,
        regions,
        scope="comment.block",
        flags=sublime.DRAW_NO_OUTLINE,
    )

此外掛使用了 view.substr,才可以獲取到每個檔案在 Sublime Text 中最新的 Buffer,而不是硬碟中的舊存檔。並且加入 Event Listener 在每個檔案讀入時,根據設定啟用標記。

class EvtListener(sublime_plugin.EventListener):
    def on_load_async(self, view):
        window = view.window()

        if _get_setting(window, DP_SETTING_HL_INACTIVE):
            _mark_inactive_code(view)
        else:
            _unmark_inactive_code(view)

Edit Compile Configurations

加入 Command 提供使用者選擇 Configuration Flag 設定,透過item.kind 加入綠色勾勾以提示目前的選中。

class SelectConfiguration(sublime_plugin.WindowCommand):
    def run(self):
        # ...
        items, selected_index = _get_config_selection_items(self.window)
        item = sublime.QuickPanelItem("Default (without compiler flags)")
        item.kind = (
            (sublime.KIND_ID_COLOR_GREENISH, "✓", "")
            if selected_index == -1
            else (0, "", "")
        )
        items.append(item)

        self.window.show_quick_panel(
            items,
            on_select=self._on_select,
            selected_index=selected_index,
        )

    def _on_select(self, selected_index):
        # ...
        self.window.run_command("rebuild_define_database")
        for view in self.window.views(include_transient=True):
            _unmark_inactive_code(view)

其他實作請自行參考 Github 原始碼。

Add Menu Items

在外掛根目錄中加入 Main.sublime-menu 檔案可以在 Menu 中加入客製化的選單。

Add Default Key-Bindings

在外掛根目錄中加入 Default.sublime-keymap 檔案可以加入預設的 Key Binding。

Deploy for Package Control

這次等待 Pull Request Merge 前等了一陣子,中間遇到幾個問題:

  • 原先有預設綁定 Mouse 快捷鍵,但滑鼠按鍵太少,不建議預設綁定,改建議使用者自行加入
  • 外掛中缺少 LICENSE 聲明檔

修改之後就成功上線啦!

Installation

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

Source Code

ukyouz/SublimeText-DefineParser

 

References