源码阅读
插件激活函数activate
路径packages/extension/src/extensionMain.ts
activate
函数是vscode插件的激活函数,即主函数,必不可少,插件安装激活时进行触发,阅读vscode插件源码一般先看这个函数
ts
export const activate: (
context: vscode.ExtensionContext
) => Promise<void> = async context => {
// 获取插件的`activationOnLanguage`属性(插件激活语言)的用户配置值,但是这里并没有赋值给任何变量,应该是作者写错了
vscode.workspace
.getConfiguration('auto-rename-tag')
.get('activationOnLanguage');
/**
* 是否需要开启插件
* @param document 接收一个编辑器对象
* @returns 返回一个`Boolean`值,表示插件是否需要开启
*/
const isEnabled = (document: vscode.TextDocument | undefined) => {
// 如果没有编辑器对象,不开启插件
if (!document) {
return false;
}
// 获取编辑器对象的语言
const languageId = document.languageId;
// 如果是`html`或者`handlebars`语言,就再获取vscode的`editor`配置中的`renameOnType`和`linkedEditing`是否开启,如果开启了这两个属性,就不开启插件了。这两个属性是vscode内置的配置属性,可以实现标签重命名效果,新版本vscode中`renameOnType`属性废弃,被`linkedEditing`取代了,但这两个属性目前还是没有插件全面,比如`linkedEditing`对`jsx`的支持还有问题
if (languageId === 'html' || languageId === 'handlebars') {
const editorSettings = vscode.workspace.getConfiguration(
'editor',
document
);
if (
editorSettings.get('renameOnType') ||
editorSettings.get('linkedEditing')
) {
return false;
}
}
// 获取插件的当前编辑器对象的配置
const config = vscode.workspace.getConfiguration(
'auto-rename-tag',
document.uri
);
// 激活语言配置,如果激活语言配置是`*`即`任何`,或者编辑器对象的语言包含在激活语言配置数组中,则开启插件
const languages = config.get<string[]>('activationOnLanguage', ['*']);
return languages.includes('*') || languages.includes(languageId);
};
// `context.subscriptions.push`添加了一个可被清理的对象到插件的订阅中,`vscode.workspace.onDidChangeConfiguration`事件,监听配置更改,配置有所更改时触发,event.affectsConfiguration`也很好理解,是否影响了指定名称插件的配置,这里写了注释,表示清除`vscode.workspace.getConfiguration`的缓存,但是没看出来做了啥,这段代码是多写的嘛?
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(event => {
// purges cache for `vscode.workspace.getConfiguration`
if (!event.affectsConfiguration('auto-rename-tag')) {
return;
}
})
);
// 创建与服务器的连
const clientOptions: LanguageClientOptions = {
documentSelector: [
{
scheme: '*'
}
]
};
const languageClientProxy = await createLanguageClientProxy(
context,
'auto-rename-tag',
'Auto Rename Tag',
clientOptions
);
// `context.subscriptions.push`添加了一个可被清理的对象到插件的订阅中。这个对象包含一个`dispose`方法,用于执行资源清理。当插件被禁用或销毁时,VSCode会自动调用这些`dispose`方法,从而进行资源的释放。总而言之,`dispose`函数是用于释放资源和执行清理操作的一种规范方法,确保在插件不再需要某些资源时,这些资源能够被正确释放。
let activeTextEditor: vscode.TextEditor | undefined =
vscode.window.activeTextEditor;
let changeListener: Disposable | undefined;
context.subscriptions.push({
dispose() {
if (changeListener) {
changeListener.dispose();
changeListener = undefined;
}
}
});
const setupChangeListener = () => {
if (changeListener) {
return;
}
// 创建一个`vscode.workspace.onDidChangeTextDocument`事件,编辑器内容变化会触发该事件
changeListener = vscode.workspace.onDidChangeTextDocument(async event => {
if (event.document !== activeTextEditor?.document) {
return;
}
if (!isEnabled(event.document)) {
changeListener?.dispose();
changeListener = undefined;
return;
}
// event.contentChanges是编辑器的修改内容
if (event.contentChanges.length === 0) {
return;
}
// 获取编辑器内的内容
const currentText = event.document.getText();
const tags: Tag[] = [];
let totalInserted = 0;
// slice浅拷贝,根据rangeOffset排好序
const sortedChanges = event.contentChanges
.slice()
.sort((a, b) => a.rangeOffset - b.rangeOffset);
const keys = Object.keys(wordsAtOffsets);
// {
// oldWord: "<title",
// word: "<1",
// offset: 389,
// previousOffset: 389,
// }
for (const change of sortedChanges) {
for (const key of keys) {
const parsedKey = parseInt(key, 10);
// 修改
if (
change.rangeOffset <= parsedKey &&
parsedKey <= change.rangeOffset + change.rangeLength
) {
delete wordsAtOffsets[key];
}
}
assertDefined(previousText);
debugger;
// 获取更改开始位置所在行的信息
const line = event.document.lineAt(change.range.start.line);
// 获取行开始位置索引
const lineStart = event.document.offsetAt(line.range.start);
// change.rangeOffset 被替换的区域的偏移量
const lineChangeOffset = change.rangeOffset - lineStart;
// 测试用例: '<1 id="three-container"></div>'
// "<"
const lineLeft = line.text.slice(0, lineChangeOffset + totalInserted);
// '1 id="three-container"></div>'
const lineRight = line.text.slice(lineChangeOffset + totalInserted);
// "<"
const lineTagNameLeft = lineLeft.match(tagNameReLeft);
// "1"
const lineTagNameRight = lineRight.match(tagNameRERight);
// 'div id="three-container"></div>\n\n<div id="instructions">\n\tclick and drag to control the animation\n</div>\n<!-- partial -->\n <script src='//cdnjs.cloudflare.com/ajax/libs/three.js/r75/three.min.js'></script>\n<script src='//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js'></script>\n<script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/175711/bas.js'></script>\n<script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/175711/OrbitControls-2.js'></script><script src="./script.js"></script>\n\n</body>\n</html>\n'
const previousTextRight = previousText.slice(change.rangeOffset);
// div
const previousTagNameRight = previousTextRight.match(tagNameRERight);
let newWord: string;
let oldWord: string;
if (!lineTagNameLeft) {
totalInserted += change.text.length - change.rangeLength;
continue;
}
// "<"
newWord = lineTagNameLeft[0];
oldWord = lineTagNameLeft[0];
if (lineTagNameRight) {
// "<1"
newWord += lineTagNameRight[0];
}
if (previousTagNameRight) {
// "<title"
oldWord += previousTagNameRight[0];
}
const offset =
change.rangeOffset - lineTagNameLeft[0].length + totalInserted;
tags.push({
oldWord,
word: newWord,
offset,
previousOffset: offset - totalInserted
});
totalInserted += change.text.length - change.rangeLength;
}
updateWordsAtOffset(tags);
if (tags.length === 0) {
previousText = currentText;
return;
}
assertDefined(vscode.window.activeTextEditor);
previousText = currentText;
doAutoCompletionElementRenameTag(languageClientProxy, tags);
});
};
setPreviousText(vscode.window.activeTextEditor);
setupChangeListener();
context.subscriptions.push(
// `vscode.window.onDidChangeActiveTextEditor`更改聚焦的编辑器触发,触发后重新设置监听器
vscode.window.onDidChangeActiveTextEditor(textEditor => {
activeTextEditor = textEditor;
const doument = activeTextEditor?.document;
if (!isEnabled(doument)) {
if (changeListener) {
changeListener.dispose();
changeListener = undefined;
}
return;
}
setPreviousText(textEditor);
setupChangeListener();
})
);
};
export const activate: (
context: vscode.ExtensionContext
) => Promise<void> = async context => {
// 获取插件的`activationOnLanguage`属性(插件激活语言)的用户配置值,但是这里并没有赋值给任何变量,应该是作者写错了
vscode.workspace
.getConfiguration('auto-rename-tag')
.get('activationOnLanguage');
/**
* 是否需要开启插件
* @param document 接收一个编辑器对象
* @returns 返回一个`Boolean`值,表示插件是否需要开启
*/
const isEnabled = (document: vscode.TextDocument | undefined) => {
// 如果没有编辑器对象,不开启插件
if (!document) {
return false;
}
// 获取编辑器对象的语言
const languageId = document.languageId;
// 如果是`html`或者`handlebars`语言,就再获取vscode的`editor`配置中的`renameOnType`和`linkedEditing`是否开启,如果开启了这两个属性,就不开启插件了。这两个属性是vscode内置的配置属性,可以实现标签重命名效果,新版本vscode中`renameOnType`属性废弃,被`linkedEditing`取代了,但这两个属性目前还是没有插件全面,比如`linkedEditing`对`jsx`的支持还有问题
if (languageId === 'html' || languageId === 'handlebars') {
const editorSettings = vscode.workspace.getConfiguration(
'editor',
document
);
if (
editorSettings.get('renameOnType') ||
editorSettings.get('linkedEditing')
) {
return false;
}
}
// 获取插件的当前编辑器对象的配置
const config = vscode.workspace.getConfiguration(
'auto-rename-tag',
document.uri
);
// 激活语言配置,如果激活语言配置是`*`即`任何`,或者编辑器对象的语言包含在激活语言配置数组中,则开启插件
const languages = config.get<string[]>('activationOnLanguage', ['*']);
return languages.includes('*') || languages.includes(languageId);
};
// `context.subscriptions.push`添加了一个可被清理的对象到插件的订阅中,`vscode.workspace.onDidChangeConfiguration`事件,监听配置更改,配置有所更改时触发,event.affectsConfiguration`也很好理解,是否影响了指定名称插件的配置,这里写了注释,表示清除`vscode.workspace.getConfiguration`的缓存,但是没看出来做了啥,这段代码是多写的嘛?
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(event => {
// purges cache for `vscode.workspace.getConfiguration`
if (!event.affectsConfiguration('auto-rename-tag')) {
return;
}
})
);
// 创建与服务器的连
const clientOptions: LanguageClientOptions = {
documentSelector: [
{
scheme: '*'
}
]
};
const languageClientProxy = await createLanguageClientProxy(
context,
'auto-rename-tag',
'Auto Rename Tag',
clientOptions
);
// `context.subscriptions.push`添加了一个可被清理的对象到插件的订阅中。这个对象包含一个`dispose`方法,用于执行资源清理。当插件被禁用或销毁时,VSCode会自动调用这些`dispose`方法,从而进行资源的释放。总而言之,`dispose`函数是用于释放资源和执行清理操作的一种规范方法,确保在插件不再需要某些资源时,这些资源能够被正确释放。
let activeTextEditor: vscode.TextEditor | undefined =
vscode.window.activeTextEditor;
let changeListener: Disposable | undefined;
context.subscriptions.push({
dispose() {
if (changeListener) {
changeListener.dispose();
changeListener = undefined;
}
}
});
const setupChangeListener = () => {
if (changeListener) {
return;
}
// 创建一个`vscode.workspace.onDidChangeTextDocument`事件,编辑器内容变化会触发该事件
changeListener = vscode.workspace.onDidChangeTextDocument(async event => {
if (event.document !== activeTextEditor?.document) {
return;
}
if (!isEnabled(event.document)) {
changeListener?.dispose();
changeListener = undefined;
return;
}
// event.contentChanges是编辑器的修改内容
if (event.contentChanges.length === 0) {
return;
}
// 获取编辑器内的内容
const currentText = event.document.getText();
const tags: Tag[] = [];
let totalInserted = 0;
// slice浅拷贝,根据rangeOffset排好序
const sortedChanges = event.contentChanges
.slice()
.sort((a, b) => a.rangeOffset - b.rangeOffset);
const keys = Object.keys(wordsAtOffsets);
// {
// oldWord: "<title",
// word: "<1",
// offset: 389,
// previousOffset: 389,
// }
for (const change of sortedChanges) {
for (const key of keys) {
const parsedKey = parseInt(key, 10);
// 修改
if (
change.rangeOffset <= parsedKey &&
parsedKey <= change.rangeOffset + change.rangeLength
) {
delete wordsAtOffsets[key];
}
}
assertDefined(previousText);
debugger;
// 获取更改开始位置所在行的信息
const line = event.document.lineAt(change.range.start.line);
// 获取行开始位置索引
const lineStart = event.document.offsetAt(line.range.start);
// change.rangeOffset 被替换的区域的偏移量
const lineChangeOffset = change.rangeOffset - lineStart;
// 测试用例: '<1 id="three-container"></div>'
// "<"
const lineLeft = line.text.slice(0, lineChangeOffset + totalInserted);
// '1 id="three-container"></div>'
const lineRight = line.text.slice(lineChangeOffset + totalInserted);
// "<"
const lineTagNameLeft = lineLeft.match(tagNameReLeft);
// "1"
const lineTagNameRight = lineRight.match(tagNameRERight);
// 'div id="three-container"></div>\n\n<div id="instructions">\n\tclick and drag to control the animation\n</div>\n<!-- partial -->\n <script src='//cdnjs.cloudflare.com/ajax/libs/three.js/r75/three.min.js'></script>\n<script src='//cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js'></script>\n<script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/175711/bas.js'></script>\n<script src='https://s3-us-west-2.amazonaws.com/s.cdpn.io/175711/OrbitControls-2.js'></script><script src="./script.js"></script>\n\n</body>\n</html>\n'
const previousTextRight = previousText.slice(change.rangeOffset);
// div
const previousTagNameRight = previousTextRight.match(tagNameRERight);
let newWord: string;
let oldWord: string;
if (!lineTagNameLeft) {
totalInserted += change.text.length - change.rangeLength;
continue;
}
// "<"
newWord = lineTagNameLeft[0];
oldWord = lineTagNameLeft[0];
if (lineTagNameRight) {
// "<1"
newWord += lineTagNameRight[0];
}
if (previousTagNameRight) {
// "<title"
oldWord += previousTagNameRight[0];
}
const offset =
change.rangeOffset - lineTagNameLeft[0].length + totalInserted;
tags.push({
oldWord,
word: newWord,
offset,
previousOffset: offset - totalInserted
});
totalInserted += change.text.length - change.rangeLength;
}
updateWordsAtOffset(tags);
if (tags.length === 0) {
previousText = currentText;
return;
}
assertDefined(vscode.window.activeTextEditor);
previousText = currentText;
doAutoCompletionElementRenameTag(languageClientProxy, tags);
});
};
setPreviousText(vscode.window.activeTextEditor);
setupChangeListener();
context.subscriptions.push(
// `vscode.window.onDidChangeActiveTextEditor`更改聚焦的编辑器触发,触发后重新设置监听器
vscode.window.onDidChangeActiveTextEditor(textEditor => {
activeTextEditor = textEditor;
const doument = activeTextEditor?.document;
if (!isEnabled(doument)) {
if (changeListener) {
changeListener.dispose();
changeListener = undefined;
}
return;
}
setPreviousText(textEditor);
setupChangeListener();
})
);
};
setupChangeListener函数解析
对onDidChangeTextDocument
返回的结果进行了处理,处理成自己需要的格式
请求服务器来处理标签,返回处理结果
js
const results = await askServerForAutoCompletionsElementRenameTag(
languageClientProxy,
vscode.window.activeTextEditor.document,
tags
);
const results = await askServerForAutoCompletionsElementRenameTag(
languageClientProxy,
vscode.window.activeTextEditor.document,
tags
);
根据处理结果修改编辑器内容
vscode.window.activeTextEditor.edit
api用于修改编辑器内容
js
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 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
}
);
wordsAtOffsets有啥用
wordsAtOffsets
是一个对象,用于存储标签的信息。它以标签在文档中的偏移量(offset)作为键,记录了每个标签的旧名称(oldWord)和新名称(newWord)。在代码的执行过程中,wordsAtOffsets
用于跟踪标签的名称变化。
具体用途如下:
- 在
updateWordsAtOffset
函数中,当收到标签信息时,会更新wordsAtOffsets
对象。它会检查标签列表中的每个标签,并根据偏移量来更新或添加标签信息。这样做是为了在接下来的操作中,可以根据标签的偏移量快速找到对应的旧名称。 - 在
doAutoCompletionElementRenameTag
函数中,根据之前记录的标签信息和当前的文本内容,会计算需要自动重命名的标签,并将相关信息发送给语言服务器。这个过程中,wordsAtOffsets
可以帮助确定每个标签的旧名称。 - 在
applyResults
函数中,通过wordsAtOffsets
对象,可以将自动重命名的结果应用到活动文本编辑器中,将旧的标签名称替换为新的标签名称。
总之,wordsAtOffsets
是一个辅助数据结构,用于记录标签的名称信息,并在标签自动重命名的过程中帮助进行相关操作。
请求服务器自动完成标签重命名的结果
请求服务器自动完成标签重命名的结果
ts
const askServerForAutoCompletionsElementRenameTag: (
languageClientProxy: LanguageClientProxy,
document: vscode.TextDocument,
tags: Tag[]
) => Promise<Result[]> = async (languageClientProxy, document, tags) => {
const params: Params = {
textDocument:
languageClientProxy.code2ProtocolConverter.asVersionedTextDocumentIdentifier(
document
),
tags
};
return languageClientProxy.sendRequest(autoRenameTagRequestType, params);
};
const askServerForAutoCompletionsElementRenameTag: (
languageClientProxy: LanguageClientProxy,
document: vscode.TextDocument,
tags: Tag[]
) => Promise<Result[]> = async (languageClientProxy, document, tags) => {
const params: Params = {
textDocument:
languageClientProxy.code2ProtocolConverter.asVersionedTextDocumentIdentifier(
document
),
tags
};
return languageClientProxy.sendRequest(autoRenameTagRequestType, params);
};
doAutoCompletionElementRenameTag
函数中调用
ts
const results = await askServerForAutoCompletionsElementRenameTag(
languageClientProxy,
vscode.window.activeTextEditor.document,
tags
);
const results = await askServerForAutoCompletionsElementRenameTag(
languageClientProxy,
vscode.window.activeTextEditor.document,
tags
);
客户端languageClientProxy.sendRequest
发送请求,那么再看下服务端server/serverMain.ts
怎么接受请求
ts
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))
);
最后再来看下autoRenameTag
方法,流程上结束了,至于doAutoRenameTag
的具体实现,就不在这里介绍了
ts
// 这里的ts类型是( documents: TextDocuments<TextDocument> ) => (params: Params) => Promise<Result[]>
// 表示该函数接受一个 documents 参数,然后返回一个函数,返回的函数接受一个 params 参数,返回一个 Promise,解析为 Result[] 数组。
export const autoRenameTag: (
documents: TextDocuments<TextDocument>
) => (params: Params) => Promise<Result[]> =
(documents) =>
// 解构赋值获取 params 参数中的 textDocument, tags
async ({ textDocument, tags }) => {
await new Promise((r) => setTimeout(r, 20));
const document = documents.get(textDocument.uri);
if (!document) {
return NULL_AUTO_RENAME_TAG_RESULT;
}
if (textDocument.version !== document.version) {
return NULL_AUTO_RENAME_TAG_RESULT;
}
const text = document.getText();
const results: Result[] = tags
.map((tag) => {
// service中定义的doAutoRenameTag方法
const result = doAutoRenameTag(
text,
tag.offset,
tag.word,
tag.oldWord,
document.languageId
);
if (!result) {
return result;
}
(result as any).originalOffset = tag.offset;
(result as any).originalWord = tag.word;
return result as Result;
})
.filter(Boolean) as Result[];
return results;
};
// 这里的ts类型是( documents: TextDocuments<TextDocument> ) => (params: Params) => Promise<Result[]>
// 表示该函数接受一个 documents 参数,然后返回一个函数,返回的函数接受一个 params 参数,返回一个 Promise,解析为 Result[] 数组。
export const autoRenameTag: (
documents: TextDocuments<TextDocument>
) => (params: Params) => Promise<Result[]> =
(documents) =>
// 解构赋值获取 params 参数中的 textDocument, tags
async ({ textDocument, tags }) => {
await new Promise((r) => setTimeout(r, 20));
const document = documents.get(textDocument.uri);
if (!document) {
return NULL_AUTO_RENAME_TAG_RESULT;
}
if (textDocument.version !== document.version) {
return NULL_AUTO_RENAME_TAG_RESULT;
}
const text = document.getText();
const results: Result[] = tags
.map((tag) => {
// service中定义的doAutoRenameTag方法
const result = doAutoRenameTag(
text,
tag.offset,
tag.word,
tag.oldWord,
document.languageId
);
if (!result) {
return result;
}
(result as any).originalOffset = tag.offset;
(result as any).originalWord = tag.word;
return result as Result;
})
.filter(Boolean) as Result[];
return results;
};
updateWordsAtOffset:更新标签信息的函数
ts
// 更新标签信息的函数
const updateWordsAtOffset: (tags: Tag[]) => void = tags => {
const keys = Object.keys(wordsAtOffsets);
if (keys.length > 0) {
if (keys.length !== tags.length) {
wordsAtOffsets = {};
}
for (const tag of tags) {
if (!wordsAtOffsets.hasOwnProperty(tag.previousOffset)) {
wordsAtOffsets = {};
break;
}
}
}
for (const tag of tags) {
wordsAtOffsets[tag.offset] = {
oldWord:
(wordsAtOffsets[tag.previousOffset] &&
wordsAtOffsets[tag.previousOffset].oldWord) ||
tag.oldWord,
newWord: tag.word
};
if (tag.previousOffset !== tag.offset) {
delete wordsAtOffsets[tag.previousOffset];
}
tag.oldWord = wordsAtOffsets[tag.offset].oldWord;
}
};
// 更新标签信息的函数
const updateWordsAtOffset: (tags: Tag[]) => void = tags => {
const keys = Object.keys(wordsAtOffsets);
if (keys.length > 0) {
if (keys.length !== tags.length) {
wordsAtOffsets = {};
}
for (const tag of tags) {
if (!wordsAtOffsets.hasOwnProperty(tag.previousOffset)) {
wordsAtOffsets = {};
break;
}
}
}
for (const tag of tags) {
wordsAtOffsets[tag.offset] = {
oldWord:
(wordsAtOffsets[tag.previousOffset] &&
wordsAtOffsets[tag.previousOffset].oldWord) ||
tag.oldWord,
newWord: tag.word
};
if (tag.previousOffset !== tag.offset) {
delete wordsAtOffsets[tag.previousOffset];
}
tag.oldWord = wordsAtOffsets[tag.offset].oldWord;
}
};