如何實現一個給中英文間加空格的 VS Code 擴展

介紹實現一個 VS Code 擴展的過程,觸類旁通,經過這個例子,你也能夠很輕鬆地寫出其它相似的擴展。git

GitHub 項目地址github

References

Note

跟不少人同樣,有種強迫症,看見中英文間不加空格的排版,就渾身不舒服,非要把它改過來。作筆記的時候常常要修改從網上拷貝來的內容,空格是手動一個個加的,煩了,後來就想,總該有個插件能幫咱們作這件事吧,因而 google "vscode 插件 中英文 空格",沒搜到有價值的內容,google "中英文 空格",找到了 V2EX 上 中英混排手動擋 --「爲何我就是能這樣嫺熟地加上空格呢?」 這篇文章,繼而知道了 pangu 這個項目,這是一個能夠給半角和全角字符間自動加空格的 JavaScript 實現,這不正是我想要的嗎,但在它的介紹頁面上,各類基於它的編輯器插件都有了,惟獨缺了我大 VS Code,因而想,不行咱就本身擼一個唄,又不是多難的事,核心實現都有了,不就是調一個方法的事嗎?npm

(後話:要是我一開始就知道搜索 "pangu" 這個關鍵字,我也就不造這個輪子了,等我寫完代碼,往 VS Code Marketplace 一上傳,再一搜 "vscode-pangu",結果就發現了一個相同的實現:halfcrazy/vscode-pangu,失敬失敬!)visual-studio-code

pangu 這個庫提供了一個核心方法來轉換字符串 (內部的具體實現是經過正則匹配來作的),以下所示:api

const refinedText = pangu.spacing(originText)

// 示例
const refinedText = pangu.spacing("這是一個VS Code擴展")
// refinedText: "這是一個 VS Code 擴展"
複製代碼

所以插件要作的事情就很簡單了,獲取當前編輯器內的全部文本,調用此方法,獲得加了空格後的文本,替換原來的文本便可。因此關鍵在於,如何獲取編輯器的文本,如何替換。毫無疑問,這些須要 VS Code 的 extension API 來完成。數組

VS Code extension API Document,這裏,我要吐槽一下它的文檔,我是第一次見把全部 API 塞在一個頁面裏的,API 的介紹也過於簡潔。app

VS Code 自帶一個清除多餘的行尾空格的命令,按下 cmd + shift + p,在彈出的命令窗口中輸入 trim,選中 Trim Trailing Whitespace 並回車執行。編輯器

咱們想實現的這個插件和這個命令是相似的,按下 cmd + shift + p,在彈出的命令窗口中輸入相似 add space 執行,只不過咱們不是要清除多餘空格,而是加空格,但本質是同樣的,修改編輯器中的文本。visual-studio

VS Code extension 文檔提供了一個 Hello World 範例,很慶幸的是,這個範例正好是咱們所須要的。這個範例是這樣工做的,在命令窗口中輸入 Hello World,會彈出一個提示窗,代碼是這樣的:ui

// extension.js
function activate(context) {
    let disposable = vscode.commands.registerCommand('extension.sayHello', function () {
        // The code you place here will be executed every time your command is executed

        // Display a message box to the user
        vscode.window.showInformationMessage('Hello World!');
    });
    context.subscriptions.push(disposable);
}
複製代碼

其它的咱們均可以不須要理解,只須要把 vscode.window.showInformationMessage('Hello World!'); 這行代碼替換成咱們本身的邏輯,即獲取文本,加空格,替換原來的文本,這個插件就基本完成了。

首先來看怎麼獲取文本。在 Hello World 範例 這篇文章中,咱們能簡單瞭解到如下幾種對象:

  • Window 對象 - 表示當前 VS Code 的整個窗口,用 vscode.window 獲得這個 Window 對象。
  • TextEditor 對象 - VS Code 的整個窗口中可能打開了多個 tab,每個 tab 就是一個 TextEditor 對象,但咱們只須要那個當前激活的 tab,咱們用 window.activeTextEditor 屬性來取得當前工做中的 tab,即 TextEditor 對象。
  • TextDocument 對象 - 每一個 TextEditor 中都有一個文檔,這個文檔就是 TextDocument 對象,咱們用 editor.document 屬性來取得 TextEditor 對象中的 TextDocument 對象。TextDocument 對象有一個 getText() 方法來取得其中的全部文本。

