VSCode Extension中的Virtual Documents使用笔记
我们在用TypeScript编写VSCode Extension应用时,可以通过VSCode API提供的内置Command "vscode.diff"来快速比较两个文档,有关该命令的参数介绍可以查看官方文档。基本用法如下:
vscode.commands.executeCommand("vscode.diff", vscode.Uri.file(filePath1), vscode.Uri.file(filePath2), "Comparing Files");
这里的filePath1和filePath2为要进行比较的两个文档的路径。也就是说,这两个文档是必须真实存在的,而且路径能够被VSCode访问。有时为了需要,在进行比较时我们也可以将文档内容暂时输出到系统临时目录,然后从临时目录加载文档内容。获取系统临时目录的方法可以参考下面的代码:
import * as os from "os"; import * as path from "path"; import * as process from "process"; let platform = os.platform(); let isWin = platform === "win32"; let isLinux = platform === "linux"; let tempDir = isWin ? process.env.TEMP : (isLinux ? path.join(process.env.HOME, 'tmp') : process.env.TMPDIR); console.log(tempDir);
但是使用系统临时目录会带来另外一个问题,看下面的截图,在比较文档的界面右上角,有一个菜单可以直接点击打开文档,此时是从临时目录打开的,但有时我们并不想让用户知道文档是暂时存放在临时目录里的。有没有什么解决办法呢?我没有找到通过配置的方式将该菜单隐藏或者改变其行为,但是有两个变通的方法:一是不使用系统临时目录,仍然从文档的原始位置进行加载;二是使用VSCode提供的Virtual Documents。
下面是使用Virtual Documents之后的界面,可以看到与之前相比少了显示文档的路径和打开文档的菜单。
下面是具体的实现。
按照官方文档的介绍,我们需要定义一个TextDocumentContentProvider类的实例,其中的provideTextDocumentContent方法会返回Virtual Documents的具体内容。
1 private async getDocumentText(fileFullPath: string): Promise<string> { 2 return new Promise<string>(resolve => { 3 fs.readFile(fileFullPath, "UTF-8", (err, data) => { 4 if (err) { 5 console.log(err); 6 resolve(""); 7 } else { 8 resolve(data); 9 } 10 }); 11 }); 12 } 13 14 private sourceProvider = new class implements vscode.TextDocumentContentProvider { 15 onDidChangeEmitter = new vscode.EventEmitter<vscode.Uri>(); 16 onDidChange = this.onDidChangeEmitter.event; 17 18 async provideTextDocumentContent(uri: vscode.Uri): Promise<string> { 19 let oSource = await getDocumentText(uri.path); 20 let oSourceContent = JSON.stringify(oSource, null, "\t"); 21 return oSourceContent; 22 } 23 };
其中第19行通过getDocumentText方法从指定的路径读取了文档的内容,然后使用JSON.stringify将其格式化为标准的JSON文档,并返回对应的内容。onDidChange提供了文档被更新时的事件,我们可以在文档内容被修改时手动触发该事件,稍后会介绍。
接下来是注册command。
subscriptions.push(vscode.workspace.registerTextDocumentContentProvider("sourceSchema", sourceProvider));
注意这里的第一个参数schema非常重要!后面在传递文件url时都要带上这个schema,相当于它是一个标识,用来表示对该url的操作都通过sourceProvider类提供的方法来处理。
相应地,如果vscode.diff的第二个文档你也希望使用Virtual Documents,那么就还需要定义TextDocumentContentProvider类的另一个实例。provideTextDocumentContent方法
1 subscriptions.push(vscode.workspace.registerTextDocumentContentProvider("compareSchema", new class implements vscode.TextDocumentContentProvider { 2 onDidChangeEmitter = new vscode.EventEmitter<vscode.Uri>(); 3 onDidChange = this.onDidChangeEmitter.event; 4 5 async provideTextDocumentContent(uri: vscode.Uri): Promise<string> { 6 return ""; // return file content from here 7 } 8 }));
这里我简化了代码实现,没有单独定义TextDocumentContentProvider类的实例,而是直接通过subscriptions.push来注册command。其中的provideTextDocumentContent方法可以根据需要返回文档的内容,这里就不再给出对应的代码。
好了,下面我们来看看在vscode.diff中如何使用它们。
const uriSource = vscode.Uri.parse("sourceSchema:" + filePath1); const uriCompare = vscode.Uri.parse("compareSchema:" + filePath2); vscode.commands.executeCommand("vscode.diff", uriSource, uriCompare, "Comparing Files");
注意这里的filePath1和filePath2的前面都要加上对应的schema,否则文档被打开时就不会使用对应的TextDocumentContentProvider类的实例来处理,而会报找不到文档路径的错误。
当修改文档内容后,可以手动触发TextDocumentContentProvider类的onDidChange事件来刷新已打开的文档内容,下面是对应的伪代码:
await writeFile(vscode.Uri.parse(filePath1).fsPath, fileContent);
sourceProvider.onDidChangeEmitter.fire(vscode.Uri.parse("sourceSchema:" + filePath1));
另一个需要注意的地方是,通过Virtual Documents创建的文档都是以只读方式打开的,所以用户无法在编辑器中手动修改文件内容,我们可以在TextDocumentContentProvider类的provideTextDocumentContent方法中将已修改的内容返回。其它有关Document提供的事件可以查阅官方文档。