Skip to content
索引

源码阅读

插件激活函数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.editapi用于修改编辑器内容

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 用于跟踪标签的名称变化。

具体用途如下:

  1. updateWordsAtOffset 函数中,当收到标签信息时,会更新 wordsAtOffsets 对象。它会检查标签列表中的每个标签,并根据偏移量来更新或添加标签信息。这样做是为了在接下来的操作中,可以根据标签的偏移量快速找到对应的旧名称。
  2. doAutoCompletionElementRenameTag 函数中,根据之前记录的标签信息和当前的文本内容,会计算需要自动重命名的标签,并将相关信息发送给语言服务器。这个过程中,wordsAtOffsets 可以帮助确定每个标签的旧名称。
  3. 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;
  }
};

Released under the MIT License.