最終,咱們經過

const originText = vscode.window.activeTextEditor.document.getText()
複製代碼

取得當前正在編輯的文檔的全部文本。

既然拿到了原始文本,處理就很好辦了 (此處忽略了經過 npm 安裝 pangu 的過程):

const refinedText = pangu.spacing(originText)
複製代碼

接着,咱們就該用新文本替換舊文本了,想着既然有 document.getText() 方法,就該有一個配套的 document.setText(string) 方法吧,三行代碼搞定插件,簡單粗暴:

vscode.window.activeTextEditor.document.setText(refinedText)
複製代碼

結果 too young too simple, sometime naive! TextDocument 竟然沒有這樣的方法,也沒有相似的方法,困惑了,究竟是怎麼才能操做原來的文本呢?

找到網上爲數很少的一篇介紹如何使用插件編輯文檔的文章 - Visual Studio Code Extensions: Editing the Document,在這篇文章中,逐漸瞭解到 VS Code 插件編輯文本內容的核心思想。

這個思想體如今一種對象上 - TextEdit 對象 (注意,不是 TextEditor)。一個 TextEdit 對象就表示對文本的一次操做。

對文本的操做無外乎三種:增長,刪除,替換,但其實歸結起來,增長和刪除,也算是替換操做。增長,用新的字符串,替換空字符串;刪除,用空字符串替換原來的字符串。

對於要換替換的對象,既原來的字符串,咱們要知道它在文檔中所處的位置,這個位置包括起始位置和結束位置,每一個位置都應該包括它所在的行號和所在行內的編號,這兩個位置組成了一個區間。

VS Code 用 Position 對象來表徵文檔內一個字符所在的位置,它有兩個屬性:

  • line - 行號
  • character - 所在行內的編號

一個起始 Position 和一個結尾 Position,兩個 Position 組成了 Range 對象,這個 Range 對象就表明了一串連續的字符。

這樣,咱們有了要替換的對象,又有新的字符串,咱們就能夠定義出一個 TextEdit 對象來表示這樣一次替換操做。

const aTextReplace = new vscode.TextEdit(range, newText)
複製代碼

好比,咱們要把第 2 行第 3 個字符,到第 5 行第 6 個字符,刪除掉,即用空字符串替換它,代碼以下:

const start = new vscode.Position(2, 3)
const end = new vscode.Position(5, 6)
const range = new vscode.Range(start, end)
const aTextDel = new vscode.TextEdit(range, '')
複製代碼

上面前三行代碼能夠簡化成

const range = new vscode.Range(2, 3, 5, 6)
複製代碼

第四行代碼 TextEdit 對象能夠用 TextEdit.delete(range) 靜態方法生成:

const aTextDel = vscode.TextEdit.delete(range)
複製代碼

Range 和 TextEdit,我認爲是操做文本的核心概念,理解它這兩個對象,其它的也就沒什麼難的了。

可是,到目前爲止,TextEdit 還只是定義了一個將被應用的操做,但尚未真正地被應用到文本上,那怎麼來把這個操做真正執行呢。

這裏又涉及到一個新的對象 - WorkspaceEdit 對象。WorkspaceEdit 能夠理解成 TextEdit 的容器。TextEdit 只是對文本的一次操做,若是咱們須要對這個文本同時進行屢次操做,好比全局替換,咱們就要定義多個 TextEdit 對象,並把這些對象放到一個數組裏,再把這個數組放到 WorkspaceEdit 對象中。

更強大的在於,WorkspaceEdit 支持對多個文檔同時進行屢次操做,所以,每一個 TextEdit 數組必然須要對應一個文檔對象,WorkspaceEdit 使用 uri 來表徵一個文檔,uri 能夠從 document.uri 屬性得到。

