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

利用 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)

分享創造快樂

© 2022 知識星球   網站地圖