歡迎光臨
每天分享高質量文章

利用 CTags 開發一個 Sublime Text 程式碼補完外掛

在用 Sublime Text 開發的過程中,我發現了一個問題:Sublime Text 本身的自動完成功能只搜尋當前檢視中正在編輯檔案的函式,當我想用其他檔案中自定義的函式時,是沒有自動完成功能的。
— Nickhopps


致謝
轉載自 | https://www.infiniture.cn/articles/1227.html
 作者 | Nickhopps

喜歡使用 Sublime Text 的朋友們都知道,Sublime Text 相當於 Linux 上的 Vim,它們都具有很強的可擴充套件功能,功能多樣的同時速度也很快,對於處理小型檔案和專案效率特別高,因此如果不是特別複雜的專案,我一般都是用 Sublime Text 編寫以及編譯的。

然而在用 Sublime Text 開發的過程中,我發現了一個問題:Sublime Text 本身的自動完成功能只搜尋當前檢視中正在編輯檔案的函式,當我想用其他檔案中自定義的函式時,是沒有自動完成功能的。而當自定義函式過多時,效率會大大降低,於是我開始尋找具有相關功能的外掛。

一開始我用了非常熱門的 “SublimeCodeIntel” 外掛,試了一下的確非常好用,但是可惜的是,這個外掛不支援 C/C++,而且佔用的空間非常大,追求簡潔輕便的我不得不另闢蹊徑。後來又找到一款 “All AutoComplete” 外掛,這款外掛擴充套件了 Sublime Text 預設的自動完成功能,可以在當前檢視開啟的所有檔案裡面尋找定義的函式和變數,儘管用起來效果不錯,但是它的問題也很明顯,必須要同時開啟多個檔案才行,非常不方便,於是我又放棄了。

在 Package Control 上找了許久,也沒能找到我想要的外掛,於是我開始考慮不如自己寫一個這樣的外掛,剛好藉此機會入門 Python。這時我剛好想到能不能利用 CTags,它能把當前專案中的所有自定義函式提取出來,生成 .tags 檔案,並提供符號跳轉功能,只要提取 .tags 檔案裡面的資訊,用正則匹配,然後新增到 Sublime Text 的自動完成函式中不就行了。

為了完成這個外掛,我在網上搜索相關資訊,找到相關素材並重新構思了一下,同時參考了 All Complete 外掛的原始碼。

需要提一下,在 Sublime Text 下安裝 CTags 的方法這裡不會提到,因此麻煩各位自行查詢。

外掛構思

◈

讀取設定,設定中新增的語言禁用外掛功能

◈

檢測 .tag 檔案是否存在,不存在則直接 return

◈

讀取當前檔案夾中的 .tag 檔案

◈

正則匹配函式名

◈

正則匹配函式體

◈

新增到自動完成的介面上

開始編寫

新建外掛

剛開始接觸 Sublime Text 外掛的編寫,當然需要先瞭解 Sublime Text 提供的各種介面,為此,我去 Sublime Text 的官網找到了相關檔案:How to Create a Sublime Text Plugin[1],以及 Sublime Text Unofficial Documentation[2]。

首先,在 Sublime Text  中選擇 “Tools -> Developer -> New Plugin” 新建一個最基本的外掛檔案:

  1. import sublime

  2. import sublime_plugin

  3. class ExampleCommand(sublime_plugin.TextCommand):

  4.    def run(self, edit):

  5.        self.view.insert(edit, 0, "Hello, World!")

這裡的 sublime 和 sublime_plugin 是 Sublime 必需的模組,其中具體的類和方法可以參考官方的 API Reference[3]。

接著,把這個檔案儲存到 Package檔案夾(預設的儲存位置 User 檔案夾的上一層)的 CTagsAutoComplete 檔案夾(新建)下,並命名為 CTagsAutoComplete.py。儘管命名並沒有什麼限制,但最好還是以外掛的名稱來統一命名。

然後回到 Sublime Text 中,透過快捷鍵 Ctrl+` 進入 Sublime Text 的 Command Console,然後輸入 view.run_command('example'),如果下方顯示 “Hello World”,說明外掛已經正常載入。

這裡之所以直接用 'example',是因為 Command 命令的名稱是根據大寫字元進行拆分的,例子中的 ExampleCommand 在 Command 中 為 'example_command',直接輸入 'example' 也可以訪問。

文中的術語

◈

Window:Sublime Text 的當前視窗物件

◈

View:Sublime Text 當前視窗中開啟的檢視物件

◈

Command Palette:Sublime Text 中透過快捷鍵 Ctrl+Shift+P 開啟的互動式串列

確定外掛介面型別

Sublime Text 下的外掛命令有 3 種命令型別(都來自於 sublime_plugin 模組):

◈

TextCommand Class[4]:透過 View 物件提供對選定檔案/緩衝區的內容的訪問。

◈

WindowCommand Class[5]:透過 Window 物件提供當前視窗的取用

◈

ApplicationCommand Class[6]:這個類沒有取用任何特定視窗或檔案/緩衝區,因此很少使用

2 種事件監聽型別:

◈

EventListener Class[7]:監聽 Sublime Text 中各種事件並執行一次命令

◈

ViewEventListener Class[8]:為 EventListener 提供類似事件處理的類,但系結到特定的 view。

2 種輸入處理程式:

◈

TextInputHandler Class[9]:可用於接受 Command Palette 中的文字輸入。

◈

ListInputHandler Class[10]:可用於接受來自 Command Palette 中串列項的選擇輸入。

因為我要實現的功能比較簡單,只需要監聽輸入事件並觸發自動完成功能,因此需要用到 EventListener Class。在該類下麵找到了 on_query_completions 方法用來處理觸發自動完成時執行的命令。接著修改一下剛才的程式碼:

  1. import sublime

  2. import sublime_plugin

  3. class CTagsAutoComplete(sublime_plugin.EventListener):

  4.    def on_query_completions(self, view, prefix, locations):

◈

view:當前檢視

◈

prefix:觸發自動完成時輸入的文字

◈

locations: 觸發自動完成時輸入在快取區中的位置,可以透過這個引數判斷語言來執行不同命令

◈

傳回型別:

◈ return None
◈ return [["trigger \t hint", "contents"]...],其中 \t hint 為可選內容,給自動完成的函式名稱新增一個提示
◈ return (results, flag),其中 results 是包含自動完成陳述句的 list,如上;flag 是一個額外引數,可用來控制是否顯示 Sublime Text 自帶的自動完成功能

讀取 CTags 檔案

為了讀取 .tag 檔案,首先得判斷當前專案是否開啟,同時 .tag 檔案是否存在,然後讀取 .tag 檔案中的所有內容:

  1. import sublime

  2. import sublime_plugin

  3. import os

  4. import re

  5. class CTagsAutoComplete(sublime_plugin.EventListener):

  6.    def on_query_completions(self, view, prefix, locations):

  7.        results = []

  8.        ctags_paths = [folder + '\.tags' for folder in view.window().folders()]

  9.        ctags_rows  = []

  10.        for ctags_path in ctags_paths:

  11.            if not is_file_exist(view, ctags_path):

  12.                return []

  13.            ctags_path = str(ctags_path)

  14.            ctags_file = open(ctags_path, encoding = 'utf-8')

  15.            ctags_rows += ctags_file.readlines()

  16.            ctags_file.close()

  17. def is_file_exist(view, file):

  18.    if (not view.window().folders() or not os.path.exists(file)):

  19.        return False

  20.    return True

透過上述操作,即可讀取當前專案下所有的 .tag 檔案中的內容。

分析 CTags 檔案

首先是獲取 .tags 檔案中,包含 prefix 的行:

  1. for rows in ctags_rows:

  2.    target = re.findall('^' + prefix + '.*', rows)

一旦找到,就透過正則運算式對該行資料進行處理:

  1. if target:

  2.    matched = re.split('\t', str(target[0]))

  3.    trigger = matched[0] # 傳回的第一個引數,函式名稱

  4.    trigger += '\t(%s)' % 'CTags' # 給函式名稱後加上標識 'CTags'

  5.    contents = re.findall(prefix + '[0-9a-zA-Z_]*\(.*\)', str(matched[2])) # 傳回的第二個引數,函式的具體定義

  6.    if (len(matched) > 1 and contents):

  7.        results.append((trigger, contents[0]))

  8.        results = list(set(results)) # 去除重覆的函式

  9.        results.sort() # 排序

處理完成之後就可以傳回了,考慮到最好只顯示 .tags 中的函式,我不需要顯示 Sublime Text 自帶的自動完成功能(提取當前頁面中的變數和函式),因此我的傳回結果如下:

  1. return (results, sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS)

新增配置檔案

考慮到能夠關閉外掛的功能,因此需要新增一個配置檔案,用來指定不開啟外掛功能的語言,這裡我參考了 “All AutoComplete” 的程式碼:

  1. def plugin_loaded():

  2.    global settings

  3.    settings = sublime.load_settings('CTagsAutoComplete.sublime-settings')

  4. def is_disabled_in(scope):

  5.    excluded_scopes = settings.get("exclude_from_completion", [])

  6.    for excluded_scope in excluded_scopes:

  7.        if scope.find(excluded_scope) != -1:

  8.            return True

  9.    return False

  10. if is_disabled_in(view.scope_name(locations[0])):

  11.    return []

這裡用到的配置檔案需要新增到外掛所在的檔案夾中,名稱為 CTagsAutoComplete.sublime-settings,其內容為:

  1. {

  2.    // An array of syntax names to exclude from being autocompleted.

  3.    "exclude_from_completion": [

  4.        "css",

  5.        "html"

  6.    ]

  7. }

新增設定檔案

有了配置檔案,還需要在 Sublime Text 的 “Preferences -> Package settings” 下新增相應的設定,同樣也是放在外掛所在檔案夾中,名稱為 Main.sublime-menu:

  1. [

  2.    {

  3.        "caption": "Preferences",

  4.        "mnemonic": "n",

  5.        "id": "preferences",

  6.        "children": [

  7.            {

  8.                "caption": "Package Settings",

  9.                "mnemonic": "P",

  10.                "id": "package-settings",

  11.                "children": [

  12.                    {

  13.                        "caption": "CTagsAutoComplete",

  14.                        "children": [

  15.                            {

  16.                                "command": "open_file",

  17.                                "args": {

  18.                                    "file": "${packages}/CTagsAutoComplete/CTagsAutoComplete.sublime-settings"

  19.                                },

  20.                                "caption": "Settings"

  21.                            }

  22.                        ]

  23.                    }

  24.                ]

  25.            }

  26.        ]

  27.    }

  28. ]

總結

首先給出外掛的完整原始碼:

  1. import sublime

  2. import sublime_plugin

  3. import os

  4. import re

  5. def plugin_loaded():

  6.    global settings

  7.    settings = sublime.load_settings('CTagsAutoComplete.sublime-settings')

  8. class CTagsAutoComplete(sublime_plugin.EventListener):

  9.    def on_query_completions(self, view, prefix, locations):

  10.        if is_disabled_in(view.scope_name(locations[0])):

  11.            return []

  12.        results = []

  13.        ctags_paths = [folder + '\.tags' for folder in view.window().folders()]

  14.        ctags_rows  = []

  15.        for ctags_path in ctags_paths:

  16.            if not is_file_exist(view, ctags_path):

  17.                return []

  18.            ctags_path = str(ctags_path)

  19.            ctags_file = open(ctags_path, encoding = 'utf-8')

  20.            ctags_rows += ctags_file.readlines()

  21.            ctags_file.close()

  22.        for rows in ctags_rows:

  23.            target = re.findall('^' + prefix + '.*', rows)

  24.            if target:

  25.                matched = re.split('\t', str(target[0]))

  26.                trigger = matched[0]

  27.                trigger += '\t(%s)' % 'CTags'

  28.                contents = re.findall(prefix + '[0-9a-zA-Z_]*\(.*\)', str(matched[2]))

  29.                if (len(matched) > 1 and contents):

  30.                    results.append((trigger, contents[0]))

  31.                    results = list(set(results))

  32.                    results.sort()

  33.        return (results, sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS)

  34. def is_disabled_in(scope):

  35.    excluded_scopes = settings.get("exclude_from_completion", [])

  36.    for excluded_scope in excluded_scopes:

  37.        if scope.find(excluded_scope) != -1:

  38.            return True

  39.    return False

  40. def is_file_exist(view, file):

  41.    if (not view.window().folders() or not os.path.exists(file)):

  42.        return False

  43.    return True

  44. plugin_loaded()

之後我會把這個外掛整合好後,上傳到 Package Control 上,從而方便更多人使用。透過這次入門,我嘗到了甜頭,未來的開發過程中,可能會出現各種各樣獨特的需求,如果已有的外掛無法提供幫助,那就自己上吧。

贊(0)

分享創造快樂

© 2026 知識星球   網站地圖