前言
繼上次的 Simple Fuzzy,這次基於 C-define Parser 又製作了一個實用 Sublime Text 外掛 Define Parser。主要想要解決的痛點就是在 Sublime Text 裡面看 C 程式碼時無法區分當下程式碼是否被 #if/#elif 區塊隱藏。
先看完成效果圖:
本外掛是基於 C-define Parser 修改的開發,而 Sublime Text 外掛的製作基礎可以參考 Simple Fuzzy 一文。
Plugin Implemenatation
這次想要達到以下效果:
- 打開 C 原始碼資料夾自動 Build Define Database
- 利用產生的 Define 資料庫標示未使用區域,實作上 Highlight 為 Comment 區
- 可加入快捷鍵快速查看游標處的展開值
- 可新增 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