介绍实现一个 VS Code 扩展的过程,触类旁通,经过这个例子,你也能够很轻松地写出其它相似的扩展。git
GitHub 项目地址github
跟不少人同样,有种强迫症,看见中英文间不加空格的排版,就浑身不舒服,非要把它改过来。作笔记的时候常常要修改从网上拷贝来的内容,空格是手动一个个加的,烦了,后来就想,总该有个插件能帮咱们作这件事吧,因而 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 范例 这篇文章中,咱们能简单了解到如下几种对象:
vscode.window
获得这个 Window 对象。window.activeTextEditor
属性来取得当前工做中的 tab,即 TextEditor 对象。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 对象来表征文档内一个字符所在的位置,它有两个属性:
一个起始 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 中对文本的操做主要使用如下对象和属性、方法: