源码解析
vscode语言服务器
语言服务器是一种特殊的 Visual Studio Code 扩展,可为许多编程语言提供编辑体验。 使用语言服务器,您可以实现自动完成、错误检查(诊断)、跳转到定义以及 VS Code 支持的许多其他语言功能。
然而,在 VS Code 中实现对语言功能的支持时,我们发现了三个常见问题:
首先,语言服务器通常以其本机编程语言实现,这给将它们与具有 Node.js 运行时的 VS Code 集成带来了挑战 。
此外,语言功能可能是资源密集型的。 例如,为了正确验证文件,语言服务器需要解析大量文件,为它们建立抽象语法树并执行静态程序分析。 这些操作可能会导致大量的 CPU 和内存使用,我们需要确保 VS Code 的性能不受影响。
最后,将多种语言工具与多个代码编辑器集成可能需要付出巨大的努力。 从语言工具的角度来看,它们需要适应具有不同API的代码编辑器。 从代码编辑者的角度来看,他们不能期望语言工具提供任何统一的 API。 这使得在 N 个代码编辑器中实现对 M 种语言的语言支持成为 M * N 的工作。
为了解决这些问题,Microsoft 指定了语言服务器协议 Language Server Protocol, 该协议标准化了语言工具和代码编辑器之间的通信。 这样,语言服务器可以用任何语言实现并在自己的进程中运行,以避免性能成本,因为它们通过语言服务器协议与代码编辑器进行通信。 此外,任何符合 LSP 的语言工具都可以与多个符合 LSP 的代码编辑器集成,并且任何符合 LSP 的代码编辑器都可以轻松选择多个符合 LSP 的语言工具。 LSP 对于语言工具提供商和代码编辑器供应商来说都是双赢!
实现语言服务器
在 VS Code 中,语言服务器有两部分:
语言客户端:用 JavaScript / TypeScript 编写的普通 VS Code 扩展。 此扩展可以访问所有 VS Code 命名空间 API。 语言服务器:在单独进程中运行的语言分析工具。
如上所述,在单独的进程中运行语言服务器有两个好处:
该分析工具可以用任何语言实现,只要它能够按照语言服务器协议与语言客户端进行通信即可。 由于语言分析工具通常会占用大量 CPU 和内存,因此在单独的进程中运行它们可以避免性能成本。 例如,HTML 语言客户端和 PHP 语言客户端是用 TypeScript 编写的普通 VS Code 扩展。 它们各自实例化一个对应的Language Server,并通过LSP与其进行通信。 尽管PHP语言服务器是用PHP编写的,但它仍然可以通过LSP与PHP语言客户端进行通信。
serverMain.ts:创建与vscode客户端的连接
import { TextDocument } from 'vscode-languageserver-textdocument';
import {
createConnection,
TextDocuments,
TextDocumentSyncKind
} from 'vscode-languageserver/node';
import { autoRenameTag, autoRenameTagRequestType } from './autoRenameTag';
import {
enableBetterErrorHandlingAndLogging,
handleError
} from './errorHandlingAndLogging';
const connection = createConnection();
const documents = new TextDocuments(TextDocument);
enableBetterErrorHandlingAndLogging(connection);
connection.onInitialize(() => ({
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental
}
}));
connection.onInitialized(() => {
console.log('Auto Rename Tag has been initialized.');
});
const handleRequest: <Params, Result>(
fn: (params: Params) => Result
) => (params: Params) => Result = fn => params => {
try {
return fn(params);
} catch (error) {
handleError(error);
throw error;
}
};
connection.onRequest(
autoRenameTagRequestType,
handleRequest(autoRenameTag(documents))
);
documents.listen(connection);
connection.listen();
import { TextDocument } from 'vscode-languageserver-textdocument';
import {
createConnection,
TextDocuments,
TextDocumentSyncKind
} from 'vscode-languageserver/node';
import { autoRenameTag, autoRenameTagRequestType } from './autoRenameTag';
import {
enableBetterErrorHandlingAndLogging,
handleError
} from './errorHandlingAndLogging';
const connection = createConnection();
const documents = new TextDocuments(TextDocument);
enableBetterErrorHandlingAndLogging(connection);
connection.onInitialize(() => ({
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental
}
}));
connection.onInitialized(() => {
console.log('Auto Rename Tag has been initialized.');
});
const handleRequest: <Params, Result>(
fn: (params: Params) => Result
) => (params: Params) => Result = fn => params => {
try {
return fn(params);
} catch (error) {
handleError(error);
throw error;
}
};
connection.onRequest(
autoRenameTagRequestType,
handleRequest(autoRenameTag(documents))
);
documents.listen(connection);
connection.listen();
使用vscode-languageserver-textdocument
模块进行文本文档的访问和操作。下面是对代码的解释:
- 导入模块:
import { TextDocument } from "vscode-languageserver-textdocument";
import { createConnection, TextDocuments, TextDocumentSyncKind } from "vscode-languageserver/node";
import { autoRenameTag, autoRenameTagRequestType } from "./autoRenameTag";
import { enableBetterErrorHandlingAndLogging, handleError } from "./errorHandlingAndLogging";
import { TextDocument } from "vscode-languageserver-textdocument";
import { createConnection, TextDocuments, TextDocumentSyncKind } from "vscode-languageserver/node";
import { autoRenameTag, autoRenameTagRequestType } from "./autoRenameTag";
import { enableBetterErrorHandlingAndLogging, handleError } from "./errorHandlingAndLogging";
首先,代码导入了所需的模块。vscode-languageserver-textdocument
模块用于访问和操作文本文档,createConnection
和TextDocuments
模块用于创建与客户端的连接和管理文本文档集合。另外,代码还导入了自定义的autoRenameTag
和错误处理相关的模块。
- 创建连接和文档集合:
const connection = createConnection();
const documents = new TextDocuments(TextDocument);
const connection = createConnection();
const documents = new TextDocuments(TextDocument);
代码创建了与客户端的连接实例,并使用TextDocuments
类创建了一个documents
对象,用于管理打开的文本文档。
- 初始化和事件处理:
enableBetterErrorHandlingAndLogging(connection);
connection.onInitialize(() => ({
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
},
}));
connection.onInitialized(() => {
console.log("Auto Rename Tag has been initialized.");
});
enableBetterErrorHandlingAndLogging(connection);
connection.onInitialize(() => ({
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
},
}));
connection.onInitialized(() => {
console.log("Auto Rename Tag has been initialized.");
});
代码注册了一些事件处理程序。enableBetterErrorHandlingAndLogging
函数用于改善错误处理和日志记录。onInitialize
事件处理程序在插件初始化时被触发,返回了插件的功能。在这个例子中,只定义了文本文档同步方式为增量同步。onInitialized
事件处理程序在插件初始化完成后被触发,这里简单地输出一条初始化完成的日志信息。
- 请求处理和自动重命名标签:
const handleRequest: <Params, Result>(
fn: (params: Params) => Result
) => (params: Params) => Result = (fn) => (params) => {
try {
return fn(params);
} catch (error) {
handleError(error);
throw error;
}
};
connection.onRequest(
autoRenameTagRequestType,
handleRequest(autoRenameTag(documents))
);
const handleRequest: <Params, Result>(
fn: (params: Params) => Result
) => (params: Params) => Result = (fn) => (params) => {
try {
return fn(params);
} catch (error) {
handleError(error);
throw error;
}
};
connection.onRequest(
autoRenameTagRequestType,
handleRequest(autoRenameTag(documents))
);
这段代码定义了一个handleRequest
函数,用于处理请求并捕获可能的错误。然后,使用onRequest
方法注册了处理自动重命名标签请求的处理程序,当收到该请求时,会调用autoRenameTag
函数进行处理。
- 监听文档变化和启动连接:
documents.listen(connection);
connection.listen();
documents.listen(connection);
connection.listen();
通过调用documents.listen
方法,将documents
对象与连接关联起来,使其能够处理文档的打开、关闭、更改等事件。最后,调用connection.listen
方法启动连接,使插件能够接收来自客户端的请求并发送响应。
createLanguageClientProxy 创建语言客户端代理
插件客户端请求服务器
import * as vscode from 'vscode';
import {
Code2ProtocolConverter,
LanguageClient,
LanguageClientOptions,
RequestType,
ServerOptions,
TransportKind
} from 'vscode-languageclient/node';
type VslSendRequest = <P, R, E>(
type: RequestType<P, R, E>,
params: P
) => Thenable<R>;
export interface LanguageClientProxy {
readonly code2ProtocolConverter: Code2ProtocolConverter;
readonly sendRequest: VslSendRequest;
}
export const createLanguageClientProxy: (
context: vscode.ExtensionContext,
id: string,
name: string,
clientOptions: LanguageClientOptions
) => Promise<LanguageClientProxy> = async (
context,
id,
name,
clientOptions
) => {
const serverModule = context.asAbsolutePath('../server/dist/serverMain.js');
const serverOptions: ServerOptions = {
run: { module: serverModule, transport: TransportKind.ipc },
debug: {
module: serverModule,
transport: TransportKind.ipc,
options: { execArgv: ['--nolazy', '--inspect=6009'] }
}
};
const outputChannel = vscode.window.createOutputChannel(name);
clientOptions.outputChannel = {
name: outputChannel.name,
append() {},
appendLine(value: string) {
try {
const message = JSON.parse(value);
if (!message.isLSPMessage) {
outputChannel.appendLine(value);
}
} catch (error) {
if (typeof value !== 'object') {
outputChannel.appendLine(value);
}
}
},
replace(value) {
outputChannel.replace(value);
},
clear() {
outputChannel.clear();
},
show() {
outputChannel.show();
},
hide() {
outputChannel.hide();
},
dispose() {
outputChannel.dispose();
}
};
const languageClient = new LanguageClient(
id,
name,
serverOptions,
clientOptions
);
languageClient.registerProposedFeatures();
context.subscriptions.push(languageClient.start());
await languageClient.onReady();
const languageClientProxy: LanguageClientProxy = {
code2ProtocolConverter: languageClient.code2ProtocolConverter,
sendRequest: (type, params) => languageClient.sendRequest(type, params)
};
return languageClientProxy;
};
import * as vscode from 'vscode';
import {
Code2ProtocolConverter,
LanguageClient,
LanguageClientOptions,
RequestType,
ServerOptions,
TransportKind
} from 'vscode-languageclient/node';
type VslSendRequest = <P, R, E>(
type: RequestType<P, R, E>,
params: P
) => Thenable<R>;
export interface LanguageClientProxy {
readonly code2ProtocolConverter: Code2ProtocolConverter;
readonly sendRequest: VslSendRequest;
}
export const createLanguageClientProxy: (
context: vscode.ExtensionContext,
id: string,
name: string,
clientOptions: LanguageClientOptions
) => Promise<LanguageClientProxy> = async (
context,
id,
name,
clientOptions
) => {
const serverModule = context.asAbsolutePath('../server/dist/serverMain.js');
const serverOptions: ServerOptions = {
run: { module: serverModule, transport: TransportKind.ipc },
debug: {
module: serverModule,
transport: TransportKind.ipc,
options: { execArgv: ['--nolazy', '--inspect=6009'] }
}
};
const outputChannel = vscode.window.createOutputChannel(name);
clientOptions.outputChannel = {
name: outputChannel.name,
append() {},
appendLine(value: string) {
try {
const message = JSON.parse(value);
if (!message.isLSPMessage) {
outputChannel.appendLine(value);
}
} catch (error) {
if (typeof value !== 'object') {
outputChannel.appendLine(value);
}
}
},
replace(value) {
outputChannel.replace(value);
},
clear() {
outputChannel.clear();
},
show() {
outputChannel.show();
},
hide() {
outputChannel.hide();
},
dispose() {
outputChannel.dispose();
}
};
const languageClient = new LanguageClient(
id,
name,
serverOptions,
clientOptions
);
languageClient.registerProposedFeatures();
context.subscriptions.push(languageClient.start());
await languageClient.onReady();
const languageClientProxy: LanguageClientProxy = {
code2ProtocolConverter: languageClient.code2ProtocolConverter,
sendRequest: (type, params) => languageClient.sendRequest(type, params)
};
return languageClientProxy;
};
插件主函数
- 插件会把每次的修改内容发送给vscode服务器,服务器处理后,返回处理结果
- 获取返回结果后调用
vscode.window.activeTextEditor.edit
方法来修改编辑器从而达到重命名标签的效果(这一部分代码看applyResults
函数) doAutoCompletionElementRenameTag
方法里调用applyResults
applyResults函数
const prev = vscode.window.activeTextEditor.document.version;
const applied = await vscode.window.activeTextEditor.edit(
editBuilder => {
assertDefined(vscode.window.activeTextEditor);
for (const result of results) {
const startPosition =
vscode.window.activeTextEditor.document.positionAt(
result.startOffset
);
const endPosition = vscode.window.activeTextEditor.document.positionAt(
result.endOffset
);
const range = new vscode.Range(startPosition, endPosition);
editBuilder.replace(range, result.tagName);
}
},
{
undoStopBefore: false,
undoStopAfter: false
}
);
const next = vscode.window.activeTextEditor.document.version;
const prev = vscode.window.activeTextEditor.document.version;
const applied = await vscode.window.activeTextEditor.edit(
editBuilder => {
assertDefined(vscode.window.activeTextEditor);
for (const result of results) {
const startPosition =
vscode.window.activeTextEditor.document.positionAt(
result.startOffset
);
const endPosition = vscode.window.activeTextEditor.document.positionAt(
result.endOffset
);
const range = new vscode.Range(startPosition, endPosition);
editBuilder.replace(range, result.tagName);
}
},
{
undoStopBefore: false,
undoStopAfter: false
}
);
const next = vscode.window.activeTextEditor.document.version;
vscode.window.activeTextEditor.document.version
文档版本号是一个整数值,用于标识文档的当前版本。因为调用vscode.window.activeTextEditor.edit
,会进行文档的修改操作,当文档的内容发生更改时,版本号会递增,表示文档已经被修改。
这里的 undoStopBefore 和 undoStopAfter 是两个布尔类型的属性,用于控制编辑操作的撤销行为。undoStopBefore:表示在编辑操作执行前是否要插入撤销点。如果设置为 false,则编辑操作不会被记录为撤销的起始点,这意味着在执行撤销操作时,编辑操作将被跳过。undoStopAfter:表示在编辑操作执行后是否要插入撤销点。如果设置为 false,则编辑操作不会被记录为撤销的结束点,这意味着在执行恢复操作时,编辑操作将被跳过。 通过将这两个属性设置为 false,可以避免每次替换都生成大量的撤销记录。
脚本(scripts/package.js)
const path = require('path');
const fs = require('fs-extra');
const root = path.join(__dirname, '..');
if (!fs.existsSync(path.join(root, 'dist'))) {
fs.mkdirSync(path.join(root, 'dist'));
}
// @ts-ignore
const pkg = require('../packages/extension/package.json');
pkg.main = './packages/extension/dist/extensionMain.js';
delete pkg.dependencies;
delete pkg.devDependencies;
delete pkg.scripts;
delete pkg.enableProposedApi;
fs.writeFileSync(
path.join(root, 'dist/package.json'),
`${JSON.stringify(pkg, null, 2)}\n`
);
fs.copyFileSync(
path.join(root, 'README.md'),
path.join(root, 'dist/README.md')
);
fs.copyFileSync(
path.join(root, 'CHANGELOG.md'),
path.join(root, 'dist/CHANGELOG.md')
);
fs.copyFileSync(path.join(root, 'LICENSE'), path.join(root, 'dist/LICENSE'));
fs.ensureDirSync(path.join(root, 'dist/images'));
fs.copyFileSync(
path.join(root, 'images/logo.png'),
path.join(root, 'dist/images/logo.png')
);
let extensionMain = fs
.readFileSync(
path.join(root, `dist/packages/extension/dist/extensionMain.js`)
)
.toString();
extensionMain = extensionMain.replace(
'../server/dist/serverMain.js',
'./packages/server/dist/serverMain.js'
);
fs.writeFileSync(
path.join(root, `dist/packages/extension/dist/extensionMain.js`),
extensionMain
);
const path = require('path');
const fs = require('fs-extra');
const root = path.join(__dirname, '..');
if (!fs.existsSync(path.join(root, 'dist'))) {
fs.mkdirSync(path.join(root, 'dist'));
}
// @ts-ignore
const pkg = require('../packages/extension/package.json');
pkg.main = './packages/extension/dist/extensionMain.js';
delete pkg.dependencies;
delete pkg.devDependencies;
delete pkg.scripts;
delete pkg.enableProposedApi;
fs.writeFileSync(
path.join(root, 'dist/package.json'),
`${JSON.stringify(pkg, null, 2)}\n`
);
fs.copyFileSync(
path.join(root, 'README.md'),
path.join(root, 'dist/README.md')
);
fs.copyFileSync(
path.join(root, 'CHANGELOG.md'),
path.join(root, 'dist/CHANGELOG.md')
);
fs.copyFileSync(path.join(root, 'LICENSE'), path.join(root, 'dist/LICENSE'));
fs.ensureDirSync(path.join(root, 'dist/images'));
fs.copyFileSync(
path.join(root, 'images/logo.png'),
path.join(root, 'dist/images/logo.png')
);
let extensionMain = fs
.readFileSync(
path.join(root, `dist/packages/extension/dist/extensionMain.js`)
)
.toString();
extensionMain = extensionMain.replace(
'../server/dist/serverMain.js',
'./packages/server/dist/serverMain.js'
);
fs.writeFileSync(
path.join(root, `dist/packages/extension/dist/extensionMain.js`),
extensionMain
);
这段代码是一个Node.js脚本,它执行了一系列文件操作和路径处理操作。一步一步解释每个部分的功能:
- 引入
path
和fs-extra
模块:
const path = require('path');
const fs = require('fs-extra');
const path = require('path');
const fs = require('fs-extra');
这里使用了Node.js内置的path
模块来处理文件路径,以及第三方库fs-extra
来进行文件操作,它是fs
模块的增强版。
- 定义根路径:
const root = path.join(__dirname, '..');
const root = path.join(__dirname, '..');
这行代码将当前脚本文件的路径(__dirname
)与其父目录(..
)拼接起来,得到了根路径。
- 创建
dist
目录(如果不存在):
if (!fs.existsSync(path.join(root, 'dist'))) {
fs.mkdirSync(path.join(root, 'dist'));
}
if (!fs.existsSync(path.join(root, 'dist'))) {
fs.mkdirSync(path.join(root, 'dist'));
}
这段代码首先使用path.join
方法将根路径与子路径dist
拼接起来,然后通过fs.existsSync
检查该路径是否存在。如果路径不存在,则使用fs.mkdirSync
创建该目录。
- 加载
package.json
文件并进行修改:
const pkg = require('../packages/extension/package.json');
pkg.main = './packages/extension/dist/extensionMain.js';
delete pkg.dependencies;
delete pkg.devDependencies;
delete pkg.scripts;
delete pkg.enableProposedApi;
const pkg = require('../packages/extension/package.json');
pkg.main = './packages/extension/dist/extensionMain.js';
delete pkg.dependencies;
delete pkg.devDependencies;
delete pkg.scripts;
delete pkg.enableProposedApi;
这部分代码通过require
方法加载了../packages/extension/package.json
文件,并将其内容赋值给变量pkg
。然后,它修改了pkg
对象的一些属性,包括main
属性的值以及删除了dependencies
、devDependencies
、scripts
和enableProposedApi
属性。
- 将修改后的
package.json
文件写入到dist
目录中:
fs.writeFileSync(
path.join(root, 'dist/package.json'),
`${JSON.stringify(pkg, null, 2)}\n`
);
fs.writeFileSync(
path.join(root, 'dist/package.json'),
`${JSON.stringify(pkg, null, 2)}\n`
);
这行代码使用fs.writeFileSync
方法将修改后的pkg
对象以格式化的JSON字符串形式写入到dist/package.json
文件中。
- 复制其他文件到
dist
目录:
fs.copyFileSync(
path.join(root, 'README.md'),
path.join(root, 'dist/README.md')
);
fs.copyFileSync(
path.join(root, 'CHANGELOG.md'),
path.join(root, 'dist/CHANGELOG.md')
);
fs.copyFileSync(path.join(root, 'LICENSE'), path.join(root, 'dist/LICENSE'));
fs.ensureDirSync(path.join(root, 'dist/images'));
fs.copyFileSync(
path.join(root, 'images/logo.png'),
path.join(root, 'dist/images/logo.png')
);
fs.copyFileSync(
path.join(root, 'README.md'),
path.join(root, 'dist/README.md')
);
fs.copyFileSync(
path.join(root, 'CHANGELOG.md'),
path.join(root, 'dist/CHANGELOG.md')
);
fs.copyFileSync(path.join(root, 'LICENSE'), path.join(root, 'dist/LICENSE'));
fs.ensureDirSync(path.join(root, 'dist/images'));
fs.copyFileSync(
path.join(root, 'images/logo.png'),
path.join(root, 'dist/images/logo.png')
);
这部分代码使用fs.copyFileSync
方法将README.md
、CHANGELOG.md
和LICENSE
文件以及images/logo.png
文件复制到dist
目录中。如果dist/images
目录不存在,将使用fs.ensureDirSync
方法创建该目录。
- 修改扩展的主文件路径:
let extensionMain = fs
.readFileSync(
path.join(root, `dist/packages/extension/dist/extensionMain.js`)
)
.toString();
extensionMain = extensionMain.replace(
'../server/dist/serverMain.js',
'./packages/server/dist/serverMain.js'
);
fs.writeFileSync(
path.join(root, `dist/packages/extension/dist/extensionMain.js`),
extensionMain
);
let extensionMain = fs
.readFileSync(
path.join(root, `dist/packages/extension/dist/extensionMain.js`)
)
.toString();
extensionMain = extensionMain.replace(
'../server/dist/serverMain.js',
'./packages/server/dist/serverMain.js'
);
fs.writeFileSync(
path.join(root, `dist/packages/extension/dist/extensionMain.js`),
extensionMain
);
这段代码读取dist/packages/extension/dist/extensionMain.js
文件的内容,并将其赋值给变量extensionMain
。然后,它使用replace
方法将文件内容中的../server/dist/serverMain.js
替换为./packages/server/dist/serverMain.js
。最后,将修改后的内容写回到dist/packages/extension/dist/extensionMain.js
文件中。
这段代码的目的是构建和准备项目的发布版本,将需要的文件和目录复制到dist
目录,并对package.json
和扩展的主文件进行一些修改和调整。
server项目依赖service项目
server项目中调用service项目的方法,可以看见只写了一个service
,而不是相对路径指向service项目主文件
import { doAutoRenameTag } from 'service';
import { doAutoRenameTag } from 'service';
再看下service项目的package.json,可以看到service
就是service项目的包名
{
"name": "service",
...
}
{
"name": "service",
...
}
这是通过references实现的
这段代码片段是一个server项目中的tsconfig.json
文件的一部分,其中包含了一个references
数组。
在TypeScript中,references
用于指定项目之间的依赖关系。在这个例子中,references
数组包含了一个对象,该对象具有一个path
属性,指定了另一个项目的tsconfig.json
文件的路径。
"references": [
{
"path": "../service/tsconfig.json"
}
]
"references": [
{
"path": "../service/tsconfig.json"
}
]
这段代码的意思是,当前的server项目依赖于位于相对路径../service/tsconfig.json
的service项目。通过将这个tsconfig.json
文件添加到当前项目的references
数组中,TypeScript编译器将会考虑到这个依赖关系,并在构建或编译项目时处理和包含被引用的项目。
这种依赖关系可以确保在构建或编译项目时,被引用的项目先进行处理,以便在当前项目中正确地使用其定义、类型和其他资源。这在多项目的代码库中很常见,其中一个项目依赖于另一个项目的代码或类型定义。
service项目
service项目中其实只做了一件事,实现了一个doAutoRenameTag
函数
export { doAutoRenameTag } from './doAutoRenameTag';
export { doAutoRenameTag } from './doAutoRenameTag';
doAutoRenameTag
函数源码解析
import { getMatchingTagPairs } from './getMatchingTagPairs';
import {
createScannerFast,
ScannerStateFast
} from './htmlScanner/htmlScannerFast';
import { isSelfClosingTagInLanguage } from './isSelfClosingTag';
import { getNextClosingTagName } from './util/getNextClosingTagName';
import { getPreviousOpeningTagName } from './util/getPreviousOpenTagName';
export const doAutoRenameTag: (
text: string,
offset: number,
newWord: string,
oldWord: string,
languageId: string
) =>
| {
startOffset: number;
endOffset: number;
tagName: string;
}
| undefined = (text, offset, newWord, oldWord, languageId) => {
const matchingTagPairs = getMatchingTagPairs(languageId);
const isSelfClosingTag = isSelfClosingTagInLanguage(languageId);
const isReact =
languageId === 'javascript' ||
languageId === 'typescript' ||
languageId === 'javascriptreact' ||
languageId === 'typescriptreact';
const scanner = createScannerFast({
input: text,
initialOffset: 0,
initialState: ScannerStateFast.WithinContent,
matchingTagPairs
});
if (newWord.startsWith('</')) {
scanner.stream.goTo(offset);
const tagName = newWord.slice(2);
const oldTagName = oldWord.slice(2);
if (oldTagName.startsWith('script') || oldTagName.startsWith('style')) {
const tag = `<${oldTagName}`;
let i = scanner.stream.position;
let found = false;
while (i--) {
if (text.slice(i).startsWith(tag)) {
found = true;
break;
}
}
if (!found) {
return undefined;
}
return {
startOffset: i + 1,
endOffset: i + 1 + oldTagName.length,
tagName
};
}
const parent = getPreviousOpeningTagName(
scanner,
scanner.stream.position,
isSelfClosingTag,
isReact
);
if (!parent) {
return undefined;
}
if (parent.tagName === tagName) {
return undefined;
}
if (parent.tagName !== oldTagName) {
return undefined;
}
if (!parent.seenRightAngleBracket) {
return undefined;
}
const startOffset = parent.offset;
const endOffset = parent.offset + parent.tagName.length;
return {
startOffset,
endOffset,
tagName
};
} else {
scanner.stream.goTo(offset + 1);
const tagName = newWord.slice(1);
const oldTagName = oldWord.slice(1);
if (oldTagName.startsWith('script') || oldTagName.startsWith('style')) {
const hasAdvanced = scanner.stream.advanceUntilEitherChar(
['>'],
true,
isReact
);
if (!hasAdvanced) {
return undefined;
}
const match = text
.slice(scanner.stream.position)
.match(new RegExp(`</${oldTagName}`));
if (!match) {
return undefined;
}
const index = match.index as number;
return {
startOffset: scanner.stream.position + index + 2,
endOffset: scanner.stream.position + index + 2 + oldTagName.length,
tagName
};
}
const hasAdvanced = scanner.stream.advanceUntilEitherChar(
['<', '>'],
true,
isReact
);
// if start tag is not closed, return undefined
if (scanner.stream.peekRight(0) === '<') {
return undefined;
}
if (!hasAdvanced) {
return undefined;
}
if (scanner.stream.peekLeft(1) === '/') {
return undefined;
}
const possibleEndOfStartTag = scanner.stream.position;
// check if we might be at an end tag
while (scanner.stream.peekLeft(1).match(/[a-zA-Z\-\:]/)) {
scanner.stream.goBack(1);
if (scanner.stream.peekLeft(1) === '/') {
return undefined;
}
}
scanner.stream.goTo(possibleEndOfStartTag);
scanner.stream.advance(1);
const nextClosingTag = getNextClosingTagName(
scanner,
scanner.stream.position,
isSelfClosingTag,
isReact
);
if (!nextClosingTag) {
return undefined;
}
if (nextClosingTag.tagName === tagName) {
return undefined;
}
if (nextClosingTag.tagName !== oldTagName) {
return undefined;
}
const previousOpenTag = getPreviousOpeningTagName(
scanner,
offset,
isSelfClosingTag,
isReact
);
if (
previousOpenTag &&
previousOpenTag.tagName === oldTagName &&
previousOpenTag.indent === nextClosingTag.indent
) {
return undefined;
}
const startOffset = nextClosingTag.offset;
const endOffset = nextClosingTag.offset + nextClosingTag.tagName.length;
return {
startOffset,
endOffset,
tagName
};
}
};
// const testCase = {
// text: '<div>\n <di>\n <div></div>\n</div>',
// offset: 8,
// newWord: '<di',
// oldWord: '<div'
// };
// doAutoRenameTag(
// testCase.text,
// testCase.offset,
// testCase.newWord,
// testCase.oldWord,
// 'html'
// ); //?
// doAutoRenameTag(
// `<div>
// <div>
// <div></div>
// </div>`,
// 9,
// '<span',
// '<div',
// 'html'
// ); //?
import { getMatchingTagPairs } from './getMatchingTagPairs';
import {
createScannerFast,
ScannerStateFast
} from './htmlScanner/htmlScannerFast';
import { isSelfClosingTagInLanguage } from './isSelfClosingTag';
import { getNextClosingTagName } from './util/getNextClosingTagName';
import { getPreviousOpeningTagName } from './util/getPreviousOpenTagName';
export const doAutoRenameTag: (
text: string,
offset: number,
newWord: string,
oldWord: string,
languageId: string
) =>
| {
startOffset: number;
endOffset: number;
tagName: string;
}
| undefined = (text, offset, newWord, oldWord, languageId) => {
const matchingTagPairs = getMatchingTagPairs(languageId);
const isSelfClosingTag = isSelfClosingTagInLanguage(languageId);
const isReact =
languageId === 'javascript' ||
languageId === 'typescript' ||
languageId === 'javascriptreact' ||
languageId === 'typescriptreact';
const scanner = createScannerFast({
input: text,
initialOffset: 0,
initialState: ScannerStateFast.WithinContent,
matchingTagPairs
});
if (newWord.startsWith('</')) {
scanner.stream.goTo(offset);
const tagName = newWord.slice(2);
const oldTagName = oldWord.slice(2);
if (oldTagName.startsWith('script') || oldTagName.startsWith('style')) {
const tag = `<${oldTagName}`;
let i = scanner.stream.position;
let found = false;
while (i--) {
if (text.slice(i).startsWith(tag)) {
found = true;
break;
}
}
if (!found) {
return undefined;
}
return {
startOffset: i + 1,
endOffset: i + 1 + oldTagName.length,
tagName
};
}
const parent = getPreviousOpeningTagName(
scanner,
scanner.stream.position,
isSelfClosingTag,
isReact
);
if (!parent) {
return undefined;
}
if (parent.tagName === tagName) {
return undefined;
}
if (parent.tagName !== oldTagName) {
return undefined;
}
if (!parent.seenRightAngleBracket) {
return undefined;
}
const startOffset = parent.offset;
const endOffset = parent.offset + parent.tagName.length;
return {
startOffset,
endOffset,
tagName
};
} else {
scanner.stream.goTo(offset + 1);
const tagName = newWord.slice(1);
const oldTagName = oldWord.slice(1);
if (oldTagName.startsWith('script') || oldTagName.startsWith('style')) {
const hasAdvanced = scanner.stream.advanceUntilEitherChar(
['>'],
true,
isReact
);
if (!hasAdvanced) {
return undefined;
}
const match = text
.slice(scanner.stream.position)
.match(new RegExp(`</${oldTagName}`));
if (!match) {
return undefined;
}
const index = match.index as number;
return {
startOffset: scanner.stream.position + index + 2,
endOffset: scanner.stream.position + index + 2 + oldTagName.length,
tagName
};
}
const hasAdvanced = scanner.stream.advanceUntilEitherChar(
['<', '>'],
true,
isReact
);
// if start tag is not closed, return undefined
if (scanner.stream.peekRight(0) === '<') {
return undefined;
}
if (!hasAdvanced) {
return undefined;
}
if (scanner.stream.peekLeft(1) === '/') {
return undefined;
}
const possibleEndOfStartTag = scanner.stream.position;
// check if we might be at an end tag
while (scanner.stream.peekLeft(1).match(/[a-zA-Z\-\:]/)) {
scanner.stream.goBack(1);
if (scanner.stream.peekLeft(1) === '/') {
return undefined;
}
}
scanner.stream.goTo(possibleEndOfStartTag);
scanner.stream.advance(1);
const nextClosingTag = getNextClosingTagName(
scanner,
scanner.stream.position,
isSelfClosingTag,
isReact
);
if (!nextClosingTag) {
return undefined;
}
if (nextClosingTag.tagName === tagName) {
return undefined;
}
if (nextClosingTag.tagName !== oldTagName) {
return undefined;
}
const previousOpenTag = getPreviousOpeningTagName(
scanner,
offset,
isSelfClosingTag,
isReact
);
if (
previousOpenTag &&
previousOpenTag.tagName === oldTagName &&
previousOpenTag.indent === nextClosingTag.indent
) {
return undefined;
}
const startOffset = nextClosingTag.offset;
const endOffset = nextClosingTag.offset + nextClosingTag.tagName.length;
return {
startOffset,
endOffset,
tagName
};
}
};
// const testCase = {
// text: '<div>\n <di>\n <div></div>\n</div>',
// offset: 8,
// newWord: '<di',
// oldWord: '<div'
// };
// doAutoRenameTag(
// testCase.text,
// testCase.offset,
// testCase.newWord,
// testCase.oldWord,
// 'html'
// ); //?
// doAutoRenameTag(
// `<div>
// <div>
// <div></div>
// </div>`,
// 9,
// '<span',
// '<div',
// 'html'
// ); //?
doAutoRenameTag
函数内部一上来先调用了这两个函数
const matchingTagPairs = getMatchingTagPairs(languageId);
const isSelfClosingTag = isSelfClosingTagInLanguage(languageId);
const matchingTagPairs = getMatchingTagPairs(languageId);
const isSelfClosingTag = isSelfClosingTagInLanguage(languageId);
直接看下这两个函数完整代码
const matchingTagPairs: { [languageId: string]: [string, string][] } = {
css: [
['/*', '*/'],
['"', '"'],
["'", "'"]
],
ejs: [['<%', '%>']],
ruby: [
['<%=', '%>'],
['"', '"'],
["'", "'"]
],
html: [
['<!--', '-->'],
['"', '"'],
["'", "'"],
['<style', '</style>'],
['<script', '</script'],
['<%=', '%>'] // support for html-webpack-plugin
],
markdown: [
['<!--', '-->'],
['"', '"'],
["'", "'"],
['```', '```'],
['<?', '?>']
],
marko: [
['<!--', '-->'],
['${', '}'],
['<html-comment>', '</html-comment>']
],
nunjucks: [
['{%', '%}'],
['{{', '}}'],
['{#', '#}']
],
plaintext: [
['<!--', '-->'],
['"', '"'],
["'", "'"]
],
php: [
['<!--', '-->'],
['<?', '?>'],
['"', '"'],
["'", "'"]
],
javascript: [
['<!--', '-->'],
['{/*', '*/}'],
["'", "'"],
['"', '"'],
['`', '`']
],
javascriptreact: [
['{/*', '*/}'],
["'", "'"],
['"', '"'],
['`', '`']
],
mustache: [['{{', '}}']],
razor: [
['<!--', '-->'],
['@{', '}'],
['"', '"'],
["'", "'"]
],
svelte: [
['<!--', '-->'],
['"', '"'],
["'", "'"]
],
svg: [
['<!--', '-->'],
['"', '"'],
["'", "'"]
],
typescript: [
['<!--', '-->'],
['{/*', '*/}'],
["'", "'"],
['"', '"'],
['`', '`']
],
typescriptreact: [
['{/*', '*/}'],
["'", "'"],
['"', '"'],
['`', '`']
],
twig: [
['<!--', '-->'],
['"', '"'],
["'", "'"],
['{{', '}}'],
['{%', '%}']
],
volt: [
['{#', '#}'],
['{%', '%}'],
['{{', '}}']
],
vue: [
['<!--', '-->'],
['"', '"'],
["'", "'"],
['{{', '}}']
],
xml: [
['<!--', '-->'],
['"', '"'],
["'", "'"],
['<?', '?>']
]
};
export const getMatchingTagPairs: (
languageId: string
) => [string, string][] = languageId =>
matchingTagPairs[languageId] || matchingTagPairs['html'];
const matchingTagPairs: { [languageId: string]: [string, string][] } = {
css: [
['/*', '*/'],
['"', '"'],
["'", "'"]
],
ejs: [['<%', '%>']],
ruby: [
['<%=', '%>'],
['"', '"'],
["'", "'"]
],
html: [
['<!--', '-->'],
['"', '"'],
["'", "'"],
['<style', '</style>'],
['<script', '</script'],
['<%=', '%>'] // support for html-webpack-plugin
],
markdown: [
['<!--', '-->'],
['"', '"'],
["'", "'"],
['```', '```'],
['<?', '?>']
],
marko: [
['<!--', '-->'],
['${', '}'],
['<html-comment>', '</html-comment>']
],
nunjucks: [
['{%', '%}'],
['{{', '}}'],
['{#', '#}']
],
plaintext: [
['<!--', '-->'],
['"', '"'],
["'", "'"]
],
php: [
['<!--', '-->'],
['<?', '?>'],
['"', '"'],
["'", "'"]
],
javascript: [
['<!--', '-->'],
['{/*', '*/}'],
["'", "'"],
['"', '"'],
['`', '`']
],
javascriptreact: [
['{/*', '*/}'],
["'", "'"],
['"', '"'],
['`', '`']
],
mustache: [['{{', '}}']],
razor: [
['<!--', '-->'],
['@{', '}'],
['"', '"'],
["'", "'"]
],
svelte: [
['<!--', '-->'],
['"', '"'],
["'", "'"]
],
svg: [
['<!--', '-->'],
['"', '"'],
["'", "'"]
],
typescript: [
['<!--', '-->'],
['{/*', '*/}'],
["'", "'"],
['"', '"'],
['`', '`']
],
typescriptreact: [
['{/*', '*/}'],
["'", "'"],
['"', '"'],
['`', '`']
],
twig: [
['<!--', '-->'],
['"', '"'],
["'", "'"],
['{{', '}}'],
['{%', '%}']
],
volt: [
['{#', '#}'],
['{%', '%}'],
['{{', '}}']
],
vue: [
['<!--', '-->'],
['"', '"'],
["'", "'"],
['{{', '}}']
],
xml: [
['<!--', '-->'],
['"', '"'],
["'", "'"],
['<?', '?>']
]
};
export const getMatchingTagPairs: (
languageId: string
) => [string, string][] = languageId =>
matchingTagPairs[languageId] || matchingTagPairs['html'];
该函数用于获取相应编程语言中匹配的标签对数组,找不到相应编程语言,就默认取html的
const tagsThatAreSelfClosingInHtml: Set<string> = new Set([
'area',
'base',
'br',
'col',
'command',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'menuitem',
'meta',
'param',
'source',
'track',
'wbr'
]);
const EMPTY_SET: Set<string> = new Set();
const tagsThatAreSelfClosing: { [languageId: string]: Set<string> } = {
css: tagsThatAreSelfClosingInHtml,
ejs: tagsThatAreSelfClosingInHtml,
ruby: tagsThatAreSelfClosingInHtml,
html: tagsThatAreSelfClosingInHtml,
markdown: tagsThatAreSelfClosingInHtml,
marko: tagsThatAreSelfClosingInHtml,
nunjucks: tagsThatAreSelfClosingInHtml,
plaintext: tagsThatAreSelfClosingInHtml,
php: tagsThatAreSelfClosingInHtml,
javascript: tagsThatAreSelfClosingInHtml,
javascriptreact: EMPTY_SET,
mustache: tagsThatAreSelfClosingInHtml,
razor: tagsThatAreSelfClosingInHtml,
svelte: tagsThatAreSelfClosingInHtml,
svg: EMPTY_SET,
typescript: tagsThatAreSelfClosingInHtml,
typescriptreact: EMPTY_SET,
twig: tagsThatAreSelfClosingInHtml,
volt: tagsThatAreSelfClosingInHtml,
vue: EMPTY_SET,
xml: EMPTY_SET
};
export const isSelfClosingTagInLanguage: (
languageId: string
) => (tagName: string) => boolean = languageId => tagName =>
(tagsThatAreSelfClosing[languageId] || tagsThatAreSelfClosing['html']).has(
tagName
);
const tagsThatAreSelfClosingInHtml: Set<string> = new Set([
'area',
'base',
'br',
'col',
'command',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'menuitem',
'meta',
'param',
'source',
'track',
'wbr'
]);
const EMPTY_SET: Set<string> = new Set();
const tagsThatAreSelfClosing: { [languageId: string]: Set<string> } = {
css: tagsThatAreSelfClosingInHtml,
ejs: tagsThatAreSelfClosingInHtml,
ruby: tagsThatAreSelfClosingInHtml,
html: tagsThatAreSelfClosingInHtml,
markdown: tagsThatAreSelfClosingInHtml,
marko: tagsThatAreSelfClosingInHtml,
nunjucks: tagsThatAreSelfClosingInHtml,
plaintext: tagsThatAreSelfClosingInHtml,
php: tagsThatAreSelfClosingInHtml,
javascript: tagsThatAreSelfClosingInHtml,
javascriptreact: EMPTY_SET,
mustache: tagsThatAreSelfClosingInHtml,
razor: tagsThatAreSelfClosingInHtml,
svelte: tagsThatAreSelfClosingInHtml,
svg: EMPTY_SET,
typescript: tagsThatAreSelfClosingInHtml,
typescriptreact: EMPTY_SET,
twig: tagsThatAreSelfClosingInHtml,
volt: tagsThatAreSelfClosingInHtml,
vue: EMPTY_SET,
xml: EMPTY_SET
};
export const isSelfClosingTagInLanguage: (
languageId: string
) => (tagName: string) => boolean = languageId => tagName =>
(tagsThatAreSelfClosing[languageId] || tagsThatAreSelfClosing['html']).has(
tagName
);
isSelfClosingTagInLanguage
函数用于根据语言判断是否是自闭合标签(包含了所有HTML中的自闭合标签)
对于vue
,将其对应的自闭合标签集合设置为EMPTY_SET
,即一个空的Set,没有包含任何标签。这是因为在Vue.js中,标签不像HTML那样有明确的自闭合和非自闭合的区分。Vue会根据数据和指令动态地生成相应的DOM结构。因此,对于Vue来说,没有固定的一组自闭合标签。代码中将EMPTY_SET
用于javascriptreact
和typescriptreact
编程语言的自闭合标签集合,这也是因为在React和TypeScript中,同样也没有固定的一组自闭合标签,而是通过组件和JSX的语法来生成DOM元素。
而在XML和SVG中,标签的自闭合性与HTML不同。在XML和SVG中,标签是否自闭合取决于其是否具有子元素。对于没有子元素的标签,可以使用自闭合形式,而对于有子元素的标签,则需要使用起始标签和结束标签来包裹子元素。
const isReact =
languageId === 'javascript' ||
languageId === 'typescript' ||
languageId === 'javascriptreact' ||
languageId === 'typescriptreact';
const isReact =
languageId === 'javascript' ||
languageId === 'typescript' ||
languageId === 'javascriptreact' ||
languageId === 'typescriptreact';
这四种语言情况都有可能存在react语法(jsx和tsx)