咱們前面獲得了 document 對象,咱們又定義了一些 TextEdit 對象,咱們把它放到 WorkspaceEdit 對象中:

let textEdits = []
textEdits.push(aTextDel)
// push more TextEdit
// textEdits.push(...)

let workspaceEdit = new vscode.WorkspaceEdit()
workspaceEdit.set(document.uri, textEdits)
複製代碼

最後,咱們終於能夠真正地執行這些操做了,使用 vscode.workspace.applyEdit() 方法來使這些操做生效:

vscode.workspace.applyEdit(workspaceEdit)
複製代碼

來看看咱們這個插件是如何實現的:

const editor = vscode.window.activeTextEditor
if (!editor) {
    return  // No open text editor
}

const document = editor.document
const lineCount = document.lineCount

let textEdits = []
for (let i=0; i<lineCount; i++) {
    const textLine = document.lineAt(i)
    const oriTrimText = textLine.text.trimRight()

    if (oriTrimText.length === 0) {
        textEdits.push(new vscode.TextEdit(textLine.range, ''))
    } else {
        const panguText = pangu.spacing(oriTrimText)
        textEdits.push(new vscode.TextEdit(textLine.range, panguText))
    }
}
let workspaceEdit = new vscode.WorkspaceEdit()
workspaceEdit.set(document.uri, textEdits)
vscode.workspace.applyEdit(workspaceEdit)
複製代碼

由於作了一些額外的操做 - 刪除多餘的尾部空格,因此代碼稍微多了一些,但總體邏輯是很是簡單的,就是遍歷每一行,經過 document.lineAt(i) 拿到每一行對象,每一行都是一個 TextLine 對象,這個對象裏有這一行全部文本的內容,和它們的 Range。若是是空行,則生成用空白文本替換原來內容的 TextEdit 對象,不然,生成用加空格後的文本替換原來文本的 TextEdit 對象。把這些 TextEdit 對象以數組的形式放到 WorkspaceEdit 對象中,最後執行這個對象中的全部操做。

WorkspaceEdit 的設計目標是同時對多個文檔進行屢次操做,若是咱們只是想對當前文檔進行編輯,用 WorkspaceEdit 有點殺雞用牛刀的感受,從上面也能夠看出,包裹的層數太多了。

若是隻對當前 tab 即 TextEditor 對象進行文本編輯,咱們可使用 TextEditor 對象的 edit() 方法,代碼是相似的,只不過不用顯式的生成 TextEdit 對象。看代碼就明白了:

editor.edit(builder => {
    for (let i=0; i<lineCount; i++) {
        const textLine = document.lineAt(i)
        const oriTrimText = textLine.text.trimRight()

        if (oriTrimText.length === 0) {
            builder.replace(textLine.range, '')
            // 等同於
            // builder.delete(textLine.range)
        } else {
            const panguText = pangu.spacing(oriTrimText)
            builder.replace(textLine.range, panguText)
        }
    }
})
複製代碼

builder.repalce(textLine.range, panguText) 就至關於執行了一個 TextEdit(textLine.range, panguText) 對象。相比之下,代碼比上面簡潔了一些。

這裏要注意,不要把循環寫在 editor.edit() 外面,我一開始就是這麼作的,致使只有第一次編輯生效 - Extension API TextEditorEdit.replace only works on primary selection?

另外,還有一個比較常見的對象 Selection,它繼承自 Range 對象,因此也是表示一個區間,它表示在 tab 中用光標選中的區域,能夠經過 editor.selection 得到。

最後,作一下總結,VS Code extension 中對文本的操做主要使用如下對象和屬性、方法:

  • Window
    • activeTextEditor
  • TextEditor
    • document
    • selection
    • edit()
  • TextDocument
    • uri
    • lineCount
    • lineAt()
    • getText()
  • WorkspaceEdit
  • TextEdit
  • Range
  • Selection
  • Position
相關文章
相關標籤/搜索