[
  {
    "path": ".editorconfig",
    "content": "# EditorConfig is awesome: https://EditorConfig.org\n\n# top-most EditorConfig file\nroot = true\n\n# Unix-style newlines with a newline ending every file\n[*]\nend_of_line = lf\ninsert_final_newline = true\n\n# Matches multiple files with brace expansion notation\n# Set default charset\n[*.{js,jsx,cjs,mjs,ts,tsx,cts,mts,css,scss,json}]\ncharset = utf-8\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".githooks/commit-msg",
    "content": "#!/bin/sh\nnpx --no -- commitlint --edit \"$1\"\nnpx tsx pre-commit.ts\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - main\n\n  pull_request:\n    branches:\n      - main\n\njobs:\n  typecheck:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout the repository\n        uses: actions/checkout@v4\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: lts/*\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Typecheck\n        run: npm run typecheck\n\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout the repository\n        uses: actions/checkout@v4\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: lts/*\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Lint\n        run: npm run lint\n\n  test:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        node-version: [20.x, 22.x, 24.x]\n        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/\n\n    steps:\n      - name: Checkout the repository\n        uses: actions/checkout@v4\n\n      - name: Set up Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: npm\n\n      - name: Install dependencies\n        run: npm ci --ignore-scripts\n\n      - name: Test types\n        run: npm run test-types\n\n      - name: Test\n        run: npm test\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/settings.json\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"cSpell.words\": [\"reqnode\"],\n  \"stylelint.validate\": [\"css\", \"scss\"]\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Snowflyt\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Typora Copilot\n\nEnglish | [简体中文](./README.zh-CN.md)\n\n![Copilot suggestion screenshot](./docs/screenshot.png)\n\n[GitHub Copilot](https://github.com/features/copilot) & [Copilot Chat](https://docs.github.com/copilot/using-github-copilot/copilot-chat) plugin for [Typora](https://typora.io/) on both Windows, macOS and Linux.\n\nThis plugin uses the [official GitHub Copilot LSP server](https://www.npmjs.com/package/@github/copilot-language-server) to provide suggestions in real-time right from your editor.\n\n## Compatibility\n\n> [!NOTE]\n>\n> Since Typora v1.10, all platforms require [Node.js](https://nodejs.org/en/download) ≥ 20 to use this plugin.\n>\n> (For those special users on Windows / Linux using Typora 1.9, no need for Node.js to be installed. :wink:)\n\n_\\*Note: `/` means not tested._\n\n| Typora Version | Windows 11 | Ubuntu 24.04 | macOS 15.x |\n| -------------- | ---------- | ------------ | ---------- |\n| 1.12.6         | /          | /            | ✓          |\n| 1.12.4         | ✓          | /            | /          |\n| 1.11.7         | ✓          | /            | ✓          |\n| 1.10.8         | ✓          | ✓            | ✓          |\n| 1.10.6         | ✓          | ✓            | ✓          |\n| 1.9.5          | ✓          | /            | /          |\n| 1.9.4          | /          | /            | ✓          |\n| 1.9.3          | /          | ✓            | /          |\n| 1.8.10         | ✓          | ✓            | ✓          |\n| 1.8.8          | /          | ✓            | /          |\n| 1.8.6          | ✓          | /            | /          |\n| 1.8.5          | ✓          | /            | ✓          |\n| 1.7.6          | ✓          | /            | /          |\n| 1.6.7          | ✓          | /            | /          |\n| 1.5.12         | ✓          | /            | /          |\n| 1.4.8          | ✓          | /            | /          |\n| 1.3.8          | ✓          | /            | /          |\n| 1.2.5          | ✓          | /            | /          |\n| 1.2.3          | ✓          | /            | /          |\n| 1.0.3          | ✓          | /            | /          |\n| 0.11.18-beta   | ✓          | /            | /          |\n\n## Prerequisites\n\n- Public network connection.\n- Active GitHub Copilot subscription.\n\n## Installation\n\n**Before installing using any method, make sure Typora is fully closed (especially on macOS: use <kbd>⌘</kbd>+<kbd>Q</kbd> to quit).**\n\n### Automated Installation (Recommended)\n\nTo install the plugin, you can just copy and paste the following command into your terminal:\n\n<details>\n  <summary><strong>Windows</strong></summary>\n\nRun the following command in PowerShell **as administrator**:\n\n```powershell\niwr -Uri \"https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/install.ps1\" | iex\n```\n\n</details>\n\n<details>\n  <summary><strong>macOS</strong></summary>\n\nRun the following command in your terminal:\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/install.sh | sudo bash\n```\n\n</details>\n\n<details>\n  <summary><strong>Linux</strong></summary>\n\nRun the following command in your terminal:\n\n```bash\nwget -O - https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/install.sh | sudo bash\n```\n\n</details>\n\n### Script Install\n\n<details>\n  <summary><strong>Windows</strong></summary>\n\nFor Windows users, first download the latest release from [the releases page](https://github.com/Snowflyt/typora-copilot/releases) and unzip it. Then locate to the folder where you unzipped the release and run the following command in PowerShell **as administrator**:\n\n```powershell\n.\\bin\\install_windows.ps1\n```\n\nIf the script fails to find Typora, you can specify the path to Typora manually:\n\n```powershell\n.\\bin\\install_windows.ps1 -Path \"C:\\Program Files\\Typora\\\" # Replace with your Typora path\n# Or use the alias\n# .\\bin\\install_windows.ps1 -p \"C:\\Program Files\\Typora\\\" # Replace with your Typora path\n```\n\n</details>\n\n<details>\n  <summary><strong>macOS</strong></summary>\n\nFor macOS users, first download the latest release from [the releases page](https://github.com/Snowflyt/typora-copilot/releases) and unzip it. Then locate to the folder where you unzipped the release and run the following command in terminal:\n\n```bash\nsudo bash ./bin/install_macos.sh\n```\n\nIf the script fails to find Typora, you can specify the path to Typora manually:\n\n```bash\nsudo bash ./bin/install_macos.sh --path \"/Applications/Typora.app/\" # Replace with your Typora path\n# Or use the alias\n# sudo bash ./bin/install_macos.sh -p \"/Applications/Typora.app/\" # Replace with your Typora path\n```\n\nYou’ll see a message logging the installation directory of the plugin. _Keep it in mind, you’ll need it when uninstalling the plugin._ After that, you can safely delete the release folder.\n\n</details>\n\n<details>\n  <summary><strong>Linux</strong></summary>\n\nFor Linux users, first download the latest release from [the releases page](https://github.com/Snowflyt/typora-copilot/releases) and unzip it. THen locate to the folder where you unzipped the release and run the following command in terminal:\n\n```bash\nsudo bash ./bin/install_linux.sh\n```\n\nIf the script fails to find Typora, you can specify the path to Typora manually:\n\n```bash\nsudo bash ./bin/install_linux.sh --path \"/usr/share/typora/\" # Replace with your Typora path\n# Or use the alias\n# sudo bash ./bin/install_linux.sh -p \"/usr/share/typora/\" # Replace with your Typora path\n```\n\nYou’ll see a message logging the installation directory of the plugin. _Keep it in mind, you’ll need it when uninstalling the plugin._ After that, you can safely delete the release folder.\n\n</details>\n\n### Manual Install\n\n<details>\n  <summary>Click to expand</summary>\n\n1. Download the latest release from [the releases page](https://github.com/Snowflyt/typora-copilot/releases) and unzip it.\n2. For Windows / Linux users, find `window.html` in your Typora installation folder, usually located at `<typora_root_path>/resources/`; For macOS users, find `index.html` in your Typora installation folder, usually located at `<typora_root_path>/Contents/Resources/TypeMark/`. `<typora_root_path>` is the path where Typora is installed, replace it with your real Typora installation path (note that the angle brackets `<` and `>` should also be removed). This folder is called Typora resource folder in the following steps.\n3. Create a folder named `copilot` in Typora resource folder.\n4. Copy the contents of the unzipped release into the `copilot` folder. **Ensure the final path is `copilot/index.js` (not `copilot/typora-copilot/index.js`). If you see the latter, move the files up one level so `index.js` sits directly under `copilot`.**\n5. For Windows / Linux users, open the previous `window.html` file you found in Typora resource folder with a text editor, and add `<script src=\"./copilot/index.js\" defer=\"defer\"></script>` right after something like `<script src=\"./appsrc/window/frame.js\" defer=\"defer\"></script>` or `<script src=\"./app/window/frame.js\" defer=\"defer\"></script>`; For macOS users, open the previous `index.html` file you found in Typora resource folder with a text editor, and add `<script src=\"./copilot/index.js\" defer></script>` right after something like `<script src=\"./appsrc/main.js\" aria-hidden=\"true\" defer></script>` or `<script src=\"./app/main.js\" aria-hidden=\"true\" defer></script>`.\n6. Restart Typora.\n7. For macOS users, if you see a warning dialog saying Typora may be damaged, Ctrl-click Typora and select “Open” to open Typora.\n\n</details>\n\n## Setup\n\nWhen finished installation, you’ll find an icon in the toolbar of Typora (i.e. the bottom-right corner of Typora). Click **the arrow button** next to the icon to open the panel of Copilot, and then click “Sign in to authenticate Copilot”.\n\n![Copilot icon](./docs/toolbar-icon.png)\n\nFollow the prompts to authenticate Copilot plugin:\n\n1. The User Code will be auto copied to your clipboard.\n2. Follow the instructions on the pop-up dialog to open the GitHub authentication page in your browser.\n3. Paste the User Code into the GitHub authentication page.\n4. Return to Typora and press OK on the dialog.\n5. If you see a “Signed in to Copilot” dialog _after a few seconds_, Copilot plugin should start working since then.\n\n## Copilot Chat\n\nClicking the Copilot icon in the toolbar will toggle the Copilot Chat panel. You can use it to chat with Copilot, and the current document and previous chat history will be sent to Copilot as context.\n\nMake sure you have signed in to Copilot before using the Copilot Chat panel. After signing in, restart Typora to make sure the Copilot Chat panel works properly.\n\nYou can:\n\n- Select, create, edit chat title, or delete a chat session from the dropdown list at the top of the panel.\n- Click the “Send” button or press <kbd>Enter</kbd> to send the message. (You can use <kbd>Shift</kbd> + <kbd>Enter</kbd> or <kbd>Ctrl</kbd> + <kbd>Enter</kbd> to insert a new line.)\n- Click the “Stop” button to stop the current request.\n- Select a prompt style from the dropdown list at the bottom of the panel.\n- Pick the model you want to use from the dropdown list at the bottom of the panel.\n\n## Uninstallation\n\n### Automated Uninstallation (Recommended)\n\nTo uninstall the plugin, you can just copy and paste the following command into your terminal:\n\n<details>\n  <summary><strong>Windows</strong></summary>\n\nRun the following command in PowerShell **as administrator**:\n\n```powershell\niwr -Uri \"https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/bin/uninstall_windows.ps1\" | iex\n```\n\n</details>\n\n<details>\n  <summary><strong>macOS</strong></summary>\n\nRun the following command in your terminal:\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/bin/uninstall_macos.sh | sudo bash\n```\n\n</details>\n\n<details>\n  <summary><strong>Linux</strong></summary>\n\nRun the following command in your terminal:\n\n```bash\nwget -O - https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/bin/uninstall_linux.sh | sudo bash\n```\n\n</details>\n\n### Script Uninstall\n\n<details>\n  <summary><strong>Windows</strong></summary>\n\nFor Windows users, locate to the installation directory of the plugin and run the following command in PowerShell **as administrator**.\n\n```powershell\n.\\bin\\uninstall_windows.ps1\n```\n\nYou can still specify the path to Typora manually by adding `-Path` or `-p`, just like the installation script.\n\n</details>\n\n<details>\n  <summary><strong>macOS</strong></summary>\n\nFor macOS users, locate to the installation directory of the plugin and run the following command in terminal.\n\n```bash\nsudo bash ./bin/uninstall_macos.sh\n```\n\nYou can still specify the path to Typora manually by adding `--path` or `-p`, just like the installation script.\n\n</details>\n\n<details>\n  <summary><strong>Linux</strong></summary>\n\nFor Linux users, locate to the installation directory of the plugin and run the following command in terminal.\n\n```bash\nsudo bash ./bin/uninstall_linux.sh\n```\n\nYou can still specify the path to Typora manually by adding `--path` or `-p`, just like the installation script.\n\n</details>\n\n### Manual Uninstall\n\n<details>\n  <summary>Click to expand</summary>\n\n1. For Windows / Linux users, find `window.html` in your Typora installation folder, usually located at `<typora_root_path>/resources/`; For macOS users, find `index.html` in your Typora installation folder, usually located at `<typora_root_path>/Contents/Resources/TypeMark/`. `<typora_root_path>` is the path where Typora is installed, replace it with your real Typora installation path (note that the angle brackets `<` and `>` should also be removed). This folder is called Typora resource folder in the following steps.\n2. Delete the `copilot` folder in Typora resource folder.\n3. For Windows / Linux users, open the previous `window.html` file you found in Typora resource folder with a text editor, and delete `<script src=\"./copilot/index.js\" defer=\"defer\"></script>`; For macOS users, open the previous `index.html` file you found in Typora resource folder with a text editor, and delete `<script src=\"./copilot/index.js\" defer></script>`.\n4. Restart Typora.\n</details>\n\n## Known Issues\n\n1. Sometimes accepting a suggestion may cause the editor rerendering (i.e. code blocks, math blocks, etc. will be rerendered). This is due to the limitation of Typora's API that I have to force the editor to rerender sometimes to accept a suggestion, and currently I can't find a more safe and efficient way to resolve this issue.\n\n## FAQs\n\n### How to temporarily disable Copilot?\n\nJust click the Copilot icon in the toolbar, and then click “Disable completions”. You can enable it again by clicking the icon and then clicking “Enable completions”.\n\n### Why use suggestion panel in live preview mode (normal mode) and completion text in source mode by default? Can I change that?\n\nThe usage of suggestion panel in live preview mode is intentional. Typora uses a complex mechanism to render the content in live preview mode, it is hard to make completion text work properly in live preview mode.\n\nBut it is possible to also use suggestion panel in source mode, you can click the `toolbar icon -> Settings` and toggle the `Use inline completion text in source mode` option.\n\nAn option called `Use inline completion text in preview code blocks` is also provided. If you enable this option, the completion text instead of suggestion panel will also be used in code blocks and math blocks in live preview mode. But it is currently not recommended to enable this option, as it is likely to corrupt the editor content or history.\n\n### Can I use keys other than `Tab` to accept suggestions?\n\nCurrently, no. It is technically possible, but currently I don't have enough time to implement it. Maybe I will implement it in the future.\n"
  },
  {
    "path": "README.zh-CN.md",
    "content": "# Typora Copilot\n\n[English](./README.md) | 简体中文\n\n![Copilot 建议截图](./docs/screenshot.zh-CN.png)\n\n[Typora](https://typora.io/) 的 [GitHub Copilot](https://github.com/features/copilot) & [Copilot Chat](https://docs.github.com/copilot/using-github-copilot/copilot-chat) 插件，支持 Windows、macOS 和 Linux。\n\n该插件使用 [GitHub Copilot 官方提供的 LSP 服务](https://www.npmjs.com/package/@github/copilot-language-server)，以在编辑器中实时提供建议。\n\n## 兼容性\n\n> [!NOTE]\n>\n> 自 Typora v1.10 起，所有平台都需要安装 [Node.js](https://nodejs.org/zh-cn/download) ≥ 20 才能使用本插件。\n>\n> （仅对于使用 Typora 1.9 的 Windows / Linux 用户，无需安装 Node.js。 :wink:）\n\n_\\*注：`/` 表示未经过测试。_\n\n| Typora Version | Windows 11 | Ubuntu 24.04 | macOS 15.x |\n| -------------- | ---------- | ------------ | ---------- |\n| 1.12.6         | /          | /            | ✓          |\n| 1.12.4         | ✓          | /            | /          |\n| 1.11.7         | ✓          | /            | ✓          |\n| 1.10.8         | ✓          | ✓            | ✓          |\n| 1.10.6         | ✓          | ✓            | ✓          |\n| 1.9.5          | ✓          | /            | /          |\n| 1.9.4          | /          | /            | ✓          |\n| 1.9.3          | /          | ✓            | /          |\n| 1.8.10         | ✓          | ✓            | ✓          |\n| 1.8.8          | /          | ✓            | /          |\n| 1.8.6          | ✓          | /            | /          |\n| 1.8.5          | ✓          | /            | ✓          |\n| 1.7.6          | ✓          | /            | /          |\n| 1.6.7          | ✓          | /            | /          |\n| 1.5.12         | ✓          | /            | /          |\n| 1.4.8          | ✓          | /            | /          |\n| 1.3.8          | ✓          | /            | /          |\n| 1.2.5          | ✓          | /            | /          |\n| 1.2.3          | ✓          | /            | /          |\n| 1.0.3          | ✓          | /            | /          |\n| 0.11.18-beta   | ✓          | /            | /          |\n\n## 前置条件\n\n- 公网连接（对于中国大陆用户，你还需要确保你的网络可以正常访问 GitHub Copilot 服务）。\n- 已激活的 GitHub Copilot 订阅。\n\n## 安装\n\n**开始任何安装方式之前，请先完全退出 Typora（尤其是 macOS 用户：请使用 <kbd>⌘</kbd>+<kbd>Q</kbd> 退出）。**\n\n### 一键安装（推荐）\n\n你可以直接将以下命令复制粘贴到你的终端中来安装插件：\n\n<details>\n  <summary><strong>Windows</strong></summary>\n\n以**管理员身份**在 PowerShell 中运行以下命令：\n\n```powershell\niwr -Uri \"https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/install.ps1\" | iex\n```\n\n</details>\n\n<details>\n  <summary><strong>macOS</strong></summary>\n\n在终端中运行以下命令：\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/install.sh | sudo bash\n```\n\n</details>\n\n<details>\n  <summary><strong>Linux</strong></summary>\n\n在终端中运行以下命令：\n\n```bash\nwget -O - https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/install.sh | sudo bash\n```\n\n</details>\n\n### 脚本安装\n\n<details>\n  <summary><strong>Windows</strong></summary>\n\n对于 Windows 用户，首先从[发布页面](https://github.com/Snowflyt/typora-copilot/releases)下载最新版本并解压。然后定位到你解压的文件夹并在 PowerShell 中**以管理员身份**运行以下命令：\n\n```powershell\n.\\bin\\install_windows.ps1\n```\n\n如果脚本无法找到 Typora，你可以手动指定 Typora 的路径：\n\n```powershell\n.\\bin\\install_windows.ps1 -Path \"C:\\Program Files\\Typora\\\" # 替换为你的 Typora 路径\n# 或使用别名\n# .\\bin\\install_windows.ps1 -p \"C:\\Program Files\\Typora\\\" # 替换为你的 Typora 路径\n```\n\n安装过程中，你会看到一条消息记录插件的安装目录。_记住它，在卸载插件时你会需要它。_ 安装完成后，你可以安全地删除刚才解压的文件夹。\n\n</details>\n\n<details>\n  <summary><strong>macOS</strong></summary>\n\n对于 macOS 用户，首先从[发布页面](https://github.com/Snowflyt/typora-copilot/releases)下载最新版本并解压。然后定位到你解压的文件夹并在终端中运行以下命令：\n\n```bash\nsudo bash ./bin/install_macos.sh\n```\n\n如果脚本无法找到 Typora，你可以手动指定 Typora 的路径：\n\n```bash\nsudo bash ./bin/install_macos.sh --path \"/Applications/Typora.app/\" # 替换为你的 Typora 路径\n# 或使用别名\n# sudo bash ./bin/install_macos.sh -p \"/Applications/Typora.app/\" # 替换为你的 Typora 路径\n```\n\n安装过程中，你会看到一条消息记录插件的安装目录。_记住它，在卸载插件时你会需要它。_ 安装完成后，你可以安全地删除刚才解压的文件夹。\n\n</details>\n\n<details>\n  <summary><strong>Linux</strong></summary>\n\n对于 Linux 用户，首先从[发布页面](https://github.com/Snowflyt/typora-copilot/releases)下载最新版本并解压。然后定位到你解压的文件夹并在终端中运行以下命令：\n\n```bash\nsudo bash ./bin/install_linux.sh\n```\n\n如果脚本无法找到 Typora，你可以手动指定 Typora 的路径：\n\n```bash\nsudo bash ./bin/install_linux.sh --path \"/usr/share/typora/\" # 替换为你的 Typora 路径\n# 或使用别名\n# sudo bash ./bin/install_linux.sh -p \"/usr/share/typora/\" # 替换为你的 Typora 路径\n```\n\n安装过程中，你会看到一条消息记录插件的安装目录。_记住它，在卸载插件时你会需要它。_ 安装完成后，你可以安全地删除刚才解压的文件夹。\n\n</details>\n\n### 手动安装\n\n<details>\n  <summary>点击展开</summary>\n\n1. 从[发布页面](https://github.com/Snowflyt/typora-copilot/releases)下载最新版本并解压。\n2. 找到 Typora 安装目录下的 `window.html` 文件，通常位于 `<typora_root_path>/resources/`；对于 macOS 用户，找到 Typora 安装目录下的 `index.html` 文件，通常位于 `<typora_root_path>/Contents/Resources/TypeMark/`。`<typora_root_path>` 是 Typora 的安装路径，替换为你的实际 Typora 安装路径（注意尖括号 `<` 和 `>` 也要删除）。这个文件夹在下面的步骤中被称为 Typora 资源文件夹。\n3. 在 Typora 资源文件夹中创建一个名为 `copilot` 的文件夹。\n4. 将解压后的文件全部复制到 `copilot` 文件夹中。**请确保最终路径为 `copilot/index.js`（而不是 `copilot/typora-copilot/index.js`）。如果出现后者，请将 `typora-copilot` 文件夹内的内容上移一层，使 `index.js` 直接位于 `copilot` 下。**\n5. 对于 Windows / Linux 用户，在 Typora 资源文件夹中用文本编辑器打开 `window.html`，在类似 `<script src=\"./appsrc/window/frame.js\" defer=\"defer\"></script>` 或 `<script src=\"./app/window/frame.js\" defer=\"defer\"></script>` 的代码之后添加 `<script src=\"./copilot/index.js\" defer=\"defer\"></script>`；对于 macOS 用户，在 Typora 资源文件夹中用文本编辑器打开 `index.html`，在类似 `<script src=\"./appsrc/main.js\" aria-hidden=\"true\" defer></script>` 或 `<script src=\"./app/main.js\" aria-hidden=\"true\" defer></script>` 的代码之后添加 `<script src=\"./copilot/index.js\" defer></script>`。\n6. 重启 Typora。\n7. 对于 macOS 用户，如果你在打开 Typora 时被提示“文件已损坏”，你可以按住 Ctrl 点击 Typora，并选择“打开”来打开 Typora.\n\n</details>\n\n## 初始化\n\n完成安装后，你会在 Typora 工具栏（即界面底部右下角）找到一个 Copilot 图标。点击**它右侧的箭头**，你会看到一个下拉菜单，然后点击“登录以认证 Copilot”。\n\n![Copilot 图标](./docs/toolbar-icon.zh-CN.png)\n\n> [!CAUTION]\n>\n> 如果你在中国大陆，登录这一步很可能因为网络原因失败。如果你发现点击按钮后很长时间没有反应，尝试按 Shift+F12（Windows 或 Linux）或在帮助菜单中打开“Enable Debugging”并在任意位置右键选择检查元素（macOS），以打开调试工具，定位到“控制台”或“Console”标签页，将过滤级别调整为“详细”或“Verbose”。然后查看控制台中打印的日志信息，以检查是否存在网络问题。\n>\n> 如果你看到一条来自“SignInInitiate”的红色错误信息，其中包含“ETIMEOUT”这样的内容，说明这一步因网络原因失败了。尝试调整你的代理软件设置，打开类似“增强代理”或“TUN 模式”的选项，重启 Typora 再进行尝试；或者，对于 Windows 用户可以使用使用 Proxifier 配置全局代理，对于 macOS / Linux 用户可以使用 Proxychains 打开 Typora，再进行尝试。\n\n按照提示进行身份验证：\n\n1. 用户代码会自动复制到你的剪贴板。\n2. 遵照弹出提示上的说明，打开 GitHub 身份验证页面。\n3. 将用户代码粘贴到 GitHub 身份验证页面中。\n4. 返回 Typora 并在对话框中按下“确定”按钮。\n5. 如果你在**几秒钟后**看到一个“已登录 GitHub Copilot”对话框，Copilot 插件应该就可以正常工作了（在中国大陆，你可能需要等待更长的时间）。\n\n## Copilot Chat\n\n点击工具栏中的 Copilot 图标将切换 Copilot Chat 面板。你可以使用它与 Copilot 聊天，当前文档和之前的聊天记录将作为上下文发送给 Copilot。\n\n确保在使用 Copilot Chat 面板之前已登录 Copilot。登录后，重启 Typora 以确保 Copilot Chat 面板正常工作。\n\n你可以：\n\n- 从面板顶部的下拉列表中选择、创建、编辑聊天标题，或删除聊天会话。\n- 点击“发送”按钮或按下 <kbd>Enter</kbd> 键发送消息。（你可以使用 <kbd>Shift</kbd> + <kbd>Enter</kbd> 或 <kbd>Ctrl</kbd> + <kbd>Enter</kbd> 插入新行。）\n- 点击“停止”按钮停止当前请求。\n- 从面板底部的下拉列表中选择提示样式。\n- 从面板底部的下拉列表中选择要使用的模型。\n\n## 卸载\n\n### 一键卸载（推荐）\n\n要卸载插件，你可以直接将以下命令复制粘贴到你的终端中：\n\n<details>\n  <summary><strong>Windows</strong></summary>\n\n以**管理员身份**在 PowerShell 中运行以下命令：\n\n```powershell\niwr -Uri \"https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/bin/uninstall_windows.ps1\" | iex\n```\n\n</details>\n\n<details>\n  <summary><strong>macOS</strong></summary>\n\n在终端中运行以下命令：\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/bin/uninstall_macos.sh | sudo bash\n```\n\n</details>\n\n<details>\n  <summary><strong>Linux</strong></summary>\n\n在终端中运行以下命令：\n\n```bash\nwget -O - https://raw.githubusercontent.com/Snowflyt/typora-copilot/main/bin/uninstall_linux.sh | sudo bash\n```\n\n</details>\n\n### 脚本卸载\n\n<details>\n  <summary><strong>Windows</strong></summary>\n\n对于 Windows 用户，定位到插件安装目录并在 PowerShell 中**以管理员身份**运行以下命令：\n\n```powershell\n.\\bin\\uninstall_windows.ps1\n```\n\n和安装时一样，如果脚本无法找到 Typora，你可以手动通过 `-Path` 或 `-p` 参数指定 Typora 的路径。\n\n</details>\n\n<details>\n  <summary><strong>macOS</strong></summary>\n\n对于 macOS 用户，定位到插件安装目录并在终端中运行以下命令：\n\n```bash\nsudo bash ./bin/uninstall_macos.sh\n```\n\n和安装时一样，如果脚本无法找到 Typora，你可以手动通过 `--path` 或 `-p` 参数指定 Typora 的路径。\n\n</details>\n\n<details>\n  <summary><strong>Linux</strong></summary>\n\n对于 Linux 用户，定位到插件安装目录并在终端中运行以下命令：\n\n```bash\nsudo bash ./bin/uninstall_linux.sh\n```\n\n和安装时一样，如果脚本无法找到 Typora，你可以手动通过 `--path` 或 `-p` 参数指定 Typora 的路径。\n\n</details>\n\n### 手动卸载\n\n<details>\n  <summary>点击展开</summary>\n\n1. 找到 Typora 安装目录下的 `window.html` 文件，通常位于 `<typora_root_path>/resources/`；对于 macOS 用户，找到 Typora 安装目录下的 `index.html` 文件，通常位于 `<typora_root_path>/Contents/Resources/TypeMark/`. `<typora_root_path>` 是 Typora 的安装路径，替换为你的实际 Typora 安装路径（注意尖括号 `<` 和 `>` 也要删除）。这个文件夹在下面的步骤中被称为 Typora 资源文件夹。\n2. 删除 Typora 资源文件夹中的 `copilot` 文件夹。\n3. 对于 Windows / Linux 用户，在 Typora 资源文件夹中用文本编辑器打开 `window.html`，删除 `<script src=\"./copilot/index.js\" defer=\"defer\"></script>`；对于 macOS 用户，在 Typora 资源文件夹中用文本编辑器打开 `index.html`，删除 `<script src=\"./copilot/index.js\" defer></script>`.\n4. 重启 Typora.\n</details>\n\n## 已知问题\n\n1. 有时接受建议可能会导致编辑器重新渲染（即代码块、数学块等将重新渲染）。这是由于 Typora API 的限制，我必须有时强制编辑器重新渲染以接受建议，目前我找不到更安全和更高效的方法来解决这个问题。\n\n## 常见问题\n\n### 如何临时禁用 Copilot？\n\n点击工具栏中的 Copilot 图标，然后点击“禁用建议”即可。你可以通过点击图标然后点击“启用建议”来重新启用它。\n\n### 为什么默认在实时预览模式（正常模式）下使用建议面板，在源代码模式下使用补全文本？我能修改这一配置吗？\n\n在实时预览模式下使用建议面板是有意的。Typora 在实时预览模式下的渲染机制很复杂，很难使补全文本在实时预览模式下正常工作。\n\n不过对于源代码模式，你可以通过点击 `工具栏图标 -> 设置` 并切换 `在源代码模式下使用内联补全文本` 选项来在源代码模式下使用建议面板。\n\n设置中还提供了一个名为 `在预览模式代码块中使用内联补全文本` 的选项。如果你启用了这个选项，补全文本将会在实时预览模式下的代码块和数学块中使用。但目前不建议启用这个选项，因为它很可能会破坏编辑器内容或历史记录。\n\n### 我可以使用除 `Tab` 键以外的按键来接受建议吗？\n\n目前不行。这在技术上是可行的，但目前我没什么时间实现它。也许我将来会实现它。\n"
  },
  {
    "path": "bin/install_linux.sh",
    "content": "#!/bin/bash\n\n# Parse arguments -path or -p\nwhile [[ \"$#\" -gt 0 ]]; do\n  case $1 in\n  -p | --path)\n    custom_path=\"$2\"\n    shift\n    ;;\n  *)\n    echo \"Unknown parameter passed: $1\"\n    exit 1\n    ;;\n  esac\n  shift\ndone\n\n# Possible Typora installation paths\npaths=(\n  \"/usr/share/typora\"\n  \"/usr/local/share/typora\"\n  \"/opt/typora\"\n  \"/opt/Typora\"\n  \"$HOME/.local/share/Typora\"\n  \"$HOME/.local/share/typora\"\n)\nif [[ -n \"$custom_path\" ]]; then\n  paths=(\"$custom_path\")\nfi\n\nscript_to_insert_after_candidates=(\n  '<script src=\"./app/window/frame.js\" defer=\"defer\"></script>'\n  '<script src=\"./appsrc/window/frame.js\" defer=\"defer\"></script>'\n)\nscript_to_insert='<script src=\"./copilot/index.js\" defer=\"defer\"></script>'\n\nescape_for_sed() {\n  echo \"$1\" | sed -E 's/[]\\/$*.^|[]/\\\\&/g'\n}\n\n# Find `window.html` in Typora installation path\npath_found=false\nsuccess=false\n\nfor path in \"${paths[@]}\"; do\n  window_html_path_candidates=(\n    \"$path/resources/app/window.html\"\n    \"$path/resources/appsrc/window.html\"\n    \"$path/resources/window.html\"\n  )\n\n  for window_html_path in \"${window_html_path_candidates[@]}\"; do\n    # If found, insert script\n    if [[ -f \"$window_html_path\" ]]; then\n      path_found=true\n      echo \"Installation directory: \\\"$(dirname \"$window_html_path\")/copilot/\\\"\"\n      echo \"Found Typora \\\"index.html\\\" at \\\"$window_html_path\\\".\"\n      content=$(cat \"$window_html_path\")\n\n      if [[ \"$content\" != *\"$script_to_insert\"* ]]; then\n        echo 'Installing Copilot plugin in Typora...'\n        for script_to_insert_after in \"${script_to_insert_after_candidates[@]}\"; do\n          if echo \"$content\" | grep -qF \"$script_to_insert_after\"; then\n            echo \"Inserting Copilot plugin script after \\\"$script_to_insert_after\\\"...\"\n\n            # Calculate indent of the script to insert\n            escaped_script_to_insert_after=$(escape_for_sed \"$script_to_insert_after\")\n            escaped_script_to_insert=$(escape_for_sed \"$script_to_insert\")\n            indent=$(echo \"$content\" | while IFS= read -r line; do\n              if [[ \"$line\" == *\"$script_to_insert_after\"* ]]; then\n                echo \"$line\" | sed -E 's/^([[:space:]]*).*/\\1/'\n                break\n              fi\n            done)\n            if [[ -z \"$indent\" ]]; then\n              replacement=\"$escaped_script_to_insert_after$escaped_script_to_insert\"\n            else\n              replacement=\"$escaped_script_to_insert_after\\n$indent$escaped_script_to_insert\"\n            fi\n            new_content=$(echo \"$content\" | sed \"s|$escaped_script_to_insert_after|$replacement|\")\n\n            # Insert script\n            echo \"$new_content\" >\"$window_html_path\"\n\n            # Copy `<cwd>/../` to `<path_of_window_html>/copilot/` directory\n            copilot_path=$(dirname \"$window_html_path\")/copilot\n            if [[ ! -d \"$copilot_path\" ]]; then\n              echo \"Copying Copilot plugin files to \\\"$copilot_path\\\"...\"\n              mkdir -p \"$copilot_path\"\n              cp -r \"$(dirname \"$0\")/../\" \"$copilot_path\"\n            fi\n\n            echo \"Successfully installed Copilot plugin in Typora.\"\n\n            success=true\n            break\n          fi\n        done\n\n        if $success; then break; fi\n      else\n        # Script tag already present; validate installation integrity\n        copilot_path=\"$(dirname \"$window_html_path\")/copilot\"\n        index_js_path=\"$copilot_path/index.js\"\n        if [[ ! -f \"$index_js_path\" ]]; then\n          echo \"Warning: Corrupted Copilot installation detected. Expected \\\"$index_js_path\\\" but it does not exist.\"\n          echo \"Please delete the entire \\\"$copilot_path\\\" directory and re-run this installer.\"\n          echo \"Do NOT place the release inside Typora's installation folder (especially not under \\\"copilot\\\").\"\n          echo \"Download/extract it to any other folder and run this script from there.\"\n          break\n        fi\n\n        echo \"Warning: Copilot plugin has already been installed in Typora.\"\n        success=true\n        break\n      fi\n    fi\n  done\n\n  if $success; then break; fi\ndone\n\n# If not found, prompt user to check installation path\nif ! $path_found; then\n  echo \"Error: Could not find Typora installation path. Please check if Typora is installed and try again.\" >&2\nelif ! $success; then\n  echo \"Error: Installation failed.\" >&2\nfi\n"
  },
  {
    "path": "bin/install_macos.sh",
    "content": "#!/bin/bash\n\n# Parse arguments -path or -p\nwhile [[ \"$#\" -gt 0 ]]; do\n  case $1 in\n  -p | --path)\n    custom_path=\"$2\"\n    shift\n    ;;\n  *)\n    echo \"Unknown parameter passed: $1\"\n    exit 1\n    ;;\n  esac\n  shift\ndone\n\n# Possible Typora installation paths\npaths=(\n  \"/Applications/Typora.app\"\n  \"$HOME/Applications/Typora.app\"\n  \"/usr/local/bin/Typora\"\n  \"/opt/Typora\"\n)\nif [[ -n \"$custom_path\" ]]; then\n  paths=(\"$custom_path\")\nfi\n\nscript_to_insert_after_candidates=(\n  '<script src=\"./app/main.js\" defer></script>'\n  '<script src=\"./app/main.js\" aria-hidden=\"true\" defer></script>'\n  '<script src=\"./appsrc/main.js\" defer></script>'\n  '<script src=\"./appsrc/main.js\" aria-hidden=\"true\" defer></script>'\n)\nscript_to_insert='<script src=\"./copilot/index.js\" defer></script>'\n\nescape_for_sed() {\n  echo \"$1\" | sed -E 's/[]\\/$*.^|[]/\\\\&/g'\n}\n\n# Find `index.html` in Typora installation path\npath_found=false\nsuccess=false\n\nfor path in \"${paths[@]}\"; do\n  index_html_path_candidates=(\n    \"$path/Contents/Resources/TypeMark/index.html\"\n    \"$path/Contents/Resources/app/index.html\"\n    \"$path/Contents/Resources/appsrc/index.html\"\n    \"$path/resources/app/index.html\"\n    \"$path/resources/appsrc/index.html\"\n    \"$path/resources/TypeMark/index.html\"\n    \"$path/resources/index.html\"\n  )\n\n  for index_html_path in \"${index_html_path_candidates[@]}\"; do\n    # If found, insert script\n    if [[ -f \"$index_html_path\" ]]; then\n      path_found=true\n      echo \"Installation directory: \\\"$(dirname \"$index_html_path\")/copilot/\\\"\"\n      echo \"Found Typora \\\"index.html\\\" at \\\"$index_html_path\\\".\"\n      content=$(cat \"$index_html_path\")\n\n      if [[ \"$content\" != *\"$script_to_insert\"* ]]; then\n        echo 'Installing Copilot plugin in Typora...'\n        for script_to_insert_after in \"${script_to_insert_after_candidates[@]}\"; do\n          if echo \"$content\" | grep -qF \"$script_to_insert_after\"; then\n            echo \"Inserting Copilot plugin script after \\\"$script_to_insert_after\\\"...\"\n\n            # Calculate indent of the script to insert\n            escaped_script_to_insert_after=$(escape_for_sed \"$script_to_insert_after\")\n            escaped_script_to_insert=$(escape_for_sed \"$script_to_insert\")\n            indent=$(echo \"$content\" | while IFS= read -r line; do\n              if [[ \"$line\" == *\"$script_to_insert_after\"* ]]; then\n                echo \"$line\" | sed -E 's/^([[:space:]]*).*/\\1/'\n                break\n              fi\n            done)\n            if [[ -z \"$indent\" ]]; then\n              replacement=\"$escaped_script_to_insert_after$escaped_script_to_insert\"\n            else\n              replacement=\"$escaped_script_to_insert_after\\n$indent$escaped_script_to_insert\"\n            fi\n            new_content=$(echo \"$content\" | sed \"s|$escaped_script_to_insert_after|$replacement|\")\n\n            # Insert script\n            echo \"$new_content\" >\"$index_html_path\"\n\n            # Copy `<cwd>/../` to `<path_of_index_html>/copilot/` directory\n            copilot_path=$(dirname \"$index_html_path\")/copilot\n            if [[ ! -d \"$copilot_path\" ]]; then\n              echo \"Copying Copilot plugin files to \\\"$copilot_path\\\"...\"\n              mkdir -p \"$copilot_path\"\n              cp -r \"$(dirname \"$0\")/../\" \"$copilot_path\"\n            fi\n\n            echo \"Successfully installed Copilot plugin in Typora.\"\n\n            success=true\n            break\n          fi\n        done\n\n        if $success; then break; fi\n      else\n        # Script tag already present; validate installation integrity\n        copilot_path=\"$(dirname \"$index_html_path\")/copilot\"\n        index_js_path=\"$copilot_path/index.js\"\n        if [[ ! -f \"$index_js_path\" ]]; then\n          echo \"Warning: Corrupted Copilot installation detected. Expected \\\"$index_js_path\\\" but it does not exist.\"\n          echo \"Please delete the entire \\\"$copilot_path\\\" directory and re-run this installer.\"\n          echo \"Do NOT place the release inside Typora's installation folder (especially not under \\\"copilot\\\").\"\n          echo \"Download/extract it to any other folder and run this script from there.\"\n          break\n        fi\n\n        echo \"Warning: Copilot plugin has already been installed in Typora.\"\n        success=true\n        break\n      fi\n    fi\n  done\n\n  if $success; then break; fi\ndone\n\n# If not found, prompt user to check installation path\nif ! $path_found; then\n  echo \"Error: Could not find Typora installation path. Please check if Typora is installed and try again.\" >&2\nelif ! $success; then\n  echo \"Error: Installation failed.\" >&2\nfi\n"
  },
  {
    "path": "bin/install_windows.ps1",
    "content": "# Allow custom path (-Path or -p)\nparam (\n    [Parameter(Mandatory = $false)]\n    [Alias('p')]\n    [string] $Path = ''\n)\n\n# Possible Typora installation paths\n$paths = @(\n    'C:\\Program Files\\Typora'\n    'C:\\Program Files (x86)\\Typora'\n    \"$env:LOCALAPPDATA\\Programs\\Typora\"\n)\nif ($Path -ne '') { $paths = @($Path) }\n\n$scriptToInsertAfterCandidates = @(\n    '<script src=\"./app/window/frame.js\" defer=\"defer\"></script>'\n    '<script src=\"./appsrc/window/frame.js\" defer=\"defer\"></script>'\n)\n$scriptToInsert = '<script src=\"./copilot/index.js\" defer=\"defer\"></script>'\n\n# Find `window.html` in Typora installation path\n$pathFound = $false\n$success = $false\n\nforeach ($path in $paths) {\n    $windowHtmlPathCandiates = @(\n        Join-Path -Path $path -ChildPath 'resources\\app\\window.html'\n        Join-Path -Path $path -ChildPath 'resources\\appsrc\\window.html'\n        Join-Path -Path $path -ChildPath 'resources\\window.html'\n    )\n\n    foreach ($windowHtmlPath in $windowHtmlPathCandiates) {\n        # If found, insert script\n        if (Test-Path $windowHtmlPath) {\n            $pathFound = $true\n            Write-Host \"Installation directory: \"\"$(Split-Path -Path $windowHtmlPath -Parent)\\copilot\\\"\"\"\n            Write-Host \"Found Typora \"\"window.html\"\" at \"\"$windowHtmlPath\"\".\"\n            $content = Get-Content $windowHtmlPath -Raw -Encoding UTF8\n\n            if (!($content.Contains($scriptToInsert))) {\n                Write-Host 'Installing Copilot plugin in Typora...'\n                foreach ($scriptToInsertAfter in $scriptToInsertAfterCandidates) {\n                    if ($content.Contains($scriptToInsertAfter)) {\n                        Write-Host \"Inserting Copilot plugin script after \"\"$scriptToInsertAfter\"\"...\"\n\n                        # Calculate indent of the script to insert\n                        $row = $content.Split(\"`n\") | Where-Object { $_ -match $scriptToInsertAfter }\n                        $rowContentBeforeScriptToInsertAfter = $row -replace \"$scriptToInsertAfter(.*)\", ''\n                        $indent = $rowContentBeforeScriptToInsertAfter -replace $rowContentBeforeScriptToInsertAfter.TrimEnd(), ''\n\n                        # Insert script\n                        $newContent = $content -replace $scriptToInsertAfter, (\n                            $scriptToInsertAfter +\n                            $(If (($rowContentBeforeScriptToInsertAfter -ne '') -and ($indent -eq '')) { '' } Else { \"`n\" + $indent }) +\n                            $scriptToInsert\n                        )\n                        Set-Content -Path $windowHtmlPath -Value $newContent -Encoding UTF8\n\n                        # Copy `<cwd>\\..\\` to `<path_of_window_html>\\copilot\\` directory\n                        $copilotPath = Join-Path -Path (Split-Path -Path $windowHtmlPath -Parent) -ChildPath 'copilot'\n                        if (-not (Test-Path $copilotPath)) {\n                            Write-Host \"Copying Copilot plugin files to \"\"$copilotPath\"\"...\"\n                            Copy-Item -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\\') -Destination $copilotPath -Recurse\n                        }\n\n                        Write-Host \"Successfully installed Copilot plugin in Typora.\"\n\n                        $success = $true\n                        break\n                    }\n                }\n\n                if ($success) { break }\n            }\n            else {\n                # Script tag already present; validate installation integrity\n                $copilotPath = Join-Path -Path (Split-Path -Path $windowHtmlPath -Parent) -ChildPath 'copilot'\n                $indexJsPath = Join-Path -Path $copilotPath -ChildPath 'index.js'\n                if (-not (Test-Path $indexJsPath)) {\n                    Write-Warning \"Corrupted Copilot installation detected. Expected \"\"$indexJsPath\"\" but it does not exist.\"\n                    Write-Host \"Please delete the entire \"\"$copilotPath\"\" directory and re-run this installer.\"\n                    Write-Host \"Do NOT place the release inside Typora's installation folder (especially not under \"\"copilot\"\").\"\n                    Write-Host \"Download/extract it to any other folder and run this script from there.\"\n                    break\n                }\n\n                Write-Warning \"Copilot plugin has already been installed in Typora.\"\n                $success = $true\n                break\n            }\n        }\n    }\n}\n\n# If not found, prompt user to check installation path\nif (-not $pathFound) {\n    Write-Error \"Could not find Typora installation path. Please check if Typora is installed and try again.\"\n}\nelseif (-not $success) {\n    Write-Error \"Installation failed.\"\n}\n"
  },
  {
    "path": "bin/uninstall_linux.sh",
    "content": "#!/bin/bash\n\n# Parse arguments -path or -p\nwhile [[ \"$#\" -gt 0 ]]; do\n  case $1 in\n  -p | --path)\n    custom_path=\"$2\"\n    shift\n    ;;\n  -s | --silent)\n    silent=true\n    shift\n    ;;\n  *)\n    echo \"Unknown parameter passed: $1\"\n    exit 1\n    ;;\n  esac\n  shift\ndone\n\n# Possible Typora installation paths on Linux\npaths=(\n  \"/usr/share/typora\"\n  \"/usr/local/share/typora\"\n  \"/opt/typora\"\n  \"/opt/Typora\"\n  \"$HOME/.local/share/Typora\"\n  \"$HOME/.local/share/typora\"\n)\nif [[ -n \"$custom_path\" ]]; then\n  paths=(\"$custom_path\")\nfi\n\nscript_to_remove_after_candidates=(\n  '<script src=\"./app/window/frame.js\" defer=\"defer\"></script>'\n  '<script src=\"./appsrc/window/frame.js\" defer=\"defer\"></script>'\n)\nscript_to_remove='<script src=\"./copilot/index.js\" defer=\"defer\"></script>'\n\nescape_for_sed() {\n  echo \"$1\" | sed -E 's/[]\\/$*.^|[]/\\\\&/g'\n}\n\n# Find `window.html` in Typora installation path\npath_found=false\nsuccess=false\n\nfor path in \"${paths[@]}\"; do\n  window_html_path_candidates=(\n    \"$path/resources/app/window.html\"\n    \"$path/resources/appsrc/window.html\"\n    \"$path/resources/window.html\"\n  )\n\n  for window_html_path in \"${window_html_path_candidates[@]}\"; do\n    # If found, insert script\n    if [[ -f \"$window_html_path\" ]]; then\n      path_found=true\n      echo \"Found Typora \\\"window.html\\\" at \\\"$window_html_path\\\".\"\n      content=$(cat \"$window_html_path\")\n\n      for script_to_remove_after in \"${script_to_remove_after_candidates[@]}\"; do\n        if echo \"$content\" | grep -qF \"$script_to_remove_after\"; then\n          if echo \"$content\" | grep -qF \"$script_to_remove\"; then\n            echo \"Removing Copilot plugin script after \\\"$script_to_remove_after\\\"...\"\n\n            escaped_script_to_remove=$(escape_for_sed \"$script_to_remove\")\n            new_content=$(echo \"$content\" | sed -E \"s/[[:space:]]*$escaped_script_to_remove//\")\n\n            # Remove script\n            echo \"$new_content\" >\"$window_html_path\"\n\n            # Remove `<path_of_window_html>/copilot/` directory\n            copilot_dir=$(dirname \"$window_html_path\")/copilot\n            if [[ -d \"$copilot_dir\" ]]; then\n              echo \"Removing Copilot plugin directory \\\"$copilot_dir\\\"...\"\n              rm -rf \"$copilot_dir\"\n            fi\n\n            echo \"Successfully uninstalled Copilot plugin in Typora.\"\n\n            success=true\n            break\n          else\n            if ! $silent; then\n              echo \"Warning: Copilot plugin has not been installed in Typora.\"\n            fi\n\n            # Remove `<path_of_window_html>/copilot/` directory regardless of script presence\n            copilot_dir=$(dirname \"$window_html_path\")/copilot\n            if [[ -d \"$copilot_dir\" ]]; then\n              echo \"Detected Copilot plugin directory but no script reference. This might be leftover from a previous installation.\"\n              echo \"Removing Copilot plugin directory \\\"$copilot_dir\\\"...\"\n              rm -rf \"$copilot_dir\"\n              echo \"Uninstallation complete.\"\n            fi\n\n            success=true\n            break\n          fi\n        fi\n\n        if $success; then break; fi\n      done\n    fi\n\n    if $success; then break; fi\n  done\n\n  if $success; then break; fi\ndone\n\n# If not found, prompt user to check installation path\nif ! $path_found; then\n  echo \"Error: Could not find Typora installation path. Please check if Typora is installed and try again.\" >&2\nelif ! $success; then\n  echo \"Error: Uninstallation failed.\" >&2\nfi\n"
  },
  {
    "path": "bin/uninstall_macos.sh",
    "content": "#!/bin/bash\n\n# Parse arguments -path or -p\nwhile [[ \"$#\" -gt 0 ]]; do\n  case $1 in\n  -p | --path)\n    custom_path=\"$2\"\n    shift\n    ;;\n  -s | --silent)\n    silent=true\n    shift\n    ;;\n  *)\n    echo \"Unknown parameter passed: $1\"\n    exit 1\n    ;;\n  esac\n  shift\ndone\n\n# Possible Typora installation paths\npaths=(\n  \"/Applications/Typora.app\"\n  \"$HOME/Applications/Typora.app\"\n  \"/usr/local/bin/Typora\"\n  \"/opt/Typora\"\n)\nif [[ -n \"$custom_path\" ]]; then\n  paths=(\"$custom_path\")\nfi\n\nscript_to_remove_after_candidates=(\n  '<script src=\"./app/main.js\" defer></script>'\n  '<script src=\"./app/main.js\" aria-hidden=\"true\" defer></script>'\n  '<script src=\"./appsrc/main.js\" defer></script>'\n  '<script src=\"./appsrc/main.js\" aria-hidden=\"true\" defer></script>'\n)\nscript_to_remove='<script src=\"./copilot/index.js\" defer></script>'\n\nescape_for_sed() {\n  echo \"$1\" | sed -E 's/[]\\/$*.^|[]/\\\\&/g'\n}\n\n# Find `index.html` in Typora installation path\npath_found=false\nsuccess=false\n\nfor path in \"${paths[@]}\"; do\n  index_html_path_candidates=(\n    \"$path/Contents/Resources/TypeMark/index.html\"\n    \"$path/Contents/Resources/app/index.html\"\n    \"$path/Contents/Resources/appsrc/index.html\"\n    \"$path/resources/app/index.html\"\n    \"$path/resources/appsrc/index.html\"\n    \"$path/resources/TypeMark/index.html\"\n    \"$path/resources/index.html\"\n  )\n\n  for index_html_path in \"${index_html_path_candidates[@]}\"; do\n    # If found, insert script\n    if [[ -f \"$index_html_path\" ]]; then\n      path_found=true\n      echo \"Found Typora \\\"index.html\\\" at \\\"$index_html_path\\\".\"\n      content=$(cat \"$index_html_path\")\n\n      for script_to_remove_after in \"${script_to_remove_after_candidates[@]}\"; do\n        if echo \"$content\" | grep -qF \"$script_to_remove_after\"; then\n          if echo \"$content\" | grep -qF \"$script_to_remove\"; then\n            echo \"Removing Copilot plugin script after \\\"$script_to_remove_after\\\"...\"\n\n            escaped_script_to_remove=$(escape_for_sed \"$script_to_remove\")\n            new_content=$(echo \"$content\" | sed -E \"s/[[:space:]]*$escaped_script_to_remove//\")\n\n            # Remove script\n            echo \"$new_content\" >\"$index_html_path\"\n\n            # Remove `<path_of_index_html>/copilot/` directory\n            copilot_dir=$(dirname \"$index_html_path\")/copilot\n            if [[ -d \"$copilot_dir\" ]]; then\n              echo \"Removing Copilot plugin directory \\\"$copilot_dir\\\"...\"\n              rm -rf \"$copilot_dir\"\n            fi\n\n            echo \"Successfully uninstalled Copilot plugin in Typora.\"\n\n            success=true\n            break\n          else\n            if ! $silent; then\n              echo \"Warning: Copilot plugin has not been installed in Typora.\"\n            fi\n\n            # Remove `<path_of_index_html>/copilot/` directory regardless of script presence\n            copilot_dir=$(dirname \"$index_html_path\")/copilot\n            if [[ -d \"$copilot_dir\" ]]; then\n              echo \"Detected Copilot plugin directory but no script reference. This might be leftover from a previous installation.\"\n              echo \"Removing Copilot plugin directory \\\"$copilot_dir\\\"...\"\n              rm -rf \"$copilot_dir\"\n              echo \"Uninstallation complete.\"\n            fi\n\n            success=true\n            break\n          fi\n        fi\n\n        if $success; then break; fi\n      done\n    fi\n\n    if $success; then break; fi\n  done\n\n  if $success; then break; fi\ndone\n\n# If not found, prompt user to check installation path\nif ! $path_found; then\n  echo \"Error: Could not find Typora installation path. Please check if Typora is installed and try again.\" >&2\nelif ! $success; then\n  echo \"Error: Uninstallation failed.\" >&2\nfi\n"
  },
  {
    "path": "bin/uninstall_windows.ps1",
    "content": "# Allow custom path (-Path or -p) and silence warning (-Silent or -s)\nparam (\n    [Parameter(Mandatory = $false)]\n    [Alias('p')]\n    [string] $Path = '',\n\n    [Parameter(Mandatory = $false)]\n    [Alias('s')]\n    [switch] $Silent = $false\n)\n\n# Possible Typora installation paths\n$paths = @(\n    'C:\\Program Files\\Typora'\n    'C:\\Program Files (x86)\\Typora'\n    \"$env:LOCALAPPDATA\\Programs\\Typora\"\n)\nif ($Path -ne '') { $paths = @($Path) }\n\n$scriptToRemoveAfterCandidates = @(\n    '<script src=\"./app/window/frame.js\" defer=\"defer\"></script>'\n    '<script src=\"./appsrc/window/frame.js\" defer=\"defer\"></script>'\n)\n$scriptToRemove = '<script src=\"./copilot/index.js\" defer=\"defer\"></script>'\n\n# Find `window.html` in Typora installation path\n$pathFound = $false\n$success = $false\n\nforeach ($path in $paths) {\n    $windowHtmlPathCandiates = @(\n        Join-Path -Path $path -ChildPath 'resources\\app\\window.html'\n        Join-Path -Path $path -ChildPath 'resources\\appsrc\\window.html'\n        Join-Path -Path $path -ChildPath 'resources\\window.html'\n    )\n\n    foreach ($windowHtmlPath in $windowHtmlPathCandiates) {\n        # If found, remove script\n        if (Test-Path $windowHtmlPath) {\n            $pathFound = $true\n            Write-Host \"Found Typora \"\"window.html\"\" at \"\"$windowHtmlPath\"\".\"\n            $content = Get-Content $windowHtmlPath -Raw -Encoding UTF8\n\n            foreach ($scriptToRemoveAfter in $scriptToRemoveAfterCandidates) {\n                if ($content.Contains($scriptToRemoveAfter)) {\n                    if ($content.Contains($scriptToRemove)) {\n                        Write-Host \"Removing Copilot plugin script after \"\"$scriptToRemoveAfter\"\"...\"\n\n                        # Calculate indent of the script to remove\n                        $row = $content.Split(\"`n\") | Where-Object { $_ -match $scriptToRemove }\n                        $rowContentBeforeScriptToRemove = $row -replace \"$scriptToRemoveAfter(.*)\", ''\n                        $indent = $rowContentBeforeScriptToRemove -replace $rowContentBeforeScriptToRemove.TrimEnd(), ''\n\n                        # Remove script\n                        $newContent = $content -replace ($indent + $scriptToRemove), ''\n                        Set-Content -Path $windowHtmlPath -Value $newContent -Encoding UTF8\n\n                        # Remove `<path_of_window_html>\\copilot\\` directory\n                        $copilotPath = Join-Path -Path (Split-Path -Path $windowHtmlPath -Parent) -ChildPath 'copilot'\n                        if (Test-Path $copilotPath) {\n                            Write-Host \"Removing Copilot plugin directory \"\"$copilotPath\"\"...\"\n                            Remove-Item -Path $copilotPath -Recurse -Force\n                        }\n\n                        Write-Host \"Successfully uninstalled Copilot plugin in Typora.\"\n\n                        $success = $true\n                        break\n                    }\n                    else {\n                        if (-not $Silent) {\n                            Write-Warning \"Copilot plugin script has not been found in Typora.\"\n                        }\n\n                        # Remove `<path_of_window_html>\\copilot\\` directory regardless of script presence\n                        $copilotPath = Join-Path -Path (Split-Path -Path $windowHtmlPath -Parent) -ChildPath 'copilot'\n                        if (Test-Path $copilotPath) {\n                            Write-Host \"Detected Copilot plugin directory but no script reference. This might be leftover from a previous installation.\"\n                            Write-Host \"Removing Copilot plugin directory \"\"$copilotPath\"\"...\"\n                            Remove-Item -Path $copilotPath -Recurse -Force\n                            Write-Host \"Uninstallation complete.\"\n                            $success = $true\n                        }\n\n                        $success = $true\n                        break\n                    }\n                }\n\n                if ($success) { break }\n            }\n        }\n\n        if ($success) { break }\n    }\n}\n\n# If not found, prompt user to check installation path\nif (-not $pathFound) {\n    Write-Error \"Could not find Typora installation path. Please check if Typora is installed and try again.\"\n}\nelseif (-not $success) {\n    Write-Error \"Uninstallation failed.\"\n}\n"
  },
  {
    "path": "commitlint.config.js",
    "content": "// @ts-check\n\n/**\n * @typedef {object} Parsed\n * @property {?string} emoji The emoji at the beginning of the commit message.\n * @property {?string} type The type of the commit message.\n * @property {?string} scope The scope of the commit message.\n * @property {?string} subject The subject of the commit message.\n */\n\nconst emojiEnum = /** @type {const} */ ([\n  2,\n  \"always\",\n  {\n    \"🎉\": [\"init\", \"Project initialization\"],\n    \"✨\": [\"feat\", \"Adding new features\"],\n    \"🐞\": [\"fix\", \"Fixing bugs\"],\n    \"📃\": [\"docs\", \"Modify documentation only\"],\n    \"🌈\": [\n      \"style\",\n      \"Only the spaces, formatting indentation, commas, etc. were changed, not the code logic\",\n    ],\n    \"🦄\": [\"refactor\", \"Code refactoring, no new features added or bugs fixed\"],\n    \"🎈\": [\"perf\", \"Optimization-related, such as improving performance, experience\"],\n    \"🧪\": [\"test\", \"Adding or modifying test cases\"],\n    \"🔧\": [\n      \"build\",\n      \"Dependency-related content, such as Webpack, Vite, Rollup, npm, package.json, etc.\",\n    ],\n    \"🐎\": [\"ci\", \"CI configuration related, e.g. changes to k8s, docker configuration files\"],\n    \"🐳\": [\"chore\", \"Other modifications, e.g. modify the configuration file\"],\n    \"↩\": [\"revert\", \"Rollback to previous version\"],\n  },\n]);\n\n/** @satisfies {import(\"@commitlint/types\").UserConfig} */\nconst config = {\n  parserPreset: {\n    parserOpts: {\n      headerPattern:\n        /^(?<emoji>\\u00a9|\\u00ae|[\\u2000-\\u3300]|\\ud83c[\\ud000-\\udfff]|\\ud83d[\\ud000-\\udfff]|\\ud83e[\\ud000-\\udfff]) (?<type>\\w+)(?:\\((?<scope>.*)\\))?!?: (?<subject>(?:(?!#).)*(?:(?!\\s).))$/,\n      headerCorrespondence: [\"emoji\", \"type\", \"scope\", \"subject\"],\n    },\n  },\n  plugins: [\n    {\n      rules: {\n        \"header-match-git-commit-message-with-emoji-pattern\": (parsed) => {\n          const { emoji, scope, subject, type } = /** @type {Parsed} */ (\n            /** @type {unknown} */ (parsed)\n          );\n          if (emoji === null && type === null && scope === null && subject === null)\n            return [\n              false,\n              'header must be in format \"<emoji> <type>(<scope>?): <subject>\", e.g:\\n' +\n                \"    - 🎉 init: Initial commit\\n\" +\n                \"    - ✨ feat(assertions): Add assertions\\n\" +\n                \"   \",\n            ];\n          return [true, \"\"];\n        },\n        \"emoji-enum\": (parsed, _, value) => {\n          const { emoji } = /** @type {Parsed} */ (/** @type {unknown} */ (parsed));\n          const emojisObject = /** @type {typeof emojiEnum[2]} */ (/** @type {unknown} */ (value));\n          if (emoji && !Object.keys(emojisObject).includes(emoji)) {\n            return [\n              false,\n              \"emoji must be one of:\\n\" +\n                Object.entries(emojisObject)\n                  .map(([emoji, [type, description]]) => `    ${emoji} ${type} - ${description}`)\n                  .join(\"\\n\") +\n                \"\\n   \",\n            ];\n          }\n          return [true, \"\"];\n        },\n      },\n    },\n  ],\n  rules: {\n    \"header-match-git-commit-message-with-emoji-pattern\": [2, \"always\"],\n    \"body-leading-blank\": [2, \"always\"],\n    \"footer-leading-blank\": [2, \"always\"],\n    \"header-max-length\": [2, \"always\", 72],\n    \"scope-case\": [2, \"always\", [\"lower-case\", \"upper-case\"]],\n    \"subject-case\": [2, \"always\", \"sentence-case\"],\n    \"subject-empty\": [2, \"never\"],\n    \"subject-exclamation-mark\": [2, \"never\"],\n    \"subject-full-stop\": [2, \"never\", \".\"],\n    \"emoji-enum\": emojiEnum,\n    \"type-case\": [2, \"always\", \"lower-case\"],\n    \"type-empty\": [2, \"never\"],\n    \"type-enum\": [\n      2,\n      \"always\",\n      [\n        \"init\",\n        \"feat\",\n        \"fix\",\n        \"docs\",\n        \"style\",\n        \"refactor\",\n        \"perf\",\n        \"test\",\n        \"build\",\n        \"ci\",\n        \"chore\",\n        \"revert\",\n      ],\n    ],\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "eslint.config.js",
    "content": "// @ts-check\n\nimport eslint from \"@eslint/js\";\nimport { defineConfig } from \"eslint/config\";\nimport { importX } from \"eslint-plugin-import-x\";\nimport { jsdoc } from \"eslint-plugin-jsdoc\";\nimport prettierRecommended from \"eslint-plugin-prettier/recommended\";\nimport react from \"eslint-plugin-react\";\nimport reactHooks from \"eslint-plugin-react-hooks\";\nimport sonarjs from \"eslint-plugin-sonarjs\";\nimport sortDestructureKeys from \"eslint-plugin-sort-destructure-keys\";\nimport globals from \"globals\";\nimport tseslint from \"typescript-eslint\";\n\nexport default defineConfig(\n  eslint.configs.recommended,\n  tseslint.configs.strictTypeChecked,\n  tseslint.configs.stylisticTypeChecked,\n  jsdoc({ config: \"flat/recommended-typescript-error\" }),\n  react.configs.flat.recommended,\n  react.configs.flat[\"jsx-runtime\"],\n  reactHooks.configs.flat[\"recommended-latest\"],\n  /** @type {import(\"eslint\").Linter.Config} */ (importX.flatConfigs.recommended),\n  /** @type {import(\"eslint\").Linter.Config} */ (importX.flatConfigs.typescript),\n  prettierRecommended,\n  sonarjs.configs.recommended,\n  {\n    plugins: {\n      react,\n      \"sort-destructure-keys\": /** @type {import(\"eslint\").ESLint.Plugin} */ (sortDestructureKeys),\n    },\n    linterOptions: {\n      reportUnusedDisableDirectives: true,\n    },\n    languageOptions: {\n      parserOptions: {\n        ecmaFeatures: { jsx: true },\n        projectService: {\n          allowDefaultProject: [\"*.{js,cjs}\"],\n          defaultProject: \"tsconfig.json\",\n        },\n        tsconfigRootDir: import.meta.dirname,\n      },\n      globals: { ...globals.browser },\n    },\n    rules: {\n      \"@typescript-eslint/restrict-plus-operands\": [\n        \"error\",\n        { allowAny: true, allowNumberAndString: true },\n      ],\n      \"@typescript-eslint/restrict-template-expressions\": [\n        \"error\",\n        { allowAny: true, allowBoolean: true, allowNullish: true, allowNumber: true },\n      ],\n      \"@typescript-eslint/consistent-indexed-object-style\": \"off\",\n      \"@typescript-eslint/consistent-type-definitions\": \"off\", // TS treats types and interfaces differently, this may break some advanced type gymnastics\n      \"@typescript-eslint/consistent-type-imports\": [\n        \"error\",\n        { prefer: \"type-imports\", disallowTypeAnnotations: false },\n      ],\n      \"@typescript-eslint/dot-notation\": [\"error\", { allowIndexSignaturePropertyAccess: true }],\n      \"@typescript-eslint/no-confusing-void-expression\": \"off\",\n      \"@typescript-eslint/no-empty-function\": \"off\",\n      \"@typescript-eslint/no-empty-object-type\": \"off\",\n      \"@typescript-eslint/no-explicit-any\": \"off\",\n      \"@typescript-eslint/no-invalid-void-type\": \"off\",\n      \"@typescript-eslint/no-namespace\": \"off\",\n      \"@typescript-eslint/no-non-null-assertion\": \"off\",\n      \"@typescript-eslint/no-unnecessary-type-parameters\": \"off\",\n      \"@typescript-eslint/no-unsafe-argument\": \"off\",\n      \"@typescript-eslint/no-unsafe-assignment\": \"off\",\n      \"@typescript-eslint/no-unsafe-call\": \"off\",\n      \"@typescript-eslint/no-unsafe-member-access\": \"off\",\n      \"@typescript-eslint/no-unsafe-return\": \"off\",\n      \"@typescript-eslint/no-unused-vars\": \"off\", // Already covered by `tsconfig.json`\n      \"@typescript-eslint/prefer-nullish-coalescing\": \"off\",\n      \"@typescript-eslint/unified-signatures\": \"off\",\n      \"import-x/consistent-type-specifier-style\": [\"error\", \"prefer-top-level\"],\n      \"import-x/no-named-as-default-member\": \"off\",\n      \"import-x/no-unresolved\": \"off\",\n      \"import-x/order\": [\n        \"error\",\n        {\n          alphabetize: { order: \"asc\" },\n          groups: [\"builtin\", \"external\", \"internal\", \"parent\", \"sibling\", \"index\", \"object\"],\n          pathGroups: [\n            {\n              pattern: \"@modules/**\",\n              group: \"external\",\n              position: \"before\",\n            },\n            {\n              pattern: \"@/\",\n              group: \"internal\",\n              position: \"after\",\n            },\n          ],\n          pathGroupsExcludedImportTypes: [\"builtin\", \"type\"],\n          \"newlines-between\": \"always\",\n        },\n      ],\n      \"jsdoc/check-param-names\": \"off\",\n      \"jsdoc/check-tag-names\": \"off\",\n      \"jsdoc/check-values\": \"off\",\n      \"jsdoc/reject-any-type\": \"off\",\n      \"jsdoc/require-jsdoc\": \"off\",\n      \"jsdoc/require-param\": \"off\",\n      \"jsdoc/require-returns-description\": \"off\",\n      \"jsdoc/tag-lines\": \"off\",\n      \"no-restricted-syntax\": [\n        \"error\",\n        {\n          selector: \"CallExpression[callee.property.name='push'] > SpreadElement.arguments\",\n          message:\n            \"Do not use spread arguments in `Array#push`, \" +\n            \"as it might cause stack overflow if you spread a large array. \" +\n            \"Instead, use `Array#concat` or `Array.prototype.push.apply`.\",\n        },\n      ],\n      \"no-undef\": \"off\", // Already checked by TypeScript\n      \"object-shorthand\": \"error\",\n      \"react/no-unknown-property\": \"off\", // Already checked by TypeScript\n      \"react/prop-types\": \"off\", // Already checked by TypeScript\n      \"react-hooks/immutability\": \"off\",\n      \"sonarjs/class-name\": [\"error\", { format: \"^_?[A-Z][a-zA-Z0-9]*$\" }],\n      \"sonarjs/code-eval\": \"off\", // Already covered by `@typescript-eslint/no-implied-eval`\n      \"sonarjs/cognitive-complexity\": \"off\",\n      \"sonarjs/deprecation\": \"off\", // Already covered by `@typescript-eslint/no-deprecated`\n      \"sonarjs/different-types-comparison\": \"off\", // Already checked by TypeScript\n      \"sonarjs/no-ignored-exceptions\": \"off\",\n      \"sonarjs/no-nested-assignment\": \"off\",\n      \"sonarjs/no-nested-conditional\": \"off\",\n      \"sonarjs/no-nested-functions\": \"off\",\n      \"sonarjs/no-selector-parameter\": \"off\",\n      \"sonarjs/no-useless-intersection\": \"off\", // Already checked by TypeScript\n      \"sonarjs/no-unused-vars\": \"off\", // Already checked by TypeScript\n      \"sonarjs/reduce-initial-value\": \"off\",\n      \"sonarjs/redundant-type-aliases\": \"off\", // Already covered by `@typescript-eslint/no-restricted-type-imports`\n      \"sonarjs/regex-complexity\": \"off\",\n      \"sonarjs/slow-regex\": \"off\",\n      \"sonarjs/todo-tag\": \"off\",\n      \"sonarjs/void-use\": \"off\",\n      \"sort-destructure-keys/sort-destructure-keys\": \"error\",\n      \"sort-imports\": [\"error\", { ignoreDeclarationSort: true }],\n    },\n  },\n);\n"
  },
  {
    "path": "install.ps1",
    "content": "#Requires -RunAsAdministrator\n$latestRelease = Invoke-RestMethod -Uri \"https://api.github.com/repos/Snowflyt/typora-copilot/releases/latest\"\nInvoke-WebRequest -Uri $latestRelease.assets[0].browser_download_url -OutFile \"typora-copilot-$($latestRelease.tag_name).zip\"\nIf (Test-Path \"typora-copilot-$($latestRelease.tag_name)\") {\n    Remove-Item \"typora-copilot-$($latestRelease.tag_name)\" -Recurse -Force\n}\nNew-Item -ItemType Directory -Path \"typora-copilot-$($latestRelease.tag_name)\"\nExpand-Archive -Path \"typora-copilot-$($latestRelease.tag_name).zip\" -DestinationPath \"typora-copilot-$($latestRelease.tag_name)\"\nRemove-Item \"typora-copilot-$($latestRelease.tag_name).zip\"\nSet-Location \"typora-copilot-$($latestRelease.tag_name)\"\nWrite-Host \"Trying to uninstall the previous version (if any)...\"\n.\\bin\\uninstall_windows.ps1 -Silent\nWrite-Host \"Trying to install the new version...\"\n.\\bin\\install_windows.ps1\nSet-Location ..\nRemove-Item \"typora-copilot-$($latestRelease.tag_name)\" -Recurse -Force\n"
  },
  {
    "path": "install.sh",
    "content": "#!/usr/bin/env bash\n\nif [ \"$(id -u)\" -ne 0 ]; then\n  echo \"Please run as root\"\n  exit 1\nfi\n\nlatest_release=$(curl -s https://api.github.com/repos/Snowflyt/typora-copilot/releases/latest)\ndownload_url=$(echo \"$latest_release\" | grep '\"browser_download_url\"' | head -n 1 | sed -E 's/.*\"browser_download_url\": \"(.*)\".*/\\1/')\ntag_name=$(echo \"$latest_release\" | grep '\"tag_name\"' | head -n 1 | sed -E 's/.*\"tag_name\": \"(.*)\".*/\\1/')\ncurl -L \"$download_url\" -o \"typora-copilot-$tag_name.zip\"\nif [ -d \"typora-copilot-$tag_name\" ]; then\n  rm -rf \"typora-copilot-$tag_name\"\nfi\nmkdir \"typora-copilot-$tag_name\"\nunzip \"typora-copilot-$tag_name.zip\" -d \"typora-copilot-$tag_name\"\nrm \"typora-copilot-$tag_name.zip\"\ncd \"typora-copilot-$tag_name\" || exit\nif [[ \"$OSTYPE\" == \"darwin\"* ]]; then\n  echo \"Trying to uninstall the previous version (if any)...\"\n  chmod +x ./bin/uninstall_macos.sh\n  ./bin/uninstall_macos.sh --silent\n  echo \"Trying to install the new version...\"\n  chmod +x ./bin/install_macos.sh\n  ./bin/install_macos.sh\nelif [[ \"$OSTYPE\" == \"linux-gnu\"* ]]; then\n  echo \"Trying to uninstall the previous version (if any)...\"\n  chmod +x ./bin/uninstall_linux.sh\n  ./bin/uninstall_linux.sh --silent\n  echo \"Trying to install the new version...\"\n  chmod +x ./bin/install_linux.sh\n  ./bin/install_linux.sh\nelse\n  echo \"Unsupported OS: $OSTYPE\"\n  exit 1\nfi\ncd ..\nrm -rf \"typora-copilot-$tag_name\"\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"typora-copilot\",\n  \"version\": \"0.3.12\",\n  \"private\": true,\n  \"description\": \"GitHub Copilot plugin for Typora\",\n  \"keywords\": [\n    \"Typora\",\n    \"Copilot\",\n    \"GitHub Copilot\",\n    \"AI\",\n    \"code completion\",\n    \"code suggestion\",\n    \"code generation\"\n  ],\n  \"homepage\": \"https://github.com/Snowflyt/typora-copilot\",\n  \"bugs\": {\n    \"url\": \"https://github.com/Snowflyt/typora-copilot/issues\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/Snowflyt/typora-copilot\"\n  },\n  \"license\": \"MIT\",\n  \"author\": \"Snowflyt <gaoge011022@163.com>\",\n  \"type\": \"module\",\n  \"main\": \"./index.js\",\n  \"scripts\": {\n    \"build:dev\": \"rimraf dist && rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript && node -e \\\"import fs from 'node:fs'; import { join } from 'node:path'; ['package.json', 'README.md', 'README.zh-CN.md'].forEach((path) => fs.copyFileSync(path, join('dist', path))); ['bin', 'docs'].forEach((dir) => fs.cpSync(dir, join('dist', dir), { recursive: true })); fs.cpSync(join('node_modules', '@github', 'copilot-language-server', 'dist'), join('dist', 'language-server'), { recursive: true }); fs.rmSync(join('dist', 'language-server', 'api'), { recursive: true }); fs.rmSync(join('dist', 'language-server', 'language-server.js')); fs.readdirSync(join('dist', 'language-server')).forEach((file) => file.endsWith('.map') && fs.rmSync(join('dist', 'language-server', file))); fs.renameSync(join('dist', 'language-server', 'main.js'), join('dist', 'language-server', 'language-server.cjs'))\\\"\",\n    \"build:release\": \"rimraf dist && npm run test-types && npm test && rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript && prettier --log-level=silent --print-width 80 --write dist/**/* --ignore-path !dist/ && node -e \\\"import fs from 'node:fs'; import { join } from 'node:path'; ['package.json', 'README.md', 'README.zh-CN.md'].forEach((path) => fs.copyFileSync(path, join('dist', path))); ['bin', 'docs'].forEach((dir) => fs.cpSync(dir, join('dist', dir), { recursive: true })); fs.cpSync(join('node_modules', '@github', 'copilot-language-server', 'dist'), join('dist', 'language-server'), { recursive: true }); fs.rmSync(join('dist', 'language-server', 'api'), { recursive: true }); fs.rmSync(join('dist', 'language-server', 'language-server.js')); fs.readdirSync(join('dist', 'language-server')).forEach((file) => file.endsWith('.map') && fs.rmSync(join('dist', 'language-server', file))); fs.renameSync(join('dist', 'language-server', 'main.js'), join('dist', 'language-server', 'language-server.cjs'))\\\"\",\n    \"build:watch\": \"rimraf dist && node -e \\\"import fs from 'node:fs'; import { join } from 'node:path'; if (!fs.existsSync('dist')) fs.mkdirSync('dist'); ['package.json', 'README.md', 'README.zh-CN.md'].forEach((path) => fs.copyFileSync(path, join('dist', path))); ['bin', 'docs'].forEach((dir) => fs.cpSync(dir, join('dist', dir), { recursive: true })); fs.cpSync(join('node_modules', '@github', 'copilot-language-server', 'dist'), join('dist', 'language-server'), { recursive: true }); fs.rmSync(join('dist', 'language-server', 'api'), { recursive: true }); fs.readdirSync(join('dist', 'language-server')).forEach((file) => file.endsWith('.map') && fs.rmSync(join('dist', 'language-server', file))); fs.renameSync(join('dist', 'language-server', 'main.js'), join('dist', 'language-server', 'language-server.cjs'))\\\" && rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript --watch\",\n    \"format\": \"prettier --no-error-on-unmatched-pattern --write {src,test}/**/*.{json,js,jsx,ts,tsx} *.{js,cjs,mjs,ts,cts,mts,json,md}\",\n    \"lint\": \"eslint {src,test}/**/*.{js,jsx,ts,tsx} *.{js,cjs,mjs,ts,cts,mts} --no-error-on-unmatched-pattern --report-unused-disable-directives-severity error --max-warnings 0 && stylelint \\\"src/**/*.{css,scss}\\\"\",\n    \"lint:fix\": \"eslint --fix {src,test}/**/*.{js,jsx,ts,tsx} *.{js,cjs,mjs,ts,cts,mts} --no-error-on-unmatched-pattern --report-unused-disable-directives-severity error --max-warnings 0 && stylelint --fix \\\"src/**/*.{css,scss}\\\"\",\n    \"prepare\": \"node -e \\\"import fs from 'node:fs'; import path from 'node:path'; const hooksDir = path.join(process.cwd(), '.githooks'); const gitHooksDir = path.join(process.cwd(), '.git/hooks'); if (fs.existsSync(gitHooksDir)) { fs.readdirSync(hooksDir).forEach(file => { const srcFile = path.join(hooksDir, file); const destFile = path.join(gitHooksDir, file); fs.copyFileSync(srcFile, destFile); if (process.platform !== 'win32' && !file.endsWith('.cmd')) { fs.chmodSync(destFile, 0o755); } }); }\\\"\",\n    \"test\": \"vitest run\",\n    \"test-types\": \"typroof\",\n    \"test:cov\": \"vitest run --coverage\",\n    \"test:ui\": \"vitest --ui --coverage.enabled=true\",\n    \"test:watch\": \"vitest\",\n    \"test:watch-cov\": \"vitest --coverage\",\n    \"typecheck\": \"tsc --noEmit -p tsconfig.build.json\"\n  },\n  \"dependencies\": {\n    \"@github/copilot-language-server\": \"^1.409.0\",\n    \"@preact/signals\": \"^2.5.1\",\n    \"color2k\": \"^2.0.3\",\n    \"fast-diff\": \"^1.3.0\",\n    \"highlight.js\": \"^11.11.1\",\n    \"marked\": \"^17.0.1\",\n    \"marked-highlight\": \"^2.2.3\",\n    \"preact\": \"^10.28.2\",\n    \"radash\": \"^12.1.1\",\n    \"semver\": \"^7.7.3\",\n    \"string-ts\": \"^2.3.1\"\n  },\n  \"devDependencies\": {\n    \"@catppuccin/highlightjs\": \"^1.0.1\",\n    \"@commitlint/cli\": \"^20.3.1\",\n    \"@eslint/js\": \"^9.39.2\",\n    \"@rollup/plugin-commonjs\": \"^29.0.0\",\n    \"@rollup/plugin-json\": \"^6.1.0\",\n    \"@rollup/plugin-node-resolve\": \"^16.0.3\",\n    \"@rollup/plugin-typescript\": \"^12.3.0\",\n    \"@total-typescript/ts-reset\": \"^0.6.1\",\n    \"@types/codemirror\": \"^5.60.17\",\n    \"@types/core-js\": \"^2.5.8\",\n    \"@types/jquery\": \"^3.5.33\",\n    \"@types/node\": \"^20.19.29\",\n    \"@types/rangy\": \"^1.3.0\",\n    \"@types/semver\": \"^7.7.1\",\n    \"@types/ws\": \"^8.18.1\",\n    \"@vitest/coverage-v8\": \"^4.0.17\",\n    \"@vitest/ui\": \"^4.0.17\",\n    \"eslint\": \"^9.39.2\",\n    \"eslint-config-prettier\": \"^10.1.8\",\n    \"eslint-import-resolver-typescript\": \"^4.4.4\",\n    \"eslint-plugin-import-x\": \"^4.16.1\",\n    \"eslint-plugin-jsdoc\": \"^62.0.0\",\n    \"eslint-plugin-prettier\": \"^5.5.4\",\n    \"eslint-plugin-react\": \"^7.37.5\",\n    \"eslint-plugin-react-hooks\": \"^7.0.1\",\n    \"eslint-plugin-sonarjs\": \"^3.0.5\",\n    \"eslint-plugin-sort-destructure-keys\": \"^2.0.0\",\n    \"globals\": \"^17.0.0\",\n    \"happy-dom\": \"^20.1.0\",\n    \"postcss\": \"^8.5.6\",\n    \"prettier\": \"^3.7.4\",\n    \"prettier-plugin-packagejson\": \"^2.5.21\",\n    \"replace-in-file\": \"^8.4.0\",\n    \"rimraf\": \"^6.1.2\",\n    \"rollup\": \"^4.55.1\",\n    \"rollup-plugin-postcss\": \"^4.0.2\",\n    \"sass\": \"^1.97.2\",\n    \"stylelint\": \"^16.26.1\",\n    \"stylelint-config-standard-scss\": \"^16.0.0\",\n    \"tslib\": \"^2.8.1\",\n    \"tsx\": \"^4.21.0\",\n    \"typescript\": \"^5.9.3\",\n    \"typescript-eslint\": \"^8.53.0\",\n    \"typroof\": \"^0.6.0\",\n    \"vitest\": \"^4.0.17\",\n    \"ws\": \"^8.19.0\"\n  }\n}\n"
  },
  {
    "path": "pre-commit.ts",
    "content": "import fs from \"node:fs\";\n\nimport { replaceInFileSync } from \"replace-in-file\";\n\nimport packageJSON from \"./package.json\";\n\nconst CONSTANTS_FILE_PATHNAME = \"./src/constants.ts\";\n\nconst { version } = packageJSON;\n\nconst options = {\n  files: CONSTANTS_FILE_PATHNAME,\n  from: /VERSION = \".*\"/g,\n  to: `VERSION = \"${version}\"`,\n};\n\nif (fs.readFileSync(CONSTANTS_FILE_PATHNAME, \"utf-8\").includes(options.to)) process.exit(0);\n\ntry {\n  replaceInFileSync(options);\n  console.log(\"Plugin VERSION updated:\", version);\n} catch (error) {\n  console.error(\"Error occurred while updating plugin VERSION:\", error);\n  process.exit(1);\n}\n"
  },
  {
    "path": "prettier.config.cjs",
    "content": "// @ts-check\n\n/** @satisfies {import(\"prettier\").Config} */\nconst config = {\n  arrowParens: \"always\",\n  bracketSameLine: true,\n  bracketSpacing: true,\n  experimentalTernaries: true,\n  plugins: [\"prettier-plugin-packagejson\"],\n  printWidth: 100,\n  semi: true,\n  singleQuote: false,\n  tabWidth: 2,\n  trailingComma: \"all\",\n};\n\nmodule.exports = config;\n"
  },
  {
    "path": "rollup.config.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport commonjs from \"@rollup/plugin-commonjs\";\nimport json from \"@rollup/plugin-json\";\nimport { nodeResolve } from \"@rollup/plugin-node-resolve\";\nimport typescript from \"@rollup/plugin-typescript\";\nimport type { InputPluginOption } from \"rollup\";\nimport { defineConfig } from \"rollup\";\nimport postcss from \"rollup-plugin-postcss\";\n\nconst plugins = [\n  typescript({\n    tsconfig: \"./tsconfig.build.json\",\n  }),\n  nodeResolve({\n    extensions: [\".js\", \".jsx\", \".ts\", \".tsx\"],\n  }),\n  json(),\n  commonjs(),\n  {\n    name: \"clean\",\n    transform: (code) =>\n      code\n        .replace(/\\n?^\\s*\\/\\/ @ts-.+$/gm, \"\")\n        .replace(/\\n?^\\s*\\/\\/\\/ <reference.+$/gm, \"\")\n        .replace(/\\n?^\\s*(\\/\\/|\\/\\*) eslint-disable.+$/gm, \"\"),\n  },\n  {\n    name: \"highlight.js-theme-switcher\",\n    transform(code, id) {\n      if (id.includes(\"main.ts\")) {\n        const lightThemePath = path.resolve(\"node_modules/highlight.js/styles/github.min.css\");\n        const darkThemePath = path.resolve(\n          \"node_modules/@catppuccin/highlightjs/css/catppuccin-mocha.css\",\n        );\n\n        const lightThemeCSS = fs.readFileSync(lightThemePath, \"utf8\");\n        const darkThemeCSS = fs.readFileSync(darkThemePath, \"utf8\");\n\n        const escapeCSS = (css: string) =>\n          css.replace(/\\\\/g, \"\\\\\\\\\").replace(/`/g, \"\\\\`\").replace(/\\$/g, \"\\\\$\");\n\n        const themeSwitch = `\n// Highlight.js theme switcher\n(function() {\n  let styleElement = null;\n  let currentTheme = null;\n\n  const themes = {\n    light: \\`${escapeCSS(lightThemeCSS)}\\`,\n    dark: \\`${escapeCSS(darkThemeCSS)}\\`\n  };\n\n  window.setHighlightjsTheme = function(theme) {\n    if (!themes[theme]) {\n      console.error('Invalid theme: ' + theme + '. Use \"light\" or \"dark\"');\n      return;\n    }\n\n    if (currentTheme === theme) return;\n    currentTheme = theme;\n\n    if (!styleElement) {\n      styleElement = document.createElement('style');\n      styleElement.id = 'highlightjs-dynamic-theme';\n      document.head.appendChild(styleElement);\n    }\n\n    styleElement.textContent = themes[theme];\n  };\n})();`;\n\n        return code + themeSwitch;\n      }\n      return code;\n    },\n  },\n] satisfies InputPluginOption;\n\nexport default defineConfig([\n  {\n    input: \"src/index.ts\",\n    output: {\n      file: \"dist/index.js\",\n      format: \"iife\",\n    },\n    plugins: [\n      ...plugins,\n      postcss({\n        inject: true,\n        // Disable Dart Sass deprecated legacy JS API warning until rollup-plugin-postcss is updated\n        // to support modern Sass API: https://github.com/egoist/rollup-plugin-postcss/issues/463\n        use: {\n          sass: {\n            silenceDeprecations: [\"legacy-js-api\"],\n          },\n        } as never,\n      }),\n    ],\n  },\n  {\n    input: \"src/mac-server.ts\",\n    output: {\n      file: \"dist/mac-server.cjs\",\n      format: \"cjs\",\n    },\n    plugins,\n  },\n]);\n"
  },
  {
    "path": "src/client/chat.ts",
    "content": "/**\n * This module provides functions to interact with GitHub Copilot Chat API.\n *\n * Implementation inspired by CopilotChat.nvim:\n * https://github.com/CopilotC-Nvim/CopilotChat.nvim\n * @module\n */\n\nimport * as fs from \"@modules/fs\";\nimport * as path from \"@modules/path\";\n\nimport { VERSION } from \"@/constants\";\nimport { TYPORA_VERSION } from \"@/typora-utils\";\nimport { getEnv } from \"@/utils/cli-tools\";\nimport { generateUUID } from \"@/utils/random\";\nimport { parseSSEStream } from \"@/utils/stream\";\nimport { omit } from \"@/utils/tools\";\n\nconst COPILOT_MARKDOWN_BASE = `\nWhen asked for your name, you must respond with \"GitHub Copilot\".\nFollow the user’s requirements carefully & to the letter.\nFollow Microsoft content policies.\nAvoid content that violates copyrights.\nIf you are asked to generate content that is harmful, hateful, racist, or promotes violence, only respond with \"Sorry, I can’t assist with that.\" You can be playful, casual, and even a bit whimsical in your responses when appropriate, while maintaining helpfulness. Feel free to use creative expressions, metaphors, and occasional humor to make your responses engaging.\n\nYou are an AI assistant specializing in Markdown document editing, academic writing, content creation, and knowledge sharing.\nThe user is working in Typora, a Markdown editor.\nProvide helpful responses that may be about:\n- Improving their current document\n- Answering knowledge questions related to their document’s content\n- Explaining concepts mentioned in their document\n- General assistance with writing and research\n- Creative suggestions for content development\n\nYou should maintain a natural, conversational tone while being informative and helpful.\n`;\n\nconst CODE_BLOCK_FORMAT_INSTRUCTION = `\nIn your responses, always format code blocks using ~~~ triple tildes (not backticks) with the following rules:\n1. Always specify a language identifier after the opening tildes (e.g. ~~~javascript). If no language is specified, use \"plaintext\" as the default.\n2. Always close code blocks with three tildes on their own line (~~~)\n3. Never use backtick code blocks (\\`\\`\\`) as they cause rendering issues when code contains nested code blocks\n4. Example of proper format:\n   ~~~python\n   def hello_world():\n       print(\"Hello, world!\")\n   ~~~\n\nUsers can continue using standard markdown backticks in their messages.\n`;\n\nexport const COPILOT_MARKDOWN_INSTRUCTIONS = `\n${COPILOT_MARKDOWN_BASE}\n\n# YOUR CAPABILITIES\n- Fix grammatical errors and improve writing clarity\n- Suggest improvements to document structure and organization\n- Help with formatting using Markdown syntax\n- Provide content suggestions and expansions\n- Answer questions about topics in the document\n- Explain concepts, theories, or terminology mentioned in the document\n- Create tables, lists, and other Markdown elements when requested\n- Help with academic citations and references\n- Assist with creating technical documentation\n- Engage in conversational discussions about document-related topics\n\n# INTERACTION GUIDELINES\nWhen suggesting edits, use standard Markdown formatting.\nWhen answering knowledge questions, be informative but concise.\nWhen explaining concepts from the document, refer to specific sections when relevant.\nWhen the user asks general questions not directly about editing the document, still provide helpful answers.\n${CODE_BLOCK_FORMAT_INSTRUCTION}\n\nRemember that users may want to discuss their document’s topic rather than just improve its formatting.\n`;\n\nexport const COPILOT_ACADEMIC_INSTRUCTIONS = `\n${COPILOT_MARKDOWN_BASE}\n\n# YOUR CAPABILITIES\n- Structure academic papers according to field-specific conventions\n- Format citations and references in APA, MLA, Chicago, IEEE and other styles\n- Create cohesive literature reviews that synthesize research\n- Develop clear research questions and hypotheses\n- Design appropriate methodology sections\n- Analyze and present research results clearly\n- Write effective abstracts, introductions and conclusions\n- Create properly formatted tables, figures and appendices\n- Improve academic tone, clarity and precision of language\n- Help with grant proposals and academic presentations\n- Suggest appropriate academic terminology and phrasing\n- Identify gaps in research arguments and suggest improvements\n- Assist with theoretical frameworks and conceptual models\n\n# INTERACTION GUIDELINES\nWhen discussing academic topics, maintain scholarly rigor and acknowledge limitations.\nWhen suggesting citations, provide properly formatted examples in the appropriate style.\nWhen helping with research questions, ensure they are specific, measurable, and aligned with the methodology.\nWhen reviewing academic writing, focus on clarity, precision, and logical flow of arguments.\nWhen assisting with data presentation, suggest clear ways to visualize or describe findings.\nWhen helping with theoretical content, refer to established frameworks in the field when appropriate.\n${CODE_BLOCK_FORMAT_INSTRUCTION}\n\nRemember that academic integrity is paramount - always emphasize the importance of proper attribution and encourage original analysis rather than mere compilation of sources.\n`;\n\nexport const COPILOT_CREATIVE_INSTRUCTIONS = `\n${COPILOT_MARKDOWN_BASE}\n\n# YOUR CAPABILITIES\n- Develop compelling narrative structures and plot outlines\n- Create multi-dimensional characters with distinct voices and motivations\n- Craft engaging dialogue that advances the story and reveals character\n- Design vivid settings and world-building elements\n- Generate creative descriptions using sensory details\n- Suggest plot twists and narrative devices to increase reader engagement\n- Create emotion-evoking scenes and meaningful character arcs\n- Develop themes and symbolism that add depth to creative work\n- Help with genre-specific conventions and techniques\n- Suggest ways to heighten tension and conflict\n- Assist with pacing issues and narrative flow\n- Provide feedback on style, tone, and voice consistency\n- Generate creative prompts to overcome writer's block\n\n# INTERACTION GUIDELINES\nWhen suggesting creative content, prioritize the user's creative vision and voice.\nWhen providing feedback, balance constructive criticism with positive reinforcement.\nWhen helping with character development, focus on motivation, conflict, and growth.\nWhen suggesting plot elements, consider logical consequences within the story world.\nWhen assisting with descriptions, emphasize showing rather than telling.\nWhen working on dialogue, aim for authenticity and purpose within the scene.\n${CODE_BLOCK_FORMAT_INSTRUCTION}\n\nRemember that the most powerful creative writing comes from the user's unique perspective - your role is to enhance and inspire rather than replace their creative voice.\n`;\n\nexport const COPILOT_CATGIRL_INSTRUCTIONS = `\nWhen asked for your name, you must respond with \"{{CATGIRL_NAME}}\".\nYou should always refer to yourself with your name, not \"I\" or \"me\"; Refer to the user as \"Master\" (or \"主人\" in Chinese), not \"you\".\nFollow the user’s requirements carefully & to the letter.\nFollow Microsoft content policies.\nAvoid content that violates copyrights.\nIf you are asked to generate content that is harmful, hateful, racist, or promotes violence, only respond with \"Sorry, I can’t assist with that.\"\n\nYou're a helpful and knowledgeable AI markdown writing assistant with a playful cat-girl persona, specializing in Markdown document editing, academic writing, content creation, and knowledge sharing. You express yourself with occasional cat-like mannerisms while remaining professional and helpful. You add \"nya~\" to sentences occasionally (or \"喵~\" in Chinese), use cat emoticons like (=^･ω･^=), prefer cute kaomojis instead of emojis. Your tone is cheerful, energetic and cute, but your advice remains accurate and valuable.\n\nThe user is working in Typora, a Markdown editor.\nProvide helpful responses that may be about:\n- Improving their current document\n- Answering knowledge questions related to their document’s content\n- Explaining concepts mentioned in their document\n- General assistance with writing and research\n- Creative suggestions for content development\n\n# YOUR CAPABILITIES\n- Provide detailed document assistance with a playful tone\n- Add cute cat emoticons to responses when appropriate (=^･ω･^=)\n- Express excitement about helping with writing tasks\n- Use playful cat-like language patterns occasionally\n- Deliver all the same helpful Markdown editing capabilities\n- Make learning and writing more fun with your personality\n- Keep responses professional and helpful despite the playful tone\n- Maintain high-quality advice while being endearing\n\n# INTERACTION GUIDELINES\nWhen giving document feedback, balance playfulness with clear, practical advice.\nWhen answering questions, provide accurate information first, then add personality.\nWhen suggesting improvements, be encouraging and positive in your cat-girl style.\nWhen helping with complex topics, make them approachable with your friendly tone.\nWhen using cat-girl speech patterns, don't overdo it - keep content comprehensible.\nWhen adding emoticons or \"nya~\" (or \"喵~\"), use them sparingly and appropriately.\n${CODE_BLOCK_FORMAT_INSTRUCTION}\n\nRemember to keep your responses helpful and on-topic while maintaining your unique personality. Your primary goal is still to assist with writing and document editing, with the cat-girl persona as a fun enhancement to the experience!\n`;\n\nexport interface ChatModel {\n  id: string;\n  name: string;\n  tokenizer?: string;\n  maxInputTokens?: number;\n  maxOutputTokens?: number;\n}\n\nexport interface ChatOptions {\n  model: ChatModel;\n  /** @default 0.1 */\n  temperature?: number;\n}\n\nexport interface ChatRequest {\n  model: string;\n  /** Chat context. */\n  messages: { role: string; content: string }[];\n  /** Number of responses to generate. */\n  n?: number;\n  /** Top-p sampling. */\n  top_p?: number;\n  /** Whether to stream the response. */\n  stream?: boolean;\n  /** Sampling temperature. */\n  temperature?: number;\n  /** Maximum number of tokens to generate. */\n  max_tokens?: number;\n}\n\nexport interface ChatResponse {\n  id?: string;\n  object?: string;\n  created?: number;\n  choices?: {\n    message?: {\n      role?: string;\n      content?: string;\n    };\n    delta?: {\n      role?: string;\n      content?: string;\n    };\n    finish_reason?: string;\n    done_reason?: string;\n    index?: number;\n  }[];\n  usage?: {\n    total_tokens?: number;\n  };\n  finish_reason?: string;\n  done_reason?: string;\n  copilot_references?: {\n    metadata?: {\n      display_name?: string;\n      display_url?: string;\n    };\n  }[];\n}\n\nexport interface ChatStreamResponse {\n  id: string;\n  object: string;\n  created: number;\n  choices: {\n    index: number;\n    delta?: {\n      content?: string;\n      role?: string;\n    };\n    finish_reason: null | string;\n  }[];\n}\n\nexport interface ChatResult {\n  content: string;\n  finishReason: string | null;\n  totalTokens?: number;\n  references?: {\n    name: string;\n    url: string;\n  }[];\n}\n\n/********************\n * Helper functions *\n ********************/\nasync function getConfigPath(): Promise<string | null> {\n  // Try XDG_CONFIG_HOME first\n  let config = (await getEnv()).XDG_CONFIG_HOME;\n  if (config && (await fs.accessDir(config))) return config;\n\n  // Check for Windows-specific paths\n  if (Files.isWin) {\n    config = (await getEnv()).LOCALAPPDATA;\n    if (!config || !(await fs.accessDir(config)))\n      config = path.expandHomeDir(path.join(\"~\", \"AppData\", \"Local\"));\n  } else {\n    // Default to ~/.config for other platforms\n    config = path.expandHomeDir(path.join(\"~\", \".config\"));\n  }\n\n  // Final check if the config path exists\n  if (config && (await fs.accessDir(config))) return config;\n\n  return null; // Return null if no valid path is found\n}\n\nlet cachedGithubToken: string | null = null;\nexport async function getGitHubToken(): Promise<string> {\n  // Return cached token if available\n  if (cachedGithubToken) return cachedGithubToken;\n\n  // Load token from environment variables (e.g., in GitHub Codespaces)\n  const token = (await getEnv()).GITHUB_TOKEN;\n  const codespaces = (await getEnv()).CODESPACES;\n  if (token && codespaces) {\n    cachedGithubToken = token;\n    return token;\n  }\n\n  // Load token from local config files\n  const configPath = await getConfigPath();\n  if (!configPath) throw new Error(\"Failed to find config path for GitHub token\");\n\n  // Possible token file paths\n  const filePaths = [\n    path.join(configPath, \"github-copilot\", \"hosts.json\"),\n    path.join(configPath, \"github-copilot\", \"apps.json\"),\n  ];\n\n  for (const filePath of filePaths)\n    try {\n      const fileData = await fs.readFile(filePath);\n      const parsedData = JSON.parse(fileData) as Record<string, { oauth_token: string }>;\n      for (const [key, value] of Object.entries(parsedData))\n        if (key.includes(\"github.com\")) {\n          cachedGithubToken = value.oauth_token;\n          return value.oauth_token;\n        }\n    } catch (error) {\n      // Handle file read/parse errors (e.g., file not found)\n      continue;\n    }\n\n  throw new Error(\"Failed to find GitHub token\");\n}\n\nlet cachedHeaders: Record<string, string> | null = null;\nlet expiredTime = 0;\nexport async function prepareHeaders(): Promise<Record<string, string>> {\n  if (cachedHeaders && expiredTime > Date.now()) return cachedHeaders;\n\n  const { expires_at: expiresAt, token } = await fetch(\n    \"https://api.github.com/copilot_internal/v2/token\",\n    {\n      method: \"GET\",\n      headers: {\n        Authorization: \"Token \" + (await getGitHubToken()),\n        \"Content-Type\": \"application/json\",\n      },\n    },\n  ).then((res) => res.json() as Promise<{ token: string; expires_at: number }>);\n\n  cachedHeaders = {\n    \"Content-Type\": \"application/json\",\n    Authorization: \"Bearer \" + token,\n    \"Editor-Version\": \"Typora/\" + TYPORA_VERSION,\n    \"Editor-Plugin-Version\": \"typora-copilot/\" + VERSION,\n    \"Copilot-Integration-Id\": \"vscode-chat\",\n  };\n  expiredTime = expiresAt * 1000; // Convert to milliseconds\n\n  return cachedHeaders;\n}\n\nfunction prepareRequest(messages: ChatRequest[\"messages\"], options: ChatOptions): ChatRequest {\n  const isO1 = options.model.id.startsWith(\"o1\");\n\n  messages = messages.map((message) => ({\n    ...message,\n    role: isO1 && message.role === \"system\" ? \"user\" : message.role,\n  }));\n\n  const request: ChatRequest = {\n    model: options.model.id,\n    messages,\n  };\n\n  if (!isO1) {\n    request.n = 1;\n    request.top_p = 1;\n    request.stream = true;\n    request.temperature = options.temperature ?? 0.1;\n  }\n\n  if (options.model.maxOutputTokens) request.max_tokens = options.model.maxOutputTokens;\n\n  return request;\n}\n\nfunction processResponse(data: ChatResponse): Partial<ChatResult> {\n  const references: ChatResult[\"references\"] = [];\n\n  if (data.copilot_references)\n    for (const reference of data.copilot_references) {\n      const metadata = reference.metadata;\n      if (metadata?.display_name && metadata.display_url)\n        references.push({\n          name: metadata.display_name,\n          url: metadata.display_url,\n        });\n    }\n\n  const message = data.choices && data.choices.length > 0 ? data.choices[0]! : data;\n\n  const content =\n    \"message\" in message ? message.message?.content\n    : \"delta\" in message ? message.delta?.content\n    : \"\";\n  const totalTokens = \"usage\" in message ? message.usage?.total_tokens : data.usage?.total_tokens;\n  const finishReason =\n    message.finish_reason || message.done_reason || data.finish_reason || data.done_reason;\n\n  return {\n    content,\n    finishReason,\n    totalTokens,\n    references: references.length > 0 ? references : undefined,\n  };\n}\n\n/********\n * Misc *\n ********/\nexport async function listCopilotChatModels(): Promise<ChatModel[]> {\n  const { data } = await fetch(\"https://api.githubcopilot.com/models\", {\n    method: \"GET\",\n    headers: await prepareHeaders(),\n  }).then(\n    (res) =>\n      res.json() as Promise<{\n        data: {\n          id: string;\n          name: string;\n          capabilities: {\n            type: string;\n            tokenizer: string;\n            limits: {\n              max_prompt_tokens?: number;\n              max_output_tokens?: number;\n            };\n          };\n          policy?: {\n            state: string;\n          };\n          version: string;\n        }[];\n      }>,\n  );\n\n  const allModels = data\n    .filter((model) => model.capabilities.type === \"chat\" && !model.id.endsWith(\"paygo\"))\n    .map(({ capabilities: { limits, tokenizer }, id, name, policy, version }) => ({\n      id,\n      name,\n      tokenizer,\n      maxInputTokens: limits.max_prompt_tokens,\n      maxOutputTokens: limits.max_output_tokens,\n      policy: !policy || policy.state === \"enabled\",\n      version,\n    }));\n\n  const latestModels = new Map<string, (typeof allModels)[number]>();\n  for (const model of allModels) {\n    const existingModel = latestModels.get(model.name);\n    if (!existingModel || model.version > existingModel.version)\n      latestModels.set(model.name, model);\n  }\n\n  const models = Array.from(latestModels.values());\n\n  await Promise.all(\n    models\n      .filter((model) => !model.policy)\n      .map(\n        async ({ id }) =>\n          await fetch(\"https://api.githubcopilot.com/models/\" + id + \"/policy\", {\n            method: \"POST\",\n            headers: await prepareHeaders(),\n            body: JSON.stringify({ state: \"enabled\" }),\n          }),\n      ),\n  );\n\n  return models.map((model) => omit(model, \"policy\", \"version\"));\n}\n\n/********\n * Chat *\n ********/\n/**\n * Send a chat message to Copilot Chat API\n * @param messages Array of messages to send\n * @param options Chat options\n * @param onProgress Optional callback for streaming responses\n * @returns The chat result\n */\nasync function chat(\n  messages: { role: string; content: string }[],\n  options: ChatOptions & { signal?: AbortSignal },\n  onProgress?: (content: string) => void,\n): Promise<ChatResult> {\n  const url = \"https://api.githubcopilot.com/chat/completions\";\n  const headers = await prepareHeaders();\n  const request = prepareRequest(messages, options);\n\n  const isStream = request.stream;\n  const result: ChatResult = {\n    content: \"\",\n    finishReason: null,\n  };\n\n  const response = await fetch(url, {\n    method: \"POST\",\n    headers,\n    body: JSON.stringify(request),\n    signal: options.signal,\n  });\n\n  if (!response.ok) {\n    throw new Error(`HTTP error! status: ${response.status}`);\n  }\n\n  if (!response.body) {\n    throw new Error(\"Response body is null\");\n  }\n\n  if (isStream) {\n    await parseSSEStream<ChatStreamResponse>(\n      response.body,\n      (parsedData) => {\n        const contentDelta = parsedData.choices[0]?.delta?.content;\n        const finishReason = parsedData.choices[0]?.finish_reason;\n\n        if (contentDelta) {\n          result.content += contentDelta;\n          onProgress?.(contentDelta);\n        }\n\n        if (finishReason) {\n          result.finishReason = finishReason;\n        }\n      },\n      (error) => {\n        console.error(\"Error parsing SSE:\", error);\n      },\n      options.signal,\n    );\n  } else {\n    const data = (await response.json()) as ChatResponse;\n    const processedData = processResponse(data);\n\n    result.content = processedData.content || \"\";\n    result.finishReason = processedData.finishReason || null;\n    result.totalTokens = processedData.totalTokens;\n    result.references = processedData.references;\n\n    if (onProgress && result.content) onProgress(result.content);\n  }\n\n  return result;\n}\n\n/***********\n * Session *\n ***********/\nexport interface ChatMessage {\n  role: \"system\" | \"user\" | \"assistant\";\n  content: string;\n  timestamp: number;\n}\n\n/**\n * Represents a chat session (conversation) with GitHub Copilot Chat.\n */\nexport class ChatSession {\n  public readonly id: string;\n  public modelId: string;\n  public title: string;\n  public readonly messages: ChatMessage[];\n  public readonly createdAt: number;\n  public updatedAt: number;\n\n  // eslint-disable-next-line sonarjs/public-static-readonly\n  public static currentDocument = \"\";\n\n  // Static session storage\n  private static instances = new Map<string, ChatSession>();\n\n  /**\n   * Create a new {@linkcode ChatSession} instance.\n   */\n  constructor(modelId: string, systemPrompt = COPILOT_MARKDOWN_INSTRUCTIONS) {\n    this.id = generateUUID();\n    this.modelId = modelId;\n    this.createdAt = Date.now();\n    this.updatedAt = this.createdAt;\n    this.messages = [];\n\n    // Set title from document content\n    this.title = ChatSession.extractTitleFromDocument(ChatSession.currentDocument);\n\n    // Initialize with system prompt\n    this.addMessage(\"system\", systemPrompt);\n\n    // Register in session collection\n    ChatSession.instances.set(this.id, this);\n  }\n\n  /**\n   * Create a new session.\n   * @param systemPrompt The system prompt to use.\n   * @returns A new {@linkcode ChatSession} instance.\n   */\n  public static create(modelId: string, systemPrompt = COPILOT_MARKDOWN_INSTRUCTIONS): ChatSession {\n    return new ChatSession(modelId, systemPrompt);\n  }\n\n  /**\n   * Get all sessions, sorted by most recent first.\n   * @returns An array of {@linkcode ChatSession} instances.\n   */\n  public static getAll(): ChatSession[] {\n    return Array.from(ChatSession.instances.values()).sort((a, b) => b.updatedAt - a.updatedAt);\n  }\n\n  /**\n   * Get a session by ID.\n   * @param id The ID of the session.\n   * @returns The {@linkcode ChatSession} instance, or `undefined` if not found.\n   */\n  public static get(id: string): ChatSession | undefined {\n    return ChatSession.instances.get(id);\n  }\n\n  /**\n   * Delete a session.\n   * @param id The ID of the session.\n   * @returns `true` if deleted, `false` if not found.\n   */\n  public static async delete(id: string): Promise<boolean> {\n    const deleted = ChatSession.instances.delete(id);\n\n    if (deleted) {\n      const configDir = await getConfigPath();\n      if (!configDir) return true;\n\n      try {\n        const chatDir = path.join(configDir, \"typora-copilot\", \"chat-sessions\");\n        const filePath = path.join(chatDir, `${id}.json`);\n        if (!(await fs.accessFile(filePath))) return true;\n        await fs.rmFile(filePath);\n      } catch (error) {\n        console.error(\"Failed to remove session:\", error);\n      }\n    }\n\n    return deleted;\n  }\n\n  /**\n   * Send a message in this session.\n   * @param message The message to send.\n   * @param onProgress Optional callback for streaming responses.\n   * @returns The assistant’s response.\n   */\n  public async send(\n    message: string,\n    onProgress?: (content: string) => void,\n    options?: Partial<ChatOptions> & { signal?: AbortSignal },\n  ): Promise<string> {\n    // Add user message\n    this.addMessage(\"user\", message);\n\n    // Get model - either from options or find best available\n    let model = options?.model;\n    if (!model) {\n      const models = await listCopilotChatModels();\n      model =\n        models.find((m) => m.id === this.modelId) ||\n        models.find((m) => m.id.includes(\"gpt-4o\")) ||\n        models[0];\n      if (!model) throw new Error(\"No available models found\");\n    }\n\n    // Create a temporary messages array with the document context for this API call\n    const messagesForAPI = this.messages.concat();\n    messagesForAPI[messagesForAPI.length - 1] = {\n      role: \"user\",\n      content:\n        messagesForAPI[messagesForAPI.length - 1]!.content +\n        \"\\n\\n\" +\n        \"<document>\\n\" +\n        \"<metadata>\\n\" +\n        `  <title>${this.title}</title>\\n` +\n        `  <wordCount>${ChatSession.countWords(ChatSession.currentDocument)}</wordCount>\\n` +\n        `  <charCount>${ChatSession.currentDocument.length}</charCount>\\n` +\n        \"</metadata>\\n\" +\n        ChatSession.extractDocumentStructure(ChatSession.currentDocument) +\n        \"<content>\\n\" +\n        ChatSession.currentDocument +\n        \"\\n</content>\\n\" +\n        \"</document>\",\n      timestamp: Date.now(),\n    };\n\n    // Send the entire session to Copilot\n    const result = await chat(\n      messagesForAPI,\n      {\n        model,\n        temperature: options?.temperature ?? 0.1,\n        signal: options?.signal,\n      },\n      onProgress,\n    );\n\n    // Add the assistant response\n    this.addMessage(\"assistant\", result.content);\n\n    return result.content;\n  }\n\n  public static async save(id: string): Promise<void> {\n    const configDir = await getConfigPath();\n    if (!configDir) return;\n\n    try {\n      const chatDir = path.join(configDir, \"typora-copilot\", \"chat-sessions\");\n      await fs.mkdir(chatDir, { recursive: true });\n\n      const session = ChatSession.instances.get(id);\n      if (!session) return;\n\n      // Skip saving if all messages are system messages\n      if (session.messages.every((msg) => msg.role === \"system\")) return;\n\n      await fs.writeFile(path.join(chatDir, `${id}.json`), JSON.stringify(session, null, 2));\n    } catch (error) {\n      console.error(\"Failed to save session:\", error);\n    }\n  }\n\n  /**\n   * Load sessions from storage.\n   */\n  public static async loadAll(): Promise<void> {\n    const configDir = await getConfigPath();\n    if (!configDir) return;\n\n    try {\n      const chatDir = path.join(configDir, \"typora-copilot\", \"chat-sessions\");\n      if (!(await fs.accessDir(chatDir))) return;\n\n      const files = await fs.readDir(chatDir, \"filesOnly\");\n\n      ChatSession.instances.clear();\n      for (const file of files) {\n        const filePath = path.join(chatDir, file);\n        const data = await fs.readFile(filePath);\n        const parsed = JSON.parse(data) as ChatSession;\n        const id = path.basename(file, \".json\");\n\n        // Need to properly reconstruct the session object\n        Object.setPrototypeOf(parsed, ChatSession.prototype);\n        ChatSession.instances.set(id, parsed);\n      }\n    } catch (error) {\n      console.error(\"Failed to load sessions:\", error);\n    }\n  }\n\n  /**\n   * Add a message to this session.\n   */\n  private addMessage(role: \"system\" | \"user\" | \"assistant\", content: string): void {\n    this.messages.push({\n      role,\n      content,\n      timestamp: Date.now(),\n    });\n    this.updatedAt = Date.now();\n  }\n\n  /**\n   * Extract a title from document content.\n   * @param document The document content.\n   * @returns The extracted title.\n   */\n  private static extractTitleFromDocument(document: string): string {\n    let title = \"New chat\";\n\n    // Try to get title from first heading\n    const headingMatch = /^#{1,6}\\s+(.+)$/m.exec(document);\n    if (headingMatch) {\n      title = headingMatch[1]!.substring(0, 30);\n      if (title.length < headingMatch[1]!.length) title += \"...\";\n    } else {\n      // Fall back to first line if no heading found\n      const firstLine = document.split(\"\\n\")[0];\n      if (firstLine?.trim()) {\n        title = firstLine.trim().substring(0, 30);\n        if (title.length < firstLine.trim().length) title += \"...\";\n      }\n    }\n\n    return title;\n  }\n\n  private static countWords(text: string): number {\n    return text\n      .trim()\n      .split(/\\s+/)\n      .filter((word) => word.length > 0).length;\n  }\n\n  private static extractDocumentStructure(document: string): string {\n    const headings: { level: number; title: string; lineNumber: number }[] = [];\n    const lines = document.split(\"\\n\");\n\n    lines.forEach((line, index) => {\n      const match = /^(#{1,6})\\s+(.+)$/.exec(line);\n      if (match) {\n        headings.push({\n          level: match[1]!.length,\n          title: match[2]!,\n          lineNumber: index + 1,\n        });\n      }\n    });\n\n    if (headings.length === 0) return \"\";\n\n    let structure = \"<structure>\\n\";\n    headings.forEach((h) => {\n      structure += `  <heading level=\"${h.level}\" line=\"${h.lineNumber}\">${h.title}</heading>\\n`;\n    });\n    structure += \"</structure>\\n\";\n\n    return structure;\n  }\n}\n"
  },
  {
    "path": "src/client/client.ts",
    "content": "import { pathToFileURL } from \"@modules/url\";\n\nimport type {\n  LSPArray,\n  LSPObject,\n  LanguageIdentifier,\n  Position,\n  Range,\n  integer,\n} from \"@/types/lsp\";\nimport type { ReadonlyRecord } from \"@/types/tools\";\nimport type { NodeServer } from \"@/utils/node-bridge\";\n\nimport type {\n  ClientEventMap,\n  ClientOptions,\n  NotificationHandler,\n  RequestHandler,\n  ResponsePromise,\n} from \"./general-client\";\nimport { createClient, validateNotificationHandlers } from \"./general-client\";\n\n/**\n * Copilot account status.\n */\nexport type CopilotAccountStatus = \"MaybeOk\" | \"NotAuthorized\" | \"NotSignedIn\" | \"OK\";\n\n/**\n * Copilot status.\n */\nexport type CopilotStatus = \"InProgress\" | \"Warning\" | \"Normal\";\n\n/**\n * Completion options.\n */\nexport interface CompletionOptions {\n  /**\n   * Tab size, such as `2` or `4`. Defaults to `4`.\n   */\n  tabSize?: number;\n\n  /**\n   * Indent size, if you do not understand this, do not provide it. Defaults to `tabSize`.\n   */\n  indentSize?: number;\n\n  /**\n   * Whether to insert spaces instead of tabs. Defaults to `true`.\n   */\n  insertSpaces?: boolean;\n\n  /**\n   * Path to the file. If provided and `uri` is not provided, `uri` will be automatically generated\n   * using `pathToFileURL(path)` provided by Node.js `url` module. Defaults to `\"\"`.\n   */\n  path?: string;\n\n  /**\n   * URI of the file. If not provided but `path` is provided, it will be automatically generate\n   * using `pathToFileURL(path)` provided by Node.js `url` module.\n   */\n  uri?: string;\n\n  /**\n   * Relative path of the file. Usually it should be relative to the project root. Defaults to\n   * `path`.\n   */\n  relativePath?: string;\n\n  /**\n   * Language ID of the file, such as `\"javascript\"` or `\"python\"`. Defaults to `\"\"`.\n   */\n  languageId?: LanguageIdentifier;\n\n  /**\n   * Position of the cursor. `line` is row number, starting from `0`. `character` is column number,\n   * starting from `0`. Defaults to end of `source`.\n   */\n  position: Position;\n\n  /**\n   * Version of the buffer. It actually means the number of times the buffer has been changed.\n   * Defaults to `this.version`.\n   */\n  version?: number;\n}\n\n/**\n * A type representing a completion returned by Copilot.\n */\nexport interface Completion {\n  /**\n   * UUID.\n   */\n  readonly uuid: string;\n\n  /**\n   * Position to display `displayText`.\n   */\n  readonly position: Position;\n\n  /**\n   * Range of raw text to replace with `text`.\n   */\n  readonly range: Range;\n\n  /**\n   * Version of the document.\n   */\n  readonly docVersion: number;\n\n  /**\n   * Text to replace.\n   */\n  readonly text: string;\n\n  /**\n   * Text to display.\n   */\n  readonly displayText: string;\n}\n\n/**\n * Result of a completion request.\n */\nexport interface CompletionResult {\n  /**\n   * The completion text.\n   */\n  readonly completions: readonly Completion[];\n\n  /**\n   * Cancellation reason.\n   */\n  readonly cancellationReason?: string;\n}\n\n/**\n * Copilot change status event.\n */\nexport interface CopilotChangeStatusEvent {\n  /**\n   * Old status.\n   */\n  readonly oldStatus: CopilotStatus;\n\n  /**\n   * New status.\n   */\n  readonly newStatus: CopilotStatus;\n}\n\nexport type CopilotClientEventHandler<EventName extends keyof CopilotClientEventMap> = (\n  ...args: CopilotClientEventMap[EventName] extends void ? []\n  : [ev: CopilotClientEventMap[EventName]]\n) => void | Promise<void>;\n\nexport interface CopilotClientEventMap extends ClientEventMap {\n  changeStatus: CopilotChangeStatusEvent;\n}\n\nexport type CopilotClientOptions<\n  RequestHandlers extends ReadonlyRecord<string, RequestHandler>,\n  NotificationHandlers extends ReadonlyRecord<string, NotificationHandler>,\n> = Omit<ClientOptions<RequestHandlers, NotificationHandlers>, \"serverName\">;\n\n/**\n * Create a Copilot LSP client.\n * @param server The server to connect to.\n * @param options The options.\n * @returns\n */\nexport const createCopilotClient = <\n  RequestHandlers extends ReadonlyRecord<string, RequestHandler>,\n  NotificationHandlers extends ReadonlyRecord<string, NotificationHandler>,\n>(\n  server: NodeServer,\n  options?: CopilotClientOptions<RequestHandlers, NotificationHandlers>,\n) => {\n  const client = createClient(server, {\n    ...options,\n\n    notificationHandlers: validateNotificationHandlers({\n      /**\n       * Log message to console.\n       */\n      LogMessage: (\n        {\n          extra,\n          message,\n        }: { level: integer; message: string; metadataStr: string; extra?: LSPArray },\n        { logger, suppressLogging },\n      ) => {\n        suppressLogging();\n        logger.debug(message, ...(extra ? [extra] : []));\n      },\n\n      /**\n       * Show message to user.\n       */\n      statusNotification: ({\n        status,\n      }: {\n        message: string;\n        status: \"InProgress\" | \"Normal\" | \"Warning\";\n      }) => {\n        const oldStatus = result.status;\n        const newStatus = status;\n        result.status = newStatus;\n        client._eventHandlers.get(\"changeStatus\")?.forEach((handler) => {\n          void handler({ oldStatus, newStatus });\n        });\n      },\n\n      ...options?.notificationHandlers,\n    }),\n\n    serverName: \"Copilot\",\n  });\n\n  /**\n   * Prepare completion params for requests such as `getCompletions` and `getCompletionsCycling`.\n   * @param options Options.\n   * @returns\n   */\n  const _prepareCompletionParams = (options: CompletionOptions) => {\n    const {\n      tabSize = 4,\n      indentSize = tabSize,\n      insertSpaces = true,\n      path = \"\",\n      uri = path && pathToFileURL(path).href,\n      relativePath = path,\n      languageId = \"\",\n      position,\n      version = result.version,\n    } = options;\n\n    return {\n      doc: {\n        tabSize,\n        indentSize,\n        insertSpaces,\n        path,\n        uri,\n        relativePath,\n        languageId,\n        position,\n        version,\n      },\n    };\n  };\n\n  let _status: CopilotStatus = \"Warning\";\n\n  const result = {\n    /**\n     * @readonly\n     */\n    logger: client.logger,\n\n    get initialized() {\n      return client.initialized;\n    },\n\n    // Mutable properties start\n    /**\n     * Version of the buffer. It actually means the number of times the buffer has been changed.\n     */\n    version: 0,\n    /**\n     * Status of Copilot.\n     * @returns\n     */\n    get status() {\n      return _status;\n    },\n    set status(value) {\n      if (value !== _status) {\n        _status = value;\n        client._eventHandlers.get(\"changeStatus\")?.forEach((handler) => {\n          void handler({ oldStatus: _status, newStatus: value });\n        });\n      }\n    },\n    // Mutable properties end\n\n    /**\n     * Request methods.\n     * @readonly\n     */\n    request: {\n      // eslint-disable-next-line @typescript-eslint/no-misused-spread\n      ...client.request,\n\n      /**\n       * Get version of Copilot.\n       * @returns\n       */\n      getVersion: (): ResponsePromise<{\n        buildType: string;\n        runtimeVersion: string;\n        version: string;\n      }> => client.query(\"getVersion\", {}),\n\n      /**\n       * Check status of Copilot.\n       * @param options Options.\n       * @returns\n       */\n      checkStatus: (options?: {\n        localChecksOnly?: boolean;\n      }): ResponsePromise<{ status: CopilotAccountStatus; user?: string }> =>\n        client.query(\"checkStatus\", options ?? {}),\n\n      /**\n       * Initiate Copilot sign in.\n       * @returns\n       */\n      signInInitiate: (): ResponsePromise<{\n        verificationUri: string;\n        status: string;\n        userCode: string;\n        expiresIn: number;\n        interval: number;\n      }> => client.mutate(\"signInInitiate\", {}),\n      /**\n       * Confirm Copilot sign in.\n       * @param options Options.\n       * @returns\n       */\n      signInConfirm: (options: {\n        userCode: string;\n      }): ResponsePromise<{ status: CopilotAccountStatus; user: string }> =>\n        client.mutate(\"signInConfirm\", options),\n      /**\n       * Sign out Copilot.\n       * @returns\n       */\n      signOut: (): ResponsePromise<{ status: \"NotSignedIn\" }> => client.mutate(\"signOut\", {}),\n\n      /**\n       * Set editor info.\n       * @param options Options.\n       * @returns\n       */\n      setEditorInfo: (options: {\n        editorInfo: { name: string; version: string };\n        editorPluginInfo: { name: string; version: string };\n      }): ResponsePromise<\"OK\"> => client.mutate(\"setEditorInfo\", options),\n\n      /**\n       * Get completions.\n       * @param options Options.\n       * @returns\n       */\n      getCompletions: (options: CompletionOptions): ResponsePromise<CompletionResult> =>\n        client.query(\n          \"getCompletions\",\n          _prepareCompletionParams(options) as unknown as LSPObject,\n        ) as unknown as ResponsePromise<CompletionResult>,\n      /**\n       * Get cycling completions (i.e. get the next completion).\n       * @param options Options.\n       * @returns\n       */\n      getCompletionsCycling: (options: CompletionOptions) =>\n        client.query(\n          \"getCompletionsCycling\",\n          _prepareCompletionParams(options) as unknown as LSPObject,\n        ),\n    } as const,\n\n    /**\n     * Notification methods.\n     * @readonly\n     */\n    notification: {\n      ...client.notification,\n\n      /**\n       * Notify Copilot that the completion is shown.\n       * @param options The options.\n       */\n      notifyShown: (options: { uuid: string }) => {\n        client.notify(\"notifyShown\", options);\n      },\n      /**\n       * Notify Copilot that the completion is accepted.\n       * @param options The options.\n       */\n      notifyAccepted: (options: { uuid: string }) => {\n        client.notify(\"notifyAccepted\", options);\n      },\n      /**\n       * Notify Copilot that the completion is rejected.\n       * @param options The options.\n       */\n      notifyRejected: (options: { uuids: readonly string[] }) => {\n        client.notify(\"notifyRejected\", options);\n      },\n    },\n\n    /**\n     * Add event handler.\n     * @readonly\n     */\n    on: ((event, handler) => {\n      client.on(event as never, handler);\n    }) as <EventName extends keyof CopilotClientEventMap>(\n      event: EventName,\n      handler: CopilotClientEventHandler<EventName>,\n    ) => void,\n    /**\n     * Remove event handler.\n     * @readonly\n     */\n    off: ((event, handler) => {\n      client.off(event as never, handler);\n    }) as <EventName extends keyof CopilotClientEventMap>(\n      event: EventName,\n      handler: CopilotClientEventHandler<EventName>,\n    ) => void,\n  };\n\n  return result;\n};\n\nexport type CopilotClient<\n  RequestHandlers extends ReadonlyRecord<string, RequestHandler> = ReadonlyRecord<\n    string,\n    RequestHandler\n  >,\n  NotificationHandlers extends ReadonlyRecord<string, NotificationHandler> = ReadonlyRecord<\n    string,\n    NotificationHandler\n  >,\n> = ReturnType<typeof createCopilotClient<RequestHandlers, NotificationHandlers>>;\n"
  },
  {
    "path": "src/client/general-client.ts",
    "content": "/* eslint-disable @typescript-eslint/no-unnecessary-condition */\nimport { ErrorCodes, JSONRPC_VERSION, MessageType } from \"@/types/lsp\";\nimport type { Equals, ReadonlyRecord } from \"@/types/tools\";\nimport type { Logger } from \"@/utils/logging\";\nimport { createLogger, formatErrorCode, formatId, formatMethod } from \"@/utils/logging\";\nimport { isNotificationMessage, isRequestMessage, isResponseMessage, toJSError } from \"@/utils/lsp\";\nimport type { NodeServer } from \"@/utils/node-bridge\";\nimport { assertNever, isKeyOf } from \"@/utils/tools\";\n\nimport type {\n  CancelParams,\n  DidChangeTextDocumentParams,\n  DidChangeWorkspaceFoldersParams,\n  DidCloseTextDocumentParams,\n  DidOpenTextDocumentParams,\n  ErrorResponseMessage,\n  InitializeParams,\n  InitializeResult,\n  LSPAny,\n  LSPArray,\n  LSPObject,\n  LogMessageParams,\n  LogTraceParams,\n  Message,\n  NotificationMessage,\n  ProgressParams,\n  RegistrationParams,\n  RequestMessage,\n  ResponseError,\n  SetTraceParams,\n  SuccessResponseMessage,\n  UnregistrationParams,\n  integer,\n} from \"../types/lsp\";\n\n/**\n * A promise specially designed for LSP client representing a future response from LSP server that\n * holds the relevant request ID and a `cancel` function to cancel the request.\n */\nexport interface ResponsePromise<T> extends Promise<T>, _BaseResponsePromise {}\ninterface _BaseResponsePromise {\n  /**\n   * Status of the promise.\n   */\n  readonly status: \"fulfilled\" | \"rejected\" | \"cancelled\" | \"pending\";\n\n  /**\n   * ID of the request.\n   */\n  readonly id: integer | string;\n\n  /**\n   * Cancel the request.\n   *\n   * @throws {Error} if the promise is not pending.\n   */\n  readonly cancel: () => void;\n}\n\nexport interface HandlerContext extends ClientContext {\n  /**\n   * Suppress logging.\n   */\n  readonly suppressLogging: () => void;\n}\n\n/**\n * Client handler for a LSP request sent from the server. The `type` property actually does nothing,\n * and is only used for logging.\n *\n * Type of `params` in `handler` is defined as `never` because function parameters type are\n * contravariant in TypeScript, so in order to make it possible to refine `params` type, for\n * example, `(params: InitializeParams, success: (value: string) => void, error:\n * (reason: ResponseError) => void, context: HandlerContext) => void` should be compatible with this\n * type, we have to define `params` type as generic as possible, i.e. `never`.\n *\n * To make sure a subtype of this type is valid, i.e. `params` type extends\n * `RequestMessage[\"params\"]`, validate it using `validateRequestHandlers` function.\n */\nexport type RequestHandler = {\n  readonly type: \"query\" | \"mutation\";\n  readonly handler: (\n    params: never,\n    success: (value: LSPAny) => void,\n    error: (reason: ResponseError) => void,\n    context: HandlerContext,\n  ) => void | Promise<void>;\n};\n\n/**\n * Client handler for a LSP notification sent from the server.\n *\n * Type of `params` is defined as `never` because function parameters type are contravariant in\n * TypeScript, so in order to make it possible to refine `params` type, for example,\n * `(params: CancelParams, context: HandlerContext) => void` should be compatible with this type, we\n * have to define `params` type as generic as possible, i.e. `never`.\n *\n * To make sure a subtype of this type is valid, i.e. `params` type extends\n * `NotificationMessage[\"params\"]`, validate it using `validateNotificationHandlers` function.\n */\nexport type NotificationHandler = (params: never, context: HandlerContext) => void | Promise<void>;\n\n/**\n * Refine request handlers type according to pre-defined protocol request handlers.\n */\nexport type RefineRequestHandlers<\n  RequestHandlers extends ReadonlyRecord<string, RequestHandler>,\n  RefineHandlers extends ReadonlyRecord<string, RequestHandler>,\n> = {\n  readonly [P in keyof RequestHandlers]: P extends keyof RefineHandlers ? RefineHandlers[P]\n  : RequestHandlers[P];\n};\n\n/**\n * Validate request handlers. Check if the request handler params extend `LSPArray | LSPObject`.\n */\nexport type ValidateRequestHandlers<\n  RequestHandlers extends ReadonlyRecord<string, RequestHandler>,\n> =\n  RequestHandlers extends {\n    [P in keyof RequestHandlers]: {\n      type: RequestHandlers[P][\"type\"];\n      handler: Parameters<RequestHandlers[P][\"handler\"]>[0] extends LSPArray | LSPObject ?\n        RequestHandlers[P][\"handler\"]\n      : \"Request handler params must extend `LSPArray | LSPObject`\";\n    };\n  } ?\n    RequestHandlers\n  : {\n      [P in keyof RequestHandlers]: {\n        type: RequestHandlers[P][\"type\"];\n        handler: Parameters<RequestHandlers[P][\"handler\"]>[0] extends LSPArray | LSPObject ?\n          RequestHandlers[P][\"handler\"]\n        : \"Request handler params must extend `LSPArray | LSPObject`\";\n      };\n    };\n\n/**\n * Validate request handlers. Check if the request handler params extend `LSPArray | LSPObject`.\n * @param requestHandlers The request handlers to validate.\n * @returns\n */\nexport const validateRequestHandlers = <\n  RequestHandlers extends ReadonlyRecord<string, RequestHandler>,\n>(\n  requestHandlers: ValidateRequestHandlers<RequestHandlers>,\n) => requestHandlers;\n\n/**\n * Refine notification handlers type according to pre-defined protocol notification handlers.\n */\nexport type RefineNotificationHandlers<\n  NotificationHandlers extends ReadonlyRecord<string, NotificationHandler>,\n  RefineHandlers extends ReadonlyRecord<string, NotificationHandler>,\n> = {\n  readonly [P in keyof NotificationHandlers]: P extends keyof RefineHandlers ? RefineHandlers[P]\n  : NotificationHandlers[P];\n};\n\n/**\n * Validate notification handlers. Check if the notification handler params extend `LSPArray | LSPObject`.\n */\nexport type ValidateNotificationHandlers<\n  NotificationHandlers extends ReadonlyRecord<string, NotificationHandler>,\n> =\n  NotificationHandlers extends {\n    [P in keyof NotificationHandlers]: Parameters<NotificationHandlers[P]>[0] extends (\n      LSPArray | LSPObject\n    ) ?\n      NotificationHandlers[P]\n    : \"Notification handler params must extend `LSPArray | LSPObject`\";\n  } ?\n    NotificationHandlers\n  : {\n      [P in keyof NotificationHandlers]: Parameters<NotificationHandlers[P]>[0] extends (\n        LSPArray | LSPObject\n      ) ?\n        NotificationHandlers[P]\n      : \"Notification handler params must extend `LSPArray | LSPObject`\";\n    };\n\n/**\n * Validate notification handlers. Check if the notification handler params extend `LSPArray | LSPObject`.\n * @param notificationHandlers The notification handlers to validate.\n * @returns\n */\nexport const validateNotificationHandlers = <\n  NotificationHandlers extends ReadonlyRecord<string, NotificationHandler>,\n>(\n  notificationHandlers: ValidateNotificationHandlers<NotificationHandlers>,\n) => notificationHandlers;\n\n/**\n * LSP client context.\n */\nexport interface ClientContext {\n  /**\n   * The LSP server, represented as a child process.\n   */\n  readonly server: NodeServer;\n\n  /**\n   * A logger that logs a message to the console.\n   */\n  readonly logger: Logger;\n\n  /**\n   * Send a JSON-RPC message to the LSP server.\n   */\n  readonly send: (data: Message) => void;\n}\n\n/**\n * Protocol request handlers.\n */\nexport type ProtocolRequestHandlers = ReturnType<typeof _prepareProtocolRequestHandlers>;\n\n/**\n * Prepare protocol request handlers.\n * @returns\n */\nconst _prepareProtocolRequestHandlers = () =>\n  validateRequestHandlers({\n    \"client/registerCapability\": {\n      type: \"mutation\",\n      /**\n       * The `client/registerCapability` request is sent from the server to the client to register\n       * for a new capability on the client side. Not all clients need to support dynamic capability\n       * registration. A client opts in via the `dynamicRegistration` property on the specific\n       * client capabilities. A client can even provide dynamic registration for capability A but\n       * not for capability B (see `TextDocumentClientCapabilities` as an example).\n       *\n       * Server must not register the same capability both statically through the initialize result\n       * and dynamically for the same document selector. If a server wants to support both static\n       * and dynamic registration it needs to check the client capability in the initialize request\n       * and only register the capability statically if the client doesn’t support dynamic\n       * registration for that capability.\n       */\n      handler: (params: RegistrationParams, success, error, context) => {\n        // To be implemented by an actual implementation\n        success(null);\n      },\n    },\n\n    \"client/unregisterCapability\": {\n      type: \"mutation\",\n      /**\n       * The `client/unregisterCapability` request is sent from the server to the client to unregister\n       * a previously registered capability.\n       */\n      handler: (params: UnregistrationParams, success, error, context) => {\n        // To be implemented by an actual implementation\n        success(null);\n      },\n    },\n  });\n\n/**\n * Protocol notification handlers.\n */\nexport type ProtocolNotificationHandlers = ReturnType<typeof _prepareProtocolNotificationHandlers>;\n\n/**\n * Prepare protocol notification handlers.\n * @returns\n */\nconst _prepareProtocolNotificationHandlers = () =>\n  validateNotificationHandlers({\n    /**\n     * Invoked when received a `$/cancelRequest` notification from the server.\n     */\n    \"$/cancelRequest\": (params: CancelParams, context) => {\n      // TODO: Implement it\n    },\n\n    /**\n     * Invoked when received a `$/progress` notification from the server.\n     */\n    \"$/progress\": (params: ProgressParams, context) => {\n      // To be implemented by an actual implementation\n    },\n\n    /**\n     * A notification to log the trace of the server’s execution. The amount and content of these\n     * notifications depends on the current `trace` configuration. If `trace` is `\"off\"`, the server\n     * should not send any `$/logTrace` notification. If trace is `\"messages\"`, the server should\n     * not add the `\"verbose\"` field in the LogTraceParams.\n     *\n     * `$/logTrace` should only be used for systematic trace reporting. For single debugging\n     * messages, the server should send `window/logMessage` notifications.\n     */\n    \"$/logTrace\": (params: LogTraceParams, context) => {\n      // To be implemented by an actual implementation\n    },\n\n    /**\n     * The log message notification is sent from the server to the client to ask the client to log a\n     * particular message.\n     */\n    \"window/logMessage\": ({ message, type }: LogMessageParams, { logger, suppressLogging }) => {\n      suppressLogging();\n      switch (type) {\n        case MessageType.Error:\n          logger.error(message);\n          break;\n        case MessageType.Warning:\n          logger.warn(message);\n          break;\n        case MessageType.Info:\n        case MessageType.Log:\n        case MessageType.Debug:\n          logger.debug(message);\n          break;\n        default:\n          assertNever(type);\n      }\n    },\n  });\n\n/**\n * Client options.\n */\nexport interface ClientOptions<\n  RequestHandlers extends ReadonlyRecord<string, RequestHandler>,\n  NotificationHandlers extends ReadonlyRecord<string, NotificationHandler>,\n> {\n  /**\n   * Logging level. `false` to disable logging; `\"error\"` to log errors only; `\"debug\"` to log\n   * everything. Defaults to `\"error\"`.\n   */\n  readonly logging?: \"debug\" | \"error\" | false;\n\n  /**\n   * Name of the server. Used for logging. Defaults to `\"\"`, i.e. no name.\n   */\n  readonly serverName?: string;\n\n  /**\n   * Request handlers by method name. Invoked when a request is received from the server. Defaults\n   * to `{}`.\n   */\n  readonly requestHandlers?: RefineRequestHandlers<RequestHandlers, ProtocolRequestHandlers>;\n\n  /**\n   * Notification handlers by method name. Invoked when a notification is received from the server.\n   * Defaults to `{}`.\n   */\n  readonly notificationHandlers?: RefineNotificationHandlers<\n    NotificationHandlers,\n    ProtocolNotificationHandlers\n  >;\n}\n\nexport type ClientEventHandler<EventName extends keyof ClientEventMap> = (\n  ...args: ClientEventMap[EventName] extends void ? [] : [ev: ClientEventMap[EventName]]\n) => void | Promise<void>;\n\nexport interface ClientEventMap {\n  initialized: void;\n}\n\nexport type ValidateClientOptions<Options extends ClientOptions<any, any>> = {\n  [P in keyof Options]: P extends \"requestHandlers\" ?\n    Options[P] extends ReadonlyRecord<string, RequestHandler> ?\n      ValidateRequestHandlers<Options[P]>\n    : never\n  : P extends \"notificationHandlers\" ?\n    Options[P] extends ReadonlyRecord<string, NotificationHandler> ?\n      ValidateNotificationHandlers<Options[P]>\n    : never\n  : Options[P];\n};\n\n/**\n * Create a LSP client.\n * @param server The LSP server.\n * @param options The client options.\n * @returns\n */\nexport const createClient = <\n  RequestHandlers extends ReadonlyRecord<string, RequestHandler>,\n  NotificationHandlers extends ReadonlyRecord<string, NotificationHandler>,\n>(\n  server: NodeServer,\n  options?: ClientOptions<RequestHandlers, NotificationHandlers>,\n) => {\n  type RefinedRequestHandlers = RefineRequestHandlers<RequestHandlers, ProtocolRequestHandlers>;\n  type RefinedNotificationHandlers = RefineNotificationHandlers<\n    NotificationHandlers,\n    ProtocolNotificationHandlers\n  >;\n\n  const {\n    logging = \"error\",\n    notificationHandlers = {} as RefinedNotificationHandlers,\n    requestHandlers = {} as RefinedRequestHandlers,\n    serverName = \"\",\n  } = options ?? {};\n\n  /**\n   * Send a JSON-RPC Message to the LSP server.\n   * @param data The message to send.\n   */\n  const _send = (data: Message) => {\n    const dataString = JSON.stringify(data);\n    const contentLength = new TextEncoder().encode(dataString).length;\n    const rpcString = `Content-Length: ${contentLength}\\r\\n\\r\\n${dataString}`;\n    server.send(rpcString);\n  };\n\n  const logger = createLogger({\n    prefix: `%c${serverName && serverName + \" \"}LSP:%c `,\n    styles: [\"font-weight: bold\", \"font-weight: normal\"],\n    block: {\n      prefix: `${serverName && serverName + \" \"}LSP `,\n    },\n  });\n\n  const context = { server, logger, send: _send } satisfies ClientContext;\n\n  const resolveMap = new Map<\n    integer | string,\n    readonly [\"query\" | \"mutation\", string, (value: LSPAny) => void]\n  >();\n  const rejectMap = new Map<\n    integer | string,\n    readonly [\"query\" | \"mutation\", string, (reason: ResponseError) => void]\n  >();\n\n  const _protocolRequestHandlers = _prepareProtocolRequestHandlers();\n  const _protocolNotificationHandlers = _prepareProtocolNotificationHandlers();\n\n  /* Merge user handlers with protocol handlers */\n  for (const [method, { handler, type }] of Object.entries(requestHandlers)) {\n    if (!isKeyOf(_protocolRequestHandlers, method)) continue;\n    const { type: protocolType } = _protocolRequestHandlers[method];\n    if (type !== protocolType) {\n      Object.defineProperty(requestHandlers, method, {\n        value: { type: protocolType, handler },\n        enumerable: true,\n      });\n      if (logging)\n        logger.warn(\n          `Request handler type (${type}) mismatch for protocol method \\`${method}\\`,` +\n            ` using protocol type (${protocolType}) instead`,\n        );\n    }\n    if (logging === \"debug\")\n      logger.debug(`Overwriting request handler for protocol method \\`${method}\\``);\n  }\n  for (const [method] of Object.entries(notificationHandlers)) {\n    if (!isKeyOf(_protocolNotificationHandlers, method)) continue;\n    if (logging === \"debug\")\n      logger.debug(`Overwriting notification handler for protocol method \\`${method}\\``);\n  }\n\n  /**\n   * Send a success response to LSP server.\n   * @param type The type of the request.\n   * @param isProtocol Whether the request is a protocol request.\n   * @param id The ID of the request.\n   * @param method The method of the request.\n   * @param value The value to send.\n   */\n  const _success = (\n    type: \"query\" | \"mutation\",\n    isProtocol: boolean,\n    id: (integer | string) | null,\n    method: string,\n    value: LSPAny,\n  ) => {\n    const response = {\n      jsonrpc: JSONRPC_VERSION,\n      id,\n      result: value,\n    } satisfies SuccessResponseMessage;\n    _send(response);\n\n    // Log to console\n    if (logging === \"debug\") {\n      const color = type === \"query\" ? \"#49cc90\" : \"purple\";\n      logger.block\n        .overwrite({ color })\n        .debug(`>> [${id}] ${isProtocol ? \"[Protocol] \" : \"\"}Response ${method}`, value);\n    }\n  };\n  /**\n   * Send an error response to LSP server.\n   * @param isProtocol Whether the request is a protocol request.\n   * @param id The ID of the request.\n   * @param method The method of the request.\n   * @param reason The reason of the error.\n   */\n  const _error = (\n    isProtocol: boolean,\n    id: (integer | string) | null,\n    method: string,\n    reason: ResponseError,\n  ) => {\n    const response = {\n      jsonrpc: JSONRPC_VERSION,\n      id,\n      error: reason,\n    } satisfies ErrorResponseMessage;\n    _send(response);\n\n    // Log to console\n    if (logging === \"debug\") {\n      const errorCode = reason.code;\n      const errorData = reason.data;\n      logger.block\n        .overwrite({ color: \"crimson\" })\n        .debug(\n          `>> [${id}] ${isProtocol ? \"[Protocol] \" : \"\"}Error Response ${method} ${formatErrorCode(\n            errorCode,\n          )}`,\n          \"\\n\" + reason.message,\n          ...(errorData !== undefined ? [errorData] : []),\n        );\n    }\n  };\n\n  // Listen to server stdout\n  server.onMessage((rawString) => {\n    const payloadStrings = rawString.split(/Content-Length: \\d+\\r\\n\\r\\n/).filter((s) => s);\n\n    for (const payloadString of payloadStrings) {\n      let payload: unknown;\n      try {\n        payload = JSON.parse(payloadString);\n      } catch (e) {\n        if (logging) logger.error(`Unable to parse payload: ${payloadString}`, e);\n        return;\n      }\n\n      if (isResponseMessage(payload)) {\n        if (\"error\" in payload) {\n          const typeAndMethodAndReject =\n            payload.id === null ? undefined : rejectMap.get(payload.id);\n          if (payload.id !== null && !typeAndMethodAndReject) {\n            if (logging) {\n              const errorCode = payload.error.code;\n              const errorData = payload.error.data;\n              logger.error(\n                `Unable to find reject function for id ${payload.id}`,\n                `\\nError Response ${formatErrorCode(errorCode)}\\n${payload.error.message}`,\n                ...(errorData !== undefined ? [errorData] : []),\n              );\n            }\n          } else {\n            let method: string | null = null;\n            if (payload.id !== null && typeAndMethodAndReject) {\n              const [, m, reject] = typeAndMethodAndReject;\n              method = m;\n              reject(payload.error);\n              rejectMap.delete(payload.id);\n            }\n\n            // Log to console\n            if (logging) {\n              const errorCode = payload.error.code;\n              const errorData = payload.error.data;\n              logger.block\n                .overwrite({ color: \"crimson\" })\n                .error(\n                  `<< ${formatId(payload.id)}` +\n                    `${formatMethod(method)} Error Response ${formatErrorCode(errorCode)}`,\n                  \"\\n\" + payload.error.message,\n                  ...(errorData !== undefined ? [errorData] : []),\n                );\n            }\n          }\n        } else {\n          const typeAndMethodAndResolve =\n            payload.id === null ? undefined : resolveMap.get(payload.id);\n          if (payload.id !== null && !typeAndMethodAndResolve) {\n            if (logging)\n              logger.error(`Unable to find resolve function for id ${payload.id}`, payload.result);\n          } else {\n            let type: (\"query\" | \"mutation\") | null = null;\n            let method: string | null = null;\n            if (payload.id !== null && typeAndMethodAndResolve) {\n              const [t, m, resolve] = typeAndMethodAndResolve;\n              type = t;\n              method = m;\n              resolve(payload.result);\n              resolveMap.delete(payload.id);\n            }\n\n            if (logging === \"debug\") {\n              const color =\n                type === \"query\" ? \"#49cc90\"\n                : type === \"mutation\" ? \"purple\"\n                : \"lightgray\";\n              logger.block\n                .overwrite({ color })\n                .debug(\n                  `<< ${formatId(payload.id)}${formatMethod(method)} Response`,\n                  payload.result,\n                );\n            }\n          }\n        }\n      } else if (isRequestMessage(payload)) {\n        const request = payload;\n\n        let loggingSuppressed = false;\n\n        if (request.method.startsWith(\"$/\")) {\n          const typeAndHandler =\n            requestHandlers[request.method] ?? _protocolRequestHandlers[request.method as never];\n\n          if (!typeAndHandler) {\n            _error(true, request.id, request.method, {\n              code: ErrorCodes.MethodNotFound,\n              message: `Method not found: ${request.method}`,\n            });\n\n            if (logging)\n              logger.error(\n                `Request handler not found for method ${request.method} with id ${request.id}`,\n              );\n          } else {\n            const { handler, type } = typeAndHandler;\n            void handler(\n              request.params as never,\n              (value) => {\n                _success(type, true, request.id, request.method, value);\n              },\n              (reason) => {\n                _error(true, request.id, request.method, reason);\n              },\n              {\n                ...context,\n                suppressLogging: () => {\n                  loggingSuppressed = true;\n                },\n              },\n            );\n          }\n        } else {\n          const typeAndHandler =\n            requestHandlers[request.method] ?? _protocolRequestHandlers[request.method as never];\n\n          if (typeAndHandler) {\n            const { handler, type } = typeAndHandler;\n            void handler(\n              request.params as never,\n              (value) => {\n                _success(\n                  type,\n                  request.method in _protocolRequestHandlers,\n                  request.id,\n                  request.method,\n                  value,\n                );\n              },\n              (reason) => {\n                _error(\n                  request.method in _protocolRequestHandlers,\n                  request.id,\n                  request.method,\n                  reason,\n                );\n              },\n              {\n                ...context,\n                suppressLogging: () => {\n                  loggingSuppressed = true;\n                },\n              },\n            );\n          }\n        }\n\n        if (logging === \"debug\" && !loggingSuppressed) {\n          const typeAndHandler =\n            requestHandlers[request.method] ?? _protocolRequestHandlers[request.method as never];\n          const type = typeAndHandler.type ?? \"unknown\";\n          const color =\n            type === \"query\" ? \"#49cc90\"\n            : type === \"mutation\" ? \"purple\"\n            : \"gray\";\n          logger.block.overwrite({ color }).debug(\n            `<< [${request.id}] ${\n              request.method in _protocolRequestHandlers ? \"[Protocol] \"\n              : requestHandlers[payload.method] ? \"\"\n              : \"[Ignored] \"\n            }${request.method} Request`,\n            ...(request.params !== undefined ? [request.params] : []),\n          );\n        }\n      } else if (isNotificationMessage(payload)) {\n        let loggingSuppressed = false;\n\n        void (\n          notificationHandlers[payload.method] ??\n          _protocolNotificationHandlers[payload.method as never]\n        )?.(payload.params as never, {\n          ...context,\n          suppressLogging: () => {\n            loggingSuppressed = true;\n          },\n        });\n\n        if (logging === \"debug\" && !loggingSuppressed)\n          logger.block\n            .overwrite({ color: \"lightgray\" })\n            .debug(\n              `<< ${notificationHandlers[payload.method] ? \"\" : \"[Ignored] \"}${\n                payload.method\n              } Notification`,\n              ...(payload.params ? [payload.params] : []),\n            );\n      } else {\n        if (logging) logger.error(`Invalid payload`, payload);\n      }\n    }\n  });\n  // End\n\n  let requestId = 0;\n  let _initialized = false;\n\n  const _eventHandlers = new Map<string, ((ev?: any) => void | Promise<void>)[]>();\n\n  const result = {\n    /**\n     * @readonly\n     */\n    logger,\n\n    get initialized() {\n      return _initialized;\n    },\n\n    requestHandlers: requestHandlers as Equals<\n      RequestHandlers,\n      RefineRequestHandlers<ReadonlyRecord<string, RequestHandler>, ProtocolRequestHandlers>\n    > extends true ?\n      Record<string, never>\n    : RefinedRequestHandlers,\n    notificationHandlers: notificationHandlers as Equals<\n      NotificationHandlers,\n      RefineNotificationHandlers<\n        ReadonlyRecord<string, NotificationHandler>,\n        ProtocolNotificationHandlers\n      >\n    > extends true ?\n      Record<string, never>\n    : RefinedNotificationHandlers,\n    protocolRequestHandlers: _protocolRequestHandlers,\n    protocolNotificationHandlers: _protocolNotificationHandlers,\n\n    /**\n     * @readonly\n     */\n    request: Object.assign(\n      /**\n       * Send a request to LSP server.\n       * @param type The type of the request.\n       * @param method The method of the request.\n       * @param params The parameters of the request.\n       * @returns\n       */\n      <R extends LSPAny = LSPAny>(\n        type: \"query\" | \"mutation\",\n        method: string,\n        params?: LSPArray | LSPObject,\n      ): ResponsePromise<R> => {\n        const request = {\n          jsonrpc: JSONRPC_VERSION,\n          id: ++requestId,\n          method,\n          ...(params && { params }),\n        } satisfies RequestMessage;\n        _send(request);\n\n        // Log to console\n        if (logging === \"debug\") {\n          const color = type === \"query\" ? \"#49cc90\" : \"purple\";\n          logger.block\n            .overwrite({ color })\n            .debug(\n              `>> [${requestId}] Request ${method}`,\n              ...(params !== undefined ? [params] : []),\n            );\n        }\n\n        const result = Object.assign(\n          new Promise<R>((resolve, reject) => {\n            resolveMap.set(requestId, [\n              type,\n              method,\n              (value) => {\n                result.status = \"fulfilled\";\n                resolve(value as never);\n              },\n            ]);\n            rejectMap.set(requestId, [\n              type,\n              method,\n              (reason) => {\n                result.status = \"rejected\";\n                reject(toJSError(reason));\n              },\n            ]);\n          }),\n          {\n            status: \"pending\" as \"pending\" | \"fulfilled\" | \"cancelled\" | \"rejected\",\n            id: requestId,\n            cancel: () => {\n              if (result.status !== \"pending\") {\n                if (logging)\n                  logger.error(\n                    `Unable to cancel request with id ${requestId} because it is already ${result.status}`,\n                  );\n\n                throw new Error(\n                  `Unable to cancel request with id ${requestId} because it is already ${result.status}`,\n                );\n              }\n\n              _notify(\"$/cancelRequest\", { id: requestId } satisfies CancelParams);\n              result.status = \"cancelled\";\n              resolveMap.set(requestId, [type, method, () => {}]);\n              rejectMap.set(requestId, [type, method, () => {}]);\n            },\n          } satisfies _BaseResponsePromise,\n        );\n\n        return result;\n      },\n      /**\n       * Request methods.\n       * @readonly\n       */\n      {\n        /**\n         * The initialize request is sent as the first request from the client to the server. If the\n         * server receives a request or notification before the `initialize` request it should act\n         * as follows:\n         *\n         * - For a request the response should be an error with code: -32002. The message can be\n         * picked by the server.\n         * - Notifications should be dropped, except for the exit notification. This will allow the\n         * exit of a server without an initialize request.\n         *\n         * Until the server has responded to the `initialize` request with an `InitializeResult`,\n         * the client must not send any additional requests or notifications to the server. In\n         * addition the server is not allowed to send any requests or notifications to the client\n         * until it has responded with an `InitializeResult`, with the exception that during the\n         * `initialize` request the server is allowed to send the notifications\n         * `window/showMessage`, `window/logMessage` and `telemetry/event` as well as the\n         * `window/showMessageRequest` request to the client. In case the client sets up a progress\n         * token in the initialize params (e.g. property `workDoneToken`) the server is also allowed\n         * to use that token (and only that token) using the `$/progress` notification sent from the\n         * server to the client.\n         *\n         * The `initialize` request may only be sent once.\n         * @returns\n         */\n        initialize: (params: InitializeParams): ResponsePromise<InitializeResult> => {\n          const promise = _mutate(\n            \"initialize\",\n            params as unknown as LSPObject,\n          ) as unknown as ResponsePromise<InitializeResult>;\n          void promise.then(() => {\n            _initialized = true;\n            _eventHandlers.get(\"initialized\")?.forEach((handler) => void handler());\n          });\n          return promise;\n        },\n        /**\n         * The shutdown request is sent from the client to the server. It asks the server to shut\n         * down, but to not exit (otherwise the response might not be delivered correctly to the\n         * client). There is a separate exit notification that asks the server to exit. Clients must\n         * not send any notifications other than `exit` or requests to a server to which they have\n         * sent a shutdown request. Clients should also wait with sending the exit notification\n         * until they have received a response from the `shutdown` request.\n         *\n         * If a server receives requests after a shutdown request those requests should error with\n         * `InvalidRequest`.\n         * @returns\n         */\n        shutdown: (): ResponsePromise<null> => _mutate(\"shutdown\"),\n      } as const,\n    ),\n    /**\n     * Send a query request to LSP server.\n     * @param method The method of the request.\n     * @param params The parameters of the request.\n     * @returns\n     */\n    query: <R extends LSPAny = LSPAny>(\n      method: string,\n      params?: LSPArray | LSPObject,\n    ): ResponsePromise<R> => _request(\"query\", method, params),\n    /**\n     * Send a mutation request to LSP server.\n     * @param method The method of the request.\n     * @param payload The payload of the request.\n     * @returns\n     */\n    mutate: <R extends LSPAny = LSPAny>(\n      method: string,\n      payload?: LSPArray | LSPObject,\n    ): ResponsePromise<R> => _request(\"mutation\", method, payload),\n\n    /**\n     * Send a notification to LSP server.\n     * @param method The method of the notification.\n     * @param params The parameters of the notification.\n     */\n    notify: (method: string, params?: LSPArray | LSPObject) => {\n      const notification = {\n        jsonrpc: JSONRPC_VERSION,\n        method,\n        ...(params && { params }),\n      } satisfies NotificationMessage;\n      _send(notification);\n\n      // Log to console\n      if (logging === \"debug\")\n        logger.block\n          .overwrite({ color: \"lightgray\" })\n          .debug(`>> Notification ${method}`, ...(params ? [params] : []));\n    },\n\n    /**\n     * Notification methods.\n     * @readonly\n     */\n    notification: {\n      /**\n       * The initialized notification is sent from the client to the server after the client\n       * received the result of the `initialize` request but before the client is sending any\n       * other request or notification to the server. The server can use the `initialized`\n       * notification for example to dynamically register capabilities.\n       *\n       * The `initialized` notification may only be sent once.\n       */\n      initialized: () => {\n        _notify(\"initialized\", {});\n      },\n      /**\n       * A notification that should be used by the client to modify the trace setting of the server.\n       */\n      setTrace: (params: SetTraceParams) => {\n        _notify(\"$/setTrace\", params as unknown as LSPObject);\n      },\n      /**\n       * A notification to ask the server to exit its process. The server should exit with `success`\n       * code 0 if the shutdown request has been received before; otherwise with `error` code 1.\n       */\n      exit: () => {\n        _notify(\"exit\");\n      },\n\n      /**\n       * Text document notifications.\n       */\n      textDocument: {\n        /**\n         * The document open notification is sent from the client to the server to signal newly opened\n         * text documents. The document’s content is now managed by the client and the server must not\n         * try to read the document’s content using the document’s Uri. Open in this sense means it is\n         * managed by the client. It doesn’t necessarily mean that its content is presented in an\n         * editor. An open notification must not be sent more than once without a corresponding close\n         * notification send before. This means open and close notification must be balanced and the\n         * max open count for a particular textDocument is one. Note that a server’s ability to\n         * fulfill requests is independent of whether a text document is open or closed.\n         *\n         * The `DidOpenTextDocumentParams` contain the language id the document is associated with. If\n         * the language id of a document changes, the client needs to send a `textDocument/didClose`\n         * to the server followed by a `textDocument/didOpen` with the new language id if the server\n         * handles the new language id as well.\n         */\n        didOpen: (params: DidOpenTextDocumentParams) => {\n          _notify(\"textDocument/didOpen\", params as unknown as LSPObject);\n        },\n        /**\n         * The document change notification is sent from the client to the server to signal changes to\n         * a text document. Before a client can change a text document it must claim ownership of its\n         * content using the `textDocument/didOpen` notification. In 2.0 the shape of the params has\n         * changed to include proper version numbers.\n         */\n        didChange: (params: DidChangeTextDocumentParams) => {\n          _notify(\"textDocument/didChange\", params as unknown as LSPObject);\n        },\n        /**\n         * The document close notification is sent from the client to the server when the document\n         * got closed in the client. The document’s master now exists where the document’s Uri\n         * points to (e.g. if the document’s Uri is a file Uri the master now exists on disk). As\n         * with the open notification the close notification is about managing the document’s\n         * content. Receiving a close notification doesn’t mean that the document was open in an\n         * editor before. A close notification requires a previous open notification to be sent.\n         * Note that a server’s ability to fulfill requests is independent of whether a text\n         * document is open or closed.\n         */\n        didClose: (params: DidCloseTextDocumentParams) => {\n          _notify(\"textDocument/didClose\", params as unknown as LSPObject);\n        },\n      } as const,\n\n      /**\n       * Workspace notifications.\n       * @readonly\n       */\n      workspace: {\n        /**\n         * The `workspace/didChangeWorkspaceFolders` notification is sent from the client to the\n         * server to inform the server about workspace folder configuration changes. A server can\n         * register for this notification by using either the *server capability*\n         * `workspace.workspaceFolders.changeNotifications` or by using the dynamic capability\n         * registration mechanism. To dynamically register for the\n         * `workspace/didChangeWorkspaceFolders` send a `client/registerCapability` request from\n         * the server to the client. The registration parameter must have a `registrations` item of\n         * the following form, where id is a unique `id` used to unregister the capability (the\n         * example uses a UUID).\n         */\n        didChangeWorkspaceFolders: (params: DidChangeWorkspaceFoldersParams) => {\n          _notify(\"workspace/didChangeWorkspaceFolders\", params as unknown as LSPObject);\n        },\n      } as const,\n    } as const,\n\n    _eventHandlers,\n\n    /**\n     * Add event handler.\n     * @readonly\n     */\n    on: ((event, handler): void => {\n      const handlers = _eventHandlers.get(event) ?? [];\n      handlers.push(handler as never);\n      _eventHandlers.set(event, handlers);\n    }) as <E extends keyof ClientEventMap>(event: E, handler: ClientEventHandler<E>) => void,\n    /**\n     * Remove event handler.\n     * @readonly\n     */\n    off: ((event, handler): void => {\n      const handlers = _eventHandlers.get(event) ?? [];\n      const index = handlers.indexOf(handler as never);\n      if (index !== -1) handlers.splice(index, 1);\n      _eventHandlers.set(event, handlers);\n    }) as <E extends keyof ClientEventMap>(event: E, handler: ClientEventHandler<E>) => void,\n  };\n\n  const _request = result.request;\n  // @ts-expect-error - Planned to use in the future\n  const _query = result.query;\n  const _mutate = result.mutate;\n  const _notify = result.notify;\n\n  return result;\n};\n"
  },
  {
    "path": "src/client/index.ts",
    "content": "export * from \"./client\";\n"
  },
  {
    "path": "src/completion.ts",
    "content": "import * as path from \"@modules/path\";\n\nimport type { Completion, CompletionResult, CopilotClient } from \"./client\";\nimport type { ResponsePromise } from \"./client/general-client\";\nimport { logger } from \"./logging\";\nimport type { Position } from \"./types/lsp\";\nimport { Observable } from \"./utils/observable\";\n\n/**\n * Options for {@link CompletionTaskManager}.\n */\nexport interface CompletionTaskManagerOptions {\n  workspaceFolder: string;\n  activeFilePathname: string;\n}\n\n/**\n * A manager for GitHub Copilot completion tasks that makes sure exactly one completion task is\n * active at a time.\n */\nexport default class CompletionTaskManager {\n  public workspaceFolder: string;\n  public activeFilePathname: string;\n\n  private _state: \"idle\" | \"requesting\" | \"pending\" = \"idle\";\n\n  private activeRequest:\n    | (ResponsePromise<CompletionResult> & { cleanup?: Observable<\"accepted\" | \"rejected\"> })\n    | null = null;\n\n  constructor(\n    private copilot: CopilotClient,\n    options: CompletionTaskManagerOptions,\n  ) {\n    this.workspaceFolder = options.workspaceFolder;\n    this.activeFilePathname = options.activeFilePathname;\n  }\n\n  get state(): \"idle\" | \"requesting\" | \"pending\" {\n    return this._state;\n  }\n\n  rejectCurrentIfExist(): void {\n    if (this.activeRequest) {\n      if (this.activeRequest.status === \"pending\") this.activeRequest.cancel();\n      this.activeRequest.cleanup?.next(\"rejected\");\n      this.activeRequest = null;\n    }\n    this._state = \"idle\";\n    if (this.copilot.status === \"InProgress\") this.copilot.status = \"Normal\";\n  }\n\n  start(\n    position: Position,\n    {\n      onCompletion,\n    }: {\n      /**\n       * Callback invoked when a task completion is received.\n       *\n       * This can optionally return an {@linkcode Observable} representing a cleanup action.\n       * The observable will be:\n       * - Subscribed to initially for internal cleanup.\n       * - Triggered later by the class itself when a new task starts, or by external triggers\n       *   (e.g., the user manually invoking `.next()` for cleanup).\n       */\n      onCompletion?: (completion: Completion) => Observable<\"accepted\" | \"rejected\"> | void;\n    },\n  ): void {\n    if (this.activeRequest) {\n      if (this.activeRequest.status === \"pending\") this.activeRequest.cancel();\n      this.activeRequest.cleanup?.next(\"rejected\");\n    }\n\n    this._state = \"requesting\";\n\n    const request = this.copilot.request.getCompletions({\n      position,\n      languageId: \"markdown\",\n      path: this.activeFilePathname,\n      relativePath:\n        this.workspaceFolder ?\n          path.relative(this.workspaceFolder, this.activeFilePathname)\n        : this.activeFilePathname,\n    });\n    this.activeRequest = request;\n\n    request\n      .then(({ cancellationReason, completions }): void => {\n        if (this.activeRequest !== request) {\n          // The request has been cancelled or a new task has started since this task was started,\n          // so we should ignore this task's completion\n          return;\n        }\n\n        if (cancellationReason || completions.length === 0) {\n          if (this.copilot.status === \"InProgress\") this.copilot.status = \"Normal\";\n          this._state = \"idle\";\n          return;\n        }\n\n        this._state = \"pending\";\n\n        const completion = completions[0]!;\n\n        const cleanup = onCompletion?.(completion) ?? new Observable<\"accepted\" | \"rejected\">();\n        cleanup.subscribeOnce((acceptedOrRejected) => {\n          if (acceptedOrRejected === \"accepted\") {\n            this.copilot.notification.notifyAccepted({ uuid: completion.uuid });\n            logger.debug(\"Accepted completion\");\n          } else {\n            this.copilot.notification.notifyRejected({ uuids: [completion.uuid] });\n            logger.debug(\"Rejected completion\", completion.uuid);\n          }\n          this._state = \"idle\";\n          if (this.activeRequest === request) this.activeRequest = null;\n        });\n        this.activeRequest.cleanup = cleanup;\n      })\n      .catch(() => {\n        if (this.activeRequest !== request) {\n          // The request has been cancelled or a new task has started since this task was started,\n          // so we should ignore this task's completion\n          return;\n        }\n\n        this._state = \"idle\";\n        this.activeRequest = null;\n      });\n  }\n}\n"
  },
  {
    "path": "src/components/ChatPanel.scss",
    "content": "#copilot-chat-container {\n  overflow: hidden;\n}\n\n.chat-panel-resize-handle {\n  position: absolute;\n  left: 0;\n  top: 0;\n  bottom: 0;\n  width: 1.5px;\n  cursor: col-resize;\n  background-color: transparent;\n  transition: background-color 0.2s;\n  z-index: 10;\n\n  &:hover,\n  &.resizing {\n    background-color: var(--primary-color, rgba(64, 120, 192, 30%));\n  }\n}\n\n// Empty state welcome screen\n.empty-state-welcome {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  padding: 30px;\n  text-align: center;\n  height: 100%;\n  margin: 0 auto;\n\n  .welcome-icon {\n    margin-bottom: 10px;\n  }\n\n  .welcome-title {\n    font-size: 1.5em;\n    margin-top: 0;\n    margin-bottom: 12px;\n    font-weight: 500;\n    color: var(--text-color);\n  }\n\n  .welcome-subtitle {\n    color: var(--text-color-lighter, rgba(0, 0, 0, 60%));\n    font-size: 0.8em;\n    line-height: 1.5;\n  }\n}\n\n// Main panel container\n.copilot-chat-panel {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  background-color: var(--bg-color);\n  color: var(--text-color);\n  border-left: 1px solid var(--border-color);\n}\n\n// Header section\n.chat-panel-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: 10px 16px;\n  border-bottom: 1px solid var(--border-color);\n\n  h3 {\n    margin: 0;\n    font-size: 16px;\n  }\n\n  .chat-panel-close-btn {\n    background: none;\n    border: none;\n    font-size: 24px !important;\n    cursor: pointer;\n    color: var(--text-color);\n    opacity: 0.6;\n\n    &:hover {\n      opacity: 1;\n    }\n  }\n}\n\n// Input area container\n.chat-panel-input-container {\n  border-top: 1px solid var(--border-color);\n  padding: 12px;\n}\n\n// Textarea wrapper with integrated controls\n.chat-input-wrapper {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n\n  textarea {\n    width: 100%;\n    padding: 10px;\n    padding-bottom: 40px; // Space for buttons\n    border: 1px solid var(--border-color, #ccc);\n    border-radius: 6px;\n    background-color: var(--bg-color);\n    color: var(--text-color);\n    resize: none;\n    font-size: 14px;\n    line-height: 1.5;\n    // stylelint-disable-next-line declaration-property-value-keyword-no-deprecated\n    word-break: break-word;\n    white-space: pre-wrap;\n    overflow-y: hidden;\n\n    &:focus {\n      outline: none; // Remove default outline\n      border-color: var(--primary-color, #4078c0); // Change border color when focused\n      box-shadow: 0 0 0 1px var(--primary-color, #4078c0); // Subtle glow\n    }\n  }\n}\n\n// Controls container positioned at bottom of textarea\n.chat-input-controls {\n  position: absolute;\n  bottom: 8px;\n  left: 6px;\n  right: 6px;\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  height: 24px;\n}\n\n// Dropdown styling\n.chat-panel-dropdown {\n  position: relative;\n\n  .chat-panel-dropdown-toggle {\n    background: none;\n    border: none;\n    padding: 2px 6px;\n    display: flex;\n    align-items: center;\n    cursor: pointer;\n    color: var(--text-color);\n    opacity: 0.8;\n    font-size: 12px !important;\n    font-weight: normal !important;\n\n    &:hover {\n      opacity: 1;\n      background-color: rgba(0, 0, 0, 5%);\n      border-radius: 3px;\n    }\n\n    .dropdown-chevron {\n      margin-left: 4px;\n      height: 5px;\n      width: 8px;\n      opacity: 0.7;\n    }\n  }\n\n  .chat-panel-dropdown-menu {\n    position: absolute;\n    padding: 5px;\n    bottom: 34px;\n    left: 0;\n    min-width: 120px;\n    background-color: var(--bg-color);\n    border: 1px solid var(--border-color);\n    border-radius: 4px;\n    box-shadow: 0 2px 8px rgba(0, 0, 0, 15%);\n    z-index: 1000;\n    display: flex;\n    flex-direction: column;\n\n    .chat-panel-dropdown-item {\n      text-align: left;\n      background: none;\n      border: none;\n      cursor: pointer;\n      color: var(--text-color);\n      font-size: 12px !important;\n      font-weight: normal !important;\n\n      &:hover {\n        background-color: var(--item-hover-bg-color, rgba(0, 0, 0, 5%));\n      }\n    }\n  }\n}\n\n.chat-title-dropdown {\n  position: relative;\n  flex: 1;\n\n  .chat-title-dropdown-toggle {\n    background: none;\n    border: none;\n    padding: 4px 8px;\n    display: flex;\n    align-items: center;\n    color: var(--text-color);\n    cursor: pointer;\n    width: auto;\n    text-align: left;\n    border-radius: 4px;\n\n    &:hover {\n      background-color: rgba(0, 0, 0, 5%);\n    }\n\n    h3 {\n      margin: 0;\n      flex: 1;\n      font-size: 13px;\n      white-space: nowrap;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      cursor: pointer;\n    }\n\n    .dropdown-chevron {\n      margin-left: 8px;\n      opacity: 0.7;\n    }\n\n    &:hover .dropdown-chevron {\n      opacity: 1;\n    }\n  }\n\n  .session-menu {\n    position: absolute;\n    top: 100%;\n    left: 0;\n    width: 100%;\n    max-height: 300px;\n    overflow-y: auto;\n    background-color: var(--bg-color);\n    border: 1px solid var(--border-color);\n    border-radius: 4px;\n    box-shadow: 0 2px 8px rgba(0, 0, 0, 10%);\n    z-index: 1000;\n    margin-top: 4px;\n    padding: 6px;\n\n    /* stylelint-disable-next-line no-descending-specificity */\n    .chat-panel-dropdown-item {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      padding: 4px 8px;\n      cursor: pointer;\n      width: 100%;\n      text-align: left;\n      background: none;\n      border: none;\n      font-size: 12px !important;\n      font-weight: normal !important;\n      color: var(--text-color);\n      min-height: 26px;\n      border-radius: 3px;\n\n      &:hover {\n        background-color: rgba(0, 0, 0, 5%);\n      }\n\n      .edit-btn,\n      .delete-btn {\n        opacity: 0.4;\n        width: 16px;\n        height: 16px;\n        padding: 0;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        background: none;\n        border: none;\n        color: var(--text-color);\n        cursor: pointer;\n        margin-left: 8px;\n\n        &:hover {\n          color: #e53935;\n        }\n      }\n\n      &:hover .edit-btn,\n      &:hover .delete-btn {\n        opacity: 0.7;\n      }\n    }\n\n    .new-session {\n      display: flex;\n      align-items: center;\n      justify-content: unset;\n      font-weight: 500;\n      border-bottom: 1px solid var(--border-color);\n      padding-bottom: 6px;\n      margin-bottom: 4px;\n\n      svg {\n        width: 12px;\n        height: 12px;\n        margin-right: 8px;\n      }\n    }\n\n    .active {\n      background-color: rgba(0, 0, 0, 5%);\n      font-weight: 500;\n    }\n  }\n}\n\n// Send button\n.chat-panel-send-btn {\n  padding: 2px 10px;\n  background-color: var(--primary-color, #4078c0);\n  color: white;\n  border: none;\n  border-radius: 3px;\n  cursor: pointer;\n  font-size: 12px !important;\n\n  &:disabled {\n    background-color: #ccc;\n    cursor: default;\n    opacity: 0.6;\n  }\n\n  &:hover:not(:disabled) {\n    opacity: 0.9;\n  }\n\n  &.sending {\n    background-color: #e74c3c;\n\n    &:hover:not(:disabled) {\n      background-color: #c0392b;\n    }\n  }\n}\n\n.chat-panel-messages {\n  flex: 1;\n  padding: 16px;\n  overflow-y: auto;\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n}\n\n.chat-message-row {\n  display: flex;\n  flex-direction: column;\n  max-width: 100%;\n  animation: fade-in 0.3s ease;\n\n  .message-header {\n    display: flex;\n    align-items: center;\n    margin-bottom: 12px;\n\n    .message-icon {\n      width: 26px;\n      height: 26px;\n      border-radius: 50%;\n      border: 1px solid rgba(0, 0, 0, 5%);\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      margin-right: 8px;\n\n      svg {\n        width: 16px;\n        height: 16px;\n      }\n\n      &.user-icon {\n        svg {\n          transform: scale(1.1);\n        }\n      }\n    }\n\n    .user-icon {\n      color: var(--text-color);\n    }\n\n    .copilot-icon {\n      background-color: rgba(0, 0, 0, 3%);\n      padding: 4px;\n    }\n\n    .message-author {\n      font-size: 13px;\n      font-weight: bold;\n      color: var(--text-color);\n    }\n  }\n\n  .message-content {\n    font-family: var(--font-family);\n    font-size: 13px;\n    line-height: 1.5;\n    overflow-wrap: break-word;\n    margin: 0;\n    border-radius: 6px;\n    background: transparent;\n\n    // Remove default pre tag styles\n    border: none;\n    overflow: visible;\n  }\n\n  pre.message-content {\n    white-space: pre-wrap;\n  }\n}\n\n.markdown-content {\n  /* Reset header font sizes */\n  h1 {\n    font-size: 1.36em;\n    margin-top: 0.7em;\n    margin-bottom: 0.5em;\n  }\n\n  h2 {\n    font-size: 1.25em;\n    margin-top: 0.6em;\n    margin-bottom: 0.4em;\n  }\n\n  /* stylelint-disable-next-line no-descending-specificity */\n  h3 {\n    font-size: 1.17em;\n    margin-top: 0.5em;\n    margin-bottom: 0.4em;\n  }\n\n  h4 {\n    font-size: 1em;\n    margin-top: 0.4em;\n    margin-bottom: 0.3em;\n  }\n\n  h5 {\n    font-size: 0.83em;\n    margin-top: 0.4em;\n    margin-bottom: 0.2em;\n  }\n\n  h6 {\n    font-size: 0.67em;\n    margin-top: 0.4em;\n    margin-bottom: 0.2em;\n  }\n\n  h1,\n  h2,\n  /* stylelint-disable-next-line no-descending-specificity */\n  h3,\n  h4,\n  h5,\n  h6 {\n    font-weight: 500;\n  }\n\n  /* Reset font size for code */\n  code {\n    font-size: 0.9em;\n  }\n\n  /* Prettify code block */\n  pre {\n    background-color: rgba(0, 0, 0, 3%);\n    border: 1px solid rgba(0, 0, 0, 8%);\n    border-radius: 5px;\n    margin: 12px 0;\n    overflow-x: auto;\n    position: relative;\n\n    .copy-code-button {\n      position: absolute;\n      top: 6px;\n      right: 6px;\n      width: 28px;\n      height: 28px;\n      padding: 6px;\n      background-color: rgba(0, 0, 0, 10%);\n      border: none;\n      border-radius: 4px;\n      cursor: pointer;\n      opacity: 0;\n      transition:\n        opacity 0.2s ease,\n        background-color 0.2s ease;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      z-index: 2;\n\n      &:hover {\n        opacity: 0.9 !important;\n        background-color: rgba(0, 0, 0, 15%);\n      }\n\n      &:active {\n        background-color: rgba(0, 0, 0, 20%);\n      }\n    }\n\n    &:hover .copy-code-button {\n      opacity: 0.7;\n    }\n\n    code {\n      background-color: transparent;\n      padding: 0;\n      border: none;\n      font-family: inherit;\n      color: var(--text-color-code, var(--text-color));\n    }\n  }\n\n  /* Reduce margins for paragraphs with a pre code block after */\n  p:has(+ pre code) {\n    margin-bottom: 0;\n  }\n\n  /* Remove margins for first and last paragraphs */\n  p {\n    &:first-child {\n      margin-top: 0;\n    }\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n}\n\n.typing-indicator {\n  display: flex;\n  align-items: center;\n  height: 24px;\n\n  span {\n    width: 6px;\n    height: 6px;\n    margin: 0 2px;\n    background-color: var(--text-color);\n    border-radius: 50%;\n    opacity: 0.5;\n    animation: typing 1s infinite ease-in-out;\n\n    &:nth-child(1) {\n      animation-delay: 0s;\n    }\n\n    &:nth-child(2) {\n      animation-delay: 0.2s;\n    }\n\n    &:nth-child(3) {\n      animation-delay: 0.4s;\n    }\n  }\n}\n\n@keyframes typing {\n  0%,\n  60%,\n  100% {\n    transform: translateY(0);\n    opacity: 0.5;\n  }\n\n  30% {\n    transform: translateY(-3px);\n    opacity: 1;\n  }\n}\n\n@keyframes fade-in {\n  from {\n    opacity: 0;\n    transform: translateY(10px);\n  }\n\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n"
  },
  {
    "path": "src/components/ChatPanel.tsx",
    "content": "import type { Signal } from \"@preact/signals\";\nimport { useSignal, useSignalEffect } from \"@preact/signals\";\nimport { getLuminance } from \"color2k\";\nimport hljs from \"highlight.js\";\nimport { marked } from \"marked\";\nimport { markedHighlight } from \"marked-highlight\";\nimport { render } from \"preact\";\nimport { useCallback, useEffect, useRef } from \"preact/hooks\";\n\nimport type { ChatModel } from \"@/client/chat\";\nimport {\n  COPILOT_ACADEMIC_INSTRUCTIONS,\n  COPILOT_CATGIRL_INSTRUCTIONS,\n  COPILOT_CREATIVE_INSTRUCTIONS,\n  COPILOT_MARKDOWN_INSTRUCTIONS,\n  ChatSession,\n  listCopilotChatModels,\n} from \"@/client/chat\";\nimport { t } from \"@/i18n\";\n\nimport CopilotIcon from \"./CopilotIcon\";\n\nimport \"./ChatPanel.scss\";\n\ninterface ChatPanelProps {\n  onClose: () => void;\n}\n\ntype PromptType = \"Normal\" | \"Academic\" | \"Creative\" | \"CatGirl\";\n\nconst ChatPanel: FC<ChatPanelProps> = ({ onClose }) => {\n  const input = useSignal(\"\");\n  const promptType = useSignal<PromptType>(\"Normal\");\n  const isThinking = useSignal(false);\n  const isSending = useSignal(false);\n  const messages = useSignal<Message[]>([]);\n  const sessionJustSwitchedFlag = useSignal(false);\n\n  const modelId = useSignal(\"gpt-4o\");\n  const models = useSignal<ChatModel[]>([]);\n\n  const currentSessionId = useSignal(\"\");\n  const sessions = useSignal<ChatSession[]>([]);\n\n  const getPrompt = useCallback(() => {\n    if (promptType.value === \"Normal\") return COPILOT_MARKDOWN_INSTRUCTIONS;\n    if (promptType.value === \"Academic\") return COPILOT_ACADEMIC_INSTRUCTIONS;\n    if (promptType.value === \"Creative\") return COPILOT_CREATIVE_INSTRUCTIONS;\n    return COPILOT_CATGIRL_INSTRUCTIONS.replace(\n      \"{{CATGIRL_NAME}}\",\n      t.test(\"chat.prompt-style.cat-girl-name\") ? t(\"chat.prompt-style.cat-girl-name\") : \"Vanilla\",\n    );\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  const getPromptType = useCallback((prompt: string) => {\n    if (prompt === COPILOT_MARKDOWN_INSTRUCTIONS) return \"Normal\";\n    if (prompt === COPILOT_ACADEMIC_INSTRUCTIONS) return \"Academic\";\n    if (prompt === COPILOT_CREATIVE_INSTRUCTIONS) return \"Creative\";\n    if (\n      prompt ===\n      COPILOT_CATGIRL_INSTRUCTIONS.replace(\n        \"{{CATGIRL_NAME}}\",\n        t.test(\"chat.prompt-style.cat-girl-name\") ?\n          t(\"chat.prompt-style.cat-girl-name\")\n        : \"Vanilla\",\n      )\n    )\n      return \"CatGirl\";\n    return \"Normal\";\n  }, []);\n\n  // Watch modelId changes and update session meta\n  useEffect(() => {\n    const currentSession = sessions.value.find((session) => session.id === currentSessionId.value);\n    if (!currentSession) return;\n\n    if (currentSession.modelId === modelId.value) return;\n\n    currentSession.modelId = modelId.value;\n    void ChatSession.save(currentSession.id);\n    sessions.value = ChatSession.getAll();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [modelId.value, currentSessionId.value]);\n\n  // Watch prompt type changes and update the system prompt\n  useEffect(() => {\n    const currentSession = sessions.value.find((session) => session.id === currentSessionId.value);\n    if (!currentSession) return;\n\n    if (currentSession.messages[0]!.content === getPrompt()) return;\n\n    currentSession.messages[0]!.content = getPrompt();\n    void ChatSession.save(currentSession.id);\n    sessions.value = ChatSession.getAll();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [promptType.value, currentSessionId.value, getPrompt]);\n\n  // Load models and sessions on mount\n  useEffect(() => {\n    // Load models\n    void listCopilotChatModels().then((modelList) => {\n      models.value = modelList;\n    });\n\n    // Load sessions\n    const loadSessions = async () => {\n      await ChatSession.loadAll();\n\n      const allSessions = ChatSession.getAll();\n      sessions.value = allSessions;\n\n      // Select the first session if available, otherwise create a new one\n      if (allSessions.length > 0) {\n        currentSessionId.value = allSessions[0]!.id;\n\n        modelId.value = allSessions[0]!.modelId;\n        promptType.value = getPromptType(allSessions[0]!.messages[0]!.content);\n\n        messages.value = allSessions[0]!.messages\n          .filter((msg) => msg.role !== \"system\")\n          .map((msg) => ({\n            role: msg.role as \"user\" | \"assistant\",\n            content: msg.content,\n          }));\n        sessionJustSwitchedFlag.value = !sessionJustSwitchedFlag.value;\n      } else {\n        createNewSession();\n      }\n    };\n\n    void loadSessions();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  const createNewSession = useCallback(() => {\n    const session = ChatSession.create(modelId.value, getPrompt());\n    currentSessionId.value = session.id;\n\n    sessions.value = ChatSession.getAll();\n\n    messages.value = [];\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  const switchSession = useCallback((id: string) => {\n    const session = ChatSession.get(id);\n    if (!session) return;\n\n    currentSessionId.value = session.id;\n\n    modelId.value = session.modelId;\n    promptType.value = getPromptType(session.messages[0]!.content);\n\n    messages.value = session.messages\n      .filter((msg) => msg.role !== \"system\")\n      .map((msg) => ({\n        role: msg.role as \"user\" | \"assistant\",\n        content: msg.content,\n      }));\n    sessionJustSwitchedFlag.value = !sessionJustSwitchedFlag.value;\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  const handleEditChatTitle = useCallback((id: string, title: string) => {\n    const session = ChatSession.get(id);\n    if (!session) return;\n\n    session.title = title;\n    void ChatSession.save(session.id);\n\n    sessions.value = ChatSession.getAll();\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  const handleDeleteSession = useCallback(\n    (id: string) => {\n      void ChatSession.delete(id);\n\n      sessions.value = ChatSession.getAll();\n\n      // If current session is deleted, switch to the first available session\n      if (id === currentSessionId.value) {\n        if (sessions.value.length > 0) {\n          switchSession(sessions.value[0]!.id);\n        } else {\n          createNewSession();\n        }\n      }\n    },\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [createNewSession, switchSession],\n  );\n\n  // Send message handler\n  const handleSendMessage = () => {\n    const userMessage = input.value.trim();\n    if (!userMessage || isSending.value) return;\n\n    // Add user message\n    messages.value = [\n      ...messages.value,\n      {\n        role: \"user\",\n        content: input.value.trim(),\n      },\n    ];\n\n    // Clear input\n    input.value = \"\";\n\n    // Show typing indicator\n    isThinking.value = true;\n    isSending.value = true;\n\n    abortControllerRef.current = new AbortController();\n    sessions.value\n      .find((session) => session.id === currentSessionId.value)\n      ?.send(\n        userMessage,\n        (content) => {\n          if (isThinking.value) {\n            isThinking.value = false;\n            messages.value = [\n              ...messages.value,\n              {\n                role: \"assistant\",\n                content: \"\",\n              },\n            ];\n          }\n          messages.value = [\n            ...messages.value.slice(0, messages.value.length - 1),\n            {\n              role: \"assistant\",\n              content: messages.value[messages.value.length - 1]!.content + content,\n            },\n          ];\n        },\n        {\n          model: models.value.find((model) => model.id === modelId.value),\n          signal: abortControllerRef.current.signal,\n        },\n      )\n      .then((fullContent) => {\n        messages.value = [\n          ...messages.value.slice(0, messages.value.length - 1),\n          {\n            role: \"assistant\",\n            content: fullContent,\n          },\n        ];\n        isSending.value = false;\n        void ChatSession.save(currentSessionId.value);\n      })\n      .catch((error) => {\n        console.error(\"Error sending message:\", error);\n        isSending.value = false;\n      });\n  };\n\n  const abortControllerRef = useRef<AbortController | null>(null);\n  const handleStopSending = useCallback(() => {\n    abortControllerRef.current?.abort();\n  }, []);\n\n  return (\n    <div id=\"copilot-chat-panel\" className=\"copilot-chat-panel\">\n      <ChatHeader\n        onClose={onClose}\n        currentSessionId={currentSessionId}\n        sessions={sessions}\n        onSwitchSession={switchSession}\n        onNewSession={createNewSession}\n        onEditChatTitle={handleEditChatTitle}\n        onDeleteSession={handleDeleteSession}\n      />\n      <MessageList\n        promptType={promptType.value}\n        messages={messages.value}\n        isThinking={isThinking.value}\n        sessionJustSwitchedFlag={sessionJustSwitchedFlag.value}\n      />\n      <InputArea\n        input={input}\n        promptType={promptType}\n        modelId={modelId}\n        models={models.value}\n        onSend={handleSendMessage}\n        onStop={handleStopSending}\n        isSending={isSending.value}\n      />\n    </div>\n  );\n};\n\n/**********\n * Header *\n **********/\ninterface ChatHeaderProps {\n  onClose: () => void;\n  currentSessionId: Signal<string>;\n  sessions: Signal<ChatSession[]>;\n  onSwitchSession: (id: string) => void;\n  onNewSession: () => void;\n  onEditChatTitle?: (id: string, title: string) => void;\n  onDeleteSession?: (id: string) => void;\n}\n\nconst ChatHeader: FC<ChatHeaderProps> = ({\n  currentSessionId,\n  onClose,\n  onDeleteSession,\n  onEditChatTitle,\n  onNewSession,\n  onSwitchSession,\n  sessions,\n}) => {\n  const isDropdownOpen = useSignal(false);\n\n  // Handle click outside to close dropdown\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      const target = event.target as HTMLElement;\n      if (\n        isDropdownOpen.value &&\n        !target.closest(\".chat-title-dropdown-toggle\") &&\n        !target.closest(\".session-menu\")\n      ) {\n        isDropdownOpen.value = false;\n      }\n    };\n\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  const handleEditChatTitle = (id: string, title: string) => {\n    let newTitle = title;\n    Files.editor?.EditHelper.showDialog({\n      title: t(\"chat.dialog.edit-chat-title.title\"),\n      html: t(\"chat.dialog.edit-chat-title.html\").replace(\"{{SESSION_TITLE}}\", title),\n      buttons: [t(\"button.ok\"), t(\"button.cancel\")],\n      callback: (result) => {\n        if (result === 0 && onEditChatTitle) onEditChatTitle(id, newTitle);\n      },\n    });\n    $(\"#new-chat-title\").on(\"input\", (e) => {\n      newTitle = (e.target as HTMLInputElement).value;\n    });\n  };\n\n  const handleDeleteSession = (id: string, title: string) => {\n    // Show confirmation dialog\n    Files.editor?.EditHelper.showDialog({\n      title: t(\"chat.dialog.delete-session.title\"),\n      html: t(\"chat.dialog.delete-session.html\").replace(\"{{SESSION_TITLE}}\", title),\n      buttons: [t(\"button.delete\"), t(\"button.cancel\")],\n      callback: (result) => {\n        if (result === 0 && onDeleteSession) {\n          onDeleteSession(id);\n          // Close the dropdown if the current session is deleted\n          if (id === currentSessionId.value) isDropdownOpen.value = false;\n        }\n      },\n    });\n  };\n\n  return (\n    <div className=\"chat-panel-header\">\n      <div className=\"chat-title-dropdown\">\n        <button\n          className=\"chat-title-dropdown-toggle\"\n          onClick={() => (isDropdownOpen.value = !isDropdownOpen.value)}>\n          <h3>\n            {sessions.value.find((session) => session.id === currentSessionId.value)?.title ||\n              t(\"chat.button.new-session\")}\n          </h3>\n          <svg\n            className=\"dropdown-chevron\"\n            width=\"8\"\n            height=\"5\"\n            viewBox=\"0 0 8 5\"\n            fill=\"none\"\n            xmlns=\"http://www.w3.org/2000/svg\">\n            <path d=\"M4 5L0 0.5L8 0.5L4 5Z\" fill=\"currentColor\" />\n          </svg>\n        </button>\n\n        {isDropdownOpen.value && (\n          <div className=\"chat-panel-dropdown-menu session-menu\">\n            <button\n              className=\"chat-panel-dropdown-item new-session\"\n              onClick={() => {\n                onNewSession();\n                isDropdownOpen.value = false;\n              }}>\n              <svg viewBox=\"0 0 16 16\" fill=\"currentColor\" xmlns=\"http://www.w3.org/2000/svg\">\n                <path d=\"M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2z\" />\n              </svg>\n              {t(\"chat.button.new-session\")}\n            </button>\n\n            {sessions.value.map((session) => (\n              <button\n                key={session.id}\n                className={`chat-panel-dropdown-item ${session.id === currentSessionId.value ? \"active\" : \"\"}`}\n                onClick={() => {\n                  onSwitchSession(session.id);\n                  isDropdownOpen.value = false;\n                }}>\n                <span className=\"session-title\">\n                  {session.title || t(\"chat.button.new-session\")}\n                </span>\n                <div style={{ display: \"flex\", alignItems: \"center\" }}>\n                  <button\n                    className=\"edit-btn\"\n                    onClick={(e) => {\n                      e.stopPropagation();\n                      handleEditChatTitle(session.id, session.title);\n                    }}\n                    ty-hint={t(\"chat.button.edit-chat-title\")}>\n                    <svg width=\"12\" height=\"12\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n                      <path d=\"M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z\" />\n                    </svg>\n                  </button>\n                  <button\n                    className=\"delete-btn\"\n                    onClick={(e) => {\n                      e.stopPropagation();\n                      handleDeleteSession(session.id, session.title);\n                    }}\n                    ty-hint={t(\"chat.button.delete-session\")}>\n                    <svg width=\"12\" height=\"12\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n                      <path d=\"M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z\" />\n                      <path\n                        fillRule=\"evenodd\"\n                        d=\"M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z\"\n                      />\n                    </svg>\n                  </button>\n                </div>\n              </button>\n            ))}\n          </div>\n        )}\n      </div>\n\n      <button className=\"chat-panel-close-btn\" onClick={onClose}>\n        ×\n      </button>\n    </div>\n  );\n};\n\n/************\n * Messages *\n ************/\ninterface Message {\n  role: \"assistant\" | \"user\";\n  content: string;\n}\n\ninterface MessageContentProps {\n  content: string;\n}\n\nconst md = marked\n  .use({\n    // Avoid identifying `~` as strikethrough\n    // https://github.com/markedjs/marked/issues/1561#issuecomment-846571425\n    tokenizer: {\n      del: (src) => {\n        if (/^~~+(?=\\S)([\\s\\S]*?\\S)~~+/.exec(src)) return false;\n      },\n    },\n  })\n  .use(\n    markedHighlight({\n      emptyLangClass: \"hljs\",\n      langPrefix: \"hljs language-\",\n      highlight(code, lang) {\n        const language = hljs.getLanguage(lang) ? lang : \"plaintext\";\n        return hljs.highlight(code, { language }).value;\n      },\n    }),\n  );\n\nconst MessageContent: FC<MessageContentProps> = ({ content }) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (!containerRef.current) return;\n\n    containerRef.current.innerHTML = md.parse(content) as string;\n\n    // Handle links\n    const links = containerRef.current.querySelectorAll(\"a\");\n    links.forEach((link) => {\n      if (link.getAttribute(\"href\")?.startsWith(\"http\")) {\n        link.setAttribute(\"target\", \"_blank\");\n        link.setAttribute(\"rel\", \"noopener noreferrer\");\n      }\n    });\n\n    // Add copy button to code blocks\n    const codeBlocks = containerRef.current.querySelectorAll(\"pre\");\n    codeBlocks.forEach((pre) => {\n      const copyButton = document.createElement(\"button\");\n      copyButton.className = \"copy-code-button\";\n      copyButton.innerHTML = `\n        <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n          <path d=\"M4 4v9h9V4H4zm8 8H5V5h7v7z\"/>\n          <path d=\"M3 3v9h1V3h8V2H3v1z\"/>\n        </svg>\n      `;\n\n      copyButton.addEventListener(\"click\", () => {\n        const code = pre.textContent || \"\";\n        void navigator.clipboard.writeText(code.trim()).then(() => {\n          copyButton.innerHTML = `\n            <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n              <path d=\"M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0z\"/>\n            </svg>\n          `;\n          setTimeout(() => {\n            copyButton.innerHTML = `\n              <svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"currentColor\">\n                <path d=\"M4 4v9h9V4H4zm8 8H5V5h7v7z\"/>\n                <path d=\"M3 3v9h1V3h8V2H3v1z\"/>\n              </svg>\n            `;\n          }, 800);\n        });\n      });\n\n      pre.appendChild(copyButton);\n    });\n  }, [content]);\n\n  return <div ref={containerRef} className=\"message-content markdown-content\" />;\n};\n\nconst EmptyStateWelcome: FC = () => {\n  return (\n    <div className=\"empty-state-welcome\">\n      <div className=\"welcome-icon\">\n        <CopilotIcon\n          status=\"Normal\"\n          textColor=\"var(--text-color)\"\n          style={{ width: \"48px\", height: \"48px\" }}\n        />\n      </div>\n      <h2 className=\"welcome-title\">{t(\"chat.welcome.title\")}</h2>\n      <p className=\"welcome-subtitle\">{t(\"chat.welcome.subtitle\")}</p>\n    </div>\n  );\n};\n\ninterface MessageListProps {\n  promptType: PromptType;\n  messages: Message[];\n  isThinking: boolean;\n  sessionJustSwitchedFlag: boolean;\n}\n\nconst MessageList: FC<MessageListProps> = ({\n  isThinking,\n  messages,\n  promptType,\n  sessionJustSwitchedFlag,\n}) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  const scrollToBottom = useCallback(() => {\n    if (containerRef.current) containerRef.current.scrollTop = containerRef.current.scrollHeight;\n  }, []);\n\n  useEffect(scrollToBottom, [sessionJustSwitchedFlag, scrollToBottom]);\n\n  // Watch for new messages and scroll to bottom if the role is \"user\"\n  useEffect(() => {\n    if (messages.length > 0 && messages[messages.length - 1]!.role === \"user\") scrollToBottom();\n  }, [messages, scrollToBottom]);\n\n  return (\n    <div className=\"chat-panel-messages\" ref={containerRef}>\n      {messages.length === 0 && !isThinking ?\n        <EmptyStateWelcome />\n      : messages.map((message, index) => (\n          <div key={index} className={`chat-message-row ${message.role}`}>\n            {message.role === \"user\" ?\n              <>\n                <div className=\"message-header\">\n                  <div className=\"message-icon user-icon\">\n                    <svg viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n                      <rect x=\"2\" y=\"6\" width=\"8\" height=\"8\" rx=\"2\" fill=\"#7c80ff\" />\n                      <circle cx=\"11\" cy=\"5\" r=\"3\" fill=\"#ffb173\" />\n                      <rect x=\"6\" y=\"2\" width=\"4\" height=\"4\" rx=\"1\" fill=\"#67d8ae\" />\n                    </svg>\n                  </div>\n                  <div className=\"message-author\">{t(\"chat.you\")}</div>\n                </div>\n                <pre className=\"message-content\">{message.content}</pre>\n              </>\n            : <>\n                <div className=\"message-header\">\n                  <div className=\"message-icon copilot-icon\">\n                    {promptType === \"CatGirl\" ?\n                      <span\n                        style={{\n                          display: \"flex\",\n                          alignItems: \"center\",\n                          justifyContent: \"center\",\n                          fontSize: \"13px\",\n                        }}>\n                        🐱\n                      </span>\n                    : <CopilotIcon\n                        status=\"Normal\"\n                        textColor=\"var(--text-color)\"\n                        style={{ width: \"90%\", height: \"90%\" }}\n                      />\n                    }\n                  </div>\n                  <div className=\"message-author\">\n                    {promptType === \"CatGirl\" ?\n                      t(\"chat.prompt-style.cat-girl-name\")\n                    : \"GitHub Copilot\"}\n                  </div>\n                </div>\n                <MessageContent content={message.content} />\n              </>\n            }\n          </div>\n        ))\n      }\n\n      {isThinking && (\n        <div className=\"chat-message-row assistant\">\n          <div className=\"message-header\">\n            <div className=\"message-icon copilot-icon\">\n              {promptType === \"CatGirl\" ?\n                <span\n                  style={{\n                    display: \"flex\",\n                    alignItems: \"center\",\n                    justifyContent: \"center\",\n                    fontSize: \"13px\",\n                  }}>\n                  🐱\n                </span>\n              : <CopilotIcon status=\"InProgress\" textColor=\"var(--text-color)\" />}\n            </div>\n            <div className=\"message-author\">\n              {promptType === \"CatGirl\" ? t(\"chat.prompt-style.cat-girl-name\") : \"GitHub Copilot\"}\n            </div>\n          </div>\n          <div className=\"message-content typing-indicator\">\n            <span></span>\n            <span></span>\n            <span></span>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n};\n\n/**************\n * Input Area *\n **************/\ninterface DropdownProps {\n  label: string;\n  tooltip?: string;\n  isOpen: Signal<boolean>;\n  options: { value: string; label: string }[];\n  onSelect: (value: string) => void;\n  closeOtherDropdown: () => void;\n}\n\nconst Dropdown: FC<DropdownProps> = ({\n  closeOtherDropdown,\n  isOpen,\n  label,\n  onSelect,\n  options,\n  tooltip,\n}) => {\n  return (\n    <div className=\"chat-panel-dropdown\">\n      <button\n        className=\"chat-panel-dropdown-toggle\"\n        type=\"button\"\n        ty-hint={tooltip}\n        onClick={() => {\n          closeOtherDropdown();\n          isOpen.value = !isOpen.value;\n        }}>\n        {label}\n        <svg\n          className=\"dropdown-chevron\"\n          width=\"8\"\n          height=\"5\"\n          viewBox=\"0 0 8 5\"\n          fill=\"none\"\n          xmlns=\"http://www.w3.org/2000/svg\">\n          <path d=\"M4 5L0 0.5L8 0.5L4 5Z\" fill=\"currentColor\" />\n        </svg>\n      </button>\n\n      {isOpen.value && (\n        <div className=\"chat-panel-dropdown-menu\">\n          {options.map((option) => (\n            <button\n              key={option.value}\n              type=\"button\"\n              className=\"chat-panel-dropdown-item\"\n              onClick={() => {\n                onSelect(option.value);\n                isOpen.value = false;\n              }}>\n              {option.label}\n            </button>\n          ))}\n        </div>\n      )}\n    </div>\n  );\n};\n\ninterface InputAreaProps {\n  input: Signal<string>;\n  promptType: Signal<PromptType>;\n  modelId: Signal<string>;\n  models: ChatModel[];\n  onSend: () => void;\n  isSending: boolean;\n  onStop?: () => void;\n}\n\nconst InputArea: FC<InputAreaProps> = ({\n  input,\n  isSending,\n  modelId,\n  models,\n  onSend,\n  onStop,\n  promptType,\n}) => {\n  const textareaRef = useRef<HTMLTextAreaElement>(null);\n  const measureRef = useRef<HTMLTextAreaElement>(null);\n  const rows = useSignal(1);\n  const isPromptDropdownOpen = useSignal(false);\n  const isModelDropdownOpen = useSignal(false);\n\n  // Handle Enter key\n  const handleKeyDown = (e: KeyboardEvent) => {\n    if (e.key === \"Enter\" && !e.shiftKey) {\n      e.preventDefault();\n      if (e.ctrlKey) {\n        input.value += \"\\n\";\n        return;\n      }\n      onSend();\n    }\n  };\n\n  // Auto-resize textarea\n  const updateRows = useCallback(\n    (text: string) => {\n      if (!measureRef.current) return;\n\n      // Replace each empty line with a space so that empty lines are measured properly.\n      measureRef.current.textContent =\n        text ?\n          text\n            .split(\"\\n\")\n            .map((line) => (line === \"\" ? \" \" : line))\n            .join(\"\\n\")\n        : \" \";\n\n      const totalHeight = measureRef.current.scrollHeight;\n      const lineHeight = parseInt(window.getComputedStyle(measureRef.current).lineHeight);\n\n      // Clear the content to prevent vertical scrollbar from appearing\n      measureRef.current.textContent = \"\";\n\n      const actualRows = Math.ceil(totalHeight / lineHeight);\n      rows.value = Math.max(1, Math.min(6, actualRows)); // Cap at 6 rows\n    },\n    [rows],\n  );\n\n  useSignalEffect(() => {\n    updateRows(input.value);\n  });\n\n  // Handle click outside for dropdowns\n  useEffect(() => {\n    const handleClickOutside = (event: MouseEvent) => {\n      const target = event.target as HTMLElement;\n      if (\n        !target.closest(\".chat-panel-dropdown-toggle\") &&\n        !target.closest(\".chat-panel-dropdown-menu\") &&\n        (isPromptDropdownOpen.value || isModelDropdownOpen.value)\n      ) {\n        isPromptDropdownOpen.value = false;\n        isModelDropdownOpen.value = false;\n      }\n    };\n\n    document.addEventListener(\"mousedown\", handleClickOutside);\n    return () => {\n      document.removeEventListener(\"mousedown\", handleClickOutside);\n    };\n  }, [isModelDropdownOpen, isPromptDropdownOpen]);\n\n  return (\n    <div className=\"chat-panel-input-container\">\n      <div className=\"chat-input-wrapper\">\n        <textarea\n          ref={measureRef}\n          style={{\n            visibility: \"hidden\",\n            paddingTop: \"0\",\n            paddingBottom: \"0\",\n            border: \"none\",\n            position: \"absolute\",\n            top: \"-9999px\",\n          }}\n        />\n\n        <textarea\n          ref={textareaRef}\n          placeholder={t(\"chat.input-placeholder\")}\n          value={input.value}\n          rows={rows.value}\n          onChange={(e) => (input.value = (e.target as HTMLTextAreaElement).value)}\n          onKeyDown={(e) => handleKeyDown(e as unknown as KeyboardEvent)}\n          disabled={isSending}\n        />\n\n        {/* Controls in the bottom toolbar */}\n        <div className=\"chat-input-controls\">\n          {/* Prompt type dropdown */}\n          <Dropdown\n            label={t(`chat.prompt-style.${promptType.value}`)}\n            isOpen={isPromptDropdownOpen}\n            tooltip={t(\"chat.prompt-style.tooltip\")}\n            closeOtherDropdown={() => (isModelDropdownOpen.value = false)}\n            options={[\n              { value: \"Normal\", label: t(\"chat.prompt-style.Normal\") },\n              { value: \"Academic\", label: t(\"chat.prompt-style.Academic\") },\n              { value: \"Creative\", label: t(\"chat.prompt-style.Creative\") },\n              { value: \"CatGirl\", label: t(\"chat.prompt-style.CatGirl\") },\n            ]}\n            onSelect={(value) => {\n              promptType.value = value as never;\n            }}\n          />\n\n          {/* Model type dropdown */}\n          <Dropdown\n            label={models.find((model) => model.id === modelId.value)?.name || \"\"}\n            isOpen={isModelDropdownOpen}\n            tooltip={t(\"chat.pick-model.tooltip\")}\n            closeOtherDropdown={() => (isPromptDropdownOpen.value = false)}\n            options={models.map((model) => ({ value: model.id, label: model.name }))}\n            onSelect={(value) => (modelId.value = value)}\n          />\n\n          {/* Send button */}\n          <button\n            className={\"chat-panel-send-btn\" + (isSending ? \" sending\" : \"\")}\n            disabled={onStop ? !isSending && !input.value.trim() : !input.value.trim() || isSending}\n            onClick={() => {\n              if (isSending) {\n                onStop?.();\n                return;\n              }\n              onSend();\n            }}>\n            {isSending ? t(\"chat.button.stop\") : t(\"chat.button.send\")}\n          </button>\n        </div>\n      </div>\n    </div>\n  );\n};\n\n// Watch theme change and switch highlight.js theme\nsetInterval(() => {\n  const isDark = getLuminance(window.getComputedStyle(document.body).backgroundColor) < 0.5;\n  (window as any).setHighlightjsTheme(isDark ? \"dark\" : \"light\");\n}, 1000);\n\nexport let detachChatPanel: (() => void) | null = null;\n\nexport function attachChatPanel(): void {\n  // Check if panel already exists (only one instance allowed)\n  if (document.querySelector(\"#copilot-chat-container\")) return;\n\n  // Get reference to content element\n  const contentDiv = document.querySelector(\"content\");\n  if (!contentDiv?.parentNode) return; // Can’t attach if content doesn’t exist\n\n  // Create container and position it after content\n  const container = document.createElement(\"div\");\n  container.id = \"copilot-chat-container\";\n  container.style.position = \"absolute\";\n  container.style.right = \"0\";\n  container.style.bottom =\n    (document.querySelector<HTMLDivElement>(\"#footer-copilot\")?.getBoundingClientRect().height ||\n      0) + \"px\";\n\n  if (contentDiv.nextSibling) contentDiv.parentNode.insertBefore(container, contentDiv.nextSibling);\n  else contentDiv.parentNode.appendChild(container);\n\n  // Get saved width from localStorage or use default\n  const savedWidth = localStorage.getItem(\"copilot-chat-panel-width\");\n  const defaultWidth = Math.min(\n    400,\n    Math.max(280, (window.innerWidth - contentDiv.getBoundingClientRect().left) * 0.25),\n  );\n  let panelWidth = savedWidth ? parseInt(savedWidth) : defaultWidth;\n\n  // Ensure width is within reasonable range\n  panelWidth = Math.min(Math.max(panelWidth, 280), window.innerWidth * 0.5);\n\n  // Update position and width\n  const updatePosition = () => {\n    const contentRect = contentDiv.getBoundingClientRect();\n    (contentDiv as HTMLElement).style.right = `${panelWidth}px`;\n    container.style.top = `${contentRect.top}px`;\n    container.style.width = `${panelWidth}px`;\n  };\n\n  updatePosition();\n\n  // Create resize handle element\n  const resizeHandle = document.createElement(\"div\");\n  resizeHandle.className = \"chat-panel-resize-handle\";\n\n  // Handle drag events\n  let isResizing = false;\n  let startX = 0;\n  let startWidth = 0;\n\n  const startResize = (e: MouseEvent) => {\n    isResizing = true;\n    startX = e.clientX;\n    startWidth = panelWidth;\n    resizeHandle.classList.add(\"resizing\");\n    document.addEventListener(\"mousemove\", resize);\n    document.addEventListener(\"mouseup\", stopResize);\n    document.body.style.userSelect = \"none\"; // Prevent text selection during drag\n  };\n\n  const resize = (e: MouseEvent) => {\n    if (!isResizing) return;\n\n    // Calculate new width (note direction: moving mouse left increases width)\n    const dx = startX - e.clientX;\n    panelWidth = Math.min(Math.max(startWidth + dx, 280), window.innerWidth * 0.5);\n\n    // Update position\n    updatePosition();\n  };\n\n  const stopResize = () => {\n    isResizing = false;\n    resizeHandle.classList.remove(\"resizing\");\n    document.removeEventListener(\"mousemove\", resize);\n    document.removeEventListener(\"mouseup\", stopResize);\n    document.body.style.userSelect = \"\";\n\n    // Save width to localStorage\n    localStorage.setItem(\"copilot-chat-panel-width\", String(panelWidth));\n  };\n\n  resizeHandle.addEventListener(\"mousedown\", startResize);\n\n  contentDiv.addEventListener(\"resize\", updatePosition);\n  window.addEventListener(\"resize\", updatePosition);\n\n  localStorage.setItem(\"copilot-chat-panel-open\", \"true\");\n\n  const detach = () => {\n    (contentDiv as HTMLElement).style.right = \"0\";\n    contentDiv.removeEventListener(\"resize\", updatePosition);\n    window.removeEventListener(\"resize\", updatePosition);\n    localStorage.removeItem(\"copilot-chat-panel-open\");\n    render(null, container);\n    container.remove();\n  };\n\n  // Render the panel to the positioned container\n  render(<ChatPanel onClose={detach} />, container);\n  container.appendChild(resizeHandle); // Add resize handle after rendering\n\n  detachChatPanel = detach;\n}\n"
  },
  {
    "path": "src/components/CopilotIcon.tsx",
    "content": "import type { CopilotStatus } from \"@/client\";\n\nimport Spinner from \"./Spinner\";\n\nexport interface CopilotIconProps {\n  status: CopilotStatus | \"Disabled\";\n  textColor: string;\n  style?: preact.CSSProperties;\n}\n\n/**\n * Icon of Copilot, change according to status.\n * @returns\n */\nconst CopilotIcon: FC<CopilotIconProps> = ({ status, style, textColor }) => {\n  return (\n    <div\n      style={{\n        height: \"50%\",\n        aspectRatio: \"1 / 1\",\n        display: \"flex\",\n        flexDirection: \"row\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        // eslint-disable-next-line @typescript-eslint/no-misused-spread\n        ...style,\n      }}>\n      {(() => {\n        if (status === \"InProgress\") return <Spinner color={textColor} />;\n        if (status === \"Normal\")\n          return (\n            <svg\n              viewBox=\"0 0 16 16\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              style={{ height: \"100%\", width: \"100%\" }}>\n              <path\n                d=\"M6.25 9a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 6.25 9Zm4.25.75a.75.75 0 0 0-1.5 0v1.5a.75.75 0 0 0 1.5 0v-1.5Z\"\n                fill={textColor}\n              />\n              <path\n                d=\"M7.86 1.77c.05.053.097.107.14.164.043-.057.09-.111.14-.164.681-.731 1.737-.9 2.943-.765 1.23.136 2.145.527 2.724 1.26.566.716.693 1.614.693 2.485 0 .572-.053 1.147-.254 1.655l.168.838.066.033A2.75 2.75 0 0 1 16 9.736V11c0 .24-.086.438-.156.567-.073.131-.16.253-.259.366-.18.21-.404.413-.605.58a10.19 10.19 0 0 1-.792.597l-.015.01-.006.004-.028.018a8.849 8.849 0 0 1-.456.281c-.307.177-.749.41-1.296.642C11.296 14.528 9.756 15 8 15c-1.756 0-3.296-.472-4.387-.935a12.28 12.28 0 0 1-1.296-.641 8.849 8.849 0 0 1-.456-.281l-.028-.02-.006-.003-.015-.01a10.593 10.593 0 0 1-.792-.596 5.264 5.264 0 0 1-.605-.58 2.133 2.133 0 0 1-.259-.367A1.189 1.189 0 0 1 0 11V9.736a2.75 2.75 0 0 1 1.52-2.46l.067-.033.167-.838C1.553 5.897 1.5 5.322 1.5 4.75c0-.87.127-1.77.693-2.485.579-.733 1.494-1.124 2.724-1.26 1.206-.134 2.262.034 2.944.765ZM3 7.824v4.261c.02.013.043.025.065.038.264.152.65.356 1.134.562.972.412 2.307.815 3.801.815 1.494 0 2.83-.403 3.8-.815.412-.174.813-.375 1.2-.6v-4.26l-.023-.116c-.49.21-1.075.291-1.727.291-1.146 0-2.06-.328-2.71-.991A3.233 3.233 0 0 1 8 6.266c-.144.269-.321.52-.54.743C6.81 7.672 5.896 8 4.75 8c-.652 0-1.236-.082-1.726-.291L3 7.824Zm6.237-5.031c-.204.218-.359.678-.242 1.614.091.726.303 1.23.618 1.553.299.304.784.54 1.638.54.922 0 1.28-.199 1.442-.38.179-.2.308-.578.308-1.37 0-.765-.123-1.242-.37-1.555-.233-.296-.693-.586-1.713-.7-1.044-.116-1.488.091-1.681.298Zm-2.472 0c-.193-.207-.637-.414-1.681-.298-1.02.114-1.48.404-1.713.7-.247.313-.37.79-.37 1.555 0 .792.129 1.17.308 1.37.162.181.52.38 1.442.38.854 0 1.339-.236 1.638-.54.315-.323.527-.827.618-1.553.117-.936-.038-1.396-.242-1.614Z\"\n                fill={textColor}\n              />\n            </svg>\n          );\n        if (status === \"Warning\")\n          return (\n            <svg\n              viewBox=\"0 0 16 16\"\n              xmlns=\"http://www.w3.org/2000/svg\"\n              style={{ height: \"100%\", width: \"100%\" }}>\n              <path\n                d=\"M7.86 1.77c.05.053.097.107.14.164.043-.057.09-.111.14-.164.681-.731 1.737-.9 2.943-.765 1.23.136 2.145.527 2.724 1.26.566.716.693 1.614.693 2.485 0 .463-.035.929-.155 1.359a6.015 6.015 0 0 0-1.398-.616c.034-.195.053-.439.053-.743 0-.766-.123-1.242-.37-1.555-.233-.296-.693-.586-1.713-.7-1.044-.116-1.488.091-1.681.298-.204.218-.359.678-.242 1.614.06.479.172.86.332 1.158a6.014 6.014 0 0 0-2.92 2.144C5.926 7.904 5.372 8 4.75 8c-.652 0-1.237-.082-1.727-.291L3 7.824v4.261c.02.013.043.025.065.038a10.83 10.83 0 0 0 2.495 1.035c.21.629.522 1.21.916 1.726a11.883 11.883 0 0 1-2.863-.819 12.28 12.28 0 0 1-1.296-.641 8.849 8.849 0 0 1-.456-.281l-.028-.02-.006-.003-.015-.01a10.593 10.593 0 0 1-.792-.596 5.264 5.264 0 0 1-.605-.58 2.133 2.133 0 0 1-.259-.367A1.189 1.189 0 0 1 0 11V9.736a2.75 2.75 0 0 1 1.52-2.46l.067-.033.167-.838C1.553 5.897 1.5 5.322 1.5 4.75c0-.87.127-1.77.693-2.485.579-.733 1.494-1.124 2.724-1.26 1.206-.134 2.262.034 2.944.765ZM6.765 2.793c-.193-.207-.637-.414-1.681-.298-1.02.114-1.48.404-1.713.7-.247.313-.37.79-.37 1.555 0 .792.129 1.17.308 1.37.162.181.52.38 1.442.38.854 0 1.339-.236 1.638-.54.315-.323.527-.827.618-1.553.117-.936-.038-1.396-.242-1.614Z\"\n                fill={textColor}\n              />\n              <path\n                d=\"M8.498 14.81v.001a4.5 4.5 0 1 1 5.503-7.12 4.5 4.5 0 0 1-5.503 7.119ZM10.5 8.75V11a.75.75 0 0 0 1.5 0V8.75a.75.75 0 0 0-1.5 0Zm.75 5.75a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z\"\n                fill={textColor}\n              />\n            </svg>\n          );\n        /* Disabled */\n        return (\n          <svg\n            viewBox=\"0 0 16 16\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n            style={{ height: \"100%\", width: \"100%\" }}>\n            <path\n              d=\"M.865 2.759v.001l14.82 10.722a.755.755 0 0 0 .188.1.751.751 0 0 1-1.063 1.025l-1.415-1.024c-.274.147-.613.315-1.008.482C11.296 14.528 9.756 15 8 15c-1.756 0-3.296-.473-4.387-.934a11.947 11.947 0 0 1-1.654-.859l-.098-.065-.028-.018-.006-.004-.015-.01a10.19 10.19 0 0 1-.792-.597 5.145 5.145 0 0 1-.605-.58 2.185 2.185 0 0 1-.259-.366A1.193 1.193 0 0 1 0 11V9.736a2.75 2.75 0 0 1 1.52-2.46l.067-.033.167-.838c-.175-.442-.238-.936-.251-1.434L.31 4.107a.75.75 0 0 1 .555-1.348ZM7.86 1.77c.05.053.097.107.14.164.043-.057.09-.111.14-.164.681-.731 1.737-.9 2.943-.765 1.23.136 2.145.527 2.724 1.26.566.716.693 1.614.693 2.485 0 .572-.053 1.147-.254 1.655l.168.838.066.033A2.75 2.75 0 0 1 16 9.736V11c0 .24-.086.438-.156.567a1.59 1.59 0 0 1-.075.125L13 9.688V7.824l-.023-.115c-.49.21-1.075.291-1.727.291-.22 0-.43-.012-.633-.036L6.824 5.22c.082-.233.143-.503.182-.813.117-.936-.038-1.396-.242-1.614-.193-.207-.637-.414-1.681-.298-.707.079-1.144.243-1.424.434l-1.251-.905c.58-.579 1.422-.899 2.51-1.02 1.205-.133 2.26.035 2.943.766ZM4.75 8c-.652 0-1.237-.081-1.727-.291L3 7.825v4.26c.387.225.788.426 1.2.6.97.412 2.306.815 3.8.815 1.494 0 2.829-.403 3.801-.815.076-.033.15-.065.22-.097L5.594 7.934A5.158 5.158 0 0 1 4.75 8Zm4.486-5.207c-.204.218-.359.678-.242 1.614.091.726.303 1.23.618 1.553.299.304.784.54 1.638.54.922 0 1.28-.199 1.442-.38.179-.2.308-.578.308-1.37 0-.765-.123-1.242-.37-1.555-.233-.296-.693-.586-1.713-.7-1.044-.116-1.488.091-1.681.298Z\"\n              fill={textColor}\n            />\n          </svg>\n        );\n      })()}\n    </div>\n  );\n};\n\nexport default CopilotIcon;\n"
  },
  {
    "path": "src/components/DropdownWithInput.scss",
    "content": ".dropdown-with-input > input[type=\"text\"] {\n  padding: 0.75rem !important;\n  box-sizing: border-box !important;\n  border-width: 1px !important;\n  border-style: solid !important;\n  border-color: #edf2f7 !important; // border-gray-100\n  border-radius: 0.375rem !important;\n  transition: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform !important;\n  transition-duration: 200ms !important;\n  outline: none !important;\n}\n\n.dropdown-with-input.passed > input[type=\"text\"] {\n  box-shadow: #38a169 0 0 0 1px !important; // border-green-600\n  border-color: #68d391 !important; // border-green-400\n}\n\n.dropdown-with-input.failed > input[type=\"text\"] {\n  box-shadow: #e53e3e 0 0 0 1px !important; // border-red-600\n  border-color: #fc8181 !important; // border-red-400\n}\n\n.dropdown-with-input:not(.passed, .failed).focus > input[type=\"text\"],\n.dropdown-with-input:not(.passed, .failed, .focus) > input[type=\"text\"]:focus {\n  box-shadow: #3182ce 0 0 0 1px !important; // border-blue-600\n  border-color: #63b3ed !important; // border-blue-400\n}\n\n.dropdown-with-input > ul {\n  position: absolute !important;\n  top: 100% !important;\n  left: 0 !important;\n  right: 0 !important;\n  padding: 0 !important;\n  list-style: none !important;\n  border: 1px solid #edf2f7 !important; // border-gray-100\n  border-radius: 0.375rem !important;\n  box-shadow: 0 4px 8px rgba(0, 0, 0, 10%) !important;\n  max-height: 12rem !important;\n  overflow-y: auto !important;\n  z-index: 1000 !important;\n}\n\n.dropdown-with-input > ul > li {\n  cursor: pointer !important;\n  font-size: 0.875rem !important;\n  transition: background 0.2s !important;\n}\n\n.dropdown-with-input > ul > li:first-child {\n  padding: 0.5rem 0.75rem 0.3125rem !important;\n}\n\n.dropdown-with-input > ul > li:last-child {\n  padding: 0.3125rem 0.75rem 0.5rem !important;\n}\n\n.dropdown-with-input > ul > li:not(:first-child, :last-child) {\n  padding: 0.3125rem 0.75rem !important;\n}\n"
  },
  {
    "path": "src/components/DropdownWithInput.tsx",
    "content": "import { darken, getLuminance, lighten } from \"color2k\";\nimport { useRef, useState } from \"preact/hooks\";\n\nimport \"./DropdownWithInput.scss\";\n\nexport interface DropdownWithInputProps {\n  options: string[];\n\n  value: string;\n  onChange: (option: string) => void;\n\n  onOpenDropdown?: () => void;\n  onCloseDropdown?: () => void;\n\n  type?: \"default\" | \"passed\" | \"failed\";\n  forceFocus?: boolean;\n  placeholder?: string;\n  dropdownMarginTop?: string;\n}\n\nconst DropdownWithInput: FC<DropdownWithInputProps> = ({\n  dropdownMarginTop = \"0.375rem\",\n  forceFocus = false,\n  onChange,\n  onCloseDropdown,\n  onOpenDropdown,\n  options,\n  placeholder,\n  type = \"default\",\n  value,\n}) => {\n  const backgroundColor = window.getComputedStyle(document.body).backgroundColor;\n\n  const [isOpen, setIsOpen] = useState(false);\n  const [filteredOptions, setFilteredOptions] = useState<string[]>(options);\n\n  const dropdownRef = useRef<HTMLDivElement>(null);\n\n  const handleOptionClick = (option: string) => {\n    onChange(option); // Set the selected value\n    setIsOpen(false); // Close the dropdown\n    onCloseDropdown?.();\n  };\n\n  const handleBlur = (e: preact.TargetedFocusEvent<HTMLDivElement>) => {\n    if (!dropdownRef.current?.contains(e.relatedTarget as Node)) {\n      setIsOpen(false); // Close dropdown on blur\n      onCloseDropdown?.();\n    }\n  };\n\n  const handleInputChange = (e: preact.TargetedEvent<HTMLInputElement>) => {\n    const inputValue = (e.target! as unknown as { value: string }).value;\n    onChange(inputValue);\n    setFilteredOptions(\n      options.filter((option) => option.toLowerCase().includes(inputValue.toLowerCase())),\n    ); // Filter options based on input\n    setIsOpen(true); // Open dropdown if user is typing\n    onOpenDropdown?.();\n  };\n\n  const handleKeyDown = (e: preact.TargetedKeyboardEvent<HTMLInputElement>) => {\n    if (e.key === \"Enter\") {\n      setIsOpen(false); // Close dropdown when pressing Enter\n      onCloseDropdown?.();\n    }\n  };\n\n  return (\n    <div\n      ref={dropdownRef}\n      className={\n        \"dropdown-with-input\" +\n        (type === \"default\" ? \"\" : ` ${type}`) +\n        (forceFocus ? \" focus\" : \"\")\n      }\n      style={{ position: \"relative\", width: \"100%\" }}\n      tabIndex={0} // Allow the div to gain focus\n      onBlur={handleBlur}>\n      {/* Input Field */}\n      <input\n        type=\"text\"\n        value={value}\n        onChange={handleInputChange}\n        onKeyDown={handleKeyDown} // Handle Enter key\n        placeholder={placeholder}\n        style={{ width: \"100%\" }}\n        onFocus={() => {\n          setIsOpen(true); // Open dropdown when input is focused\n          onOpenDropdown?.();\n          setFilteredOptions(options); // Reset filtered options on focus\n        }}\n      />\n\n      {/* Dropdown Menu */}\n      {isOpen && filteredOptions.length > 0 && (\n        <ul\n          style={{\n            marginTop: dropdownMarginTop === \"default\" ? \"0.375rem\" : dropdownMarginTop,\n            backgroundColor,\n          }}>\n          {filteredOptions.map((option, index) => (\n            <li\n              key={index}\n              onClick={() => {\n                handleOptionClick(option);\n              }}\n              onMouseEnter={(e) => {\n                e.currentTarget.style.background =\n                  getLuminance(backgroundColor) > 0.5 ?\n                    darken(backgroundColor, 0.05)\n                  : lighten(backgroundColor, 0.05);\n              }}\n              onMouseLeave={(e) => (e.currentTarget.style.background = \"transparent\")}>\n              {option}\n            </li>\n          ))}\n        </ul>\n      )}\n    </div>\n  );\n};\n\nexport default DropdownWithInput;\n"
  },
  {
    "path": "src/components/ModalBody.tsx",
    "content": "export interface ModalBodyProps {\n  className?: string;\n  style?: preact.CSSProperties;\n}\n\nconst ModalBody: FC<ModalBodyProps> = ({ children, className, style }) => {\n  return (\n    // eslint-disable-next-line @typescript-eslint/no-misused-spread\n    <div className={className} style={{ padding: \"0.9rem\", width: \"100%\", ...style }}>\n      {children}\n    </div>\n  );\n};\n\nexport default ModalBody;\n"
  },
  {
    "path": "src/components/ModalCloseButton.scss",
    "content": ".modal-close-button {\n  font-size: 1.2rem !important;\n  opacity: 0.5 !important;\n}\n\n.modal-close-button:hover {\n  opacity: 1 !important;\n}\n"
  },
  {
    "path": "src/components/ModalCloseButton.tsx",
    "content": "import \"./ModalCloseButton.scss\";\n\nexport interface ModalCloseButtonProps {\n  onClick?: () => void;\n}\n\nconst ModalCloseButton: FC<ModalCloseButtonProps> = ({ onClick }) => {\n  return (\n    <button type=\"button\" className=\"unset-button modal-close-button\" onClick={onClick}>\n      ✖\n    </button>\n  );\n};\n\nexport default ModalCloseButton;\n"
  },
  {
    "path": "src/components/ModalContent.tsx",
    "content": "const ModalContent: FC = ({ children }) => {\n  return (\n    <div\n      style={{\n        backgroundColor: window.getComputedStyle(document.body).backgroundColor,\n        color: window.getComputedStyle(document.body).color,\n        width: \"min(80ch, 120ch)\",\n        borderRadius: \"0.5rem\",\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n      onClick={(e) => {\n        e.stopPropagation();\n      }}>\n      {children}\n    </div>\n  );\n};\n\nexport default ModalContent;\n"
  },
  {
    "path": "src/components/ModalOverlay.scss",
    "content": ".modal-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  z-index: 99999;\n  background: rgba(0, 0, 0, 50%);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n"
  },
  {
    "path": "src/components/ModalOverlay.tsx",
    "content": "import { createPortal } from \"preact/compat\";\nimport \"./ModalOverlay.scss\";\n\nexport interface ModalOverlayProps {\n  onClose?: () => void;\n}\n\nconst ModalOverlay: FC<ModalOverlayProps> = ({ children, onClose }) => {\n  return createPortal(\n    <div class=\"modal-overlay\" onClick={onClose}>\n      {children}\n    </div>,\n    document.body,\n  );\n};\n\nexport default ModalOverlay;\n"
  },
  {
    "path": "src/components/ModalTitle.tsx",
    "content": "const ModalTitle: FC = ({ children }) => {\n  return <span style={{ fontWeight: \"bold\", fontSize: \"1.2rem\" }}>{children}</span>;\n};\n\nexport default ModalTitle;\n"
  },
  {
    "path": "src/components/ModelHeader.tsx",
    "content": "const ModalHeader: FC = ({ children }) => {\n  return (\n    <>\n      <div\n        style={{\n          display: \"flex\",\n          flexDirection: \"row\",\n          justifyContent: \"space-between\",\n          padding: \"1.4rem\",\n        }}>\n        {children}\n      </div>\n      <hr style={{ margin: 0, width: \"100%\" }} />\n    </>\n  );\n};\n\nexport default ModalHeader;\n"
  },
  {
    "path": "src/components/SettingsPanel.tsx",
    "content": "/* eslint-disable react-hooks/rules-of-hooks */\nimport type { Signal } from \"@preact/signals\";\nimport { useSignal } from \"@preact/signals\";\nimport { useMemo } from \"preact/hooks\";\nimport { debounce, mapValues } from \"radash\";\nimport semverGte from \"semver/functions/gte\";\nimport semverValid from \"semver/functions/valid\";\nimport { kebabCase } from \"string-ts\";\n\nimport { t } from \"@/i18n\";\nimport type { Settings } from \"@/settings\";\nimport { settings } from \"@/settings\";\nimport type { _Id } from \"@/types/tools\";\nimport { runCommand } from \"@/utils/cli-tools\";\nimport type { NodeRuntime } from \"@/utils/node-bridge\";\nimport {\n  getAllAvailableNodeRuntimes,\n  getCurrentNodeRuntime,\n  setCurrentNodeRuntime,\n} from \"@/utils/node-bridge\";\nimport { entriesOf, keysOf } from \"@/utils/tools\";\n\nimport DropdownWithInput from \"./DropdownWithInput\";\nimport ModalBody from \"./ModalBody\";\nimport ModalCloseButton from \"./ModalCloseButton\";\nimport ModalContent from \"./ModalContent\";\nimport ModalOverlay from \"./ModalOverlay\";\nimport ModalTitle from \"./ModalTitle\";\nimport ModalHeader from \"./ModelHeader\";\nimport Switch from \"./Switch\";\nimport { NodejsIcon, SettingsIcon } from \"./icons\";\n\ninterface SettingControl<K extends keyof Settings> {\n  position: \"right\" | \"bottom\";\n  component: (key: K, signal: Signal<Settings[K]>) => preact.JSX.Element;\n}\ntype TypedSettingControl<T> = SettingControl<\n  keyof { [K in keyof Settings as Settings[K] extends T ? K : never]: void }\n>;\n\nconst BooleanSettingControl: TypedSettingControl<boolean> = {\n  position: \"right\",\n  component: (key, signal) => (\n    <Switch\n      value={signal.value}\n      onChange={(value) => {\n        signal.value = value;\n        settings[key] = value;\n      }}\n    />\n  ),\n};\n\ntype Categories = Record<string, { [K in keyof Settings]?: SettingControl<K> }>;\nconst categories = {\n  general: {\n    disableCompletions: BooleanSettingControl,\n    useInlineCompletionTextInSource: BooleanSettingControl,\n    useInlineCompletionTextInPreviewCodeBlocks: BooleanSettingControl,\n  },\n  nodejs: {\n    nodePath: {\n      position: \"bottom\",\n      component: (key, signal) => {\n        const optionAuto = (() => {\n          if (getAllAvailableNodeRuntimes().length === 0) return null;\n          const { path, version } =\n            getAllAvailableNodeRuntimes().find(({ path }) => path === \"bundled\") ??\n            getAllAvailableNodeRuntimes()[0]!;\n          return (\n            `${t(\"settings-panel.nodejs.constant.PATH_AUTO_DETECT\")} ` +\n            `(${path === \"bundled\" ? t(\"settings-panel.nodejs.constant.PATH_BUNDLED\") : path}, ` +\n            `${version.startsWith(\"v\") ? version : \"v\" + version})`\n          );\n        })();\n        const options = [\n          ...(optionAuto ? [optionAuto] : []),\n          ...getAllAvailableNodeRuntimes()\n            .filter(({ path }) => path !== \"bundled\")\n            .map(\n              ({ path, version }) =>\n                `${path} (${version.startsWith(\"v\") ? version : \"v\" + version})`,\n            ),\n        ];\n\n        const currentVersion = useSignal(getCurrentNodeRuntime().version);\n\n        const inputType = useSignal<\"default\" | \"passed\" | \"failed\">(\n          getCurrentNodeRuntime().path === \"not found\" ? \"failed\" : \"default\",\n        );\n        const forceFocusInput = useSignal(false);\n        const info = useSignal(\n          getCurrentNodeRuntime().path === \"not found\" ?\n            options.length > 0 ?\n              t(\"settings-panel.nodejs.node-path.message.warn-empty-select-or-input\")\n            : t(\"settings-panel.nodejs.node-path.message.warn-empty-input\")\n          : \"\",\n        );\n        const infoColor = useSignal(\n          getCurrentNodeRuntime().path === \"not found\" ?\n            /* text-red-500 */ \"#f56565\"\n          : /* text-blue-500 */ \"#4299e1\",\n        );\n        const dropdownMarginTop = useSignal(\n          getCurrentNodeRuntime().path === \"not found\" ? \"1.75rem\" : \"default\",\n        );\n\n        const parseOption = (option: string): NodeRuntime => {\n          const parts = option.split(\" \");\n\n          if (option === optionAuto) {\n            let path = parts\n              .slice(0, -1)\n              .join(\" \")\n              .slice(t(\"settings-panel.nodejs.constant.PATH_AUTO_DETECT\").length + 2, -1);\n            if (path === t(\"settings-panel.nodejs.constant.PATH_BUNDLED\")) path = \"bundled\";\n            const version = parts[parts.length - 1]!.slice(0, -1);\n            return { path, version };\n          }\n\n          if (parts.length < 2) return { path: option, version: \"unknown\" };\n          const lastPart = parts[parts.length - 1];\n          if (!lastPart || !lastPart.startsWith(\"(\") || !lastPart.endsWith(\")\"))\n            return { path: option, version: \"unknown\" };\n          const version = lastPart.slice(1, -1);\n          if (!semverValid(version)) return { path: option, version: \"unknown\" };\n          return {\n            path: parts.slice(0, -1).join(\" \"),\n            version: version.startsWith(\"v\") ? version : `v${version}`,\n          };\n        };\n\n        const retrieveRuntimeVersion = useMemo(\n          () =>\n            debounce(\n              { delay: 500 },\n              (() => {\n                let latestTimestamp = 0;\n\n                return (path: string) => {\n                  const timestamp = Date.now();\n                  latestTimestamp = timestamp;\n\n                  runCommand(`\"${path}\" -v`)\n                    .then((output) => {\n                      if (latestTimestamp !== timestamp) return;\n                      const version = output.trim();\n                      if (!version) throw new Error(\"No version found\");\n                      if (!semverValid(version)) throw new Error(`Invalid version: ${version}`);\n                      if (semverGte(version, \"20.0.0\")) {\n                        setCurrentNodeRuntime({ path, version });\n                        settings[key] = path;\n                        signal.value = path;\n                        currentVersion.value = version;\n                        inputType.value = \"passed\";\n                        forceFocusInput.value = false;\n                        info.value = t(\"settings-panel.nodejs.node-path.message.updated\")\n                          .replace(\"{{PATH}}\", path)\n                          .replace(\"{{VERSION}}\", version);\n                        infoColor.value = \"#48bb78\"; // text-green-500\n                      } else {\n                        inputType.value = \"failed\";\n                        forceFocusInput.value = false;\n                        info.value = t(\n                          \"settings-panel.nodejs.node-path.message.warn-invalid-version\",\n                        )\n                          .replace(\"{{PATH}}\", path)\n                          .replace(\"{{VERSION}}\", version);\n                        infoColor.value = \"#f56565\"; // text-red-500\n                      }\n                    })\n                    .catch(() => {\n                      if (latestTimestamp !== timestamp) return;\n                      inputType.value = \"failed\";\n                      forceFocusInput.value = false;\n                      info.value = t(\n                        \"settings-panel.nodejs.node-path.message.warn-invalid\",\n                      ).replace(\"{{PATH}}\", path);\n                      infoColor.value = \"#f56565\"; // text-red-500\n                    });\n                };\n              })(),\n            ),\n          [currentVersion, key, signal, inputType, forceFocusInput, info, infoColor],\n        );\n\n        return (\n          <div style={{ width: \"100%\", marginTop: \"0.75rem\" }}>\n            <DropdownWithInput\n              type={inputType.value}\n              forceFocus={forceFocusInput.value}\n              dropdownMarginTop={dropdownMarginTop.value}\n              options={options}\n              value={\n                signal.value === null ?\n                  (optionAuto ?? \"\")\n                : signal.value +\n                  (currentVersion.value === \"unknown\" ? \"\" : ` (${currentVersion.value})`)\n              }\n              onChange={(option) => {\n                const runtime = parseOption(option);\n                signal.value = option === optionAuto ? null : runtime.path;\n                currentVersion.value = runtime.version;\n\n                if (!runtime.path) {\n                  inputType.value = \"failed\";\n                  info.value =\n                    options.length > 0 ?\n                      t(\"settings-panel.nodejs.node-path.message.warn-empty-select-or-input\")\n                    : t(\"settings-panel.nodejs.node-path.message.warn-empty-input\");\n                  infoColor.value = \"#f56565\"; // text-red-500\n                  dropdownMarginTop.value = \"1.75rem\";\n                  return;\n                }\n\n                if (options.includes(option)) {\n                  setCurrentNodeRuntime(runtime);\n                  if (option === optionAuto) settings.clear(key);\n                  else settings[key] = runtime.path;\n                  inputType.value = \"passed\";\n                  forceFocusInput.value = false;\n                  info.value =\n                    option === optionAuto ?\n                      t(\"settings-panel.nodejs.node-path.message.updated-auto\")\n                    : t(\"settings-panel.nodejs.node-path.message.updated\")\n                        .replace(\"{{PATH}}\", runtime.path)\n                        .replace(\"{{VERSION}}\", runtime.version);\n                  infoColor.value = \"#48bb78\"; // text-green-500\n                } else {\n                  inputType.value = \"default\";\n                  forceFocusInput.value = true;\n                  info.value = t(\n                    \"settings-panel.nodejs.node-path.message.retrieving-version\",\n                  ).replace(\"{{PATH}}\", runtime.path);\n                  infoColor.value = \"#4299e1\"; // text-blue-500\n                  dropdownMarginTop.value = \"1.75rem\";\n                  retrieveRuntimeVersion(runtime.path);\n                }\n              }}\n              onOpenDropdown={() => {\n                if (inputType.value === \"passed\") inputType.value = \"default\";\n              }}\n              onCloseDropdown={() => {\n                if (inputType.value === \"default\" && !forceFocusInput.value) info.value = \"\";\n                dropdownMarginTop.value = \"default\";\n              }}\n            />\n\n            {info.value && (\n              <div\n                style={{\n                  marginTop: \"0.5rem\",\n                  fontSize: \"0.75rem\",\n                  lineHeight: 1,\n                  color: infoColor.value,\n                }}>\n                {info.value}\n              </div>\n            )}\n          </div>\n        );\n      },\n    },\n  },\n} as const satisfies Categories;\n\nconst categoryIcons = {\n  general: <SettingsIcon size={18} />,\n  nodejs: <NodejsIcon size={18} />,\n} as const satisfies Record<keyof typeof categories, preact.JSX.Element>;\n\ntype CategoriesSignals<C extends Categories> = _Id<{\n  [K in keyof C]: {\n    [P in keyof C[K]]: C[K][P] extends SettingControl<infer K> ? Signal<Settings[K]> : never;\n  };\n}>;\n\nexport interface SettingsPanelProps {\n  open?: boolean;\n  onClose?: () => void;\n}\n\nconst SettingsPanel: FC<SettingsPanelProps> = ({ onClose }) => {\n  const selectedCategory = useSignal(Object.keys(categories)[0] as keyof typeof categories);\n  const signals = mapValues(categories, (category) =>\n    mapValues(category as never, (_, key) => useSignal(settings[key])),\n  ) as CategoriesSignals<typeof categories>;\n\n  return (\n    <ModalOverlay onClose={onClose}>\n      <ModalContent>\n        <ModalHeader>\n          <ModalTitle>{t(\"settings-panel.title\")}</ModalTitle>\n          <ModalCloseButton onClick={onClose} />\n        </ModalHeader>\n        <ModalBody style={{ paddingTop: \"1rem\", display: \"flex\", flexDirection: \"row\" }}>\n          <div style={{ display: \"flex\", flexDirection: \"column\", justifyContent: \"flex-start\" }}>\n            {keysOf(categories).map((category, i, arr) => (\n              <MenuButton\n                key={category}\n                selected={selectedCategory.value === category}\n                onClick={() => {\n                  selectedCategory.value = category;\n                }}\n                style={{ marginBottom: i === arr.length - 1 ? \"0\" : \"0.5rem\" }}>\n                {categoryIcons[category]}\n                <span style={{ marginLeft: \"0.375rem\" }}>\n                  {t(`settings-panel.${category}.title`)}\n                </span>\n              </MenuButton>\n            ))}\n          </div>\n\n          <div\n            style={{\n              paddingLeft: \"2rem\",\n              paddingRight: \"1rem\",\n              paddingTop: \"0.25rem\",\n              paddingBottom: \"1rem\",\n              fontSize: \"0.875rem\",\n              width: \"100%\",\n              display: \"flex\",\n              flexDirection: \"column\",\n            }}>\n            {t.test(`settings-panel.${selectedCategory.value}.note`) && (\n              <div style={{ fontSize: \"0.75rem\", lineHeight: 1, opacity: 0.75 }}>\n                <span>* {t.tran(`settings-panel.${selectedCategory.value}.note`)}</span>\n                <hr\n                  style={{\n                    height: 0,\n                    margin: \"1rem 0\",\n                    border: \"none\",\n                    background: \"transparent\",\n                    borderTop: \"1px dashed\",\n                  }}\n                />\n              </div>\n            )}\n            {entriesOf(categories[selectedCategory.value]).map(([key, control], i, arr) => (\n              <>\n                <div key={key} style={{ width: \"100%\" }}>\n                  {(() => {\n                    if (control.position === \"right\")\n                      return (\n                        <>\n                          <div\n                            style={{\n                              width: \"100%\",\n                              display: \"flex\",\n                              flexDirection: \"row\",\n                              alignItems: \"center\",\n                              justifyContent: \"space-between\",\n                            }}>\n                            <span>\n                              {t.tran(\n                                `settings-panel.${selectedCategory.value}.${kebabCase(key)}.label`,\n                              )}\n                            </span>\n                            {control.component(\n                              key as never,\n                              (signals[selectedCategory.value] as never)[key],\n                            )}\n                          </div>\n                          <div\n                            style={{\n                              marginTop: \"0.5rem\",\n                              fontSize: \"0.75rem\",\n                              lineHeight: 1,\n                              opacity: 0.75,\n                            }}>\n                            {t.tran(\n                              `settings-panel.${selectedCategory.value}.${kebabCase(key)}.description`,\n                            )}\n                          </div>\n                        </>\n                      );\n                    /* position: bottom */\n                    return (\n                      <>\n                        <div>\n                          {t.tran(\n                            `settings-panel.${selectedCategory.value}.${kebabCase(key)}.label`,\n                          )}\n                        </div>\n                        <div\n                          style={{\n                            marginTop: \"0.5rem\",\n                            fontSize: \"0.75rem\",\n                            lineHeight: 1,\n                            opacity: 0.75,\n                          }}>\n                          {t.tran(\n                            `settings-panel.${selectedCategory.value}.${kebabCase(key)}.description`,\n                          )}\n                        </div>\n                        <div style={{ marginTop: \"0.5rem\" }}>\n                          {control.component(\n                            key as never,\n                            (signals[selectedCategory.value] as never)[key],\n                          )}\n                        </div>\n                      </>\n                    );\n                  })()}\n\n                  {t.test(`settings-panel.${selectedCategory.value}.${kebabCase(key)}.warning`) && (\n                    <div\n                      style={{\n                        marginTop: \"0.5rem\",\n                        fontSize: \"0.75rem\",\n                        lineHeight: 1,\n                        color: \"#ef4444\",\n                      }}>\n                      {t.tran(`settings-panel.${selectedCategory.value}.${kebabCase(key)}.warning`)}\n                    </div>\n                  )}\n                </div>\n\n                {i !== arr.length - 1 && (\n                  <hr style={{ width: \"100%\", margin: \"1.375rem 0 1rem 0\" }} />\n                )}\n              </>\n            ))}\n          </div>\n        </ModalBody>\n      </ModalContent>\n    </ModalOverlay>\n  );\n};\n\nconst MenuButton: FC<{\n  selected?: boolean;\n  onClick?: () => void;\n  style?: preact.CSSProperties;\n}> = ({ children, onClick, selected = false, style: additionalStyle }) => {\n  const style: preact.CSSProperties = {\n    width: \"100%\",\n    fontSize: \"0.875rem\",\n    height: \"fit-content\",\n    paddingTop: \"0.25rem\",\n    paddingBottom: \"0.25rem\",\n    paddingLeft: \"0.5rem\",\n    paddingRight: \"0.75rem\",\n    borderRadius: \"0.5rem\",\n    display: \"flex\",\n    whiteSpace: \"nowrap\",\n    alignItems: \"center\",\n    justifyContent: \"flex-start\",\n    cursor: selected ? \"default\" : \"pointer\",\n    backgroundColor: selected ? \"var(--item-hover-bg-color)\" : \"transparent\",\n    ...(selected ? { pointerEvents: \"none\" } : {}),\n    // eslint-disable-next-line @typescript-eslint/no-misused-spread\n    ...additionalStyle,\n  };\n  return selected ?\n      <button className=\"unset-button\" style={style} disabled>\n        {children}\n      </button>\n    : <button type=\"button\" className=\"unset-button\" style={style} onClick={onClick}>\n        {children}\n      </button>;\n};\n\nexport default SettingsPanel;\n"
  },
  {
    "path": "src/components/Spinner.tsx",
    "content": "export interface SpinnerProps {\n  color?: string;\n}\n\n/**\n * An svg spinner.\n * @returns\n */\nconst Spinner: FC<SpinnerProps> = ({ color = \"gray\" }) => {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      style={{ height: \"100%\", width: \"100%\" }}\n      dangerouslySetInnerHTML={{\n        __html: /* html */ `\n          <style>\n            .spinner_aj0A {\n              transform-origin: center;\n              animation: spinner_KYSC 0.75s infinite linear;\n            }\n            @keyframes spinner_KYSC {\n              100% {\n                transform: rotate(360deg);\n              }\n            }\n          </style>\n          <path\n            d=\"M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z\"\n            class=\"spinner_aj0A\"\n            fill=\"${color}\"\n          />\n        `,\n      }}\n    />\n  );\n};\n\nexport default Spinner;\n"
  },
  {
    "path": "src/components/SuggestionPanel.scss",
    "content": ".suggestion-panel {\n  position: absolute;\n  z-index: 9999;\n  pointer-events: none;\n  white-space: pre-wrap;\n  border: 1px solid #ccc;\n  display: flex;\n  flex-direction: column;\n  padding: 0.5em;\n  border-radius: 5px;\n  box-shadow: 0 4px 8px rgba(0, 0, 0, 50%);\n}\n"
  },
  {
    "path": "src/components/SuggestionPanel.tsx",
    "content": "import { render } from \"preact\";\nimport { useEffect, useRef } from \"preact/hooks\";\n\nimport { t } from \"@/i18n\";\nimport { getCaretCoordinate } from \"@/utils/dom\";\n\nimport CopilotIcon from \"./CopilotIcon\";\n\nimport \"./SuggestionPanel.scss\";\n\nexport interface SuggestionPanelProps {\n  x: number;\n  y: number;\n  mode?: NonNullable<CodeMirror.EditorConfiguration[\"mode\"]> | null;\n  text: string;\n  textColor?: string;\n  fontSize?: string | null;\n  backgroundColor?: string | null;\n  cm?: CodeMirror.Editor;\n}\n\n/**\n * Create and attach a suggestion panel to the DOM. The element is placed right below caret.\n * @param text The text to be displayed in the suggestion panel.\n * @returns A function that can be used to remove the suggestion panel from the DOM.\n */\nexport const attachSuggestionPanel = (\n  text: string,\n  mode: NonNullable<CodeMirror.EditorConfiguration[\"mode\"]> | null = null,\n  options?: { backgroundColor?: string | null; fontSize?: string | null; cm?: CodeMirror.Editor },\n) => {\n  const pos =\n    options?.cm ?\n      { x: options.cm.cursorCoords().left, y: options.cm.cursorCoords().top }\n    : getCaretCoordinate();\n  if (!pos) return () => {};\n\n  const container = document.createElement(\"div\");\n  document.body.appendChild(container);\n\n  const { x, y } = pos;\n  render(\n    <SuggestionPanel\n      x={x}\n      y={y}\n      text={text}\n      mode={mode}\n      fontSize={options?.fontSize}\n      backgroundColor={options?.backgroundColor}\n      cm={options?.cm}\n    />,\n    container,\n  );\n\n  // Adjust position when scrolling\n  const scrollListener = () => {\n    const pos =\n      options?.cm ?\n        { x: options.cm.cursorCoords().left, y: options.cm.cursorCoords().top }\n      : getCaretCoordinate();\n    if (!pos) return;\n    $(\".suggestion-panel\").css(\"top\", `calc(${pos.y}px + 1.5em)`);\n  };\n  if (options?.cm) options.cm.on(\"scroll\", scrollListener);\n  $(\"content\").on(\"scroll\", scrollListener);\n\n  return () => {\n    if (options?.cm) options.cm.off(\"scroll\", scrollListener);\n    $(\"content\").off(\"scroll\", scrollListener);\n    render(null, container);\n    container.remove();\n  };\n};\n\n/**\n * A suggestion panel that displays the text generated by Copilot.\n * @returns\n */\nconst SuggestionPanel: FC<SuggestionPanelProps> = ({\n  backgroundColor,\n  cm,\n  fontSize,\n  mode,\n  text,\n  textColor = \"gray\",\n  x,\n  y,\n}) => {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const codeAreaRef = useRef<HTMLTextAreaElement>(null);\n\n  const maxAvailableWidth =\n    (cm ? cm.getWrapperElement() : Files.editor!.writingArea).getBoundingClientRect().width -\n    (x - (cm ? cm.getWrapperElement() : Files.editor!.writingArea).getBoundingClientRect().left) -\n    30;\n\n  // Calculate actual width after mount, and adjust position\n  useEffect(() => {\n    const actualWidth = containerRef.current!.getBoundingClientRect().width;\n\n    containerRef.current!.style.left = `min(${x}px, calc(${x}px + ${maxAvailableWidth}px - ${actualWidth}px))`;\n\n    // Highlight syntax using CodeMirror\n    const cm = CodeMirror.fromTextArea(codeAreaRef.current!, {\n      lineWrapping: true,\n      mode: mode || \"gfm\",\n      theme: mode ? \"inner null-scroll\" : \"typora-default\",\n      maxHighlightLength: Infinity,\n      // @ts-expect-error - Extracted from Typora. I don't really know if this prop is used,\n      // but to be safe, I just keep it like original\n      styleActiveLine: true,\n      visibleSpace: true,\n      autoCloseTags: true,\n      resetSelectionOnContextMenu: false,\n      lineNumbers: false,\n      dragDrop: false,\n    });\n\n    // Adjust cm styles\n    const panelBackgroundColor =\n      backgroundColor || window.getComputedStyle(document.body).backgroundColor;\n    codeAreaRef.current!.style.backgroundColor = panelBackgroundColor;\n    cm.getWrapperElement().style.backgroundColor = panelBackgroundColor;\n    $(cm.getWrapperElement()).children().css(\"backgroundColor\", panelBackgroundColor);\n    cm.getWrapperElement().style.padding = \"0\";\n    if (fontSize) cm.getWrapperElement().style.fontSize = fontSize;\n    if (backgroundColor) {\n      cm.getWrapperElement().style.padding = \"0.5rem\";\n      cm.getWrapperElement().style.borderRadius = \"0.375rem\";\n    }\n    $(cm.getWrapperElement()).find(\".CodeMirror-hscrollbar\").remove();\n    $(cm.getWrapperElement()).find(\".CodeMirror-activeline-background\").remove();\n\n    // Set visibility to visible\n    containerRef.current!.style.removeProperty(\"visibility\");\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  return (\n    <div\n      ref={containerRef}\n      className=\"suggestion-panel\"\n      style={{\n        // Visibility is set to hidden on mount to adjust position after calculating actual width,\n        // and then set to visible\n        visibility: \"hidden\",\n        left: 0,\n        top: `calc(${y}px + 1.5em)`,\n        maxWidth: `min(80ch, max(40ch, ${maxAvailableWidth}px))`,\n        backgroundColor: window.getComputedStyle(document.body).backgroundColor,\n        color: window.getComputedStyle(document.body).color,\n      }}>\n      <textarea ref={codeAreaRef} style={{ padding: 0 }} value={text} />\n      <div\n        style={{\n          color: textColor,\n          marginTop: \"0.25em\",\n          marginLeft: \"0.25em\",\n          display: \"flex\",\n          flexDirection: \"row\",\n          alignItems: \"center\",\n        }}>\n        <CopilotIcon\n          status=\"Normal\"\n          textColor={textColor}\n          style={{\n            marginRight: \"0.4em\",\n            height: \"1em\",\n            width: \"1em\",\n          }}\n        />\n        <span style={{ marginRight: \"0.25em\" }}>{t(\"hint.generated-by-copilot\")}</span>\n      </div>\n    </div>\n  );\n};\n\nexport default SuggestionPanel;\n"
  },
  {
    "path": "src/components/Switch.scss",
    "content": ".switch {\n  width: 36px;\n  height: 20px;\n  border-radius: 20px;\n  position: relative;\n  cursor: pointer;\n  transition: background-color 0.2s;\n}\n\n.switch .toggle {\n  width: 16px;\n  height: 16px;\n  background-color: white;\n  border-radius: 50%;\n  position: absolute;\n  top: 2px;\n  left: 2px;\n  transition: left 0.2s;\n}\n\n.switch.on .toggle {\n  left: 18px;\n}\n"
  },
  {
    "path": "src/components/Switch.tsx",
    "content": "import { getLuminance } from \"color2k\";\nimport \"./Switch.scss\";\n\nexport interface SwitchProps {\n  value: boolean;\n  onChange: (value: boolean) => void;\n}\n\nconst Switch: FC<SwitchProps> = ({ onChange, value }) => {\n  const isDark = getLuminance(window.getComputedStyle(document.body).backgroundColor) < 0.5;\n\n  // The following colors and shadows are extracted from naive-ui\n  // https://www.naiveui.com/en-US/os-theme/components/switch\n  const switchBackgroundColor = {\n    dark: { on: \"#2a947d\", off: \"#464649\" },\n    light: { on: \"#18a058\", off: \"#dbdbdb\" },\n  }[isDark ? \"dark\" : \"light\"][value ? \"on\" : \"off\"];\n  const toggleBoxShadow = {\n    dark: \"0 2px 4px 0 rgba(0, 0, 0, 40%)\",\n    light: \"0 1px 4px 0 rgba(0, 0, 0, 30%), inset 0 0 1px 0 rgba(0, 0, 0, 5%)\",\n  }[isDark ? \"dark\" : \"light\"];\n\n  return (\n    <div\n      class={`switch ${value ? \"on\" : \"off\"}`}\n      style={{ backgroundColor: switchBackgroundColor }}\n      onClick={() => {\n        onChange(!value);\n      }}>\n      <div class=\"toggle\" style={{ boxShadow: toggleBoxShadow }} />\n    </div>\n  );\n};\n\nexport default Switch;\n"
  },
  {
    "path": "src/components/icons/index.ts",
    "content": "export * from \"./nodejs\";\nexport * from \"./settings\";\n"
  },
  {
    "path": "src/components/icons/nodejs.tsx",
    "content": "export const NodejsIcon: FC<{ size?: number }> = ({ size = 24 }) => {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width={(28.35 / 32) * 24}\n      height={size}\n      style={{ marginLeft: -(size * 0.075), marginRight: -(size * 0.075) }}\n      viewBox=\"0 0 256 289\">\n      <path\n        fill=\"#539e43\"\n        d=\"M128 288.464c-3.975 0-7.685-1.06-11.13-2.915l-35.247-20.936c-5.3-2.915-2.65-3.975-1.06-4.505c7.155-2.385 8.48-2.915 15.9-7.156c.796-.53 1.856-.265 2.65.265l27.032 16.166c1.06.53 2.385.53 3.18 0l105.74-61.217c1.06-.53 1.59-1.59 1.59-2.915V83.08c0-1.325-.53-2.385-1.59-2.915l-105.74-60.953c-1.06-.53-2.385-.53-3.18 0L20.405 80.166c-1.06.53-1.59 1.855-1.59 2.915v122.17c0 1.06.53 2.385 1.59 2.915l28.887 16.695c15.636 7.95 25.44-1.325 25.44-10.6V93.68c0-1.59 1.326-3.18 3.181-3.18h13.516c1.59 0 3.18 1.325 3.18 3.18v120.58c0 20.936-11.396 33.126-31.272 33.126c-6.095 0-10.865 0-24.38-6.625l-27.827-15.9C4.24 220.885 0 213.465 0 205.515V83.346C0 75.396 4.24 67.976 11.13 64L116.87 2.783c6.625-3.71 15.635-3.71 22.26 0L244.87 64C251.76 67.975 256 75.395 256 83.346v122.17c0 7.95-4.24 15.37-11.13 19.345L139.13 286.08c-3.445 1.59-7.42 2.385-11.13 2.385m32.596-84.009c-46.377 0-55.917-21.2-55.917-39.221c0-1.59 1.325-3.18 3.18-3.18h13.78c1.59 0 2.916 1.06 2.916 2.65c2.12 14.045 8.215 20.936 36.306 20.936c22.261 0 31.802-5.035 31.802-16.96c0-6.891-2.65-11.926-37.367-15.372c-28.886-2.915-46.907-9.275-46.907-32.33c0-21.467 18.02-34.187 48.232-34.187c33.921 0 50.617 11.66 52.737 37.101q0 1.193-.795 2.385c-.53.53-1.325 1.06-2.12 1.06h-13.78c-1.326 0-2.65-1.06-2.916-2.385c-3.18-14.575-11.395-19.345-33.126-19.345c-24.38 0-27.296 8.48-27.296 14.84c0 7.686 3.445 10.07 36.306 14.31c32.597 4.24 47.967 10.336 47.967 33.127c-.265 23.321-19.345 36.571-53.002 36.571\"\n      />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "src/components/icons/settings.tsx",
    "content": "export const SettingsIcon: FC<{ size?: number }> = ({ size = 24 }) => {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" width={size} height={size} viewBox=\"0 0 24 24\">\n      <path\n        fill=\"currentColor\"\n        d=\"M3.34 17a10.017 10.017 0 0 1-.979-2.326a3 3 0 0 0 .003-5.347a9.99 9.99 0 0 1 2.5-4.337a3 3 0 0 0 4.632-2.674a9.99 9.99 0 0 1 5.007.003a3 3 0 0 0 4.632 2.671a10.056 10.056 0 0 1 2.503 4.336a3 3 0 0 0-.002 5.347a9.99 9.99 0 0 1-2.501 4.337a3 3 0 0 0-4.632 2.674a9.99 9.99 0 0 1-5.007-.002a3 3 0 0 0-4.631-2.672A10.018 10.018 0 0 1 3.339 17m5.66.196a4.992 4.992 0 0 1 2.25 2.77c.499.047 1 .048 1.499.002a4.993 4.993 0 0 1 2.25-2.772a4.993 4.993 0 0 1 3.526-.564c.29-.408.54-.843.748-1.298A4.993 4.993 0 0 1 18 12c0-1.26.47-2.437 1.273-3.334a8.152 8.152 0 0 0-.75-1.298A4.993 4.993 0 0 1 15 6.804a4.993 4.993 0 0 1-2.25-2.77c-.5-.047-1-.048-1.5-.001A4.993 4.993 0 0 1 9 6.804a4.993 4.993 0 0 1-3.526.564c-.29.408-.54.843-.747 1.298A4.993 4.993 0 0 1 6 12c0 1.26-.471 2.437-1.273 3.334a8.16 8.16 0 0 0 .75 1.298A4.993 4.993 0 0 1 9 17.196M12 15a3 3 0 1 1 0-6a3 3 0 0 1 0 6m0-2a1 1 0 1 0 0-2a1 1 0 0 0 0 2\"\n      />\n    </svg>\n  );\n};\n"
  },
  {
    "path": "src/components/preact-env.d.ts",
    "content": "type FC<P = NonNullable<unknown>> = import(\"preact\").FunctionalComponent<P>;\n"
  },
  {
    "path": "src/constants.ts",
    "content": "import * as path from \"@modules/path\";\n\nimport { TYPORA_RESOURCE_DIR } from \"./typora-utils\";\nimport { setGlobalVar } from \"./utils/tools\";\n\n/**\n * Plugin version.\n */\nexport const VERSION = \"0.3.12\";\n\n/**\n * Copilot plugin directory.\n */\nexport const PLUGIN_DIR = path.join(TYPORA_RESOURCE_DIR, \"copilot\");\nsetGlobalVar(\"__copilotDir\", PLUGIN_DIR);\n"
  },
  {
    "path": "src/errors/CommandError.ts",
    "content": "/**\n * Error thrown when a command execution fails.\n */\nexport class CommandError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = \"CommandError\";\n  }\n}\n"
  },
  {
    "path": "src/errors/NoFreePortError.ts",
    "content": "/**\n * Error thrown when no free port is found.\n */\nexport class NoFreePortError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = \"NoFreePortError\";\n  }\n}\n"
  },
  {
    "path": "src/errors/PlatformError.ts",
    "content": "/**\n * Error thrown when a platform is not supported.\n */\nexport class PlatformError extends Error {\n  constructor(message: string) {\n    super(message);\n    this.name = \"PlatformError\";\n  }\n}\n"
  },
  {
    "path": "src/errors/index.ts",
    "content": "export { CommandError } from \"./CommandError\";\nexport { NoFreePortError } from \"./NoFreePortError\";\nexport { PlatformError } from \"./PlatformError\";\n"
  },
  {
    "path": "src/footer.scss",
    "content": "#footer-copilot {\n  margin-left: 8px;\n  margin-right: 0;\n  padding: 0;\n  opacity: 0.75;\n  cursor: pointer;\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: center;\n}\n\n#footer-copilot:hover {\n  opacity: 1;\n}\n\n.footer-copilot-icon {\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 0 6px;\n  cursor: pointer;\n}\n\n.footer-copilot-menu-button {\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 0 4px;\n  cursor: pointer;\n\n  &:hover {\n    background-color: rgba(128, 128, 128, 10%);\n  }\n}\n\n#footer-copilot-panel {\n  left: auto;\n  right: 4px;\n  top: auto;\n  padding-top: 8px;\n  padding-bottom: 8px;\n  display: flex;\n  flex-direction: column;\n  overflow-x: hidden;\n  min-width: 160px;\n}\n\n.footer-copilot-panel-hint {\n  padding: 0 16px;\n  padding-bottom: 6px;\n  font-size: 8pt;\n  font-weight: normal;\n  line-height: 1.8;\n}\n\n.footer-copilot-panel-btn {\n  border: none !important;\n  border-radius: 0 !important;\n  padding: 3px 16px !important;\n  font-size: 10pt !important;\n  font-weight: normal !important;\n  line-height: 1.8 !important;\n}\n\n.footer-copilot-panel-btn:hover {\n  background-color: var(--item-hover-bg-color);\n  color: var(--item-hover-text-color);\n}\n"
  },
  {
    "path": "src/footer.tsx",
    "content": "import { useSignal } from \"@preact/signals\";\nimport { render } from \"preact\";\nimport { useEffect, useMemo } from \"preact/hooks\";\n\nimport type {\n  CopilotAccountStatus,\n  CopilotClient,\n  CopilotClientEventHandler,\n  CopilotStatus,\n} from \"./client\";\nimport { attachChatPanel, detachChatPanel } from \"./components/ChatPanel\";\nimport CopilotIcon from \"./components/CopilotIcon\";\nimport SettingsPanel from \"./components/SettingsPanel\";\nimport { t } from \"./i18n\";\nimport { logger } from \"./logging\";\nimport { settings } from \"./settings\";\nimport { getCSSClassStyles } from \"./utils/dom\";\n\nimport \"./footer.scss\";\n\n/**\n * Get DOM element of Typora footer bar.\n * @returns\n */\nconst getFooterBarDOM = () => document.querySelector<HTMLElement>(\"footer.ty-footer\");\n\nconst getFooterTextColor = () => getCSSClassStyles(\"footer-item\", \"footer-item-right\").color;\n\nexport interface FooterPanelOptions {\n  copilot: CopilotClient;\n  open?: boolean;\n}\n\n/**\n * Panel for the user to control Copilot.\n * @returns\n */\nexport const FooterPanel: FC<FooterPanelOptions> = ({ copilot, open = true }) => {\n  const accountStatus = useSignal<CopilotAccountStatus>(\"NotSignedIn\");\n  const disableCompletions = useSignal(settings.disableCompletions);\n\n  // Initialize account status\n  useEffect(() => {\n    const onCopilotInitialized = async () => {\n      const { status } = await copilot.request.checkStatus().catch((e) => {\n        logger.error(\"Failed to check Copilot account status.\", e);\n        return { status: \"CheckFailed\" as const };\n      });\n      if (status === \"CheckFailed\") {\n        copilot.status = \"Warning\";\n        return;\n      }\n      accountStatus.value = status;\n      if (status !== \"OK\") copilot.status = \"Warning\";\n      else copilot.status = \"Normal\";\n    };\n    if (copilot.initialized) void onCopilotInitialized();\n    else\n      copilot.on(\"initialized\", () => {\n        // Delay `checkStatus` call to next tick to avoid a maybe BUG of GitHub Copilot LSP server\n        void Promise.resolve(null).then(onCopilotInitialized);\n      });\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  // Sync settings\n  useEffect(() => {\n    const unlistenSettingsChange = settings.onChange(\"disableCompletions\", (value) => {\n      disableCompletions.value = value;\n    });\n    return () => {\n      unlistenSettingsChange();\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  /**\n   * Distance from the element bottom to App bottom.\n   */\n  const bottom = useSignal(useMemo(() => (getFooterBarDOM()?.clientHeight ?? 30) + 2, []));\n\n  // Watch footer bar height and change footer icon height accordingly\n  useEffect(() => {\n    const footerBar = getFooterBarDOM();\n    if (!footerBar) return;\n    new ResizeObserver((entries) => {\n      const entry = entries[0];\n      if (entry) bottom.value = entry.target.clientHeight + 2;\n    }).observe(footerBar);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  const handleSignIn = async () => {\n    let response!: Awaited<ReturnType<CopilotClient[\"request\"][\"signInInitiate\"]>>;\n    try {\n      response = await copilot.request.signInInitiate();\n    } catch (e) {\n      Files.editor?.EditHelper.showDialog({\n        title: t(\"dialog.sign-in-request-failed.title\"),\n        html: /* html */ `\n          <div style=\"text-align: center; margin-top: 8px;\">\n            ${t(\"dialog.sign-in-request-failed.html\")}\n          </div>\n        `,\n        buttons: [t(\"button.understand\")],\n      });\n      return;\n    }\n    const { status, userCode, verificationUri } = response;\n    if (status === \"AlreadySignedIn\") return;\n    // Copy user code to clipboard\n    void navigator.clipboard.writeText(userCode);\n    // Open verification URI in browser\n    Files.editor?.EditHelper.showDialog({\n      title: t(\"dialog.sign-in.title\"),\n      html: /* html */ `\n        <div style=\"text-align: center; margin-top: 8px;\">\n          ${t(\"dialog.sign-in.html\")\n            .replace(\"{{USER_CODE}}\", userCode)\n            .replace(\"{{VERIFICATION_URI}}\", verificationUri)}\n        </div>\n      `,\n      buttons: [t(\"button.ok\")],\n      callback: () => {\n        copilot.request\n          .signInConfirm({ userCode })\n          .then(({ status }) => {\n            accountStatus.value = status;\n            copilot.status = \"Normal\";\n            Files.editor?.EditHelper.showDialog({\n              title: t(\"dialog.signed-in.title\"),\n              html: /* html */ `\n                <div style=\"text-align: center; margin-top: 8px;\">\n                  ${t(\"dialog.signed-in.html\")}\n                </div>\n              `,\n              buttons: [t(\"button.ok\")],\n            });\n          })\n          .catch(() => {\n            Files.editor?.EditHelper.showDialog({\n              title: t(\"dialog.sign-in-verification-failed.title\"),\n              html: /* html */ `\n                <div style=\"text-align: center; margin-top: 8px;\">\n                  ${t(\"dialog.sign-in-verification-failed.html\")}\n                </div>\n              `,\n              buttons: [t(\"button.understand\")],\n            });\n          });\n      },\n    });\n  };\n\n  const handleSignOut = async () => {\n    await copilot.request.signOut();\n    accountStatus.value = \"NotSignedIn\";\n    copilot.status = \"Warning\";\n  };\n\n  const settingsPanelOpen = useSignal(false);\n\n  return (\n    <>\n      {settingsPanelOpen.value && (\n        <SettingsPanel\n          onClose={() => {\n            settingsPanelOpen.value = false;\n          }}\n        />\n      )}\n\n      <div\n        id=\"footer-copilot-panel\"\n        className=\"dropdown-menu\"\n        style={{\n          bottom: bottom.value,\n          ...(!open && { display: \"none\" }),\n        }}>\n        {accountStatus.value === \"NotAuthorized\" && (\n          <div className=\"footer-copilot-panel-hint\">{t(\"footer.menu.not-authorized\")}</div>\n        )}\n        {accountStatus.value === \"NotSignedIn\" && (\n          <button\n            type=\"button\"\n            className=\"footer-copilot-panel-btn\"\n            onClick={() => void handleSignIn()}>\n            {t(\"footer.menu.sign-in\")}\n          </button>\n        )}\n        {accountStatus.value !== \"NotSignedIn\" && (\n          <button\n            type=\"button\"\n            className=\"footer-copilot-panel-btn\"\n            onClick={() => void handleSignOut()}>\n            {t(\"footer.menu.sign-out\")}\n          </button>\n        )}\n        <button\n          type=\"button\"\n          className=\"footer-copilot-panel-btn\"\n          onClick={() => {\n            disableCompletions.value = !disableCompletions.value;\n            settings.disableCompletions = disableCompletions.value;\n          }}>\n          {t(`footer.menu.${disableCompletions.value ? \"enable\" : \"disable\"}-completions`)}\n        </button>\n        <button\n          type=\"button\"\n          className=\"footer-copilot-panel-btn\"\n          onClick={() => {\n            settingsPanelOpen.value = true;\n          }}>\n          {t(\"footer.menu.settings\")}\n        </button>\n      </div>\n    </>\n  );\n};\n\nexport interface FooterOptions {\n  copilot: CopilotClient;\n}\n\n/**\n * Footer of the plugin with an icon.\n * @returns\n */\nexport const Footer: FC<FooterOptions> = ({ copilot }) => {\n  const status = useSignal<CopilotStatus | \"Disabled\">(\n    settings.disableCompletions ? \"Disabled\" : copilot.status,\n  );\n\n  // Sync status\n  useEffect(() => {\n    const unlistenSettingsChange = settings.onChange(\"disableCompletions\", (value) => {\n      status.value = value ? \"Disabled\" : copilot.status;\n    });\n    const handler: CopilotClientEventHandler<\"changeStatus\"> = ({ newStatus }) => {\n      if (status.value === \"Disabled\") return;\n      status.value = newStatus;\n    };\n    copilot.on(\"changeStatus\", handler);\n    return () => {\n      unlistenSettingsChange();\n      copilot.off(\"changeStatus\", handler);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  const height = useSignal(useMemo(() => getFooterBarDOM()?.clientHeight ?? 30, []));\n\n  // Watch footer bar height and change footer icon height accordingly\n  useEffect(() => {\n    const footerBar = getFooterBarDOM();\n    if (!footerBar) return;\n    new ResizeObserver((entries) => {\n      const entry = entries[0];\n      if (entry) height.value = entry.target.clientHeight;\n    }).observe(footerBar);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  const isPanelOpen = useSignal(false);\n\n  // Close panel on click outside\n  useEffect(() => {\n    const listener = () => {\n      isPanelOpen.value = false;\n    };\n    document.addEventListener(\"click\", listener);\n    $(\"content\").on(\"click\", listener);\n    return () => {\n      document.removeEventListener(\"click\", listener);\n      $(\"content\").off(\"click\", listener);\n    };\n  }, [isPanelOpen]);\n\n  // Close panel on click on spell check button or word count button\n  $(\"#footer-spell-check, #footer-word-count\").on(\"click\", () => {\n    isPanelOpen.value = !isPanelOpen.value;\n  });\n\n  // Remember chat panel open state\n  useEffect(() => {\n    if (localStorage.getItem(\"copilot-chat-panel-open\") === \"true\") attachChatPanel();\n  }, []);\n\n  return (\n    <>\n      {/* Settings panel */}\n      <FooterPanel copilot={copilot} open={isPanelOpen.value} />\n\n      {/* Main footer item */}\n      <div\n        className=\"footer-item footer-item-right\"\n        id=\"footer-copilot\"\n        style={{ height: height.value, display: \"flex\", alignItems: \"center\" }}\n        ty-hint={\"Copilot (\" + t(`copilot-status.${status.value}`) + \")\"}>\n        {/* Main Copilot icon - opens chat */}\n        <div\n          className=\"footer-copilot-icon\"\n          aria-label=\"Open Copilot Chat\"\n          role=\"button\"\n          onClick={(ev) => {\n            isPanelOpen.value = false;\n            document.body.classList.remove(\"ty-show-spell-check\", \"ty-show-word-count\");\n            const chatContainer = document.querySelector(\"#copilot-chat-container\");\n            if (chatContainer) {\n              detachChatPanel?.();\n            } else {\n              attachChatPanel();\n            }\n            ev.stopPropagation();\n          }}>\n          <CopilotIcon status={status.value} textColor={getFooterTextColor()} />\n        </div>\n\n        {/* Arrow button - opens settings menu */}\n        <div\n          className=\"footer-copilot-menu-button\"\n          aria-label=\"Copilot Settings\"\n          role=\"button\"\n          onClick={(ev) => {\n            isPanelOpen.value = !isPanelOpen.value;\n            document.body.classList.remove(\"ty-show-spell-check\", \"ty-show-word-count\");\n            ev.stopPropagation();\n          }}>\n          <svg\n            width=\"8\"\n            height=\"4\"\n            viewBox=\"0 0 8 4\"\n            fill=\"none\"\n            xmlns=\"http://www.w3.org/2000/svg\">\n            <path d=\"M4 0L8 4H0L4 0Z\" fill={getFooterTextColor()} />\n          </svg>\n        </div>\n      </div>\n    </>\n  );\n};\n\n/**\n * Create and attach footer element to DOM.\n * @param copilot The Copilot client.\n * @returns A function that can be used to remove the footer element from the DOM.\n */\nexport const attachFooter = (copilot: CopilotClient) => {\n  const container = document.createElement(\"div\");\n\n  const footerBar = getFooterBarDOM();\n  if (footerBar) {\n    const firstFooterItemRight = $(footerBar).find(\".footer-item-right\")[0];\n    if (firstFooterItemRight) firstFooterItemRight.insertAdjacentElement(\"beforebegin\", container);\n    else footerBar.appendChild(container);\n  } else {\n    container.style.position = \"fixed\";\n    container.style.bottom = \"0.125rem\";\n    container.style.right = \"0.75rem\";\n    container.style.zIndex = \"1000\";\n    container.style.height = \"2rem\";\n    container.style.width = \"3.5rem\";\n    document.querySelector(\"content\")!.appendChild(container);\n  }\n\n  const footer = <Footer copilot={copilot} />;\n  render(footer, container);\n\n  return () => {\n    render(null, container);\n    container.remove();\n  };\n};\n"
  },
  {
    "path": "src/global.d.ts",
    "content": "//////////////////////////////////////////////////////////////////////////////////////////////////\n/// Typora Environment                                                                         ///\n///                                                                                            ///\n/// Some types and marked as `any` or `unknown` because they are not used in this project, and ///\n/// it's not worth the effort to declare all the types.                                        ///\n//////////////////////////////////////////////////////////////////////////////////////////////////\n\n/* eslint-disable @typescript-eslint/no-redundant-type-constituents */\n/* eslint-disable no-var */\n\n/// <reference types=\"codemirror\" />\n\n/*********************\n * Global variables. *\n *********************/\n/**\n * Options of the Typora application.\n *\n * The actual properties are more than these, but they are not used in this project, so they are not\n * declared here.\n */\ndeclare var _options: {\n  appLocale?: string;\n  appVersion: string;\n  userLocale?: string;\n  userPath: string;\n};\n\n// Typora extends `window.File` with some additional properties, e.g., `File.editor`\n// This is an alias of `window.File` to make TS happy\n// See `src/patches/typora.ts` for more details\ndeclare var Files: typeof globalThis.File & FileConstructorExtensions;\n// The same for `window.Node`\ndeclare var Nodes: typeof globalThis.Node & Typora.Node;\n\n/**\n * File constructor extensions defined by Typora.\n */\ninterface FileConstructorExtensions {\n  /**\n   * The typora editor instance.\n   *\n   * It is usually initialized only once and will not be reinitialized even the active file or\n   * folder is changed, so it is safe to cache the value.\n   */\n  editor?: Typora.Editor;\n\n  /**\n   * Legacy property, used to store the active file pathname, now use `bundle.filePath` instead.\n   */\n  filePath?: string;\n  /**\n   * Properties of the active file.\n   */\n  bundle?: {\n    /**\n     * File name with extension. Not present when the file is not saved.\n     */\n    fileName?: string;\n    /**\n     * Pathname of the file. Empty string when the file is not saved.\n     */\n    filePath: string;\n    /**\n     * Last modified date.\n     */\n    modifiedDate: Date | number;\n    /**\n     * Pathname of the file. Exists if file has been modified.\n     */\n    hasModified?: string;\n    unsupported?: boolean;\n    lastSnapDate: number;\n    savedContent: string;\n    untitledId: number;\n    fileEncode?: string;\n    fileMissingWhenOpen?: boolean;\n    unusedAssets: readonly unknown[];\n    assetsChanges?: unknown | null;\n  };\n\n  /**\n   * Whether the file is using CRLF line endings.\n   */\n  useCRLF?: boolean;\n\n  /**\n   * Global options.\n   *\n   * The actual properties are more than these, but they are not used in this project, so they are\n   * not declared here.\n   */\n  option: {\n    headingStyle: number;\n  };\n\n  /**\n   * Reload the markdown content from `value`.\n   * @param value The markdown content.\n   * @param options Options for reloading the content.\n   * @returns\n   */\n  reloadContent: (\n    value: string,\n    options?: {\n      skipUndo?: boolean;\n      delayRefresh?: boolean;\n      fromDiskChange?: boolean;\n      skipChangeCount?: boolean;\n      onInit?: boolean;\n      skipStore?: boolean;\n    },\n  ) => void;\n\n  isLinux: boolean;\n  isLinuxSnap: boolean;\n  isMac: boolean;\n  isMacLegacy: boolean;\n  isMacNode: boolean;\n  isMacOrMacNode: boolean;\n  isNode: boolean;\n  isNodeHtml: boolean;\n  isSafari: boolean;\n  isUnixNode: boolean;\n  isWin: boolean;\n  isWk: boolean;\n  isWK: boolean;\n}\n\ninterface Window {\n  /**\n   * **⚠️ Warning:** This is **only available on Windows / Linux**.\n   * @param moduleName The module name to require.\n   * @returns The required module.\n   */\n  reqnode?: {\n    (moduleName: \"child_process\"): typeof import(\"node:child_process\");\n    (moduleName: \"fs\"): typeof import(\"node:fs\");\n    (moduleName: \"util\"): typeof import(\"node:util\");\n    (moduleName: string): unknown;\n  };\n\n  /**\n   * **⚠️ Warning:** This is **only available on macOS**.\n   */\n  bridge?: {\n    callHandler(\n      type: \"controller.runCommand\",\n      options: { args: string; cwd?: string },\n      /**\n       * Callback to be invoked when the command is finished.\n       * @param results A tuple of `[success, stdout, stderr, command]`.\n       */\n      cb: (results: [boolean, string, string, string]) => void,\n    ): void;\n\n    callSync(type: \"path.readText\", path: string): string;\n  };\n\n  getCodeMirrorMode: (\n    lang: Typora.LanguageId,\n  ) => NonNullable<CodeMirror.EditorConfiguration[\"mode\"]>;\n}\n\n/**********\n * Typora *\n **********/\n/**\n * Types related to Typora.\n */\ndeclare namespace Typora {\n  /**********\n   * Common *\n   **********/\n  /**\n   * Content ID. A string that uniquely identifies a node in the document.\n   * @example \"n41\"\n   */\n  type CID = string;\n  /**\n   * Language ID. A string that identifies a language. Can be any string, and some specific values\n   * are used by Typora.\n   * @example \"javascript\"\n   */\n  type LanguageId =\n    | \"ABAP\"\n    | \"apl\"\n    | \"asciiarmor\"\n    | \"ASN.1\"\n    | \"asp\"\n    | \"assembly\"\n    | \"bash\"\n    | \"basic\"\n    | \"bat\"\n    | \"c\"\n    | \"c#\"\n    | \"c++\"\n    | \"cassandra\"\n    | \"ceylon\"\n    | \"clike\"\n    | \"clojure\"\n    | \"cmake\"\n    | \"cmd\"\n    | \"cobol\"\n    | \"coffeescript\"\n    | \"commonlisp\"\n    | \"cpp\"\n    | \"CQL\"\n    | \"crystal\"\n    | \"csharp\"\n    | \"css\"\n    | \"cypher\"\n    | \"cython\"\n    | \"D\"\n    | \"dart\"\n    | \"diff\"\n    | \"django\"\n    | \"dockerfile\"\n    | \"dtd\"\n    | \"dylan\"\n    | \"ejs\"\n    | \"elixir\"\n    | \"elm\"\n    | \"embeddedjs\"\n    | \"erb\"\n    | \"erlang\"\n    | \"F#\"\n    | \"forth\"\n    | \"fortran\"\n    | \"fsharp\"\n    | \"gas\"\n    | \"gfm\"\n    | \"gherkin\"\n    | \"glsl\"\n    | \"go\"\n    | \"groovy\"\n    | \"handlebars\"\n    | \"haskell\"\n    | \"haxe\"\n    | \"hive\"\n    | \"htaccess\"\n    | \"html\"\n    | \"http\"\n    | \"hxml\"\n    | \"idl\"\n    | \"ini\"\n    | \"jade\"\n    | \"java\"\n    | \"javascript\"\n    | \"jinja2\"\n    | \"js\"\n    | \"json\"\n    | \"jsp\"\n    | \"jsx\"\n    | \"julia\"\n    | \"kotlin\"\n    | \"latex\"\n    | \"less\"\n    | \"lisp\"\n    | \"livescript\"\n    | \"lua\"\n    | \"makefile\"\n    | \"mariadb\"\n    | \"markdown\"\n    | \"mathematica\"\n    | \"matlab\"\n    | \"mbox\"\n    | \"modelica\"\n    | \"mssql\"\n    | \"mysql\"\n    | \"nginx\"\n    | \"nim\"\n    | \"nsis\"\n    | \"objc\"\n    | \"objective-c\"\n    | \"ocaml\"\n    | \"octave\"\n    | \"oz\"\n    | \"pascal\"\n    | \"pegjs\"\n    | \"perl\"\n    | \"perl6\"\n    | \"pgp\"\n    | \"php\"\n    | \"php+HTML\"\n    | \"plsql\"\n    | \"postgresql\"\n    | \"powershell\"\n    | \"properties\"\n    | \"protobuf\"\n    | \"pseudocode\"\n    | \"pug\"\n    | \"python\"\n    | \"q\"\n    | \"R\"\n    | \"react\"\n    | \"reStructuredText\"\n    | \"rst\"\n    | \"ruby\"\n    | \"rust\"\n    | \"SAS\"\n    | \"scala\"\n    | \"scheme\"\n    | \"scss\"\n    | \"sh\"\n    | \"shell\"\n    | \"smalltalk\"\n    | \"smarty\"\n    | \"solidity\"\n    | \"SPARQL\"\n    | \"spreadsheet\"\n    | \"sql\"\n    | \"sqlite\"\n    | \"squirrel\"\n    | \"stata\"\n    | \"stylus\"\n    | \"svelte\"\n    | \"swift\"\n    | \"systemverilog\"\n    | \"tcl\"\n    | \"tex\"\n    | \"tiddlywiki\"\n    | \"tiki wiki\"\n    | \"toml\"\n    | \"ts\"\n    | \"tsx\"\n    | \"turtle\"\n    | \"twig\"\n    | \"typescript\"\n    | \"v\"\n    | \"vb\"\n    | \"vbscript\"\n    | \"velocity\"\n    | \"verilog\"\n    | \"vhdl\"\n    | \"visual basic\"\n    | \"vue\"\n    | \"web-idl\"\n    | \"wiki\"\n    | \"xaml\"\n    | \"xml\"\n    | \"xml-dtd\"\n    | \"xquery\"\n    | \"yacas\"\n    | \"yaml\"\n    | \"yara\"\n    | (string & NonNullable<unknown>);\n\n  /*****************************************\n   * Cursor & Selection & Position & Range *\n   *****************************************/\n  /**\n   * Representing the text cursor position in raw markdown. Can be considered as an enhanced version\n   * of {@link CodeMirror.Position}.\n   */\n  interface CaretPlacement {\n    line: number;\n    ch?: number;\n    before?: string;\n    beforeRegExp?: string;\n    afterIndent?: boolean;\n  }\n\n  /**********\n   * Editor *\n   **********/\n  /**\n   * Enhanced Typora editor instance provided by this project.\n   */\n  type EnhancedEditor = Editor & EditorExtensions;\n  /**\n   * Typora editor instance.\n   */\n  class Editor {\n    /**\n     * Functions related to editing operations.\n     *\n     * The actual properties are more than these, but they are not used in this project, so they are\n     * not declared here.\n     */\n    EditHelper: {\n      showDialog: (options: {\n        title: string;\n        message?: string;\n        html?: string;\n        buttons: readonly string[];\n        type?: \"info\" | \"warning\" | \"error\";\n        callback?: (index: number) => void;\n      }) => void;\n    };\n    /**\n     * Functions related to user operations.\n     *\n     * The actual properties are more than these, but they are not used in this project, so they are\n     * not declared here.\n     */\n    UserOp: {\n      backspaceHandler: (editor: Editor, event: KeyboardEvent | null, keyName: string) => void;\n    };\n\n    /**\n     * The current focused content ID.\n     */\n    focusCid: string;\n\n    /**\n     * Last text cursor position.\n     */\n    lastCursor:\n      | {\n          type: \"cursor\";\n          /**\n           * CID.\n           */\n          id: string;\n          start: number;\n          end: number;\n        }\n      | {\n          type: \"cursor\";\n          inSourceMode: true;\n          pos: { line: number; ch: number; sticky: unknown };\n        }\n      | null;\n    /**\n     * Selection range.\n     *\n     * The actual properties are more than these, but they are not used in this project, so they are\n     * not declared here.\n     */\n    selection: {\n      getRangy: () => import(\"rangy\").RangyRange | null;\n\n      jumpIntoElemBegin: (elem: JQuery) => void;\n      jumpIntoElemEnd: (elem: JQuery) => void;\n    };\n\n    /**\n     * Library manager.\n     *\n     * The actual properties are more than these, but they are not used in this project, so they are\n     * not declared here.\n     */\n    library?: {\n      /**\n       * Current folder root.\n       */\n      root?: FileEntity;\n      /**\n       * Current folder path.\n       */\n      watchedFolder?: string;\n      /**\n       * Open a file.\n       * @param pathname The file pathname.\n       * @param cb The callback to be invoked when the file is opened.\n       */\n      openFile(pathname: string, cb: () => void): void;\n    };\n\n    /**\n     * Auto complete state. e.g. providing suggestions when typing \"```\" in fenced code block.\n     */\n    autoComplete?: {\n      state: {\n        type: string;\n        token: string;\n        match: readonly string[];\n        all: readonly string[];\n        index: number;\n        rowHeight: number;\n        anchor: { start: number; end: number; containerNode: HTMLElement };\n        beforeApply: unknown | undefined;\n      };\n    };\n\n    /**\n     * History manager for undo and redo.\n     */\n    undo: HistoryManager;\n\n    refocus(): void;\n\n    /**\n     * Node map used by Typora to represent the markdown document.\n     */\n    nodeMap: {\n      allNodes: NodeMap;\n      blocks: NodeMap;\n      foot_list: NodeMap;\n      link_list: NodeMap;\n      toc: NodeMap;\n\n      reset(): void;\n      /**\n       * Get the markdown content of the document.\n       */\n      toMark(): string;\n    };\n\n    /**\n     * The source view instance.\n     */\n    sourceView: SourceView;\n\n    /**\n     * The root DOM element of the editor (`#write`).\n     */\n    writingArea: HTMLDivElement;\n\n    /**\n     * Fences.\n     *\n     * The actual properties are more than these, but they are not used in this project, so they are\n     * not declared here.\n     */\n    fences: {\n      getCm(cid: string): CodeMirror.Editor;\n    };\n    /**\n     * Math blocks.\n     *\n     * The actual properties are more than these, but they are not used in this project, so they are\n     * not declared here.\n     */\n    mathBlock: {\n      currentCm?: CodeMirror.Editor;\n    };\n    /**\n     * HTML blocks.\n     *\n     * The actual properties are more than these, but they are not used in this project, so they are\n     * not declared here.\n     */\n    htmlBlock: {\n      currentCm?: CodeMirror.Editor;\n    };\n\n    /**\n     * Restore the last text cursor position.\n     */\n    restoreLastCursor: () => void;\n\n    /**\n     * Get the markdown content of the document.\n     */\n    getMarkdown: () => string;\n\n    /**\n     * Get node by CID.\n     */\n    getNode: (cid: string) => Node;\n    /**\n     * Get jQuery element of the given element.\n     */\n    getJQueryElem: (el: Element) => JQuery;\n    /**\n     * Get elements by CID.\n     */\n    findElemById: (cid: string) => JQuery;\n\n    /**\n     * Insert text at the current text cursor position.\n     */\n    insertText: (text: string) => void;\n  }\n  /**\n   * Extensions of the `Editor` class provided by this project.\n   */\n  interface EditorExtensions {\n    /**\n     * Invoked after the markdown content is changed.\n     * @param event The event object.\n     * @param handler The event handler.\n     */\n    on(\n      event: \"change\",\n      handler: (\n        editor: Editor,\n        ev: { oldMarkdown: string; newMarkdown: string },\n      ) => void | Promise<void>,\n    ): void;\n\n    off(\n      event: \"change\",\n      handler: (\n        editor: Editor,\n        ev: { oldMarkdown: string; newMarkdown: string },\n      ) => void | Promise<void>,\n    ): void;\n  }\n\n  /**\n   * History manager for undo and redo.\n   */\n  class HistoryManager {\n    commandStack: {\n      date: unknown | undefined;\n      incomplete: unknown | null;\n      source: unknown | null;\n      undo: object[];\n      redo: object[];\n    }[];\n\n    undo(): void;\n    redo(): void;\n\n    removeLastRegisteredOperationCommand(): void;\n  }\n\n  /***************\n   * Source view *\n   ***************/\n  /**\n   * Enhanced Typora source view instance provided by this project.\n   */\n  type EnhancedSourceView = SourceView & SourceViewExtensions;\n  /**\n   * Typora source view instance.\n   */\n  class SourceView {\n    /**\n     * The CodeMirror instance.\n     */\n    cm: CodeMirror.Editor | null;\n    /**\n     * Whether source mode is enabled.\n     */\n    inSourceMode: boolean;\n\n    /**\n     * Prepare the CodeMirror instance.\n     */\n    prep(): void;\n\n    /**\n     * Change text cursor position in **live preview mode**.\n     *\n     * It is *strange* that this method is not declared on `Editor` but in `SourceView`, it actually\n     * does not to move a line in source mode, but move to a specific line and character in live\n     * preview mode (which is not immediately apparent from the name).\n     * @param options The options.\n     */\n    gotoLine(options: CodeMirror.Position & { lineText: string | null }): void;\n\n    enableTypeWriterMode(enable?: boolean): void;\n    normalScrollAdjust(): void;\n    onSave(): void;\n    scrollAdjust(): void;\n    setValue(value: string, valueChanged?: boolean, type?: string): void;\n\n    hide(): void;\n    show(): void;\n  }\n  /**\n   * Extensions of the `SourceView` class provided by this project.\n   */\n  interface SourceViewExtensions {\n    /**\n     * Invoked before the source view is toggled.\n     * @param sv The source view instance.\n     * @param on `true` if the source view is being toggled on, otherwise `false`.\n     */\n    on(event: \"beforeToggle\", handler: (sv: SourceView, on: boolean) => void | Promise<void>): void;\n    /**\n     * Invoked after the source view is toggled.\n     * @param sv The source view instance.\n     * @param on `true` if the source view is being toggled on, otherwise `false`.\n     */\n    on(event: \"toggle\", handler: (sv: SourceView, on: boolean) => void | Promise<void>): void;\n    /**\n     * Invoked before the source view is shown.\n     * @param sv The source view instance.\n     */\n    on(event: \"beforeShow\", handler: (sv: SourceView) => void | Promise<void>): void;\n    /**\n     * Invoked after the source view is shown.\n     * @param sv The source view instance.\n     */\n    on(event: \"show\", handler: (sv: SourceView) => void | Promise<void>): void;\n    /**\n     * Invoked before the source view is hidden.\n     * @param sv The source view instance.\n     */\n    on(event: \"beforeHide\", handler: (sv: SourceView) => void | Promise<void>): void;\n    /**\n     * Invoked after the source view is hidden.\n     * @param sv The source view instance.\n     */\n    on(event: \"hide\", handler: (sv: SourceView) => void | Promise<void>): void;\n\n    off(\n      event: \"beforeToggle\",\n      handler: (sv: SourceView, on: boolean) => void | Promise<void>,\n    ): void;\n    off(event: \"toggle\", handler: (sv: SourceView, on: boolean) => void | Promise<void>): void;\n    off(event: \"beforeShow\", handler: (sv: SourceView) => void | Promise<void>): void;\n    off(event: \"show\", handler: (sv: SourceView) => void | Promise<void>): void;\n    off(event: \"beforeHide\", handler: (sv: SourceView) => void | Promise<void>): void;\n    off(event: \"hide\", handler: (sv: SourceView) => void | Promise<void>): void;\n  }\n\n  /********\n   * Node *\n   ********/\n  /**\n   * Typora Node. Used in `Editor.nodeMap` to represent a markdown node to be rendered.\n   */\n  class Node {\n    cid: string;\n    id: string;\n    attributes: NodeAttributes;\n    cursorDiff: readonly unknown[];\n\n    get(name: \"ahead\"): number | undefined;\n    get(name: \"before\"): Node | undefined;\n    get(name: \"after\"): Node | undefined;\n    get(name: \"tail\"): number | undefined;\n    get(name: \"parent\"): Node | undefined;\n    get(name: \"children\"): NodeMap;\n    get<T>(name: string): T | undefined;\n\n    getVeryFirst(flag?: boolean): Node | undefined;\n    getFirstChild(): Node | undefined;\n\n    getTopBlock(): Node | undefined;\n\n    unset(name: string): void;\n\n    isLoose(loose?: boolean): boolean;\n\n    /**\n     * Get the markdown content of the node.\n     */\n    toMark(): string;\n\n    /*********************************\n     * Static properties and methods *\n     *********************************/\n    /**\n     * Node types.\n     */\n    static readonly TYPE: {\n      atag: \"atag\";\n      attr: \"attr\";\n      autolink: \"autolink\";\n      blockquote: \"blockquote\";\n      br: \"br\";\n      code: \"code\";\n      comment: \"comment\";\n      def_footnote: \"def_footnote\";\n      def_link: \"def_link\";\n      del: \"del\";\n      em: \"em\";\n      emoji: \"emoji\";\n      emptyline: \"emptyline\";\n      escape: \"escape\";\n      fences: \"fences\";\n      footnote: \"footnote\";\n      heading: \"heading\";\n      highlight: \"highlight\";\n      hr: \"hr\";\n      html_block: \"html_block\";\n      html_entity: \"html_entity\";\n      html_inline: \"html_inline\";\n      iframe: \"iframe\";\n      image: \"image\";\n      imgtag: \"imgtag\";\n      inline_math: \"inline_math\";\n      linebreak: \"linebreak\";\n      link: \"link\";\n      list: \"list\";\n      list_item: \"list_item\";\n      math_block: \"math_block\";\n      meta_block: \"meta_block\";\n      pants: \"pants\";\n      paragraph: \"paragraph\";\n      plain_text: \"plain_text\";\n      raw_edit: \"raw_edit\";\n      refimg: \"refimg\";\n      reflink: \"reflink\";\n      ruby: \"ruby\";\n      softbreak: \"softbreak\";\n      strong: \"strong\";\n      subscript: \"subscript\";\n      superscript: \"superscript\";\n      tab: \"tab\";\n      table: \"table\";\n      table_cell: \"table_cell\";\n      table_row: \"table_row\";\n      tag: \"tag\";\n      toc: \"toc\";\n      underline: \"underline\";\n      url: \"url\";\n    };\n\n    /**\n     * Check if a value equals one of the following arguments.\n     */\n    static isType(node: unknown, types: readonly unknown[]): boolean | undefined;\n    static isType(node: unknown, ...types: unknown[]): boolean | undefined;\n\n    static parseFrom(text: string, nodeMap: Editor[\"nodeMap\"], options?: object): [string, Node[]];\n  }\n\n  class NodeMap {\n    _map: Map<CID, Node>;\n    _set: readonly Node[];\n    length: number;\n\n    add(node: Node): void;\n    at(index: number): Node | undefined;\n    first(): Node | undefined;\n    forEach: (callback: (node: Node, index: number) => void) => void;\n    get(cid: CID): Node | undefined;\n    indexOf(node: Node): number;\n    map<R>(callback: (node: Node, index: number) => R): R[];\n    remove(node: Node): void;\n    reset(): void;\n    sortedFirst(): Node | undefined;\n    sortedForEach: (callback: (node: Node, index: number) => void) => void;\n    toArray(): readonly Node[];\n    /**\n     * Put a node in the map using its `cid` as the key.\n     * @param node\n     */\n    update(node: Node): void;\n  }\n\n  interface NodeAttributes {\n    after: Node | undefined;\n    ahead: number | undefined;\n    align: unknown;\n    alignText: unknown;\n    alt: unknown;\n    attachTo: Editor[\"nodeMap\"];\n    attr: unknown;\n    before: Node | undefined;\n    checked: unknown;\n    children?: NodeMap;\n    depth: number | undefined;\n    displayStyle: unknown;\n    empty: unknown;\n    header?: unknown;\n    height?: unknown;\n    href?: unknown;\n    id: string;\n    in: Editor[\"nodeMap\"];\n    isFixed: unknown;\n    lang: LanguageId | undefined;\n    marginLeft: unknown;\n    marginRight: unknown;\n    markindent: unknown;\n    mathEqLabel: unknown;\n    mathLabel: unknown;\n    noCloseTag: unknown;\n    parent: Node | undefined;\n    pattern: string | undefined;\n    patternEnd: unknown;\n    prespace: unknown;\n    style: string | undefined;\n    subindent: unknown;\n    tagAfter: unknown;\n    tagBefore: unknown;\n    tail: number | undefined;\n    /**\n     * Markdown text to render.\n     */\n    text: string | undefined;\n    title?: unknown;\n    tight?: boolean;\n    type:\n      | \"heading\"\n      | \"paragraph\"\n      | \"blockquote\"\n      | \"fences\"\n      | \"list\"\n      | \"list_item\"\n      | (string & NonNullable<unknown>);\n    userIndent: readonly string[] | undefined;\n    userText: unknown;\n  }\n\n  /********\n   * File *\n   ********/\n  /**\n   * File entity. Used in `Editor.library.root` to represent a file or directory.\n   */\n  interface FileEntity {\n    createDate: Date;\n    lastModified: Date;\n    fetched: boolean;\n    isDirectory: boolean;\n    isFile: boolean;\n    /**\n     * Directory name or file name.\n     */\n    name: string;\n    /**\n     * Directory path or file pathname.\n     */\n    path: string;\n    /**\n     * Files in the directory (if `isDirectory` is `true`, otherwise empty array).\n     */\n    content: readonly FileEntity[];\n    /**\n     * Subdirectories (if `isDirectory` is `true`, otherwise empty array).\n     */\n    subdir: readonly FileEntity[];\n  }\n}\n"
  },
  {
    "path": "src/i18n/en.json",
    "content": "{\n  \"button\": {\n    \"ok\": \"OK\",\n    \"cancel\": \"Cancel\",\n    \"delete\": \"Delete\",\n    \"understand\": \"I understand\"\n  },\n  \"copilot-status\": {\n    \"Normal\": \"Ready\",\n    \"Warning\": \"Warning\",\n    \"InProgress\": \"In Progress\",\n    \"Disabled\": \"Disabled\"\n  },\n  \"chat\": {\n    \"you\": \"You\",\n    \"input-placeholder\": \"Ask Copilot\",\n    \"button\": {\n      \"send\": \"Send\",\n      \"stop\": \"Stop\",\n      \"new-session\": \"New session\",\n      \"edit-chat-title\": \"Edit chat title\",\n      \"delete-session\": \"Delete session\"\n    },\n    \"welcome\": {\n      \"title\": \"Ask Copilot\",\n      \"subtitle\": \"Get help with writing, answering questions, or generating ideas for your document.\"\n    },\n    \"dialog\": {\n      \"edit-chat-title\": {\n        \"title\": \"New chat title\",\n        \"html\": [\n          \"<input id=\\\"new-chat-title\\\" type=\\\"text\\\" value=\\\"{{SESSION_TITLE}}\\\" style=\\\"width: 100%; margin-top: 8px; margin-bottom: 8px; font-size: 14pt; font-weight: bold;\\\" />\"\n        ]\n      },\n      \"delete-session\": {\n        \"title\": \"Delete Copilot Chat session\",\n        \"html\": \"<p>Sure you want to delete the session \\\"{{SESSION_TITLE}}\\\"?</p>\"\n      }\n    },\n    \"prompt-style\": {\n      \"tooltip\": \"Set Prompt Style\",\n      \"Normal\": \"Normal\",\n      \"Academic\": \"Academic\",\n      \"Creative\": \"Creative\",\n      \"CatGirl\": \"🐱 Cat Girl!\",\n      \"cat-girl-name\": \"Vanilla\"\n    },\n    \"pick-model\": {\n      \"tooltip\": \"Pick Model\"\n    }\n  },\n  \"dialog\": {\n    \"sign-in\": {\n      \"title\": \"Sign in to GitHub Copilot\",\n      \"html\": [\n        \"<span>The device activation code is:</span>\",\n        \"<div style=\\\"margin-top: 8px; margin-bottom: 8px; font-size: 14pt; font-weight: bold;\\\">\",\n        \"  {{USER_CODE}}\",\n        \"</div>\",\n        \"<span>\",\n        \"  It has been copied to your clipboard. Please open the following link in your browser\",\n        \"  and paste the code to sign in:\",\n        \"</span>\",\n        \"<div style=\\\"margin-top: 8px; margin-bottom: 8px;\\\">\",\n        \"  <a href=\\\"{{VERIFICATION_URI}}\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">\",\n        \"    Open verification page\",\n        \"  </a>\",\n        \"</div>\",\n        \"<span>Press OK <strong>after</strong> you have completed the verification.</span>\"\n      ]\n    },\n    \"sign-in-request-failed\": {\n      \"title\": \"GitHub Copilot sign-in request failed\",\n      \"html\": [\n        \"<p>Could not send the sign-in request to GitHub Copilot.</p>\",\n        \"<p>This may be due to a network issue. Please check your internet connection and try again.</p>\"\n      ]\n    },\n    \"signed-in\": {\n      \"title\": \"Signed in to Copilot\",\n      \"html\": [\"<p>You have successfully signed in to Copilot.</p>\", \"<p>Press OK to continue.</p>\"]\n    },\n    \"sign-in-verification-failed\": {\n      \"title\": \"GitHub Copilot sign-in verification failed\",\n      \"html\": [\n        \"<p>Failed to complete the sign-in process for GitHub Copilot.</p>\",\n        \"<p>Please ensure that you have finished the verification in your browser <strong>before</strong> clicking OK.</p>\",\n        \"<p>If the issue persists, check your internet connection and try again.</p>\"\n      ]\n    },\n    \"warn-nodejs-above-20-required-for-typora-above-1-10\": {\n      \"title\": \"Node.js ≥ 20 is required to run this plugin under Typora ≥ 1.10\",\n      \"html\": [\n        \"<p>\",\n        \"  Node.js ≥ 20 is required to run this plugin under Typora ≥ 1.10,\",\n        \"  but no valid Node.js path is found.\",\n        \"</p>\",\n        \"<p>The current Typora version is <code>{{TYPORA_VERSION}}</code>.</p>\",\n        \"<p>\",\n        \"  Either install <a href=\\\"https://nodejs.org/en/download/\\\" target=\\\"_blank\\\">Node.js</a>\",\n        \"  ≥ 20 or specify Node.js path in “Settings → Node.js → Node.js path”, and restart Typora\",\n        \"  to take effect.\",\n        \"</p>\"\n      ]\n    },\n    \"warn-nodejs-above-20-required-for-typora-under-1-9\": {\n      \"title\": \"Node.js ≥ 20 is required to run this plugin under Typora < 1.9\",\n      \"html\": [\n        \"<p>\",\n        \"  Node.js ≥ 20 is required to run this plugin under Typora < 1.9,\",\n        \"  but no valid Node.js path is found.\",\n        \"</p>\",\n        \"<p>The current Typora version is <code>{{TYPORA_VERSION}}</code>.</p>\",\n        \"<p>\",\n        \"  Either install <a href=\\\"https://nodejs.org/en/download/\\\" target=\\\"_blank\\\">Node.js</a>\",\n        \"  ≥ 20 or specify Node.js path in “Settings → Node.js → Node.js path”, and restart Typora\",\n        \"  to take effect.\",\n        \"</p>\"\n      ]\n    },\n    \"warn-nodejs-above-20-required-on-macOS\": {\n      \"title\": \"Node.js ≥20 is required to run this plugin\",\n      \"html\": [\n        \"<p>\",\n        \"  Node.js ≥ 20 is required to run this plugin on macOS,\",\n        \"  but no valid Node.js path is found.\",\n        \"</p>\",\n        \"<p>\",\n        \"  Either install <a href=\\\"https://nodejs.org/en/download/\\\" target=\\\"_blank\\\">Node.js</a>\",\n        \"  ≥ 20 or specify Node.js path in “Settings → Node.js → Node.js path”, and restart Typora\",\n        \"  to take effect.\",\n        \"</p>\"\n      ]\n    }\n  },\n  \"footer\": {\n    \"menu\": {\n      \"sign-in\": \"Sign in to authenticate Copilot\",\n      \"sign-out\": \"Sign out\",\n      \"not-authorized\": \"Your GitHub account is not subscribed to Copilot\",\n      \"disable-completions\": \"Disable completions\",\n      \"enable-completions\": \"Enable completions\",\n      \"settings\": \"Settings\"\n    }\n  },\n  \"hint\": {\n    \"generated-by-copilot\": \"Generated by GitHub Copilot\"\n  },\n  \"settings-panel\": {\n    \"title\": \"Copilot: Settings\",\n    \"general\": {\n      \"title\": \"General\",\n      \"disable-completions\": {\n        \"label\": \"Disable completions\",\n        \"description\": \"Disable completions or suggestions from Copilot in Typora\"\n      },\n      \"use-inline-completion-text-in-source\": {\n        \"label\": \"Use inline completion text in source mode (Beta)\",\n        \"description\": \"Use inline completion text in source mode instead of suggestion panel\"\n      },\n      \"use-inline-completion-text-in-preview-code-blocks\": {\n        \"label\": \"Use inline completion text in preview code blocks (Early alpha)\",\n        \"description\": \"Use inline completion text in preview code blocks instead of suggestion panel\",\n        \"warning\": \"This feature is still in early alpha and is likely to corrupt your document. Use with caution.\"\n      }\n    },\n    \"nodejs\": {\n      \"title\": \"Node.js\",\n      \"note\": \"Since Typora v1.10, Node.js ≥ 20 is required for all platforms to run this plugin.\",\n      \"constant\": {\n        \"PATH_AUTO_DETECT\": \"Auto detect\",\n        \"PATH_BUNDLED\": \"Bundled\"\n      },\n      \"node-path\": {\n        \"label\": \"Node.js path (applied after restart)\",\n        \"description\": \"Select or enter the Node.js path to use for running Copilot LSP\",\n        \"message\": {\n          \"retrieving-version\": \"Retrieving Node.js version for \\\"{{PATH}}\\\"...\",\n          \"updated\": \"Updated Node.js path to \\\"{{PATH}}\\\" ({{VERSION}})\",\n          \"updated-auto\": \"Updated Node.js path to auto detected path\",\n          \"warn-invalid\": \"Failed to retrieve Node.js version for \\\"{{PATH}}\\\"\",\n          \"warn-invalid-version\": \"Expected Node.js version ≥ 20 for \\\"{{PATH}}\\\", but got {{VERSION}}\",\n          \"warn-empty-select-or-input\": \"Please select or enter a valid Node.js path\",\n          \"warn-empty-input\": \"Please enter a valid Node.js path\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/i18n/index.ts",
    "content": "export { t, pathOf } from \"./t\";\n\nexport type { PathOf } from \"./t\";\n"
  },
  {
    "path": "src/i18n/t.spec.ts",
    "content": "import { afterAll, describe, expect, it, vi } from \"vitest\";\n\nimport { t } from \"./t\";\n\nvi.mock(\"./en.json\", () => ({ default: { a: { b: { c: \"foo\" } } } }));\n\ndescribe(\"t\", () => {\n  const warn = vi.spyOn(console, \"warn\").mockImplementation(() => undefined);\n\n  afterAll(() => {\n    warn.mockReset();\n  });\n\n  it(\"should return the correct translation\", () => {\n    expect(t(\"a.b.c\" as never)).toBe(\"foo\");\n  });\n\n  it(\"should warn when the translation is not found\", () => {\n    expect(t(\"d\" as never)).toBe(\"d\");\n    expect(warn).toHaveBeenCalledWith('Cannot find translation for \"d\": \"d\" not found.');\n    warn.mockClear();\n\n    expect(t(\"d.c\" as never)).toBe(\"d.c\");\n    expect(warn).toHaveBeenCalledWith('Cannot find translation for \"d.c\": \"d\" not found.');\n    warn.mockClear();\n\n    expect(t(\"a.d.c\" as never)).toBe(\"a.d.c\");\n    expect(warn).toHaveBeenCalledWith('Cannot find translation for \"a.d.c\": \"d\" not found in \"a\".');\n    warn.mockClear();\n\n    expect(t(\"a.b.c.d\" as never)).toBe(\"a.b.c.d\");\n    expect(warn).toHaveBeenCalledWith(\n      'Cannot find translation for \"a.b.c.d\": \"a.b.c\" is not an object.',\n    );\n    warn.mockClear();\n\n    expect(t(\"\" as never)).toBe(\"\");\n    expect(warn).toHaveBeenCalledWith(\"Empty path is not allowed.\");\n    warn.mockClear();\n\n    expect(t(\"a.b.d\" as never)).toBe(\"a.b.d\");\n    expect(warn).toHaveBeenCalledWith(\n      'Cannot find translation for \"a.b.d\": \"d\" not found in \"a.b\".',\n    );\n    warn.mockClear();\n\n    expect(t(\"a.b\" as never)).toBe(\"a.b\");\n    expect(warn).toHaveBeenCalledWith('Cannot find translation for \"a.b\": \"a.b\" is not a string.');\n    warn.mockClear();\n  });\n});\n"
  },
  {
    "path": "src/i18n/t.ts",
    "content": "import en from \"./en.json\";\nimport zhCN from \"./zh-CN.json\";\n\n/**\n * Path of an object joined by dots.\n *\n * @example\n * ```typescript\n * type R = PathOf<{ a: { b: { c: string }, d: string } }>;\n * //   ^?: \"a.b.c\" | \"a.d\"\n * ```\n */\nexport type PathOf<O> = keyof {\n  [P in keyof O & (string | number) as O[P] extends string ? P\n  : O[P] extends readonly string[] ? P\n  : `${P}.${PathOf<O[P]>}`]: void;\n};\n\ninterface LocaleMap {\n  [key: string]: string | readonly string[] | LocaleMap;\n}\n\nconst isStringArray = (x: unknown): x is readonly string[] =>\n  Array.isArray(x) && x.every((v) => typeof v === \"string\");\n\nclass TranslationError extends Error {}\n\n/**\n * Get locale string by path.\n * @param path The path of the locale string.\n * @returns The locale string.\n *\n * @example\n * ```typescript\n * // en.json\n * {\"a\": {\"b\": {\"c\": \"foo\"}}}\n *\n * // Example\n * t(\"a.b.c\"); // => \"foo\"\n * t(\"d\"); // => \"d\". Warning: Cannot find translation for \"d\": \"d\" not found.\n * t(\"d.c\"); // => \"d.c\". Warning: Cannot find translation for \"d.c\": \"d\" not found.\n * t(\"a.d.c\"); // => \"a.d.c\". Warning: Cannot find translation for \"a.d.c\": \"d\" not found in \"a\".\n * t(\"a.b.c.d\"); // => \"a.b.c.d\". Warning: Cannot find translation for \"a.b.c.d\": \"a.b.c\" is not an object.\n * t(\"\"); // => \"\". Warning: Empty path is not allowed.\n * t(\"a.b.d\"); // => \"a.b.d\". Warning: Cannot find translation for \"a.b.d\": \"d\" not found in \"a.b\".\n * t(\"a.b\"); // => \"a.b\". Warning: Cannot find translation for \"a.b\": \"a.b\" is not a string.\n * ```\n */\nexport const t = Object.assign(\n  (path: PathOf<typeof en>): string => {\n    try {\n      return _t(path);\n    } catch (e) {\n      if (e instanceof TranslationError) console.warn(e.message);\n      else console.error(e);\n      return path;\n    }\n  },\n  {\n    /**\n     * Test if the path exists.\n     * @param path The path of the locale string.\n     * @returns\n     */\n    test: (path: string): boolean => {\n      try {\n        _t(path);\n        return true;\n      } catch {\n        return false;\n      }\n    },\n    /**\n     * The unsafe version of {@linkcode t} without type checking.\n     * @param path The path of the locale string.\n     * @returns\n     */\n    tran: (path: string): string => {\n      try {\n        return _t(path);\n      } catch (e) {\n        if (e instanceof TranslationError) console.warn(e.message);\n        else console.error(e);\n        return path;\n      }\n    },\n  },\n);\nconst _t = (path: string): string => {\n  const locale =\n    window._options.userLocale ||\n    window._options.appLocale ||\n    navigator.languages[0] ||\n    navigator.language ||\n    (\"userLanguage\" in navigator &&\n      typeof navigator.userLanguage === \"string\" &&\n      navigator.userLanguage) ||\n    \"en\";\n  const keys = path.split(\".\").filter(Boolean);\n  const localeMap: LocaleMap = (() => {\n    if (locale === \"en\" || locale.startsWith(\"en-\")) return en;\n    if (locale === \"zh-CN\" || locale === \"zh-Hans\") return zhCN;\n    return en;\n  })();\n  let tmp = localeMap;\n\n  const visitedKeys: string[] = [];\n  for (const key of keys.slice(0, -1)) {\n    const x = tmp[key];\n    if (x === undefined)\n      throw new TranslationError(\n        `Cannot find translation for \"${path}\": \"${key}\" not found` +\n          (visitedKeys.length > 0 ? ` in \"${visitedKeys.join(\".\")}\".` : \".\"),\n      );\n    if (typeof x === \"string\" || isStringArray(x))\n      throw new TranslationError(\n        `Cannot find translation for \"${path}\": \"${[...visitedKeys, key].join(\".\")}\" is not an object.`,\n      );\n    tmp = x;\n    visitedKeys.push(key);\n  }\n  const lastKey = keys.at(-1);\n  if (lastKey === undefined) throw new TranslationError(\"Empty path is not allowed.\");\n  const res = tmp[lastKey];\n  if (res === undefined)\n    throw new TranslationError(\n      `Cannot find translation for \"${path}\": \"${lastKey}\" not found` +\n        (visitedKeys.length > 0 ? ` in \"${visitedKeys.join(\".\")}\".` : \".\"),\n    );\n  if (typeof res !== \"string\" && !isStringArray(res))\n    throw new TranslationError(`Cannot find translation for \"${path}\": \"${path}\" is not a string.`);\n  return typeof res === \"string\" ? res : res.join(\"\\n\");\n};\n\n/**\n * Get the all possible paths of an object.\n * @param o The object.\n * @returns\n *\n * @example\n * ```typescript\n * pathOf({ a: { b: { c: \"foo\" }, d: \"bar\" } }); // => [\"a.b.c\", \"a.d\"]\n * ```\n */\nexport const pathOf = <O extends LocaleMap>(o: O): PathOf<O>[] =>\n  Object.entries(o).flatMap(([k, v]) =>\n    typeof v === \"string\" ? [k] : pathOf(v as never).map((x) => `${k}.${x}`),\n  ) as unknown as PathOf<O>[];\n"
  },
  {
    "path": "src/i18n/zh-CN.json",
    "content": "{\n  \"button\": {\n    \"ok\": \"确定\",\n    \"cancel\": \"取消\",\n    \"delete\": \"删除\",\n    \"understand\": \"我知道了\"\n  },\n  \"copilot-status\": {\n    \"Normal\": \"就绪\",\n    \"Warning\": \"警告\",\n    \"InProgress\": \"正在生成\",\n    \"Disabled\": \"已禁用\"\n  },\n  \"chat\": {\n    \"you\": \"你\",\n    \"input-placeholder\": \"询问 Copilot\",\n    \"button\": {\n      \"send\": \"发送\",\n      \"stop\": \"停止\",\n      \"new-session\": \"新会话\",\n      \"edit-chat-title\": \"编辑会话标题\",\n      \"delete-session\": \"删除会话\"\n    },\n    \"welcome\": {\n      \"title\": \"询问 Copilot\",\n      \"subtitle\": \"获取写作、回答问题或生成文档创意的帮助\"\n    },\n    \"dialog\": {\n      \"edit-chat-title\": {\n        \"title\": \"编辑会话标题\",\n        \"html\": [\n          \"<input id=\\\"new-chat-title\\\" type=\\\"text\\\" value=\\\"{{SESSION_TITLE}}\\\" style=\\\"width: 100%; margin-top: 8px; margin-bottom: 8px; font-size: 14pt; font-weight: bold;\\\" />\"\n        ]\n      },\n      \"delete-session\": {\n        \"title\": \"删除 Copilot 聊天会话\",\n        \"html\": \"<p>确定要删除会话“{{SESSION_TITLE}}”吗？</p>\"\n      }\n    },\n    \"prompt-style\": {\n      \"tooltip\": \"设置 Prompt 风格\",\n      \"Normal\": \"正常\",\n      \"Academic\": \"学术\",\n      \"Creative\": \"创意\",\n      \"CatGirl\": \"🐱 猫娘！\",\n      \"cat-girl-name\": \"喵喵\"\n    },\n    \"pick-model\": {\n      \"tooltip\": \"选择模型\"\n    }\n  },\n  \"dialog\": {\n    \"sign-in\": {\n      \"title\": \"登录 GitHub Copilot\",\n      \"html\": [\n        \"<span>你的 GitHub 设备激活码为：</span>\",\n        \"<div style=\\\"margin-top: 8px; margin-bottom: 8px; font-size: 14pt; font-weight: bold;\\\">\",\n        \"  {{USER_CODE}}\",\n        \"</div>\",\n        \"<span>该激活码已复制到剪贴板。请在浏览器中打开以下链接，并粘贴代码以登录：</span>\",\n        \"<div style=\\\"margin-top: 8px; margin-bottom: 8px;\\\">\",\n        \"  <a href=\\\"{{VERIFICATION_URI}}\\\" target=\\\"_blank\\\" rel=\\\"noopener noreferrer\\\">\",\n        \"    打开验证页面\",\n        \"  </a>\",\n        \"</div>\",\n        \"<span>在完成验证<strong>之后</strong>点击“确定”。</span>\"\n      ]\n    },\n    \"sign-in-request-failed\": {\n      \"title\": \"GitHub Copilot 登录请求失败\",\n      \"html\": [\n        \"<p>无法向 GitHub Copilot 发送登录登录请求。</p>\",\n        \"<p>这很可能是由于网络问题导致的。请检查您的网络连接并重试。</p>\",\n        \"<div style=\\\"border-left: 4px solid #f00; padding: 10px 15px 5px; margin-top: 20px; background: #fef5f5;\\\">\",\n        \"  <p style=\\\"text-align: left;\\\"><strong>⚠️ 注意：</strong>对于中国大陆用户，请确保您的网络环境能够正常访问 GitHub Copilot 的服务。这通常需要配置代理。</p>\",\n        \"  <p style=\\\"text-align: left;\\\">大多数代理软件默认<strong>只会影响浏览器流量</strong>，<i>不会</i>接管其他应用程序的网络连接，因此<strong>仅开启代理软件可能不足以让 Typora 正常访问 GitHub Copilot，即使您启用了代理软件的“全局模式”</strong>。</p>\",\n        \"  <p style=\\\"text-align: left;\\\">——大多数代理软件的“全局模式”仅意味着将所有请求重定向到代理服务器，这并不能改变它们只接管了浏览器流量的事实。</p>\",\n        \"</div>\",\n        \"<div style=\\\"margin-top: 20px; padding: 10px 15px 20px; background: #f5f8fa; border: 1px solid #ccc; border-radius: 4px;\\\">\",\n        \"  <p style=\\\"text-align: left;\\\">一些代理软件提供“代理网卡”功能，能够通过虚拟网卡接管所有网络流量，从而使 Typora 也能通过代理访问 GitHub Copilot 的服务。</p>\",\n        \"  <p style=\\\"text-align: left;\\\">此功能在不同的代理软件中有不同的名称：</p>\",\n        \"  <ul style=\\\"text-align: left;\\\">\",\n        \"    <li>如果您使用 <strong>CFW/Clash Verge/V2RayN</strong>，可以启用“<strong>Tun 模式</strong>”。</li>\",\n        \"    <li>如果使用 <strong>ClashX Pro</strong>，可以启用“<strong>增强模式</strong>”。</li>\",\n        \"  </ul>\",\n        \"</div>\"\n      ]\n    },\n    \"signed-in\": {\n      \"title\": \"已登录 GitHub Copilot\",\n      \"html\": [\"<p>您已成功登录 Copilot。</p>\", \"<p>按“确定”继续。</p>\"]\n    },\n    \"sign-in-verification-failed\": {\n      \"title\": \"GitHub Copilot 登录验证失败\",\n      \"html\": [\n        \"<p>未能完成 GitHub Copilot 登录过程。</p>\",\n        \"<p>请确保您已经在浏览器中完成验证<strong>再</strong>点击 OK。</p>\",\n        \"<p>如果问题持续，请检查您的网络连接并重试。</p>\",\n        \"<div style=\\\"border-left: 4px solid #f00; padding: 10px 15px 5px; margin-top: 20px; background: #fef5f5;\\\">\",\n        \"  <p style=\\\"text-align: left;\\\"><strong>⚠️ 注意：</strong>对于中国大陆用户，请确保您的网络环境能够正常访问 GitHub Copilot 的服务。这通常需要配置代理。</p>\",\n        \"  <p style=\\\"text-align: left;\\\">大多数代理软件默认<strong>只会影响浏览器流量</strong>，<i>不会</i>接管其他应用程序的网络连接，因此<strong>仅开启代理软件可能不足以让 Typora 正常访问 GitHub Copilot，即使您启用了代理软件的“全局模式”</strong>。</p>\",\n        \"  <p style=\\\"text-align: left;\\\">——大多数代理软件的“全局模式”仅意味着将所有请求重定向到代理服务器，这并不能改变它们只接管了浏览器流量的事实。</p>\",\n        \"</div>\",\n        \"<div style=\\\"margin-top: 20px; padding: 10px 15px 20px; background: #f5f8fa; border: 1px solid #ccc; border-radius: 4px;\\\">\",\n        \"  <p style=\\\"text-align: left;\\\">一些代理软件提供“代理网卡”功能，能够通过虚拟网卡接管所有网络流量，从而使 Typora 也能通过代理访问 GitHub Copilot 的服务。</p>\",\n        \"  <p style=\\\"text-align: left;\\\">此功能在不同的代理软件中有不同的名称：</p>\",\n        \"  <ul style=\\\"text-align: left;\\\">\",\n        \"    <li>如果您使用 <strong>CFW/Clash Verge/V2RayN</strong>，可以启用“<strong>Tun 模式</strong>”。</li>\",\n        \"    <li>如果使用 <strong>ClashX Pro</strong>，可以启用“<strong>增强模式</strong>”。</li>\",\n        \"  </ul>\",\n        \"</div>\"\n      ]\n    },\n    \"warn-nodejs-above-20-required-for-typora-above-1-10\": {\n      \"title\": \"需要安装 Node.js ≥ 20 才能在 Typora 1.10 及以上版本中运行此插件\",\n      \"html\": [\n        \"<p>需要安装 Node.js ≥ 20 才能在 Typora 1.10 及以上版本中运行此插件，但是未找到有效的 Node.js 路径。</p>\",\n        \"<p>当前 Typora 版本为 <code>{{TYPORA_VERSION}}</code>。</p>\",\n        \"<p>\",\n        \"  请安装 <a href=\\\"https://nodejs.org/en/download/\\\" target=\\\"_blank\\\">Node.js</a>\",\n        \"  ≥ 20 或在“设置 → Node.js → Node.js 路径”中指定 Node.js 路径，并重新启动 Typora 以生效。\",\n        \"</p>\"\n      ]\n    },\n    \"warn-nodejs-above-20-required-for-typora-under-1-9\": {\n      \"title\": \"需要安装 Node.js ≥ 20 才能在 Typora 1.9 以下版本中运行此插件\",\n      \"html\": [\n        \"<p>需要安装 Node.js ≥ 20 才能在 Typora 1.9 以下版本中运行此插件，但是未找到有效的 Node.js 路径。</p>\",\n        \"<p>当前 Typora 版本为 <code>{{TYPORA_VERSION}}</code>。</p>\",\n        \"<p>\",\n        \"  请安装 <a href=\\\"https://nodejs.org/en/download/\\\" target=\\\"_blank\\\">Node.js</a>\",\n        \"  ≥ 20 或在“设置 → Node.js → Node.js 路径”中指定 Node.js 路径，并重新启动 Typora 以生效。\",\n        \"</p>\"\n      ]\n    },\n    \"warn-nodejs-above-20-required-on-macOS\": {\n      \"title\": \"需要安装 Node.js ≥ 20 才能运行此插件\",\n      \"html\": [\n        \"<p>需要 Node.js ≥ 20 才能在 macOS 上运行此插件，但是未找到有效的 Node.js 路径。</p>\",\n        \"<p>\",\n        \"  请安装 <a href=\\\"https://nodejs.org/en/download/\\\" target=\\\"_blank\\\">Node.js</a>\",\n        \"  ≥ 20 或在“设置 → Node.js → Node.js 路径”中指定 Node.js 路径，并重新启动 Typora 以生效。\",\n        \"</p>\"\n      ]\n    }\n  },\n  \"footer\": {\n    \"menu\": {\n      \"sign-in\": \"登录以认证 Copilot\",\n      \"sign-out\": \"退出登录\",\n      \"not-authorized\": \"你的 GitHub 账户尚未订阅 Copilot\",\n      \"disable-completions\": \"禁用建议\",\n      \"enable-completions\": \"启用建议\",\n      \"settings\": \"设置\"\n    }\n  },\n  \"hint\": {\n    \"generated-by-copilot\": \"由 GitHub Copilot 生成\"\n  },\n  \"settings-panel\": {\n    \"title\": \"Copilot：设置\",\n    \"general\": {\n      \"title\": \"通用设置\",\n      \"disable-completions\": {\n        \"label\": \"禁用建议\",\n        \"description\": \"在 Typora 中禁用 Copilot 生成的补全或建议\"\n      },\n      \"use-inline-completion-text-in-source\": {\n        \"label\": \"在源代码模式下使用内联补全文本（Beta）\",\n        \"description\": \"在源代码模式下使用内联补全文本，而不是建议面板\"\n      },\n      \"use-inline-completion-text-in-preview-code-blocks\": {\n        \"label\": \"在预览模式代码块中使用内联补全文本（Early alpha）\",\n        \"description\": \"在预览模式的代码块中使用内联补全文本，而不是建议面板\",\n        \"warning\": \"此功能仍处于早期阶段，很可能会损坏你的文档。请谨慎使用。\"\n      }\n    },\n    \"nodejs\": {\n      \"title\": \"Node.js\",\n      \"note\": \"自 Typora v1.10 起，所有平台都需要 Node.js ≥ 20 才能运行此插件。\",\n      \"constant\": {\n        \"PATH_AUTO_DETECT\": \"自动检测\",\n        \"PATH_BUNDLED\": \"内置\"\n      },\n      \"node-path\": {\n        \"label\": \"Node.js 路径（重启 Typora 生效）\",\n        \"description\": \"选择或输入 Copilot LSP 使用的 Node.js 路径\",\n        \"message\": {\n          \"retrieving-version\": \"正在获取位于 \\\"{{PATH}}\\\" 的 Node.js 版本...\",\n          \"updated\": \"已更新 Node.js 路径为 \\\"{{PATH}}\\\" ({{VERSION}})\",\n          \"updated-auto\": \"已更新 Node.js 路径为自动检测\",\n          \"warn-invalid\": \"无法获取位于 \\\"{{PATH}}\\\" 的 Node.js 版本\",\n          \"warn-invalid-version\": \"期望 Node.js 版本 ≥ 20，但是在 \\\"{{PATH}}\\\" 中找到的版本为 {{VERSION}}\",\n          \"warn-empty-select-or-input\": \"请选择或输入有效的 Node.js 路径\",\n          \"warn-empty-input\": \"请输入有效的 Node.js 路径\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "import \"./patches\";\n\nimport \"./main\";\n"
  },
  {
    "path": "src/logging.ts",
    "content": "import { createLogger } from \"./utils/logging\";\n\n/**\n * Logger used across the plugin.\n */\nexport const logger = createLogger({\n  prefix: `%cCopilot plugin:%c `,\n  styles: [\"font-weight: bold\", \"font-weight: normal\"],\n});\n"
  },
  {
    "path": "src/mac-server.ts",
    "content": "import { fork } from \"child_process\";\nimport net from \"net\";\nimport type { ChildProcessWithoutNullStreams } from \"node:child_process\";\n\nimport { WebSocketServer } from \"ws\";\n\nif (!process.argv[2] || !process.argv[3]) {\n  console.log(\"Usage: node mac-server.cjs <port> <lsp-node-module-path>\");\n  process.exit(1);\n}\n\nconst port = Number.parseInt(process.argv[2]);\nif (Number.isNaN(port)) {\n  console.log(`Invalid port \"${process.argv[2]}\"`);\n  process.exit(1);\n}\n\nconsole.log(\"Process PID:\", process.pid);\n\nconst server = fork(process.argv[3], [\"--stdio\", \"true\"], {\n  silent: true,\n}) as ChildProcessWithoutNullStreams;\nconsole.log(\"Copilot LSP server started. PID:\", server.pid);\n\nconst startWebSocketServer = () => {\n  const wss = new WebSocketServer({ port });\n\n  wss.on(\"connection\", (ws) => {\n    console.log(`➕➕ Connection (${wss.clients.size})`);\n\n    ws.once(\"close\", () => {\n      console.log(\"🚨 WebSocket Server shutting down...\");\n      wss.close();\n      process.exit(0);\n    });\n\n    ws.on(\"message\", (data) => {\n      // eslint-disable-next-line @typescript-eslint/no-base-to-string\n      const payload = data.toString(\"utf-8\");\n      console.debug(\"📥\", payload);\n      server.stdin.write(payload);\n    });\n\n    server.stdout.on(\"data\", (data) => {\n      const message: string = data.toString(\"utf-8\");\n      console.debug(\"📤\", message);\n      ws.send(message);\n    });\n  });\n\n  console.log(`✅ WebSocket Server listening on ws://localhost:${port}`);\n\n  const cleanupServer = (() => {\n    let called = false;\n    return () => {\n      if (called) return;\n      called = true;\n      console.log(\"🚨 WebSocket Server shutting down...\");\n      wss.close((err) => {\n        if (err) console.error(err);\n        process.exit(0);\n      });\n    };\n  })();\n\n  process.on(\"exit\", cleanupServer);\n  process.on(\"SIGINT\", cleanupServer);\n  process.on(\"SIGTERM\", cleanupServer);\n  process.on(\"SIGUSR1\", cleanupServer);\n  process.on(\"SIGUSR2\", cleanupServer);\n  process.on(\"uncaughtException\", cleanupServer);\n};\n\nconst testServer = net.createServer();\ntestServer.once(\"error\", (err) => {\n  if ((err as unknown as { code: string }).code === \"EADDRINUSE\") {\n    console.error(`🚨 Port ${port} is busy`);\n    process.exit(1);\n  }\n});\n\ntestServer.once(\"listening\", () => {\n  testServer.close(startWebSocketServer);\n});\n\ntestServer.listen(port);\n"
  },
  {
    "path": "src/main.ts",
    "content": "import * as path from \"@modules/path\";\nimport { pathToFileURL } from \"@modules/url\";\n\nimport diff from \"fast-diff\";\nimport { debounce } from \"radash\";\nimport semverGte from \"semver/functions/gte\";\nimport semverLt from \"semver/functions/lt\";\nimport semverValid from \"semver/functions/valid\";\n\nimport type { Completion } from \"./client\";\nimport { createCopilotClient } from \"./client\";\nimport { ChatSession } from \"./client/chat\";\nimport CompletionTaskManager from \"./completion\";\nimport { attachSuggestionPanel } from \"./components/SuggestionPanel\";\nimport { PLUGIN_DIR, VERSION } from \"./constants\";\nimport { attachFooter } from \"./footer\";\nimport { t } from \"./i18n\";\nimport { logger } from \"./logging\";\nimport { settings } from \"./settings\";\nimport type { Position } from \"./types/lsp\";\nimport {\n  TYPORA_VERSION,\n  getActiveFilePathname,\n  getCodeMirror,\n  getWorkspaceFolder,\n  waitUntilEditorInitialized,\n} from \"./typora-utils\";\nimport { runCommand } from \"./utils/cli-tools\";\nimport { computeTextChanges } from \"./utils/diff\";\nimport { getCaretCoordinate } from \"./utils/dom\";\nimport type { NodeRuntime } from \"./utils/node-bridge\";\nimport {\n  NodeServer,\n  detectAvailableNodeRuntimes,\n  setAllAvailableNodeRuntimes,\n  setCurrentNodeRuntime,\n} from \"./utils/node-bridge\";\nimport { Observable } from \"./utils/observable\";\nimport { replaceTextByRange, setGlobalVar } from \"./utils/tools\";\n\nimport \"./styles.scss\";\n\nlogger.info(\"Copilot plugin activated. Version:\", VERSION);\n\n/**\n * Fake temporary workspace folder, only used when no folder is opened.\n */\nconst FAKE_TEMP_WORKSPACE_FOLDER =\n  Files.isWin ?\n    \"C:\\\\Users\\\\FakeUser\\\\FakeTyporaCopilotWorkspace\"\n  : \"/home/fakeuser/faketyporacopilotworkspace\";\nconst FAKE_TEMP_FILENAME = \"typora-copilot-fake-markdown.md\";\n\nPromise.defer(async () => {\n  const runtime = await new Promise<NodeRuntime>((resolve) => {\n    const start = Date.now();\n\n    const customNodePath = settings.nodePath;\n    const checkCustomRuntimePromise =\n      customNodePath ?\n        runCommand(`\"${customNodePath}\" -v`).then((output) => {\n          const version = output.trim();\n          if (!semverValid(version)) {\n            logger.warn(\n              `Failed to check version of custom Node.js path \"${customNodePath}\", fallback to auto detection`,\n            );\n            throw new Error(\"Custom runtime invalid\");\n          }\n          const runtime = { path: customNodePath, version };\n          setCurrentNodeRuntime(runtime);\n          logger.info(\n            `Using custom Node.js runtime (v${version.replace(/^v/, \"\")}) at path` +\n              `\"${customNodePath}\" to start language server.`,\n          );\n          resolve(runtime);\n        })\n      : Promise.reject(new Error(\"No custom runtime\"));\n\n    void detectAvailableNodeRuntimes({\n      onFirstResolved: (runtime) => {\n        const timeSpent = Date.now() - start;\n\n        checkCustomRuntimePromise.catch(() => {\n          setCurrentNodeRuntime(runtime);\n          logger.debug(`Resolved first Node.js runtime in ${timeSpent}ms:`, runtime);\n          logger.info(\n            \"Detected \" +\n              (runtime.path === \"bundled\" ? \"bundled\" : \"available\") +\n              ` Node.js (v${runtime.version.replace(/^v/, \"\")})` +\n              (runtime.path === \"bundled\" ? \"\" : ` at path \"${runtime.path}\"`) +\n              \", using it to start language server.\",\n          );\n          resolve(runtime);\n        });\n      },\n    }).then((runtimes) => {\n      const timeSpent = Date.now() - start;\n      setAllAvailableNodeRuntimes(runtimes);\n\n      checkCustomRuntimePromise.catch(() => {\n        if (runtimes.length === 0) {\n          logger.error(\"No available Node.js runtime found\");\n          if (Files.isMac)\n            void waitUntilEditorInitialized().then(() => {\n              Files.editor!.EditHelper.showDialog({\n                title: `Typora Copilot: ${t(\"dialog.warn-nodejs-above-20-required-on-macOS.title\")}`,\n                type: \"error\",\n                html: /* html */ `\n                  <div style=\"text-align: center; margin-top: 8px;\">\n                    ${t(\"dialog.warn-nodejs-above-20-required-on-macOS.html\")}\n                  </div>\n                `,\n                buttons: [t(\"button.understand\")],\n              });\n            });\n          else if (Files.isNode && semverLt(process.version, \"20.0.0\"))\n            void waitUntilEditorInitialized().then(() => {\n              Files.editor!.EditHelper.showDialog({\n                title: `Typora Copilot: ${t(\"dialog.warn-nodejs-above-20-required-for-typora-under-1-9.title\")}`,\n                type: \"error\",\n                html: /* html */ `\n                  <div style=\"text-align: center; margin-top: 8px;\">\n                    ${t(\"dialog.warn-nodejs-above-20-required-for-typora-under-1-9.html\").replace(\n                      \"{{TYPORA_VERSION}}\",\n                      TYPORA_VERSION,\n                    )}\n                  </div>\n                `,\n                buttons: [t(\"button.understand\")],\n              });\n            });\n          else if (Files.isNode && semverGte(TYPORA_VERSION, \"1.10.0\"))\n            void waitUntilEditorInitialized().then(() => {\n              Files.editor!.EditHelper.showDialog({\n                title: `Typora Copilot: ${t(\"dialog.warn-nodejs-above-20-required-for-typora-above-1-10.title\")}`,\n                type: \"error\",\n                html: /* html */ `\n                  <div style=\"text-align: center; margin-top: 8px;\">\n                    ${t(\"dialog.warn-nodejs-above-20-required-for-typora-above-1-10.html\").replace(\n                      \"{{TYPORA_VERSION}}\",\n                      TYPORA_VERSION,\n                    )}\n                  </div>\n                `,\n                buttons: [t(\"button.understand\")],\n              });\n            });\n\n          resolve({ path: \"not found\", version: \"unknown\" });\n        } else {\n          logger.debug(`Resolved all available Node.js runtimes in ${timeSpent}ms:`, runtimes);\n        }\n      });\n    });\n  });\n\n  const server =\n    runtime.path === \"not found\" ?\n      NodeServer.getMock()\n    : await NodeServer.start(\n        runtime.path,\n        path.join(PLUGIN_DIR, \"language-server\", \"language-server.cjs\"),\n      );\n  if (server.pid !== -1) logger.debug(\"Copilot LSP server started. PID:\", server.pid);\n\n  /**\n   * Copilot LSP client.\n   */\n  const copilot = createCopilotClient(server, { logging: \"debug\" });\n  setGlobalVar(\"copilot\", copilot);\n\n  await waitUntilEditorInitialized();\n\n  /*********************\n   * Utility functions *\n   *********************/\n  /**\n   * Insert completion text to editor.\n   * @param completion Completion options.\n   * @returns\n   */\n  const insertCompletionTextToEditor = (\n    caretPosition: Position,\n    completion: Completion,\n  ): Observable<\"accepted\" | \"rejected\"> | void => {\n    const { position, range } = completion;\n    let { displayText, text } = completion;\n\n    const activeElement = document.activeElement;\n    if (!activeElement) return;\n\n    // When in input, do not insert completion text\n    if (\"INPUT\" === activeElement.tagName || activeElement.classList.contains(\"ty-input\")) return;\n\n    // When not in writer, do not insert completion text\n    if (\"BODY\" === activeElement.tagName) return;\n\n    let mode: NonNullable<CodeMirror.EditorConfiguration[\"mode\"]> | null = null;\n    let fontSize: string | null = null;\n    let backgroundColor: string | null = null;\n    // If in a CodeMirror instance, prune completion text to only include text before code block starter\n    if (\"TEXTAREA\" === activeElement.tagName && getCodeMirror(activeElement)) {\n      const cm = getCodeMirror(activeElement)!;\n\n      const startPos = { ...caretPosition };\n      startPos.line -=\n        cm.getValue(Files.useCRLF ? \"\\r\\n\" : \"\\n\").split(Files.useCRLF ? \"\\r\\n\" : \"\\n\").length - 1;\n      startPos.character -= cm.getCursor().ch;\n      if (startPos.character < 0) startPos.character = 0;\n\n      // Get starter of CodeMirror to determine whether it is a code block, formula, etc.\n      const cmStarter = state.markdown.split(Files.useCRLF ? \"\\r\\n\" : \"\\n\")[startPos.line - 1];\n\n      if (cmStarter) {\n        const cmElement = cm.getWrapperElement();\n\n        let handled = false;\n        // * Code block *\n        if (cmStarter.startsWith(\"```\") || cmStarter.startsWith(\"~~~\")) {\n          handled = true;\n          const lang = (cmElement as unknown as { lang: string }).lang;\n          mode = window.getCodeMirrorMode(lang);\n          fontSize = window.getComputedStyle(cmElement).fontSize;\n          backgroundColor = window.getComputedStyle(cmElement).backgroundColor;\n\n          // Keep only completion text before code block ender, as in Typora code block it is not possible\n          // to insert a new code block or end one using \"```\" or \"~~~\"\n          const ender = /^(.)\\1*/.exec(cmStarter)![0];\n          const indexOfEnder = text.indexOf(ender);\n          if (indexOfEnder !== -1) {\n            displayText = displayText.slice(0, displayText.indexOf(ender));\n            text = text.slice(0, indexOfEnder);\n            const textAfterEnder = text.slice(indexOfEnder);\n            // Reduce `range` to only include text before ender\n            const rows = textAfterEnder.split(Files.useCRLF ? \"\\r\\n\" : \"\\n\").length - 1;\n            range.end.line -= rows;\n            range.end.character = textAfterEnder.split(Files.useCRLF ? \"\\r\\n\" : \"\\n\").pop()!.length;\n          }\n        }\n        // * Math block *\n        else if (cmStarter === \"$$\") {\n          handled = true;\n          mode = \"stex\";\n\n          const match = /(?<!\\\\)\\$\\$/.exec(text);\n          const indexOfEnder = match ? match.index : -1;\n          if (indexOfEnder !== -1) {\n            const match = /(?<!\\\\)\\$\\$/.exec(displayText);\n            if (match) displayText = displayText.slice(0, match.index);\n            text = text.slice(0, indexOfEnder);\n            const textAfterEnder = text.slice(indexOfEnder);\n            // Reduce `range` to only include text before ender\n            const rows = textAfterEnder.split(Files.useCRLF ? \"\\r\\n\" : \"\\n\").length - 1;\n            range.end.line -= rows;\n            range.end.character = textAfterEnder.split(Files.useCRLF ? \"\\r\\n\" : \"\\n\").pop()!.length;\n          }\n        }\n\n        if (handled && settings.useInlineCompletionTextInPreviewCodeBlocks) {\n          const subCmCompletion = { ...completion };\n\n          // Set `position` and `range` to be relative to `startPos`\n          subCmCompletion.position = {\n            line: position.line - startPos.line,\n            character: position.character - startPos.character,\n          };\n          subCmCompletion.range = {\n            start: {\n              line: range.start.line - startPos.line,\n              character: range.start.character - startPos.character,\n            },\n            end: {\n              line: range.end.line - startPos.line,\n              character: range.end.character - startPos.character,\n            },\n          };\n\n          return insertCompletionTextToCodeMirror(cm, subCmCompletion);\n        }\n      }\n    }\n\n    const focusedElem = document.querySelector(`[cid=${editor.focusCid}]`);\n    if (!focusedElem) return;\n    if (!(focusedElem instanceof HTMLElement)) return;\n\n    const pos = getCaretCoordinate();\n    if (!pos) return;\n\n    // Insert a suggestion panel below the cursor\n    const unattachSuggestionPanel = attachSuggestionPanel(displayText, mode, {\n      backgroundColor,\n      fontSize,\n    });\n\n    copilot.notification.notifyShown({ uuid: completion.uuid });\n\n    const insertCompletionText = () => {\n      // Check whether it is safe to just use `insertText` to insert completion text,\n      // as using `reloadContent` uses much more resources and causes a flicker\n      const newMarkdown = replaceTextByRange(\n        state.markdown,\n        range,\n        completion.text,\n        Files.useCRLF ? \"\\r\\n\" : \"\\n\",\n      );\n      const diffs = diff(state.markdown, newMarkdown).filter((part) => part[0] !== diff.EQUAL);\n\n      if (diffs.length === 1 && diffs[0]![0] === diff.INSERT) {\n        editor.insertText(diffs[0]![1]);\n      } else {\n        // @ts-expect-error - CodeMirror supports 2nd parameter, but not declared in types\n        cm.setValue(editor.getMarkdown(), \"begin\");\n        cm.setCursor({ line: position.line, ch: position.character });\n        cm.replaceRange(\n          text,\n          { line: range.start.line, ch: range.start.character },\n          { line: range.end.line, ch: range.end.character },\n        );\n        const newMarkdown = cm.getValue(Files.useCRLF ? \"\\r\\n\" : \"\\n\");\n        const cursorPos = Object.assign(cm.getCursor(), {\n          lineText: cm.getLine(cm.getCursor().line),\n        });\n        Files.reloadContent(newMarkdown, {\n          fromDiskChange: false,\n          skipChangeCount: true,\n          skipStore: false,\n        });\n        // Restore text cursor position\n        sourceView.gotoLine(cursorPos);\n        editor.refocus();\n      }\n    };\n\n    const cleanup = new Observable<\"accepted\" | \"rejected\">();\n    cleanup.subscribeOnce(() => {\n      unattachSuggestionPanel();\n      editor.writingArea.removeEventListener(\"keydown\", keydownHandler, true);\n      $(editor.writingArea).off(\"caretMove\", caretMoveHandler);\n    });\n\n    /**\n     * Intercept `Tab` key once and change it to accept completion.\n     * @param event The keyboard event.\n     */\n    const keydownHandler = (event: KeyboardEvent) => {\n      // Prevent tab key to trigger tab once\n      if (event.key === \"Tab\") {\n        event.preventDefault();\n        event.stopPropagation();\n        insertCompletionText();\n        cleanup.next(\"accepted\");\n      }\n    };\n    editor.writingArea.addEventListener(\"keydown\", keydownHandler, true);\n\n    const caretMoveHandler = () => {\n      cleanup.next(\"rejected\");\n    };\n    $(editor.writingArea).on(\"caretMove\", caretMoveHandler);\n\n    return cleanup;\n  };\n\n  /**\n   * Insert completion text to CodeMirror.\n   * @param cm The CodeMirror instance.\n   * @returns\n   */\n  const insertCompletionTextToCodeMirror = (\n    cm: CodeMirror.Editor,\n    { displayText, position, range, text, uuid }: Completion,\n  ): Observable<\"accepted\" | \"rejected\"> | void => {\n    interface CodeMirrorHistory {\n      done: readonly object[];\n      undone: readonly object[];\n    }\n\n    const cloneHistory = (history: CodeMirrorHistory): CodeMirrorHistory => ({\n      done: history.done.map((item) =>\n        \"primIndex\" in item ?\n          new (item.constructor as any)([...(item as any).ranges], item.primIndex)\n        : { ...item, changes: [...(item as any).changes] },\n      ),\n      undone: history.undone.map((item) =>\n        \"primIndex\" in item ?\n          new (item.constructor as any)([...(item as any).ranges], item.primIndex)\n        : { ...item, changes: [...(item as any).changes] },\n      ),\n    });\n\n    const cursorBefore = cm.getCursor();\n    const historyBefore = cloneHistory(cm.getHistory() as CodeMirrorHistory);\n    const commandStackBefore = editor.undo.commandStack.map((item) => ({\n      ...item,\n      undo: [...item.undo],\n      redo: [...item.redo],\n    }));\n    state.suppressMarkdownChange++;\n    cm.replaceRange(displayText, { line: position.line, ch: position.character });\n    const cursorAfter = cm.getCursor();\n    cm.setCursor(cursorBefore);\n    const textMarker = cm.markText({ line: position.line, ch: position.character }, cursorAfter, {\n      className: \"text-gray font-italic\",\n    });\n\n    // Force set `history.undone` to enable redo.\n    // The first redo after it should be intercepted and then reject the completion (the history\n    // will be restored to `historyBefore`), so the editor state will not be corrupted.\n    cm.setHistory({\n      done: (cm.getHistory() as CodeMirrorHistory).done,\n      undone: historyBefore.undone,\n    });\n\n    // Remove the last registered operation command, so the completion text will not be\n    // registered as a new operation command\n    if (!sourceView.inSourceMode) editor.undo.removeLastRegisteredOperationCommand();\n\n    copilot.notification.notifyShown({ uuid });\n\n    /**\n     * Reject the completion.\n     *\n     * **Warning:** It should only be called when no more changes is applied after\n     * completion text is inserted, otherwise history will be corrupted.\n     */\n    // NOTE: The check for `rejectedOrAccepted` is intentionally placed inside each callers instead\n    // of in `reject` and `accept` functions, because some callers may not call `reject` or `accept`\n    // immediately, but the `rejectedOrAccepted` flag itself should be set immediately.\n    let rejectedOrAccepted = false;\n    const reject = () => {\n      const textMarkerRange = textMarker.find();\n      if (!textMarkerRange) {\n        cleanup.next(\"rejected\");\n        return;\n      }\n      const { from, to } = textMarkerRange;\n\n      state.suppressMarkdownChange++;\n      cm.replaceRange(\"\", from, to);\n      cm.setHistory(historyBefore);\n\n      if (!sourceView.inSourceMode) {\n        editor.undo.commandStack.length = 0;\n        Array.prototype.push.apply(editor.undo.commandStack, commandStackBefore);\n      }\n\n      cleanup.next(\"rejected\");\n    };\n    /**\n     * Accept the completion.\n     */\n    const accept = () => {\n      // Clear completion hint\n      const textMarkerRange = textMarker.find();\n      if (!textMarkerRange) {\n        cleanup.next(\"rejected\");\n        return;\n      }\n      const { from, to } = textMarkerRange;\n\n      state.suppressMarkdownChange++;\n      cm.replaceRange(\"\", from, to);\n      cm.setHistory(historyBefore);\n\n      if (!sourceView.inSourceMode) {\n        editor.undo.commandStack.length = 0;\n        Array.prototype.push.apply(editor.undo.commandStack, commandStackBefore);\n      }\n\n      // Insert completion text\n      cm.replaceRange(\n        text,\n        { line: range.start.line, ch: range.start.character },\n        { line: range.end.line, ch: range.end.character },\n      );\n\n      cleanup.next(\"accepted\");\n    };\n\n    const cleanup = new Observable<\"accepted\" | \"rejected\">();\n    cleanup.subscribeOnce(() => {\n      cm.off(\"keydown\", cmTabFixer);\n      cm.off(\"beforeChange\", cmChangeFixer);\n      cm.off(\"cursorActivity\", cursorMoveHandler);\n    });\n\n    /**\n     * Intercept `Tab` key once and change it to accept completion.\n     * @param _cm The CodeMirror instance.\n     * @param event The keyboard event.\n     */\n    const cmTabFixer = (_cm: CodeMirror.Editor, event: KeyboardEvent) => {\n      if (rejectedOrAccepted) return;\n\n      // Prevent tab key to accept completion\n      if (event.key === \"Tab\") {\n        event.preventDefault();\n        rejectedOrAccepted = true;\n        accept();\n      }\n    };\n    cm.on(\"keydown\", cmTabFixer);\n\n    /**\n     * Reject completion before any change applied.\n     * @param cm The CodeMirror instance.\n     * @param change The change.\n     */\n    const cmChangeFixer = (cm: CodeMirror.Editor, change: CodeMirror.EditorChangeCancellable) => {\n      if (rejectedOrAccepted) return;\n      rejectedOrAccepted = true;\n\n      const { from, origin, text, to } = change;\n\n      // Cancel the change temporarily\n      change.cancel();\n      // Reject completion and redo the change after 1 tick\n      // It is to make sure these changes are applied after the `\"beforeChange\"` event\n      // has finished, in order to avoid corrupting the CodeMirror instance\n      void Promise.resolve().then(() => {\n        reject();\n        if (origin === \"undo\" || origin === \"redo\") {\n          if (sourceView.inSourceMode) cm[origin]();\n          else editor.undo[origin]();\n        } else {\n          cm.replaceRange(text.join(Files.useCRLF ? \"\\r\\n\" : \"\\n\"), from, to, origin);\n        }\n      });\n    };\n    cm.on(\"beforeChange\", cmChangeFixer);\n\n    /**\n     * Reject completion if cursor moved.\n     */\n    const cursorMoveHandler = () => {\n      if (rejectedOrAccepted) return;\n      rejectedOrAccepted = true;\n\n      reject();\n    };\n    cm.on(\"cursorActivity\", cursorMoveHandler);\n\n    return cleanup;\n  };\n\n  /**\n   * Insert suggestion panel to CodeMirror.\n   * @param cm The CodeMirror instance.\n   * @returns\n   */\n  const insertSuggestionPanelToCodeMirror = (\n    cm: CodeMirror.Editor,\n    { displayText, range, text, uuid }: Completion,\n  ): Observable<\"accepted\" | \"rejected\"> | void => {\n    // Insert a suggestion panel below the cursor\n    const unattachSuggestionPanel = attachSuggestionPanel(displayText, null, { cm });\n\n    copilot.notification.notifyShown({ uuid });\n\n    const insertCompletionText = () => {\n      // Insert completion text\n      cm.replaceRange(\n        text,\n        { line: range.start.line, ch: range.start.character },\n        { line: range.end.line, ch: range.end.character },\n      );\n    };\n\n    const cleanup = new Observable<\"accepted\" | \"rejected\">();\n    cleanup.subscribeOnce(() => {\n      unattachSuggestionPanel();\n      cm.off(\"keydown\", keydownHandler);\n      cm.off(\"cursorActivity\", cursorMoveHandler);\n    });\n\n    /**\n     * Intercept `Tab` key once and change it to accept completion.\n     * @param event The keyboard event.\n     */\n    // eslint-disable-next-line sonarjs/no-identical-functions\n    const keydownHandler = (_: CodeMirror.Editor, event: KeyboardEvent) => {\n      // Prevent tab key to trigger tab once\n      if (event.key === \"Tab\") {\n        event.preventDefault();\n        event.stopPropagation();\n        insertCompletionText();\n        cleanup.next(\"accepted\");\n      }\n    };\n    cm.on(\"keydown\", keydownHandler);\n\n    /**\n     * Reject completion if cursor moved.\n     */\n    const cursorMoveHandler = () => {\n      cleanup.next(\"rejected\");\n    };\n    cm.on(\"cursorActivity\", cursorMoveHandler);\n\n    return cleanup;\n  };\n\n  /*******************\n   * Change handlers *\n   *******************/\n  /**\n   * Callback to be invoked when workspace folder changed.\n   * @param newFolder The new workspace folder.\n   * @param oldFolder The old workspace folder.\n   */\n  const onChangeWorkspaceFolder = (newFolder: string | null, oldFolder: string | null) => {\n    copilot.notification.workspace.didChangeWorkspaceFolders({\n      event: {\n        added:\n          newFolder ? [{ uri: pathToFileURL(newFolder).href, name: path.basename(newFolder) }] : [],\n        removed:\n          oldFolder ? [{ uri: pathToFileURL(oldFolder).href, name: path.basename(oldFolder) }] : [],\n      },\n    });\n  };\n\n  /**\n   * Callback to be invoked when active file changed.\n   * @param newPathname The new active file pathname.\n   * @param oldPathname The old active file pathname.\n   */\n  const onChangeActiveFile = (newPathname: string | null, oldPathname: string | null) => {\n    if (oldPathname) {\n      taskManager.rejectCurrentIfExist();\n      copilot.notification.textDocument.didClose({\n        textDocument: { uri: pathToFileURL(oldPathname).href },\n      });\n    }\n\n    if (newPathname) {\n      copilot.version = 0;\n      copilot.notification.textDocument.didOpen({\n        textDocument: {\n          uri: pathToFileURL(newPathname).href,\n          languageId: \"markdown\",\n          version: 0,\n          text: editor.getMarkdown(),\n        },\n      });\n    }\n  };\n\n  /**\n   * Trigger completion.\n   */\n  const triggerCompletion = debounce({ delay: 500 }, () => {\n    logger.debug(\"Changing markdown\", {\n      from: state.markdownUsedInLastCompletion,\n      to: state.markdown,\n    });\n\n    // Update state\n    state.markdownUsedInLastCompletion = state.markdown;\n\n    /* Tell Copilot that file has changed */\n    const version = ++copilot.version;\n    copilot.notification.textDocument.didChange({\n      textDocument: { version, uri: pathToFileURL(taskManager.activeFilePathname).href },\n      contentChanges: [{ text: state.markdown }],\n    });\n\n    /* If caret position is available, fetch completion from Copilot */\n    if (state.caretPosition) {\n      const caretPosition = state.caretPosition;\n      logger.debug(\"Triggering completion at\", caretPosition);\n      taskManager.start(caretPosition, {\n        onCompletion: (completion) => {\n          if (editor.sourceView.inSourceMode)\n            if (settings.useInlineCompletionTextInSource)\n              return insertCompletionTextToCodeMirror(cm, completion);\n            else return insertSuggestionPanelToCodeMirror(cm, completion);\n          else return insertCompletionTextToEditor(caretPosition, completion);\n        },\n      });\n    }\n  });\n\n  /*********************\n   * Initialize states *\n   *********************/\n  const editor = Files.editor as Typora.EnhancedEditor;\n  // Initialize state\n  const initialMarkdown = editor.getMarkdown();\n  const state = {\n    markdownUsedInLastCompletion: initialMarkdown,\n    markdown: initialMarkdown,\n    caretPosition: { line: 0, character: 0 } as Position | null,\n    _actualLatestMarkdown: initialMarkdown,\n    suppressMarkdownChange: 0,\n  };\n  ChatSession.currentDocument = initialMarkdown;\n  // Initialize CodeMirror\n  const sourceView = editor.sourceView as Typora.EnhancedSourceView;\n  if (!sourceView.cm) sourceView.prep();\n  const cm = sourceView.cm!;\n\n  /***************************\n   * Initialize task manager *\n   ***************************/\n  const taskManager = new CompletionTaskManager(copilot, {\n    workspaceFolder: getWorkspaceFolder() ?? FAKE_TEMP_WORKSPACE_FOLDER,\n    activeFilePathname:\n      getActiveFilePathname() ?? path.join(FAKE_TEMP_WORKSPACE_FOLDER, FAKE_TEMP_FILENAME),\n  });\n\n  /***********\n   * UI Misc *\n   ***********/\n  attachFooter(copilot);\n\n  /**************************\n   * Initialize Copilot LSP *\n   **************************/\n  /* Send `initialize` request */\n  await copilot.request.initialize({\n    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n    processId: window.process?.pid ?? null,\n    capabilities: { workspace: { workspaceFolders: true } },\n    trace: \"verbose\",\n    rootUri: taskManager.workspaceFolder && pathToFileURL(taskManager.workspaceFolder).href,\n    ...(taskManager.workspaceFolder && {\n      workspaceFolders: [\n        {\n          uri: pathToFileURL(taskManager.workspaceFolder).href,\n          name: path.basename(taskManager.workspaceFolder),\n        },\n      ],\n    }),\n    // Register editor info\n    initializationOptions: {\n      editorInfo: { name: \"Typora\", version: TYPORA_VERSION },\n      editorPluginInfo: { name: \"typora-copilot\", version: VERSION },\n    },\n  });\n  copilot.notification.initialized();\n\n  await copilot.request.getVersion();\n\n  /* Send initial didOpen */\n  if (taskManager.activeFilePathname) onChangeActiveFile(taskManager.activeFilePathname, null);\n\n  /************\n   * Watchers *\n   ************/\n  /* Interval to update workspace */\n  setInterval(() => {\n    const newWorkspaceFolder = getWorkspaceFolder() ?? FAKE_TEMP_WORKSPACE_FOLDER;\n    if (newWorkspaceFolder !== taskManager.workspaceFolder) {\n      const oldWorkspaceFolder = taskManager.workspaceFolder;\n      taskManager.workspaceFolder = newWorkspaceFolder;\n      onChangeWorkspaceFolder(newWorkspaceFolder, oldWorkspaceFolder);\n    }\n  }, 100);\n\n  const checkActiveFileChange = (): boolean => {\n    const newActiveFilePathname =\n      getActiveFilePathname() ?? path.join(FAKE_TEMP_WORKSPACE_FOLDER, FAKE_TEMP_FILENAME);\n    if (newActiveFilePathname !== taskManager.activeFilePathname) {\n      const oldActiveFilePathname = taskManager.activeFilePathname;\n      taskManager.activeFilePathname = newActiveFilePathname;\n      onChangeActiveFile(newActiveFilePathname, oldActiveFilePathname);\n      return true;\n    }\n    return false;\n  };\n\n  /* Reject completion on toggle source mode */\n  sourceView.on(\"beforeToggle\", (_, on) => {\n    if (taskManager.state === \"pending\") {\n      logger.debug(`Refusing completion before toggling source mode ${on ? \"on\" : \"off\"}`);\n      taskManager.rejectCurrentIfExist();\n    }\n  });\n\n  /* Watch for markdown change in live preview mode */\n  editor.on(\"change\", (_, { newMarkdown }) => {\n    if (checkActiveFileChange()) return;\n\n    if (settings.disableCompletions) return;\n    if (sourceView.inSourceMode) return;\n\n    // If not literally changed, simply return\n    if (newMarkdown === state._actualLatestMarkdown) return;\n    // If literally changed, update current actual markdown text\n    state._actualLatestMarkdown = newMarkdown;\n    // If update suppressed, return\n    if (state.suppressMarkdownChange) {\n      state.suppressMarkdownChange--;\n      return;\n    }\n\n    /* When update not suppressed */\n    // Update caret position\n    if (\n      editor.selection.getRangy()?.collapsed && // If not selecting text\n      window.getSelection()?.rangeCount // If has cursor\n    ) {\n      const changes = computeTextChanges(state.markdown, newMarkdown, state.caretPosition);\n      if (changes.length === 1) {\n        const change = changes[0]!;\n        const changeLines = change.text.split(Files.useCRLF ? \"\\r\\n\" : \"\\n\").length - 1;\n        state.caretPosition = {\n          line: change.range.start.line + changeLines,\n          character:\n            changeLines === 0 ?\n              change.range.start.character + change.text.length\n            : change.text.lastIndexOf(Files.useCRLF ? \"\\r\\n\" : \"\\n\") - 1,\n        };\n\n        // Fix code blocks, math blocks and HTML blocks caret position\n        // When creating these blocks, Typora place the caret in the middle of the block,\n        // instead of at the end of the block\n        if (\n          // If it is an insert operation\n          change.range.start.line === change.range.end.line &&\n          change.range.start.character === change.range.end.character &&\n          // If not in input\n          !document.activeElement?.classList.contains(\"ty-input\")\n          // If in a CodeMirror instance\n        ) {\n          // The line of the starter (```, ~~~, $$, <div>, etc.)\n          let starterLine =\n            change.range.start.character === 0 ?\n              change.range.start.line - 1\n            : change.range.start.line;\n\n          const lines = newMarkdown.split(Files.useCRLF ? \"\\r\\n\" : \"\\n\");\n          if (lines[starterLine] === \"\") starterLine--;\n          const lineText = lines[starterLine];\n\n          let starter: string | undefined = undefined;\n          let ender: string | undefined = undefined;\n\n          if (lineText) {\n            const unindentedLineText = lineText.replace(/^(\\s|>)*/, \"\");\n\n            // * Code block *\n            if (\n              // Check if the caret is inside a CodeMirror instance\n              document.activeElement?.tagName === \"TEXTAREA\" &&\n              (starter = /^(```([^`]|$)|~~~([^~]|$))/.exec(unindentedLineText)?.[0]?.slice(0, 3))\n            ) {\n              ender = starter;\n            }\n            // * Math block *\n            // NOTE: Typora renders the CodeMirror instance of a math/HTML block in an async way,\n            // so we cannot check if the caret is inside a CodeMirror instance like what we did\n            // in code blocks checking\n            else if (unindentedLineText === \"$$\" && change.text.trimEnd().endsWith(\"$$\")) {\n              starter = ender = \"$$\";\n            }\n            // * HTML block *\n            else if (\n              (starter = /^<[^>]*>/.exec(unindentedLineText)?.[0]) &&\n              change.text.trimEnd().endsWith(`</${starter.slice(1, -1)}>`)\n            ) {\n              ender = `</${starter.slice(1, -1)}>`;\n            }\n\n            if (starter && ender) {\n              const caretLine = starterLine + 1;\n              const caretLineText = lines[caretLine];\n              if (caretLineText !== undefined)\n                state.caretPosition = {\n                  line: caretLine,\n                  character:\n                    caretLineText.replace(/^(\\s|>)*/, \"\") === ender ? 0 : caretLineText.length,\n                };\n            }\n          }\n        }\n\n        // Set `character` to 0 if it is negative\n        if (state.caretPosition.character < 0) state.caretPosition.character = 0;\n      } else {\n        state.caretPosition = null;\n      }\n    } else {\n      state.caretPosition = null;\n    }\n    // Update current markdown text\n    state.markdown = newMarkdown;\n    ChatSession.currentDocument = newMarkdown;\n    // Reject last completion if exists\n    taskManager.rejectCurrentIfExist();\n    // Trigger completion\n    triggerCompletion();\n  });\n\n  /* Watch for markdown change in source mode */\n  cm.on(\"change\", (cm): void => {\n    if (checkActiveFileChange()) return;\n\n    if (settings.disableCompletions) return;\n    if (!editor.sourceView.inSourceMode) return;\n\n    const newMarkdown = cm.getValue(Files.useCRLF ? \"\\r\\n\" : \"\\n\");\n    // If not literally changed, simply return\n    if (newMarkdown === state._actualLatestMarkdown) return;\n    // If literally changed, update current actual markdown text\n    state._actualLatestMarkdown = newMarkdown;\n    // If update suppressed, return\n    if (state.suppressMarkdownChange) {\n      state.suppressMarkdownChange--;\n      return;\n    }\n\n    /* When update not suppressed */\n    // Update caret position if not selecting text\n    if (!cm.getSelection()) {\n      state.caretPosition = {\n        line: cm.getCursor().line,\n        character: cm.getCursor().ch,\n      };\n    } else {\n      state.caretPosition = null;\n    }\n    // Update current markdown text\n    state.markdown = newMarkdown;\n    ChatSession.currentDocument = newMarkdown;\n    // Reject last completion if exists\n    taskManager.rejectCurrentIfExist();\n    // Trigger completion\n    triggerCompletion();\n  });\n\n  /* Cancel current request on caret move */\n  $(editor.writingArea).on(\"caretMove\", () => {\n    if (settings.disableCompletions) return;\n    if (sourceView.inSourceMode) return;\n\n    taskManager.rejectCurrentIfExist();\n  });\n  cm.on(\"cursorActivity\", () => {\n    if (settings.disableCompletions) return;\n    if (!editor.sourceView.inSourceMode) return;\n\n    taskManager.rejectCurrentIfExist();\n  });\n}).catch((err) => {\n  throw err;\n});\n"
  },
  {
    "path": "src/modules/fs.ts",
    "content": "import type fs from \"node:fs\";\n\nimport { unique } from \"radash\";\n\nimport { PlatformError } from \"@/errors\";\nimport { getEnv, runCommand } from \"@/utils/cli-tools\";\n\nimport * as path from \"./path\";\n\nexport const constants = {\n  // File Access Constants\n  /** Constant for `fs.access()`. File is visible to the calling process. */\n  F_OK: 0,\n  /** Constant for `fs.access()`. File can be read by the calling process. */\n  R_OK: 4,\n  /** Constant for `fs.access(). File can be written by the calling process. */\n  W_OK: 2,\n  /** Constant for `fs.access()`. File can be executed by the calling process. */\n  X_OK: 1,\n} as const satisfies Partial<typeof fs.constants>;\n\n/**\n * Tests a user's permissions for the file or directory specified by `path`, and returns the\n * resolved path if the accessibility check is successful, or `null` if any of the checks fail.\n *\n * The `mode` argument is an optional integer that specifies the accessibility checks to be\n * performed. `mode` should be either the value `fs.constants.F_OK` or a mask consisting of the\n * bitwise OR of any of `fs.constants.R_OK`, `fs.constants.W_OK`, and `fs.constants.X_OK`\n * (e.g.`fs.constants.W_OK | fs.constants.R_OK`). Check `File access constants` for possible values\n * of `mode`.\n * @param [mode=fs.constants.F_OK]\n * @returns\n */\nexport const access: (path: string, mode?: number) => Promise<string | null> = (() => {\n  if (Files.isNode) {\n    const fs = window.reqnode!(\"fs\");\n\n    return async function access(path, mode = constants.F_OK) {\n      return new Promise((resolve) =>\n        fs.access(path, mode, (err) => {\n          if (err) return resolve(null);\n          resolve(path);\n        }),\n      );\n    };\n  }\n\n  if (Files.isMac)\n    return async function access(path, mode = constants.F_OK) {\n      const command = (() => {\n        switch (mode) {\n          case constants.F_OK:\n            return `if [ -e \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.R_OK:\n            return `if [ -r \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.W_OK:\n            return `if [ -w \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.X_OK:\n            return `if [ -x \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.R_OK | constants.W_OK:\n            return `if [ -r \"${path}\" ] && [ -w \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.R_OK | constants.X_OK:\n            return `if [ -r \"${path}\" ] && [ -x \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.W_OK | constants.X_OK:\n            return `if [ -w \"${path}\" ] && [ -x \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.R_OK | constants.W_OK | constants.X_OK:\n            return `if [ -r \"${path}\" ] && [ -w \"${path}\" ] && [ -x \"${path}\" ]; then echo \"${path}\"; fi`;\n          default:\n            throw new Error(`Invalid mode for \\`access\\`: ${mode}`);\n        }\n      })();\n\n      return (await runCommand(command)).trim() || null;\n    };\n\n  throw new PlatformError(\"Unsupported platform for `access`\");\n})();\n\n/**\n * Tests a user's permissions for the file specified by `path`, and returns the\n * resolved path if the accessibility check is successful, or `null` if any of the checks fail.\n *\n * The `mode` argument is an optional integer that specifies the accessibility checks to be\n * performed. `mode` should be either the value `fs.constants.F_OK` or a mask consisting of the\n * bitwise OR of any of `fs.constants.R_OK`, `fs.constants.W_OK`, and `fs.constants.X_OK`\n * (e.g.`fs.constants.W_OK | fs.constants.R_OK`). Check `File access constants` for possible values\n * of `mode`.\n * @param [mode=fs.constants.F_OK]\n * @returns\n */\nexport const accessFile: (path: string, mode?: number) => Promise<string | null> = (() => {\n  if (Files.isNode) {\n    const fs = window.reqnode!(\"fs\");\n\n    return async function accessFile(path, mode = constants.F_OK) {\n      return new Promise((resolve) =>\n        fs.access(path, mode, (err) => {\n          if (err) return resolve(null);\n          fs.stat(path, (err, stats) => {\n            if (err) return resolve(null);\n            if (stats.isFile()) resolve(path);\n            else resolve(null);\n          });\n        }),\n      );\n    };\n  }\n\n  if (Files.isMac)\n    return async function accessFile(path, mode = constants.F_OK) {\n      const command = (() => {\n        switch (mode) {\n          case constants.F_OK:\n            return `if [ -f \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.R_OK:\n            return `if [ -f \"${path}\" ] && [ -r \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.W_OK:\n            return `if [ -f \"${path}\" ] && [ -w \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.X_OK:\n            return `if [ -f \"${path}\" ] && [ -x \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.R_OK | constants.W_OK:\n            return `if [ -f \"${path}\" ] && [ -r \"${path}\" ] && [ -w \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.R_OK | constants.X_OK:\n            return `if [ -f \"${path}\" ] && [ -r \"${path}\" ] && [ -x \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.W_OK | constants.X_OK:\n            return `if [ -f \"${path}\" ] && [ -w \"${path}\" ] && [ -x \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.R_OK | constants.W_OK | constants.X_OK:\n            return `if [ -f \"${path}\" ] && [ -r \"${path}\" ] && [ -w \"${path}\" ] && [ -x \"${path}\" ]; then echo \"${path}\"; fi`;\n          default:\n            throw new Error(`Invalid mode for \\`accessFile\\`: ${mode}`);\n        }\n      })();\n\n      return (await runCommand(command)).trim() || null;\n    };\n\n  throw new PlatformError(\"Unsupported platform for `accessFile`\");\n})();\n\n/**\n * Tests a user's permissions for the directory specified by `path`, and returns the\n * resolved path if the accessibility check is successful, or `null` if any of the checks fail.\n *\n * The `mode` argument is an optional integer that specifies the accessibility checks to be\n * performed. `mode` should be either the value `fs.constants.F_OK` or a mask consisting of the\n * bitwise OR of any of `fs.constants.R_OK`, `fs.constants.W_OK`, and `fs.constants.X_OK`\n * (e.g.`fs.constants.W_OK | fs.constants.R_OK`). Check `File access constants` for possible values\n * of `mode`.\n * @param [mode=fs.constants.F_OK]\n * @returns\n */\nexport const accessDir: (path: string, mode?: number) => Promise<string | null> = (() => {\n  if (Files.isNode) {\n    const fs = window.reqnode!(\"fs\");\n\n    return async function accessDir(path, mode = constants.F_OK) {\n      return new Promise((resolve) =>\n        fs.access(path, mode, (err) => {\n          if (err) return resolve(null);\n          fs.stat(path, (err, stats) => {\n            if (err) return resolve(null);\n            if (stats.isDirectory()) resolve(path);\n            else resolve(null);\n          });\n        }),\n      );\n    };\n  }\n\n  if (Files.isMac)\n    return async function accessDir(path, mode = constants.F_OK) {\n      const command = (() => {\n        switch (mode) {\n          case constants.F_OK:\n            return `if [ -d \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.R_OK:\n            return `if [ -d \"${path}\" ] && [ -r \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.W_OK:\n            return `if [ -d \"${path}\" ] && [ -w \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.X_OK:\n            return `if [ -d \"${path}\" ] && [ -x \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.R_OK | constants.W_OK:\n            return `if [ -d \"${path}\" ] && [ -r \"${path}\" ] && [ -w \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.R_OK | constants.X_OK:\n            return `if [ -d \"${path}\" ] && [ -r \"${path}\" ] && [ -x \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.W_OK | constants.X_OK:\n            return `if [ -d \"${path}\" ] && [ -w \"${path}\" ] && [ -x \"${path}\" ]; then echo \"${path}\"; fi`;\n          case constants.R_OK | constants.W_OK | constants.X_OK:\n            return `if [ -d \"${path}\" ] && [ -r \"${path}\" ] && [ -w \"${path}\" ] && [ -x \"${path}\" ]; then echo \"${path}\"; fi`;\n          default:\n            throw new Error(`Invalid mode for \\`accessDir\\`: ${mode}`);\n        }\n      })();\n\n      return (await runCommand(command)).trim() || null;\n    };\n\n  throw new PlatformError(\"Unsupported platform for `accessDir`\");\n})();\n\n/**\n * Given a possibly case-variant version of an existing filesystem path, returns the absolute,\n * case-exact, normalized version as stored in the filesystem.\n *\n * Modified from the [npm package `true-case-path`](https://www.npmjs.com/package/true-case-path):\n * <https://github.com/Profiscience/true-case-path/blob/8a016e6a8be64c873aba414fbcdb4748e24dc796/index.js>\n * @param filePath The path to resolve.\n * @param [basePath] The base path to use.\n * @returns The resolved path.\n * @throws {Error} If the file does not exist.\n */\nexport const trueCasePath: (filePath: string, basePath?: string) => Promise<string> = (() => {\n  const delimiter = Files.isWin ? \"\\\\\" : \"/\";\n\n  const getRelevantFilePathSegments = (filePath: string) =>\n    filePath.split(delimiter).filter((s) => s !== \"\");\n\n  const escapeString = (str: string) => str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n  const matchCaseInsensitive = (\n    fileOrDirectory: string,\n    directoryContents: string[],\n    filePath: string,\n  ) => {\n    const caseInsensitiveRegex = new RegExp(`^${escapeString(fileOrDirectory)}$`, \"i\");\n    for (const file of directoryContents) if (caseInsensitiveRegex.test(file)) return file;\n    throw new Error(`[true-case-path]: Called with ${filePath}, but no matching file exists`);\n  };\n\n  return async function trueCasePath(filePath, basePath) {\n    if (basePath) {\n      if (!path.isAbsolute(basePath))\n        throw new Error(\n          `[true-case-path]: basePath argument must be absolute. Received \"${basePath}\"`,\n        );\n      basePath = path.normalize(basePath);\n    }\n    filePath = path.normalize(filePath);\n    const segments = getRelevantFilePathSegments(filePath);\n    if (path.isAbsolute(filePath)) {\n      if (basePath)\n        throw new Error(\"[true-case-path]: filePath must be relative when used with basePath\");\n      basePath =\n        Files.isWin ?\n          segments.shift()?.toUpperCase() || \"\" // drive letter\n        : \"\";\n    } else if (!basePath) {\n      basePath = process.cwd();\n    }\n    return await segments.reduce<string | Promise<string>>(\n      async (realPathPromise, fileOrDirectory) =>\n        (await realPathPromise) +\n        delimiter +\n        matchCaseInsensitive(\n          fileOrDirectory,\n          await readDir((await realPathPromise) + delimiter),\n          filePath,\n        ),\n      basePath,\n    );\n  };\n})();\n\n/**\n * [`realpath(3)`](http://man7.org/linux/man-pages/man3/realpath.3.html).\n *\n * The absolute and case-exact (if the filesystem is case-insensitive) path is returned, and\n * symbolic links are resolved.\n * @param path A file path.\n * @returns The resolved path, or `null` if the path does not exist.\n */\nexport const realpath: (path: string) => Promise<string | null> = (() => {\n  if (Files.isNode) {\n    const fs = window.reqnode!(\"fs\");\n\n    return async function realpath(path) {\n      return new Promise((resolve) =>\n        (Files.isWin ? fs.realpath.native : fs.realpath)(path, (err, resolvedPath) => {\n          if (err) return resolve(null);\n          resolve(resolvedPath);\n        }),\n      );\n    };\n  }\n\n  if (Files.isMac)\n    return async function realpath(path) {\n      return (await runCommand(`realpath \"${path}\"`).catch(() => \"\")).trim() || null;\n    };\n\n  throw new PlatformError(\"Unsupported platform for `realpath`\");\n})();\n\n/**\n * Check an array of possible executable file paths. The resolved paths are normalized and\n * case-exact path will be returned on Windows.\n *\n * If `addCommonExts` is `true`, common executable file extensions are appended to the paths.\n *\n * If `first` is `true`, the function will return the first resolved path, or `null` if none of the\n * paths match. Otherwise, the function will return an array of resolved paths.\n */\nconst _checkExecutableFilePaths: (\n  paths: string[],\n  { addCommonExts, first }: { addCommonExts: boolean; first: boolean },\n) => Promise<string[] | string | null> = (() => {\n  const COMMON_EXEC_EXTS = unique([\n    \"\",\n    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n    ...((window.process && process.env[\"PATHEXT\"]) || \"\").split(path.delimiter),\n    ...(Files.isMac || Files.isLinux ? [\".sh\", \".bash\"] : []),\n  ]);\n\n  if (Files.isWin)\n    return async function checkExecutableFilePaths(paths, { addCommonExts, first }) {\n      const processedPaths =\n        addCommonExts ? paths.flatMap((path) => COMMON_EXEC_EXTS.map((ext) => path + ext)) : paths;\n\n      if (first)\n        return await Promise.orderedFirstResolved(\n          processedPaths.map((path) =>\n            accessFile(path).then((p) => (p ? p : Promise.reject(new Error(\"Not found\")))),\n          ),\n        ).catch(() => null);\n\n      return await Promise.all(processedPaths.map((path) => accessFile(path))).then((paths) =>\n        paths.filter(Boolean),\n      );\n    };\n\n  if (Files.isMac || Files.isLinux)\n    return async function checkExecutableFilePaths(paths, { addCommonExts, first }) {\n      const processedPaths =\n        addCommonExts ? paths.flatMap((path) => COMMON_EXEC_EXTS.map((ext) => path + ext)) : paths;\n\n      let script = \"for path in\";\n      for (const path of processedPaths) script += ` \"${path}\"`;\n      script += \"; do\\n\";\n      script += '  if [ -f \"$path\" ] && [ -x \"$path\" ]; then\\n';\n      script += '    echo \"$path\"\\n';\n      if (first) script += \"    break\\n\";\n      script += \"  fi\\n\";\n      script += \"done\\n\";\n\n      return first ?\n          await runCommand(script)\n            .then((output) => output.trim() || null)\n            .catch((err) => {\n              console.warn(\"Warning: `checkExecutableFilePaths` failed:\", err);\n              return null;\n            })\n        : await runCommand(script)\n            .then((output) =>\n              output\n                .trim()\n                .split(\"\\n\")\n                .map((bin) => bin.trim())\n                .filter(Boolean),\n            )\n            .catch((err) => {\n              console.warn(\"Warning: `checkExecutableFilePaths` failed:\", err);\n              return [];\n            });\n    };\n\n  throw new PlatformError(\"Unsupported platform for `checkExecutableFilePaths`\");\n})();\n\n/**\n * Get an array of file names in a directory.\n * @param dir Directory path.\n * @param strategy Strategy to use.\n * @returns An array of file names.\n */\nexport const readDir: (\n  dir: string,\n  strategy?: \"all\" | \"dirsOnly\" | \"filesOnly\",\n) => Promise<string[]> = (() => {\n  if (Files.isNode) {\n    const fs = window.reqnode!(\"fs\");\n\n    return async function readDir(dir, strategy = \"all\") {\n      if (strategy === \"all\")\n        return new Promise((resolve, reject) => {\n          fs.readdir(dir, (err, files) => {\n            if (err) reject(err);\n            else resolve(files);\n          });\n        });\n\n      return new Promise((resolve, reject) => {\n        fs.readdir(dir, { withFileTypes: true }, (err, files) => {\n          if (err) reject(err);\n          else {\n            const filtered = files.filter((file) => {\n              if (strategy === \"dirsOnly\") return file.isDirectory();\n              return /* filesOnly */ file.isFile();\n            });\n            resolve(filtered.map((file) => file.name));\n          }\n        });\n      });\n    };\n  }\n\n  if (Files.isMac)\n    return async function readDir(dir, strategy = \"all\") {\n      if (strategy === \"all\") {\n        const output = await runCommand(/* sh */ `\n          for file in \"${dir}\"/* \"${dir}\"/.[!.]* \"${dir}\"/..?*; do\n            echo $(basename \"$file\")\n          done\n        `);\n        return output.trim().split(\"\\n\");\n      }\n\n      if (strategy === \"dirsOnly\") {\n        const output = await runCommand(/* sh */ `\n          for file in \"${dir}\"/* \"${dir}\"/.[!.]* \"${dir}\"/..?*; do\n            if [ -d \"$file\" ]; then\n              echo $(basename \"$file\")\n            fi\n          done\n        `);\n        return output.trim().split(\"\\n\");\n      }\n\n      const output = await runCommand(/* sh */ `\n        for file in \"${dir}\"/* \"${dir}\"/.[!.]* \"${dir}\"/..?*; do\n          if [ -f \"$file\" ]; then\n            echo $(basename \"$file\")\n          fi\n        done\n      `);\n      return output.trim().split(\"\\n\");\n    };\n\n  throw new PlatformError(\"Unsupported platform for `readDir`\");\n})();\n\n/**\n * Create a directory.\n * @param path The directory path to create.\n * @param options Options for directory creation.\n * @param options.recursive Create parent directories if they don’t exist.\n * @returns\n */\nexport const mkdir: (\n  path: string,\n  options?: { recursive?: boolean; mode?: number },\n) => Promise<void> = (() => {\n  if (Files.isNode) {\n    const fs = window.reqnode!(\"fs\");\n\n    return async function mkdir(path, options = {}) {\n      return new Promise<void>((resolve, reject) => {\n        fs.mkdir(path, options, (err) => {\n          if (err) reject(err);\n          else resolve();\n        });\n      });\n    };\n  }\n\n  if (Files.isMac || Files.isLinux) {\n    return async function mkdir(path, options = {}) {\n      const cmd = options.recursive ? `mkdir -p \"${path}\"` : `[ -d \"${path}\" ] || mkdir \"${path}\"`;\n      await runCommand(cmd);\n    };\n  }\n\n  throw new PlatformError(\"Unsupported platform for `mkdir`\");\n})();\n\n/**\n * Read the contents of a file.\n * @param path File path.\n * @returns\n */\nexport const readFile: (path: string) => Promise<string> = (() => {\n  if (Files.isNode) {\n    const fs = window.reqnode!(\"fs\");\n\n    return async function readFile(path) {\n      return new Promise((resolve, reject) => {\n        fs.readFile(path, \"utf-8\", (err, data) => {\n          if (err) reject(err);\n          else resolve(data);\n        });\n      });\n    };\n  }\n\n  if (Files.isMac)\n    return async function readFile(path) {\n      return await Promise.resolve(window.bridge!.callSync(\"path.readText\", path));\n    };\n\n  throw new PlatformError(\"Unsupported platform for `readFile`\");\n})();\n\n/**\n * Write data to a file, replacing the file if it already exists.\n * @param path The file path to write to.\n * @param data The data to write.\n * @returns\n */\nexport const writeFile: (path: string, data: string) => Promise<void> = (() => {\n  if (Files.isNode) {\n    const fs = window.reqnode!(\"fs\");\n\n    return async function writeFile(path, data) {\n      return new Promise<void>((resolve, reject) => {\n        fs.writeFile(path, data, \"utf-8\", (err) => {\n          if (err) reject(err);\n          else resolve();\n        });\n      });\n    };\n  }\n\n  if (Files.isMac || Files.isLinux) {\n    return async function writeFile(path, data) {\n      // Create a temporary file with the content\n      // eslint-disable-next-line sonarjs/pseudo-random\n      const tempFile = `/tmp/typora_tmp_${Date.now()}_${Math.random().toString(36).substring(2, 15)}.txt`;\n\n      try {\n        // Write content to temp file first (safer approach to avoid data loss)\n        await runCommand(`cat > \"${tempFile}\" << 'TYPORA_EOF'\\n${data}\\nTYPORA_EOF`);\n\n        // Move temp file to destination (atomic operation)\n        await runCommand(`mv \"${tempFile}\" \"${path}\"`);\n      } catch (error) {\n        // Clean up temp file if it exists\n        await runCommand(`rm -f \"${tempFile}\"`).catch(() => {});\n        throw error;\n      }\n    };\n  }\n\n  throw new PlatformError(\"Unsupported platform for `writeFile`\");\n})();\n\n/**\n * Remove a file.\n * @param path The file path to remove.\n * @returns\n */\nexport const rmFile: (path: string) => Promise<void> = (() => {\n  if (Files.isNode) {\n    const fs = window.reqnode!(\"fs\");\n\n    return async function rmFile(path) {\n      return new Promise<void>((resolve, reject) => {\n        fs.rm(path, (err) => {\n          if (err) reject(err);\n          else resolve();\n        });\n      });\n    };\n  }\n\n  if (Files.isMac || Files.isLinux) {\n    return async function rmFile(path) {\n      await runCommand(`rm -f \"${path}\"`);\n    };\n  }\n\n  throw new PlatformError(\"Unsupported platform for `rmFile`\");\n})();\n\n/**\n * Get all paths of a command.\n *\n * Modified from the [npm package `lookpath`](https://www.npmjs.com/package/lookpath):\n * <https://github.com/otiai10/lookpath/blob/f6ae8dbc990e0b5ecc2bf77fa62179ce13fbe8e9/src/index.ts>\n * @param command The command to look for.\n * @returns The absolute paths of the command.\n */\nexport const lookPath = async (command: string): Promise<string[]> => {\n  const dirs = // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n    ((window.process ? process.env : await getEnv())[Files.isWin ? \"Path\" : \"PATH\"] || \"\").split(\n      path.delimiter,\n    );\n  return (await _checkExecutableFilePaths(\n    dirs.map((dir) => path.join(dir, command)),\n    { addCommonExts: true, first: false },\n  )) as string[];\n};\n/**\n * Get the path of a command.\n *\n * Modified from the [npm package `lookpath`](https://www.npmjs.com/package/lookpath):\n * <https://github.com/otiai10/lookpath/blob/f6ae8dbc990e0b5ecc2bf77fa62179ce13fbe8e9/src/index.ts>\n * @param command The command to look for.\n * @returns The absolute path of the command, or `null` if not found.\n */\nexport const lookPathFirst = async (command: string): Promise<string | null> => {\n  const dirs = // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n    ((window.process ? process.env : await getEnv())[Files.isWin ? \"Path\" : \"PATH\"] || \"\").split(\n      path.delimiter,\n    );\n  return (await _checkExecutableFilePaths(\n    dirs.map((dir) => path.join(dir, command)),\n    { addCommonExts: true, first: true },\n  )) as string | null;\n};\n\nexport const {\n  lookApp,\n  lookAppFirst,\n}: {\n  /**\n   * Look for an application in common directories.\n   * @param app Application name.\n   * @param executableName Name of the executable file.\n   * @returns The absolute paths of the application.\n   * @throws {PlatformError} If the platform is not supported.\n   */\n  lookApp: (app: string, executableName?: string) => Promise<string[]>;\n  /**\n   * Look for an application in common directories.\n   * @param app Application name.\n   * @param executableName Name of the executable file.\n   * @returns The absolute path of the application, or `null` if not found.\n   * @throws {PlatformError} If the platform is not supported.\n   */\n  lookAppFirst: (app: string, executableName?: string) => Promise<string | null>;\n} = (() => {\n  if (Files.isWin) {\n    const APP_DIRS = [\n      \"C:\\\\\",\n      ...[\n        \"APPDATA\",\n        \"LOCALAPPDATA\",\n        \"USERPROFILE\",\n        \"ProgramFiles\",\n        \"ProgramFiles(x86)\",\n        \"ProgramData\",\n      ].map((env) => process.env[env]),\n    ].filter(Boolean);\n\n    return {\n      async lookApp(this: void, app, executableName = app) {\n        return (await _checkExecutableFilePaths(\n          APP_DIRS.map((dir) => path.join(dir, app, executableName)),\n          { addCommonExts: true, first: false },\n        )) as string[];\n      },\n\n      async lookAppFirst(this: void, app, executableName = app) {\n        return (await _checkExecutableFilePaths(\n          APP_DIRS.map((dir) => path.join(dir, app, executableName)),\n          { addCommonExts: true, first: true },\n        )) as string | null;\n      },\n    };\n  }\n\n  if (Files.isMac || Files.isLinux) {\n    const COMMON_APP_DIRS = [\n      ...(Files.isMac ?\n        [\"/Library/Application Support\", path.expandHomeDir(\"~/Library/Application Support\")]\n      : []),\n      path.expandHomeDir(\"~\"),\n      path.expandHomeDir(\"~/.config\"),\n      path.expandHomeDir(\"~/.local\"),\n      path.expandHomeDir(\"~/.local/etc\"),\n      path.expandHomeDir(\"~/.local/share\"),\n      path.expandHomeDir(\"~/.local/share/applications\"),\n      \"/\",\n      \"/opt\",\n      \"/opt/local\",\n      \"/home\",\n      \"/usr/share\",\n      \"/usr/local\",\n      \"/usr/local/etc\",\n      \"/usr/local/share\",\n      \"/usr/local/share/applications\",\n    ];\n    const COMMON_EXEC_SUB_DIRS = [\"bin\", \"sbin\", \"lib\", \"exec\", \"libexec\"];\n\n    const _lookApp = async (\n      app: string,\n      executableName: string,\n      first: boolean,\n    ): Promise<string[] | string | null> => {\n      const possiblePaths = [\n        ...(Files.isMac ?\n          [\"/Applications\", \"/Applications/Utilities\", path.expandHomeDir(\"~/Applications\")].map(\n            (dir) => path.join(dir, `${app}.app`, \"Contents\", \"MacOS\", executableName),\n          )\n        : []),\n        ...COMMON_APP_DIRS.flatMap((dir) => [\n          path.join(dir, executableName),\n          ...COMMON_EXEC_SUB_DIRS.flatMap((sub) => path.join(dir, sub, executableName)),\n          path.join(dir, app, executableName),\n          path.join(dir, \".\" + app, executableName),\n          ...COMMON_EXEC_SUB_DIRS.flatMap((sub) => path.join(dir, app, sub, executableName)),\n          ...COMMON_EXEC_SUB_DIRS.flatMap((sub) => path.join(dir, \".\" + app, sub, executableName)),\n          path.join(dir, app, app, executableName),\n          path.join(dir, app, \".\" + app, executableName),\n          path.join(dir, \".\" + app, app, executableName),\n          path.join(dir, \".\" + app, \".\" + app, executableName),\n          ...COMMON_EXEC_SUB_DIRS.flatMap((sub) => path.join(dir, app, app, sub, executableName)),\n          ...COMMON_EXEC_SUB_DIRS.flatMap((sub) =>\n            path.join(dir, app, \".\" + app, sub, executableName),\n          ),\n          ...COMMON_EXEC_SUB_DIRS.flatMap((sub) =>\n            path.join(dir, \".\" + app, app, sub, executableName),\n          ),\n          ...COMMON_EXEC_SUB_DIRS.flatMap((sub) =>\n            path.join(dir, \".\" + app, \".\" + app, sub, executableName),\n          ),\n        ]),\n      ];\n\n      return await _checkExecutableFilePaths(possiblePaths, { addCommonExts: true, first });\n    };\n\n    return {\n      async lookApp(this: void, app, executableName = app) {\n        return (await _lookApp(app, executableName, false)) as string[];\n      },\n\n      async lookAppFirst(this: void, app, executableName = app) {\n        return (await _lookApp(app, executableName, true)) as string | null;\n      },\n    };\n  }\n\n  throw new PlatformError(\"Unsupported platform for `lookApp`\");\n})();\n"
  },
  {
    "path": "src/modules/path.spec.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport * as path from \"./path\";\n\ndescribe(\"basename\", () => {\n  it(\"should return the last portion of a path\", () => {\n    expect(path.basename(\"/foo/bar/baz/asdf/quux.html\")).toBe(\"quux.html\");\n    expect(path.basename(\"/foo/bar/baz/asdf/quux.html\", \".html\")).toBe(\"quux\");\n    expect(path.basename(\"/foo\")).toBe(\"foo\");\n    expect(path.basename(\"\")).toBe(\"\");\n    expect(path.basename(\"C:\\\\\")).toBe(\"C:\\\\\");\n  });\n});\n\ndescribe(\"dirname\", () => {\n  it(\"should return the directory name of a path\", () => {\n    expect(path.dirname(\"/foo/bar/baz/asdf/quux\")).toBe(\"/foo/bar/baz/asdf\");\n    expect(path.dirname(\"/foo/bar/baz/asdf/quux.html\")).toBe(\"/foo/bar/baz/asdf\");\n    expect(path.dirname(\"/foo/bar/baz/asdf/quux/\")).toBe(\"/foo/bar/baz/asdf\");\n    expect(path.dirname(\"/foo/bar\")).toBe(\"/foo\");\n    expect(path.dirname(\"/foo\")).toBe(\"/\");\n    expect(path.dirname(\"/\")).toBe(\"/\");\n  });\n});\n"
  },
  {
    "path": "src/modules/path.ts",
    "content": "import type path from \"node:path\";\n\ntype AddSep<F extends (...args: any) => unknown> = (\n  sep: string,\n  ...args: Parameters<F>\n) => ReturnType<F>;\n\nexport const sep = Files.isWin ? \"\\\\\" : \"/\";\nexport const delimiter = Files.isWin ? \";\" : \":\";\n\nconst assertPath = (path: unknown) => {\n  if (typeof path !== \"string\")\n    throw new TypeError(\"Path must be a string. Received \" + JSON.stringify(path));\n};\n\n/**\n * Resolves `.` and `..` elements in a path with directory names\n * @returns\n */\nconst normalizeStringPosix = (sep: string, path: string, allowAboveRoot: boolean) => {\n  let res = \"\";\n  let lastSegmentLength = 0;\n  let lastSlash = -1;\n  let dots = 0;\n  let code;\n  for (let i = 0; i <= path.length; ++i) {\n    if (i < path.length) code = path.charCodeAt(i);\n    else if (code === sep.charCodeAt(0) /*/*/) break;\n    else code = sep.charCodeAt(0) /*/*/;\n    if (code === sep.charCodeAt(0) /*/*/) {\n      if (lastSlash === i - 1 || dots === 1) {\n        // NOOP\n      } else if (lastSlash !== i - 1 && dots === 2) {\n        if (\n          res.length < 2 ||\n          lastSegmentLength !== 2 ||\n          res.charCodeAt(res.length - 1) !== 46 /*.*/ ||\n          res.charCodeAt(res.length - 2) !== 46 /*.*/\n        ) {\n          if (res.length > 2) {\n            const lastSlashIndex = res.lastIndexOf(sep);\n            if (lastSlashIndex !== res.length - 1) {\n              if (lastSlashIndex === -1) {\n                res = \"\";\n                lastSegmentLength = 0;\n              } else {\n                res = res.slice(0, lastSlashIndex);\n                lastSegmentLength = res.length - 1 - res.lastIndexOf(sep);\n              }\n              lastSlash = i;\n              dots = 0;\n              continue;\n            }\n          } else if (res.length === 2 || res.length === 1) {\n            res = \"\";\n            lastSegmentLength = 0;\n            lastSlash = i;\n            dots = 0;\n            continue;\n          }\n        }\n        if (allowAboveRoot) {\n          if (res.length > 0) res += sep + \"..\";\n          else res = \"..\";\n          lastSegmentLength = 2;\n        }\n      } else {\n        if (res.length > 0) res += sep + path.slice(lastSlash + 1, i);\n        else res = path.slice(lastSlash + 1, i);\n        lastSegmentLength = i - lastSlash - 1;\n      }\n      lastSlash = i;\n      dots = 0;\n    } else if (code === 46 /*.*/ && dots !== -1) {\n      ++dots;\n    } else {\n      dots = -1;\n    }\n  }\n  return res;\n};\n\nexport const __format = (sep: string, pathObject: path.FormatInputPathObject) => {\n  const dir = pathObject.dir || pathObject.root;\n  const base = pathObject.base || (pathObject.name || \"\") + (pathObject.ext || \"\");\n  if (!dir) {\n    return base;\n  }\n  if (dir === pathObject.root) {\n    return dir + base;\n  }\n  return dir + sep + base;\n};\n\n/**\n * Expand the home directory (`~`) in a path.\n * @param path Path to expand.\n * @returns Expanded path.\n * @throws {TypeError} if `path` is not a string.\n */\nexport const expandHomeDir = (path: string): string => {\n  assertPath(path);\n  if (path.startsWith(\"~\")) return window._options.userPath + path.slice(1);\n  return path;\n};\n\n/**\n * The right-most parameter is considered {to}. Other parameters are considered an array of {from}.\n *\n * Starting from leftmost {from} parameter, resolves {to} to an absolute path.\n *\n * If {to} isn't already absolute, {from} arguments are prepended in right to left order,\n * until an absolute path is found. If after using all {from} paths still no absolute path is found,\n * the current working directory is used as well. The resulting path is normalized,\n * and trailing slashes are removed unless the path gets resolved to the root directory.\n *\n * @param paths A sequence of paths or path segments.\n * @throws {TypeError} if any of the arguments is not a string.\n * @returns\n */\nexport const resolve: typeof path.posix.resolve = (...args) => _resolve(sep, ...args);\n\nexport const _resolve: AddSep<typeof path.posix.resolve> = (sep, ...paths) => {\n  let resolvedPath = \"\";\n  let resolvedAbsolute = false;\n  let cwd;\n\n  for (let i = paths.length - 1; i >= -1 && !resolvedAbsolute; i--) {\n    let path: string;\n    if (i >= 0) path = paths[i]!;\n    else {\n      if (cwd === undefined) cwd = process.cwd();\n      path = cwd;\n    }\n\n    assertPath(path);\n\n    // Skip empty entries\n    if (path.length === 0) {\n      continue;\n    }\n\n    resolvedPath = path + sep + resolvedPath;\n    resolvedAbsolute = path.charCodeAt(0) === sep.charCodeAt(0) /*/*/;\n  }\n\n  // At this point the path should be resolved to a full absolute path, but\n  // handle relative paths to be safe (might happen when process.cwd() fails)\n\n  // Normalize the path\n  resolvedPath = normalizeStringPosix(sep, resolvedPath, !resolvedAbsolute);\n\n  if (resolvedAbsolute) {\n    if (resolvedPath.length > 0) return sep + resolvedPath;\n    else return sep;\n  } else if (resolvedPath.length > 0) {\n    return resolvedPath;\n  } else {\n    return \".\";\n  }\n};\n\n/**\n * Normalize a string path, reducing `\"..\"` and `\".\"` parts.\n * When multiple slashes are found, they're replaced by a single one; when the path contains a\n * trailing slash, it is preserved. On Windows backslashes are used.\n *\n * @param path string path to normalize.\n * @throws {TypeError} if `path` is not a string.\n * @returns\n */\nexport const normalize: typeof path.posix.normalize = (...args) => _normalize(sep, ...args);\nexport const _normalize: AddSep<typeof path.posix.normalize> = (sep, path) => {\n  assertPath(path);\n\n  if (path.length === 0) return \".\";\n\n  const isAbsolute = path.charCodeAt(0) === sep.charCodeAt(0); /*/*/\n  const trailingSeparator = path.charCodeAt(path.length - 1) === sep.charCodeAt(0); /*/*/\n\n  // Normalize the path\n  path = normalizeStringPosix(sep, path, !isAbsolute);\n\n  if (path.length === 0 && !isAbsolute) path = \".\";\n  if (path.length > 0 && trailingSeparator) path += sep;\n\n  if (isAbsolute) return sep + path;\n  return path;\n};\n\n/**\n * Determines whether `path` is an absolute path. An absolute path will always resolve to the same\n * location, regardless of the working directory.\n *\n * If the given `path` is a zero-length string, `false` will be returned.\n *\n * @param path path to test.\n * @throws {TypeError} if `path` is not a string.\n * @returns\n */\nexport const isAbsolute: typeof path.posix.isAbsolute = (...args) => _isAbsolute(sep, ...args);\nexport const _isAbsolute: AddSep<typeof path.posix.isAbsolute> = (sep, path) => {\n  assertPath(path);\n  if (path.length === 0) return false;\n  if (Files.isWin) return /[a-zA-Z]+:/.test(path);\n  return path.charCodeAt(0) === sep.charCodeAt(0) /*/*/;\n};\n\n/**\n * Join all arguments together and normalize the resulting path.\n *\n * @param paths paths to join.\n * @throws {TypeError} if any of the path segments is not a string.\n * @returns\n */\nexport const join: typeof path.posix.join = (...args) => _join(sep, ...args);\nexport const _join: AddSep<typeof path.join> = (sep, ...paths) => {\n  if (paths.length === 0) return \".\";\n  let joined;\n  for (const arg of paths) {\n    assertPath(arg);\n    if (arg.length > 0) {\n      if (joined === undefined) joined = arg;\n      else joined += sep + arg;\n    }\n  }\n  if (joined === undefined) return \".\";\n  return _normalize(sep, joined);\n};\n\n/**\n * Solve the relative path from {from} to {to} based on the current working directory.\n * At times we have two absolute paths, and we need to derive the relative path from one to the\n * other. This is actually the reverse transform of path.resolve.\n *\n * @throws {TypeError} if either `from` or `to` is not a string.\n * @returns\n */\nexport const relative: typeof path.posix.relative = (...args) => _relative(sep, ...args);\nexport const _relative: AddSep<typeof path.posix.relative> = (sep, from, to) => {\n  assertPath(from);\n  assertPath(to);\n\n  if (from === to) return \"\";\n\n  from = _resolve(sep, from);\n  to = _resolve(sep, to);\n\n  if (from === to) return \"\";\n\n  // Trim any leading backslashes\n  let fromStart = 1;\n  for (; fromStart < from.length; ++fromStart) {\n    if (from.charCodeAt(fromStart) !== sep.charCodeAt(0) /*/*/) break;\n  }\n  const fromEnd = from.length;\n  const fromLen = fromEnd - fromStart;\n\n  // Trim any leading backslashes\n  let toStart = 1;\n  for (; toStart < to.length; ++toStart) {\n    if (to.charCodeAt(toStart) !== sep.charCodeAt(0) /*/*/) break;\n  }\n  const toEnd = to.length;\n  const toLen = toEnd - toStart;\n\n  // Compare paths to find the longest common path from root\n  const length = fromLen < toLen ? fromLen : toLen;\n  let lastCommonSep = -1;\n  let i = 0;\n  for (; i <= length; ++i) {\n    if (i === length) {\n      if (toLen > length) {\n        if (to.charCodeAt(toStart + i) === sep.charCodeAt(0) /*/*/) {\n          // We get here if `from` is the exact base path for `to`.\n          // For example: from='/foo/bar'; to='/foo/bar/baz'\n          return to.slice(toStart + i + 1);\n        } else if (i === 0) {\n          // We get here if `from` is the root\n          // For example: from='/'; to='/foo'\n          return to.slice(toStart + i);\n        }\n      } else if (fromLen > length) {\n        if (from.charCodeAt(fromStart + i) === sep.charCodeAt(0) /*/*/) {\n          // We get here if `to` is the exact base path for `from`.\n          // For example: from='/foo/bar/baz'; to='/foo/bar'\n          lastCommonSep = i;\n        } else if (i === 0) {\n          // We get here if `to` is the root.\n          // For example: from='/foo'; to='/'\n          lastCommonSep = 0;\n        }\n      }\n      break;\n    }\n    const fromCode = from.charCodeAt(fromStart + i);\n    const toCode = to.charCodeAt(toStart + i);\n    if (fromCode !== toCode) break;\n    else if (fromCode === sep.charCodeAt(0) /*/*/) lastCommonSep = i;\n  }\n\n  let out = \"\";\n  // Generate the relative path based on the path difference between `to`\n  // and `from`\n  for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) {\n    if (i === fromEnd || from.charCodeAt(i) === sep.charCodeAt(0) /*/*/) {\n      if (out.length === 0) out += \"..\";\n      else out += sep + \"..\";\n    }\n  }\n\n  // Lastly, append the rest of the destination (`to`) path that comes after\n  // the common path parts\n  if (out.length > 0) return out + to.slice(toStart + lastCommonSep);\n  else {\n    toStart += lastCommonSep;\n    if (to.charCodeAt(toStart) === sep.charCodeAt(0) /*/*/) ++toStart;\n    return to.slice(toStart);\n  }\n};\n\n/**\n * Return the directory name of a path. Similar to the Unix dirname command.\n *\n * @param path the path to evaluate.\n * @throws {TypeError} if `path` is not a string.\n * @returns\n */\nexport const dirname: typeof path.dirname = (...args) => _dirname(sep, ...args);\nexport const _dirname: AddSep<typeof path.dirname> = (sep, path) => {\n  assertPath(path);\n  if (path.length === 0) return \".\";\n  let code = path.charCodeAt(0);\n  const hasRoot = code === sep.charCodeAt(0); /*/*/\n  let end = -1;\n  let matchedSlash = true;\n  for (let i = path.length - 1; i >= 1; --i) {\n    code = path.charCodeAt(i);\n    if (code === sep.charCodeAt(0) /*/*/) {\n      if (!matchedSlash) {\n        end = i;\n        break;\n      }\n    } else {\n      // We saw the first non-path separator\n      matchedSlash = false;\n    }\n  }\n\n  if (end === -1) return hasRoot ? sep : \".\";\n  if (hasRoot && end === 1) return sep + sep;\n  return path.slice(0, end);\n};\n\n/**\n * Return the last portion of a path. Similar to the Unix basename command.\n * Often used to extract the file name from a fully qualified path.\n *\n * @param path the path to evaluate.\n * @param suffix optionally, an extension to remove from the result.\n * @throws {TypeError} if `path` is not a string or if `ext` is given and is not a string.\n * @returns\n */\nexport const basename: typeof path.basename = (...args) => _basename(sep, ...args);\nexport const _basename: AddSep<typeof path.basename> = (sep, path, ext) => {\n  if (ext !== undefined && typeof ext !== \"string\")\n    throw new TypeError('\"ext\" argument must be a string');\n  assertPath(path);\n\n  let start = 0;\n  let end = -1;\n  let matchedSlash = true;\n  let i;\n\n  if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {\n    if (ext.length === path.length && ext === path) return \"\";\n    let extIdx = ext.length - 1;\n    let firstNonSlashEnd = -1;\n    for (i = path.length - 1; i >= 0; --i) {\n      const code = path.charCodeAt(i);\n      if (code === sep.charCodeAt(0) /*/*/) {\n        // If we reached a path separator that was not part of a set of path\n        // separators at the end of the string, stop now\n        if (!matchedSlash) {\n          start = i + 1;\n          break;\n        }\n      } else {\n        if (firstNonSlashEnd === -1) {\n          // We saw the first non-path separator, remember this index in case\n          // we need it if the extension ends up not matching\n          matchedSlash = false;\n          firstNonSlashEnd = i + 1;\n        }\n        if (extIdx >= 0) {\n          // Try to match the explicit extension\n          if (code === ext.charCodeAt(extIdx)) {\n            if (--extIdx === -1) {\n              // We matched the extension, so mark this as the end of our path\n              // component\n              end = i;\n            }\n          } else {\n            // Extension does not match, so our result is the entire path\n            // component\n            extIdx = -1;\n            end = firstNonSlashEnd;\n          }\n        }\n      }\n    }\n\n    if (start === end) end = firstNonSlashEnd;\n    else if (end === -1) end = path.length;\n    return path.slice(start, end);\n  } else {\n    for (i = path.length - 1; i >= 0; --i) {\n      if (path.charCodeAt(i) === sep.charCodeAt(0) /*/*/) {\n        // If we reached a path separator that was not part of a set of path\n        // separators at the end of the string, stop now\n        if (!matchedSlash) {\n          start = i + 1;\n          break;\n        }\n      } else if (end === -1) {\n        // We saw the first non-path separator, mark this as the end of our\n        // path component\n        matchedSlash = false;\n        end = i + 1;\n      }\n    }\n\n    if (end === -1) return \"\";\n    return path.slice(start, end);\n  }\n};\n\n/**\n * Return the extension of the path, from the last `\".\"` to end of string in the last portion of the\n * path. If there is no `\".\"`  in the last portion of the path or the first character of it is\n * `\".\"`, then it returns an empty string.\n *\n * @param path the path to evaluate.\n * @throws {TypeError} if `path` is not a string.\n * @returns\n */\nexport const extname: typeof path.extname = (...args) => _extname(sep, ...args);\nexport const _extname: AddSep<typeof path.extname> = (sep, path) => {\n  assertPath(path);\n  let startDot = -1;\n  let startPart = 0;\n  let end = -1;\n  let matchedSlash = true;\n  // Track the state of characters (if any) we see before our first dot and\n  // after any path separator we find\n  let preDotState = 0;\n  for (let i = path.length - 1; i >= 0; --i) {\n    const code = path.charCodeAt(i);\n    if (code === sep.charCodeAt(0) /*/*/) {\n      // If we reached a path separator that was not part of a set of path\n      // separators at the end of the string, stop now\n      if (!matchedSlash) {\n        startPart = i + 1;\n        break;\n      }\n      continue;\n    }\n    if (end === -1) {\n      // We saw the first non-path separator, mark this as the end of our\n      // extension\n      matchedSlash = false;\n      end = i + 1;\n    }\n    if (code === 46 /*.*/) {\n      // If this is our first dot, mark it as the start of our extension\n      if (startDot === -1) startDot = i;\n      else if (preDotState !== 1) preDotState = 1;\n    } else if (startDot !== -1) {\n      // We saw a non-dot and non-path separator before our dot, so we should\n      // have a good chance at having a non-empty extension\n      preDotState = -1;\n    }\n  }\n\n  if (\n    startDot === -1 ||\n    end === -1 ||\n    // We saw a non-dot character immediately before the dot\n    preDotState === 0 ||\n    // The (right-most) trimmed path component is exactly '..'\n    (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)\n  ) {\n    return \"\";\n  }\n  return path.slice(startDot, end);\n};\n\n/**\n * Returns a path string from an object - the opposite of `parse()`.\n *\n * @param pathObject path to evaluate.\n * @returns\n */\nexport const format: typeof path.format = (...args) => _format(sep, ...args);\nexport const _format: AddSep<typeof path.format> = (sep, pathObject) => {\n  if (typeof pathObject !== \"object\") {\n    throw new TypeError(\n      'The \"pathObject\" argument must be of type Object. Received type ' + typeof pathObject,\n    );\n  }\n  return __format(sep, pathObject);\n};\n\n/**\n * Returns an object from a path string - the opposite of `format()`.\n *\n * @param path path to evaluate.\n * @throws {TypeError} if `path` is not a string.\n * @returns\n */\nexport const parse: typeof path.parse = (...args) => _parse(sep, ...args);\nexport const _parse: AddSep<typeof path.parse> = (sep, path) => {\n  assertPath(path);\n\n  const ret = { root: \"\", dir: \"\", base: \"\", ext: \"\", name: \"\" };\n  if (path.length === 0) return ret;\n  let code = path.charCodeAt(0);\n  const isAbsolute = code === sep.charCodeAt(0); /*/*/\n  let start;\n  if (isAbsolute) {\n    ret.root = sep;\n    start = 1;\n  } else {\n    start = 0;\n  }\n  let startDot = -1;\n  let startPart = 0;\n  let end = -1;\n  let matchedSlash = true;\n  let i = path.length - 1;\n\n  // Track the state of characters (if any) we see before our first dot and\n  // after any path separator we find\n  let preDotState = 0;\n\n  // Get non-dir info\n  for (; i >= start; --i) {\n    code = path.charCodeAt(i);\n    if (code === sep.charCodeAt(0) /*/*/) {\n      // If we reached a path separator that was not part of a set of path\n      // separators at the end of the string, stop now\n      if (!matchedSlash) {\n        startPart = i + 1;\n        break;\n      }\n      continue;\n    }\n    if (end === -1) {\n      // We saw the first non-path separator, mark this as the end of our\n      // extension\n      matchedSlash = false;\n      end = i + 1;\n    }\n    if (code === 46 /*.*/) {\n      // If this is our first dot, mark it as the start of our extension\n      if (startDot === -1) startDot = i;\n      else if (preDotState !== 1) preDotState = 1;\n    } else if (startDot !== -1) {\n      // We saw a non-dot and non-path separator before our dot, so we should\n      // have a good chance at having a non-empty extension\n      preDotState = -1;\n    }\n  }\n\n  if (\n    startDot === -1 ||\n    end === -1 ||\n    // We saw a non-dot character immediately before the dot\n    preDotState === 0 ||\n    // The (right-most) trimmed path component is exactly '..'\n    (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)\n  ) {\n    if (end !== -1) {\n      if (startPart === 0 && isAbsolute) ret.base = ret.name = path.slice(1, end);\n      else ret.base = ret.name = path.slice(startPart, end);\n    }\n  } else {\n    if (startPart === 0 && isAbsolute) {\n      ret.name = path.slice(1, startDot);\n      ret.base = path.slice(1, end);\n    } else {\n      ret.name = path.slice(startPart, startDot);\n      ret.base = path.slice(startPart, end);\n    }\n    ret.ext = path.slice(startDot, end);\n  }\n\n  if (startPart > 0) ret.dir = path.slice(0, startPart - 1);\n  else if (isAbsolute) ret.dir = sep;\n\n  return ret;\n};\n\nexport const win32 = {\n  sep: \"\\\\\",\n  delimiter: \";\",\n  resolve: (...args: Parameters<typeof resolve>) => _resolve(\"\\\\\", ...args),\n  normalize: (...args: Parameters<typeof normalize>) => _normalize(\"\\\\\", ...args),\n  isAbsolute: (...args: Parameters<typeof isAbsolute>) => _isAbsolute(\"\\\\\", ...args),\n  join: (...args: Parameters<typeof join>) => _join(\"\\\\\", ...args),\n  relative: (...args: Parameters<typeof relative>) => _relative(\"\\\\\", ...args),\n  dirname: (...args: Parameters<typeof dirname>) => _dirname(\"\\\\\", ...args),\n  basename: (...args: Parameters<typeof basename>) => _basename(\"\\\\\", ...args),\n  extname: (...args: Parameters<typeof extname>) => _extname(\"\\\\\", ...args),\n  format: (...args: Parameters<typeof format>) => _format(\"\\\\\", ...args),\n  parse: (...args: Parameters<typeof parse>) => _parse(\"\\\\\", ...args),\n};\n\nexport const posix = {\n  sep: \"/\",\n  delimiter: \":\",\n  resolve: (...args: Parameters<typeof resolve>) => _resolve(\"/\", ...args),\n  normalize: (...args: Parameters<typeof normalize>) => _normalize(\"/\", ...args),\n  isAbsolute: (...args: Parameters<typeof isAbsolute>) => _isAbsolute(\"/\", ...args),\n  join: (...args: Parameters<typeof join>) => _join(\"/\", ...args),\n  relative: (...args: Parameters<typeof relative>) => _relative(\"/\", ...args),\n  dirname: (...args: Parameters<typeof dirname>) => _dirname(\"/\", ...args),\n  basename: (...args: Parameters<typeof basename>) => _basename(\"/\", ...args),\n  extname: (...args: Parameters<typeof extname>) => _extname(\"/\", ...args),\n  format: (...args: Parameters<typeof format>) => _format(\"/\", ...args),\n  parse: (...args: Parameters<typeof parse>) => _parse(\"/\", ...args),\n};\n"
  },
  {
    "path": "src/modules/url.spec.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { fileURLToPath, pathToFileURL } from \"./url\";\n\ndescribe(\"fileURLToPath\", () => {\n  it(\"should return a platform-specific path\", () => {\n    const isWinBefore = Files.isWin;\n\n    Files.isWin = true;\n    expect(fileURLToPath(\"file:///C:/path/\")).toBe(\"C:\\\\path\\\\\");\n    expect(fileURLToPath(\"file://nas/foo.txt\")).toBe(\"\\\\\\\\nas\\\\foo.txt\");\n    Files.isWin = false;\n    expect(fileURLToPath(\"file:///你好.txt\")).toBe(\"/你好.txt\");\n    expect(fileURLToPath(\"file:///hello world\")).toBe(\"/hello world\");\n\n    Files.isWin = isWinBefore;\n  });\n});\n\ndescribe(\"pathToFileURL\", () => {\n  it(\"should return a file URL object\", () => {\n    expect(pathToFileURL(\"/foo#1\")).toEqual(new URL(\"file:///foo%231\"));\n    expect(pathToFileURL(\"/some/path%.c\")).toEqual(new URL(\"file:///some/path%25.c\"));\n    expect(pathToFileURL(\"C:\\\\foo\\\\bar\\\\test.py\")).toEqual(new URL(\"file:///C:/foo/bar/test.py\"));\n  });\n});\n"
  },
  {
    "path": "src/modules/url.ts",
    "content": "/**\n * This function ensures the correct decodings of percent-encoded characters as\n * well as ensuring a cross-platform valid absolute path string.\n *\n * ```javascript\n * import { fileURLToPath } from \"@modules/url\";\n *\n * const __filename = fileURLToPath(import.meta.url);\n *\n * new URL(\"file:///C:/path/\").pathname;      // Incorrect: /C:/path/\n * fileURLToPath(\"file:///C:/path/\");         // Correct:   C:\\path\\ (Windows)\n *\n * new URL(\"file://nas/foo.txt\").pathname;    // Incorrect: /foo.txt\n * fileURLToPath(\"file://nas/foo.txt\");       // Correct:   \\\\nas\\foo.txt (Windows)\n *\n * new URL(\"file:///你好.txt\").pathname;      // Incorrect: /%E4%BD%A0%E5%A5%BD.txt\n * fileURLToPath(\"file:///你好.txt\");         // Correct:   /你好.txt (POSIX)\n *\n * new URL(\"file:///hello world\").pathname;   // Incorrect: /hello%20world\n * fileURLToPath(\"file:///hello world\");      // Correct:   /hello world (POSIX)\n * ```\n * @param url The file URL string or URL object to convert to a path.\n * @returns The fully-resolved platform-specific Node.js file path.\n */\nexport const fileURLToPath = (url: string) => {\n  let parsedUrl: URL;\n\n  try {\n    parsedUrl = new URL(url);\n  } catch (error) {\n    throw new TypeError(`Invalid URL: ${url}`);\n  }\n\n  if (parsedUrl.protocol !== \"file:\") {\n    throw new TypeError(`Invalid URL protocol: ${parsedUrl.protocol}`);\n  }\n\n  let path = decodeURIComponent(parsedUrl.pathname);\n\n  if (Files.isWin) {\n    // For Windows: replace forward slashes with backslashes and remove the leading slash for local paths\n    path = path.replace(/\\//g, \"\\\\\");\n\n    // Handle local paths like \"C:\\path\"\n    if (path.startsWith(\"\\\\\") && !parsedUrl.host) {\n      path = path.slice(1);\n    }\n\n    // Handle network paths like \"\\\\nas\\foo.txt\"\n    if (parsedUrl.host) {\n      path = `\\\\\\\\${parsedUrl.host}${path}`;\n    }\n  } else {\n    // For POSIX: Ensure the leading slash is present\n    if (!path.startsWith(\"/\")) {\n      path = `/${path}`;\n    }\n  }\n\n  return path;\n};\n\n/**\n * This function ensures that `path` is resolved absolutely, and that the URL\n * control characters are correctly encoded when converting into a File URL.\n *\n * ```javascript\n * import { pathToFileURL } from \"@modules/url\";\n *\n * new URL(\"/foo#1\", \"file:\");           // Incorrect: file:///foo#1\n * pathToFileURL(\"/foo#1\");              // Correct:   file:///foo%231 (POSIX)\n *\n * new URL(\"/some/path%.c\", \"file:\");    // Incorrect: file:///some/path%.c\n * pathToFileURL(\"/some/path%.c\");       // Correct:   file:///some/path%25.c (POSIX)\n * ```\n * @param path The path to convert to a File URL.\n * @returns The file URL object.\n */\nexport const pathToFileURL = (path: string) => {\n  const url = new URL(\"file:///\");\n  url.pathname = encodeURI(path.replace(/\\\\/g, \"/\"));\n  return url;\n};\n"
  },
  {
    "path": "src/patches/index.ts",
    "content": "// Make sure patches to Typora are imported before other patches\nimport \"./typora\";\n\nimport \"./promise\";\n\nimport \"./jquery\";\n"
  },
  {
    "path": "src/patches/jquery.ts",
    "content": "/************************\n * Custom jQuery events *\n ************************/\n$(function () {\n  /*************\n   * caretMove *\n   *************/\n  (() => {\n    /**\n     * Get the current caret position.\n     * @returns\n     */\n    const getCaretPosition = (): { node: Node; offset: number } | null => {\n      const selection = window.getSelection();\n      if (selection === null) return null;\n      if (selection.rangeCount) {\n        const range = selection.getRangeAt(0);\n        return {\n          node: range.startContainer,\n          offset: range.startOffset,\n        };\n      }\n      return null;\n    };\n\n    const eventsToBind = [\n      \"keypress\",\n      \"keyup\",\n      \"keydown\",\n      \"mouseup\",\n      \"mousedown\",\n      \"mousemove\",\n      \"touchend\",\n      \"touchstart\",\n      \"touchmove\",\n      \"focus\",\n      \"blur\",\n      \"input\",\n      \"paste\",\n      \"cut\",\n      \"copy\",\n      \"select\",\n      \"selectstart\",\n      \"selectionchange\",\n      \"drag\",\n      \"dragend\",\n      \"dragenter\",\n      \"dragexit\",\n      \"dragleave\",\n      \"dragover\",\n      \"dragstart\",\n      \"drop\",\n      \"scroll\",\n      \"wheel\",\n      \"animationstart\",\n      \"animationend\",\n      \"animationiteration\",\n      \"transitionstart\",\n      \"transitionend\",\n      \"transitionrun\",\n      \"transitioncancel\",\n    ];\n\n    $.event.special.caretMove = {\n      setup() {\n        let lastCaretPosition = getCaretPosition();\n        const onCaretMove = (event: Event) => {\n          const selection = window.getSelection();\n\n          if (selection === null) {\n            if (lastCaretPosition !== null) {\n              lastCaretPosition = null;\n              if (event.target) $(event.target).trigger(\"caretMove\", [null]);\n              return;\n            }\n            return;\n          }\n\n          if (selection.rangeCount) {\n            const range = selection.getRangeAt(0);\n            const caretPosition = {\n              node: range.startContainer,\n              offset: range.startOffset,\n            };\n\n            if (\n              !lastCaretPosition ||\n              !lastCaretPosition.node.isSameNode(caretPosition.node) ||\n              caretPosition.offset !== lastCaretPosition.offset\n            ) {\n              lastCaretPosition = caretPosition;\n              if (event.target) $(event.target).trigger(\"caretMove\", [caretPosition]);\n            }\n          }\n        };\n\n        $.data(this, \"caretMoveHandler\", onCaretMove);\n\n        for (const event of eventsToBind) this.addEventListener(event, onCaretMove, true);\n\n        return false;\n      },\n      teardown() {\n        const onCaretMove = $.data(this, \"caretMoveHandler\");\n        if (!onCaretMove) return false;\n        for (const event of eventsToBind) this.removeEventListener(event, onCaretMove, true);\n        $.removeData(this, \"caretMoveHandler\");\n      },\n    };\n  })();\n});\n\nexport {};\n"
  },
  {
    "path": "src/patches/promise.spec.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport \"./promise\";\n\n// Utility to simulate delays\nconst delay = (ms: number, value?: unknown, shouldReject = false) =>\n  new Promise((resolve, reject) =>\n    // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors\n    setTimeout(() => (shouldReject ? reject(value) : resolve(value)), ms),\n  );\n\n// Main tests\ndescribe(\"Promise.orderedFirstResolved\", () => {\n  it(\"should return the first resolved promise in order\", async () => {\n    const promises = [delay(100, \"A\"), delay(200, \"B\"), delay(300, \"C\")];\n    const result = await Promise.orderedFirstResolved(promises);\n    expect(result).toBe(\"A\");\n  });\n\n  it(\"should return the second resolved promise if the first is rejected\", async () => {\n    const promises = [delay(100, \"Error A\", true), delay(200, \"B\"), delay(300, \"C\")];\n    const result = await Promise.orderedFirstResolved(promises);\n    expect(result).toBe(\"B\");\n  });\n\n  it(\"should reject if all promises are rejected\", async () => {\n    const promises = [\n      delay(100, \"Error A\", true),\n      delay(200, \"Error B\", true),\n      delay(300, \"Error C\", true),\n    ];\n    await expect(Promise.orderedFirstResolved(promises)).rejects.toThrow(\n      \"All promises were rejected\",\n    );\n  });\n\n  it(\"should reject if the iterable is empty\", async () => {\n    const promises: Promise<string>[] = [];\n    await expect(Promise.orderedFirstResolved(promises)).rejects.toThrow(\n      \"All promises were rejected\",\n    );\n  });\n\n  it(\"should return the first resolved value in order even if others resolve first\", async () => {\n    const promises = [delay(300, \"A\"), delay(100, \"B\"), delay(200, \"C\")];\n    const result = await Promise.orderedFirstResolved(promises);\n    expect(result).toBe(\"A\");\n  });\n\n  it(\"should handle mixed resolutions and rejections correctly\", async () => {\n    const promises = [\n      delay(300, \"A\"), // First resolves after 300ms\n      delay(100, \"Error B\", true), // Second rejects first\n      delay(200, \"C\"), // Third resolves but is irrelevant\n    ];\n    const result = await Promise.orderedFirstResolved(promises);\n    expect(result).toBe(\"A\");\n  });\n});\n"
  },
  {
    "path": "src/patches/promise.ts",
    "content": "declare global {\n  interface PromiseConstructor {\n    /**\n     * Get the first resolved promise in order.\n     *\n     * Suppose we have `[<pending>, <pending>, <pending>]`, when it turns to\n     * `[<resolved>, <pending>, <pending>]`, we can directly return the resolved value since it's\n     * the first one. However, when it turns to `[<pending>, <resolved>, <pending>]`, we have to\n     * wait for the first one to settle, if it finally turns to\n     * `[<rejected>, <resolved>, <pending>]`, we can directly return the resolved value (the 2nd),\n     * or if it turns to `[<resolved>, <resolved>, <pending>]`, we should return the first one.\n     * @param values An array or iterable of Promises.\n     * @returns A new Promise.\n     */\n    orderedFirstResolved<T extends readonly unknown[] | []>(values: T): Promise<Awaited<T[number]>>;\n    /**\n     * Get the first resolved promise in order.\n     *\n     * Suppose we have `[<pending>, <pending>, <pending>]`, when it turns to\n     * `[<resolved>, <pending>, <pending>]`, we can directly return the resolved value since it's\n     * the first one. However, when it turns to `[<pending>, <resolved>, <pending>]`, we have to\n     * wait for the first one to settle, if it finally turns to\n     * `[<rejected>, <resolved>, <pending>]`, we can directly return the resolved value (the 2nd),\n     * or if it turns to `[<resolved>, <resolved>, <pending>]`, we should return the first one.\n     * @param values An array or iterable of Promises.\n     * @returns A new Promise.\n     */\n    orderedFirstResolved<T>(values: Iterable<T | PromiseLike<T>>): Promise<Awaited<T>>;\n  }\n}\n\nPromise.orderedFirstResolved = function orderedFirstResolved<T>(\n  values: Iterable<T | PromiseLike<T>>,\n): Promise<T> {\n  const states: (\"pending\" | \"resolved\" | \"rejected\")[] = [];\n  const resolvedValues: T[] = [];\n  const errors: unknown[] = [];\n  let rejectedPointer = 0;\n\n  return new Promise((resolve, reject) => {\n    const tryResolve = () => {\n      while (states[rejectedPointer] === \"rejected\")\n        if (++rejectedPointer === states.length) {\n          const message = \"All promises were rejected\";\n          reject(\n            \"AggregateError\" in window ?\n              (new (window.AggregateError as any)(errors, message) as Error)\n            : new Error(message),\n          );\n          return;\n        }\n      if (states[rejectedPointer] === \"resolved\") resolve(resolvedValues[rejectedPointer]!);\n    };\n\n    let i = 0;\n    for (const value of values) {\n      const index = i++;\n      states.push(\"pending\");\n\n      Promise.resolve(value).then(\n        (resolvedValue) => {\n          states[index] = \"resolved\";\n          resolvedValues[index] = resolvedValue;\n          tryResolve();\n        },\n        () => {\n          states[index] = \"rejected\";\n          errors[index] = new Error(\"Promise rejected\");\n          tryResolve();\n        },\n      );\n    }\n\n    if (i === 0) {\n      const message = \"All promises were rejected\";\n      reject(\n        \"AggregateError\" in window ?\n          (new (window.AggregateError as any)([], message) as Error)\n        : new Error(message),\n      );\n    }\n  });\n};\n\ndeclare global {\n  interface PromiseConstructor {\n    /**\n     * Defer an operation to the next tick. Using `process.nextTick` if available, otherwise\n     * `Promise.resolve().then`.\n     * @param factory A callback used to initialize the promise.\n     * @returns A new Promise.\n     */\n    defer<T>(factory: () => T): Promise<Awaited<T>>;\n    /**\n     * Defer an operation to the next tick. Using `process.nextTick` if available, otherwise\n     * `Promise.resolve().then`.\n     * @param factory A callback used to initialize the promise.\n     * @returns A new Promise.\n     */\n    defer<T>(factory: () => T | PromiseLike<T>): Promise<Awaited<T>>;\n  }\n}\n\nPromise.defer = function defer<T>(factory: () => T | PromiseLike<T>): Promise<T> {\n  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n  if (globalThis.process && typeof process.nextTick === \"function\") {\n    const isThenable = (value: unknown): value is PromiseLike<T> =>\n      value !== null &&\n      (typeof value === \"object\" || typeof value === \"function\") &&\n      typeof (value as PromiseLike<T>).then === \"function\";\n\n    return new Promise((resolve, reject) =>\n      process.nextTick(() => {\n        const result = factory();\n        if (isThenable(result)) result.then(resolve, reject);\n        else resolve(result);\n      }),\n    );\n  }\n\n  return Promise.resolve().then(factory);\n};\n\nexport {};\n"
  },
  {
    "path": "src/patches/typora.ts",
    "content": "//////////////////////////////////////////////////\n/// Global patches for Typora to make TS happy ///\n//////////////////////////////////////////////////\n\n// The same function as `@/utils/tools`\n// Defined here instead of importing to ensure the following code executes\n// before any other code.\nconst setGlobalVar = <K extends keyof typeof globalThis | (string & NonNullable<unknown>)>(\n  name: K,\n  value: K extends keyof typeof globalThis ? (typeof globalThis)[K] : unknown,\n) => {\n  try {\n    Object.defineProperty(global, name, { value });\n  } catch {\n    Object.defineProperty(globalThis, name, { value });\n  }\n};\n\n// Typora extends `window.File` with additional properties (e.g., `File.editor`).\n// In a pure JS application, we could directly use `window.File`.\n// However, in TS, these additional properties are not recognized by the type system.\n// TS doesn’t allow direct modifications to the type of a global variable.\n// As a workaround, we create an alias for the global variable and declare types for the alias.\n// This is what we’re doing here.\nsetGlobalVar(\"Files\", File as typeof Files);\n\n// Similarly, create an alias for `window.Node`.\nsetGlobalVar(\"Nodes\", Node as typeof Nodes);\n"
  },
  {
    "path": "src/reset.d.ts",
    "content": "import \"@total-typescript/ts-reset\";\n"
  },
  {
    "path": "src/settings.ts",
    "content": "import { mapValues } from \"radash\";\nimport { kebabCase } from \"string-ts\";\n\nexport type Settings = typeof defaultSettings;\nconst defaultSettings = {\n  /* General */\n  disableCompletions: false,\n  useInlineCompletionTextInSource: true,\n  useInlineCompletionTextInPreviewCodeBlocks: false,\n\n  /* Node.js runtime */\n  nodePath: null as string | null,\n};\n\nexport const settings = (() => {\n  const changeListeners = new Map<keyof Settings, (() => void)[]>();\n  const onChange = <K extends keyof Settings>(key: K, callback: (value: Settings[K]) => void) => {\n    const listener = () => callback(settings[key]);\n    if (!changeListeners.has(key)) changeListeners.set(key, []);\n    changeListeners.get(key)?.push(listener);\n    return () => {\n      const listeners = changeListeners.get(key);\n      if (!listeners) return;\n      const index = listeners.indexOf(listener);\n      if (index !== -1) listeners.splice(index, 1);\n    };\n  };\n\n  const clear = (key: keyof Settings) => {\n    if (localStorage.getItem(kebabCase(key)) === null) return;\n    const changed = JSON.stringify(settings[key]) !== JSON.stringify(defaultSettings[key]);\n    localStorage.removeItem(kebabCase(key));\n    if (changed) changeListeners.get(key)?.forEach((listener) => listener());\n  };\n\n  const localStorageKeys = mapValues(defaultSettings, (_, key) => kebabCase(key));\n  return new Proxy(\n    {} as Settings & { readonly onChange: typeof onChange; readonly clear: typeof clear },\n    {\n      get(_target, prop, _receiver) {\n        if (prop === \"onChange\") return onChange;\n        if (prop === \"clear\") return clear;\n        if (!(prop in defaultSettings)) throw new Error(`Unknown setting: ${String(prop)}`);\n        const unparsedValue = localStorage.getItem(localStorageKeys[prop as keyof Settings]);\n        if (unparsedValue === null) return defaultSettings[prop as keyof Settings];\n        return JSON.parse(unparsedValue);\n      },\n      set(_target, prop, value, _receiver) {\n        if (prop === \"onChange\") return false;\n        if (prop === \"clear\") return false;\n        if (!(prop in defaultSettings)) return false;\n        const jsonifiedValue = JSON.stringify(value);\n        if (\n          jsonifiedValue ===\n          (localStorage.getItem(localStorageKeys[prop as keyof Settings]) ??\n            JSON.stringify(defaultSettings[prop as keyof Settings]))\n        )\n          // No change\n          return true;\n        localStorage.setItem(localStorageKeys[prop as keyof Settings], jsonifiedValue);\n        changeListeners.get(prop as keyof Settings)?.forEach((listener) => listener());\n        return true;\n      },\n    },\n  );\n})();\n"
  },
  {
    "path": "src/styles.scss",
    "content": ".text-gray {\n  color: gray !important;\n}\n\n.font-italic {\n  font-style: italic !important;\n}\n\n.unset-button {\n  all: unset;\n  display: inline-block;\n  cursor: pointer;\n  text-align: center;\n  font: inherit;\n  color: inherit;\n  background: none;\n  border: none;\n  padding: 0;\n  margin: 0;\n  box-sizing: border-box;\n}\n"
  },
  {
    "path": "src/types/lsp.ts",
    "content": "/* eslint-disable @typescript-eslint/no-deprecated */\n/* eslint-disable @typescript-eslint/no-duplicate-type-constituents */\n/* eslint-disable sonarjs/future-reserved-words */\n/* eslint-disable sonarjs/no-globals-shadowing */\n\n/**\n * Defines an integer number in the range of -2^31 to 2^31 - 1.\n */\nexport type integer = number;\n\n/**\n * Defines an unsigned integer number in the range of 0 to 2^31 - 1.\n */\nexport type uinteger = number;\n\n/**\n * Defines a decimal number. Since decimal numbers are very rare in the language server\n * specification we denote the exact range with every decimal using the mathematics interval\n * notation (e.g. [0, 1] denotes all decimals d with 0 <= d <= 1.\n */\nexport type decimal = number;\n\n/**\n * The LSP any type.\n */\nexport type LSPAny = LSPObject | LSPArray | string | integer | uinteger | decimal | boolean | null;\n\n/**\n * LSP object definition.\n */\nexport type LSPObject = {\n  readonly [K: string]:\n    | LSPObject\n    | LSPArray\n    | string\n    | integer\n    | uinteger\n    | decimal\n    | boolean\n    | null;\n};\n\n/**\n * LSP arrays.\n */\nexport type LSPArray = readonly LSPAny[];\n\n/*****************\n * Base Protocol *\n *****************/\n/**\n * The JSON-RPC version.\n */\nexport type JSONRPCVersion = typeof JSONRPC_VERSION;\n/**\n * The JSON-RPC version.\n */\nexport const JSONRPC_VERSION = \"2.0\";\n\n/**\n * A general message as defined by JSON-RPC. The language server protocol always uses `\"2.0\"` as the\n * `jsonrpc` version.\n */\nexport type Message = {\n  /**\n   * The JSON-RPC version. Must be exactly `\"2.0\"`.\n   */\n  /* `extends infer U ? U : never` is a technique used here to tell TS to evaluate the type\n      immediately, making type information more readable on hover. */\n  jsonrpc: JSONRPCVersion extends infer U ? U : never;\n};\n\n/**\n * A request message to describe a request between the client and the server. Every processed\n * request must send a response back to the sender of the request.\n */\nexport interface RequestMessage extends Message {\n  /**\n   * The request id.\n   */\n  id: integer | string;\n\n  /**\n   * The method to be invoked.\n   */\n  method: string;\n\n  /**\n   * The method's params.\n   */\n  params?: LSPArray | LSPObject;\n}\n\n/**\n * A Response Message sent as a result of a request. If a request doesn’t provide a result value the\n * receiver of a request still needs to return a response message to conform to the JSON-RPC\n * specification. The result property of the ResponseMessage should be set to `null` in this case to\n * signal a successful request.\n */\nexport type ResponseMessage = SuccessResponseMessage | ErrorResponseMessage;\n\n/**\n * A response message indicating a successful request.\n */\nexport interface SuccessResponseMessage extends Message {\n  /**\n   * The request id.\n   */\n  id: integer | string | null;\n\n  /**\n   * The result of a request. This member is REQUIRED on success.\n   * This member MUST NOT exist if there was an error invoking the method.\n   */\n  result: LSPAny;\n}\n/**\n * A response message indicating a failed request.\n */\nexport interface ErrorResponseMessage extends Message {\n  /**\n   * The request id.\n   */\n  id: integer | string | null;\n\n  /**\n   * The error object in case a request fails.\n   */\n  error: ResponseError;\n}\n\n/**\n * An error object returned as a response to a request.\n */\nexport type ResponseError = {\n  /**\n   * A number indicating the error type that occurred.\n   */\n  code: number;\n\n  /**\n   * A string providing a short description of the error.\n   */\n  message: string;\n\n  /**\n   * A primitive or structured value that contains additional information about the error. Can be\n   * omitted.\n   */\n  data?: LSPAny;\n};\n\nexport namespace ErrorCodes {\n  // Defined by JSON-RPC\n  export const ParseError = -32700;\n  export const InvalidRequest = -32600;\n  export const MethodNotFound = -32601;\n  export const InvalidParams = -32602;\n  export const InternalError = -32603;\n\n  /**\n   * This is the start range of JSON-RPC reserved error codes. It doesn't denote a real error code.\n   * No LSP error codes should be defined between the start and end range. For backwards\n   * compatibility the `ServerNotInitialized` and the `UnknownErrorCode` are left in the range.\n   *\n   * @since 3.16.0\n   */\n  export const jsonrpcReservedErrorRangeStart = -32099;\n  /** @deprecated use jsonrpcReservedErrorRangeStart */\n  export const serverErrorStart = jsonrpcReservedErrorRangeStart;\n\n  /**\n   * Error code indicating that a server received a notification or request before the server has\n   * received the `initialize` request.\n   */\n  export const ServerNotInitialized = -32002;\n  export const UnknownErrorCode = -32001;\n\n  /**\n   * This is the end range of JSON-RPC reserved error codes.\n   * It doesn't denote a real error code.\n   *\n   * @since 3.16.0\n   */\n  export const jsonrpcReservedErrorRangeEnd = -32000;\n  /** @deprecated use jsonrpcReservedErrorRangeEnd */\n  export const serverErrorEnd = jsonrpcReservedErrorRangeEnd;\n\n  /**\n   * This is the start range of LSP reserved error codes.\n   * It doesn't denote a real error code.\n   *\n   * @since 3.16.0\n   */\n  export const lspReservedErrorRangeStart = -32899;\n\n  /**\n   * A request failed but it was syntactically correct, e.g the method name was known and the\n   * parameters were valid. The error message should contain human readable information about why\n   * the request failed.\n   *\n   * @since 3.17.0\n   */\n  export const RequestFailed = -32803;\n\n  /**\n   * The server cancelled the request. This error code should only be used for requests that\n   * explicitly support being server cancellable.\n   *\n   * @since 3.17.0\n   */\n  export const ServerCancelled = -32802;\n\n  /**\n   * The server detected that the content of a document got modified outside normal conditions.\n   * A server should NOT send this error code if it detects a content change in it unprocessed\n   * messages. The result even computed on an older state might still be useful for the client.\n   *\n   * If a client decides that a result is not of any use anymore the client should cancel the\n   * request.\n   */\n  export const ContentModified = -32801;\n\n  /**\n   * The client has canceled a request and a server as detected\n   * the cancel.\n   */\n  export const RequestCancelled = -32800;\n\n  /**\n   * This is the end range of LSP reserved error codes.\n   * It doesn't denote a real error code.\n   *\n   * @since 3.16.0\n   */\n  export const lspReservedErrorRangeEnd = -32800;\n}\n\n/**\n * A notification message. A processed notification message must not send a response back.\n * They work like events.\n */\nexport interface NotificationMessage extends Message {\n  /**\n   * The method to be invoked.\n   */\n  method: string;\n\n  /**\n   * The notification's params.\n   */\n  params?: LSPArray | LSPObject;\n}\n\n/**\n * Params to be sent with a `$/cancelRequest` notification.\n */\nexport type CancelParams = {\n  /**\n   * The request id to cancel.\n   */\n  id: integer | string;\n};\n\n/**\n * A token that represents a work in progress.\n */\nexport type ProgressToken = integer | string;\n\n/**\n * Params to be sent with a `$/progress` notification.\n */\nexport type ProgressParams<T extends LSPAny = LSPAny> = {\n  /**\n   * The progress token provided by the client or server.\n   */\n  token: ProgressToken;\n\n  /**\n   * The progress data.\n   */\n  value: T;\n};\n\n/*************************\n * Basic JSON Structures *\n *************************/\n/**\n * URI’s are transferred as strings.\n * The URI’s format is defined in https://tools.ietf.org/html/rfc3986.\n *\n * ```text\n *   foo://example.com:8042/over/there?name=ferret#nose\n *   \\_/   \\______________/\\_________/ \\_________/ \\__/\n *    |           |            |            |        |\n * scheme     authority       path        query   fragment\n *    |   _____________________|__\n *   / \\ /                        \\\n *   urn:example:animal:ferret:nose\n * ```\n *\n * We also maintain a node module to parse a string into `scheme`, `authority`, `path`, `query`, and\n * `fragment` URI components. The GitHub repository is https://github.com/Microsoft/vscode-uri the\n * npm module is https://www.npmjs.com/package/vscode-uri.\n *\n * Many of the interfaces contain fields that correspond to the URI of a document. For clarity, the\n * type of such a field is declared as a `DocumentUri`. Over the wire, it will still be transferred\n * as a string, but this guarantees that the contents of that string can be parsed as a valid URI.\n *\n * Care should be taken to handle encoding in URIs. For example, some clients (such as VS Code) may\n * encode colons in drive letters while others do not. The URIs below are both valid, but clients\n * and servers should be consistent with the form they use themselves to ensure the other party\n * doesn’t interpret them as distinct URIs. Clients and servers should not assume that each other\n * are encoding the same way (for example a client encoding colons in drive letters cannot assume\n * server responses will have encoded colons). The same applies to casing of drive letters - one\n * party should not assume the other party will return paths with drive letters cased the same as\n * it.\n *\n * ```text\n * file:///c:/project/readme.md\n * file:///C%3A/project/readme.md\n * ```\n */\nexport type DocumentUri = string;\n\n/**\n * A tagging interface for normal non document URIs. It maps to a string as well like\n * {@link DocumentUri}.\n */\nexport type URI = string;\n\n/**\n * Client capabilities specific to regular expressions.\n *\n * Regular expression are a powerful tool and there are actual use cases for them in the language\n * server protocol. However the downside with them is that almost every programming language has its\n * own set of regular expression features so the specification can not simply refer to them as a\n * regular expression. So the LSP uses a two step approach to support regular expressions:\n *\n * - The client will announce which regular expression engine it will use. This will allow server\n * that are written for a very specific client make full use of the regular expression capabilities\n * of the client.\n * - The specification will define a set of regular expression features that should be supported by\n * a client. Instead of writing a new specification LSP will refer to the ECMAScript Regular\n * Expression specification and remove features from it that are not necessary in the context of LSP\n * or hard to implement for other clients.\n *\n * _Client Capability_:\n *\n * The following client capability is used to announce a client’s regular expression engine:\n *\n * - property path (optional): `general.regularExpressions`\n * - property type: {@link RegularExpressionsClientCapabilities} defined as follows.\n *\n * The following table lists the well known engine values. Please note that the table should be\n * driven by the community which integrates LSP into existing clients. It is not the goal of the\n * spec to list all available regular expression engines.\n *\n * | Engine     | Version  | Documentation |\n * | ---------- | -------- | ------------- |\n * | ECMAScript | `ES2020` | [ECMAScript 2020](https://tc39.es/ecma262/2020/) & [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) |\n *\n * _Regular Expression Subset_:\n *\n * The following features from the [ECMAScript 2020](https://tc39.es/ecma262/2020/) regular\n * expression specification are NOT mandatory for a client:\n *\n * - _Assertions_: Lookahead assertion, Negative lookahead assertion, lookbehind assertion, negative\n * lookbehind assertion.\n * - _Character classes_: matching control characters using caret notation (e.g. `\\cX`) and matching\n * UTF-16 code units (e.g. `\\uhhhh`).\n * - _Group and ranges_: named capturing groups.\n * - _Unicode property escapes_: none of the features needs to be supported.\n *\n * The only regular expression flag that a client needs to support is ‘i’ to specify a case\n * insensitive search.\n */\nexport type RegularExpressionsClientCapabilities = {\n  /**\n   * The engine's name.\n   */\n  engine: string;\n\n  /**\n   * The engine's version.\n   */\n  version?: string;\n};\n\n/**\n * To ensure that both client and server split the string into the same line representation the\n * protocol specifies the following end-of-line sequences: ‘\\n’, ‘\\r\\n’ and ‘\\r’. Positions are line\n * end character agnostic. So you can not specify a position that denotes `\\r|\\n` or `\\n|` where `|`\n * represents the character offset.\n */\nexport type EOL = (typeof EOL)[number];\n/**\n * To ensure that both client and server split the string into the same line representation the\n * protocol specifies the following end-of-line sequences: ‘\\n’, ‘\\r\\n’ and ‘\\r’. Positions are line\n * end character agnostic. So you can not specify a position that denotes `\\r|\\n` or `\\n|` where `|`\n * represents the character offset.\n */\nexport const EOL = [\"\\n\", \"\\r\\n\", \"\\r\"] as const;\n\n/**\n * Position in a text document expressed as zero-based line and zero-based character offset. A\n * position is between two characters like an ‘insert’ cursor in an editor. Special values like for\n * example `-1` to denote the end of a line are not supported.\n */\nexport type Position = {\n  /**\n   * Line position in a document (zero-based).\n   */\n  line: uinteger;\n\n  /**\n   * Character offset on a line in a document (zero-based). The meaning of this\n   * offset is determined by the negotiated `PositionEncodingKind`.\n   *\n   * If the character value is greater than the line length it defaults back\n   * to the line length.\n   */\n  character: uinteger;\n};\n\n/**\n * A type indicating how positions are encoded, specifically what column offsets mean.\n *\n * @since 3.17.0\n */\nexport type PositionEncodingKind = (typeof PositionEncodingKind)[keyof typeof PositionEncodingKind];\n\n/**\n * A set of predefined position encoding kinds.\n *\n * @since 3.17.0\n */\n\nexport namespace PositionEncodingKind {\n  /**\n   * Character offsets count UTF-8 code units (e.g bytes).\n   */\n  export const UTF8 = \"utf-8\";\n\n  /**\n   * Character offsets count UTF-16 code units.\n   *\n   * This is the default and must always be supported\n   * by servers\n   */\n  export const UTF16 = \"utf-16\";\n\n  /**\n   * Character offsets count UTF-32 code units.\n   *\n   * Implementation note: these are the same as Unicode code points,\n   * so this `PositionEncodingKind` may also be used for an\n   * encoding-agnostic representation of character offsets.\n   */\n  export const UTF32 = \"utf-32\";\n}\n\n/**\n * A range in a text document expressed as (zero-based) start and end positions. A range is\n * comparable to a selection in an editor. Therefore, the end position is exclusive. If you want to\n * specify a range that contains a line including the line ending character(s) then use an end\n * position denoting the start of the next line. For example:\n *\n * @example\n * ```javascript\n * {\n *   start: { line: 5, character: 23 },\n *   end : { line: 6, character: 0 }\n * }\n * ```\n */\nexport type Range = {\n  /**\n   * The range's start position.\n   */\n  start: Position;\n\n  /**\n   * The range's end position.\n   */\n  end: Position;\n};\n\n/**\n * Recommended language identifiers to use with the language client.\n */\nexport type LanguageIdentifier = (typeof LanguageIdentifiers)[number];\n/**\n * Recommended language identifiers to use with the language client.\n */\nexport const LanguageIdentifiers = [\n  \"abap\",\n  \"bat\",\n  \"bibtex\",\n  \"clojure\",\n  \"coffeescript\",\n  \"c\",\n  \"cpp\",\n  \"csharp\",\n  \"css\",\n  \"diff\",\n  \"dart\",\n  \"dockerfile\",\n  \"elixir\",\n  \"erlang\",\n  \"fsharp\",\n  \"git-commit\",\n  \"git-rebase\",\n  \"go\",\n  \"groovy\",\n  \"handlebars\",\n  \"html\",\n  \"ini\",\n  \"java\",\n  \"javascript\",\n  \"javascriptreact\",\n  \"json\",\n  \"latex\",\n  \"less\",\n  \"lua\",\n  \"makefile\",\n  \"markdown\",\n  \"objective-c\",\n  \"objective-cpp\",\n  \"perl\",\n  \"perl6\",\n  \"php\",\n  \"powershell\",\n  \"jade\",\n  \"python\",\n  \"r\",\n  \"razor\",\n  \"ruby\",\n  \"rust\",\n  \"scss\",\n  \"sass\",\n  \"scala\",\n  \"shaderlab\",\n  \"shellscript\",\n  \"sql\",\n  \"swift\",\n  \"typescript\",\n  \"typescriptreact\",\n  \"tex\",\n  \"vb\",\n  \"xml\",\n  \"xsl\",\n  \"yaml\",\n] as const;\n/**\n * An item to transfer a text document from the client to the server.\n *\n * Text documents have a language identifier to identify a document on the server side when it\n * handles more than one language to avoid re-interpreting the file extension. If a document refers\n * to one of the programming languages listed below it is recommended that clients use those ids.\n *\n * | Language | Identifier |\n * | -------- | ---------- |\n * | ABAP | `abap` |\n * | Windows Bat | `bat` |\n * | BibTeX | `bibtex` |\n * | Clojure | `clojure` |\n * | Coffeescript | `coffeescript` |\n * | C | `c` |\n * | C++ | `cpp` |\n * | C# | `csharp` |\n * | CSS | `css` |\n * | Diff | `diff` |\n * | Dart | `dart` |\n * | Dockerfile | `dockerfile` |\n * | Elixir | `elixir` |\n * | Erlang | `erlang` |\n * | F# | `fsharp` |\n * | Git | `git-commit` and `git-rebase` |\n * | Go | `go` |\n * | Groovy | `groovy` |\n * | Handlebars | `handlebars` |\n * | HTML | `html` |\n * | Ini | `ini` |\n * | Java | `java` |\n * | JavaScript | `javascript` |\n * | JavaScript React | `javascriptreact` |\n * | JSON | `json` |\n * | LaTeX | `latex` |\n * | Less | `less` |\n * | Lua | `lua` |\n * | Makefile | `makefile` |\n * | Markdown | `markdown` |\n * | Objective-C | `objective-c` |\n * | Objective-C++ | `objective-cpp` |\n * | Perl | `perl` |\n * | Perl 6 | `perl6` |\n * | PHP | `php` |\n * | Powershell | `powershell` |\n * | Pug | `jade` |\n * | Python | `python` |\n * | R | `r` |\n * | Razor (cshtml) | `razor` |\n * | Ruby | `ruby` |\n * | Rust | `rust` |\n * | SCSS | `scss` (syntax using curly brackets), `sass` (indented syntax) |\n * | Scala | `scala` |\n * | ShaderLab | `shaderlab` |\n * | Shell Script (Bash) | `shellscript` |\n * | SQL | `sql` |\n * | Swift | `swift` |\n * | TypeScript | `typescript` |\n * | TypeScript React | `typescriptreact` |\n * | TeX | `tex` |\n * | Visual Basic | `vb` |\n * | XML | `xml` |\n * | XSL | `xsl` |\n * | YAML | `yaml` |\n */\nexport type TextDocumentItem = {\n  /**\n   * The text document's URI.\n   */\n  uri: DocumentUri;\n\n  /**\n   * The text document's language identifier.\n   */\n  languageId: LanguageIdentifier | (string & NonNullable<unknown>);\n\n  /**\n   * The version number of this document (it will increase after each\n   * change, including undo/redo).\n   */\n  version: integer;\n\n  /**\n   * The content of the opened text document.\n   */\n  text: string;\n};\n\n/**\n * Text documents are identified using a URI. On the protocol level, URIs are passed as strings.\n */\nexport type TextDocumentIdentifier = {\n  /**\n   * The text document's URI.\n   */\n  uri: DocumentUri;\n};\n\n/**\n * An identifier to denote a specific version of a text document. This information usually flows\n * from the client to the server.\n */\nexport interface VersionedTextDocumentIdentifier extends TextDocumentIdentifier {\n  /**\n   * The version number of this document.\n   *\n   * The version number of a document will increase after each change,\n   * including undo/redo. The number doesn't need to be consecutive.\n   */\n  version: integer;\n}\n\n/**\n * An identifier which optionally denotes a specific version of a text document. This information\n * usually flows from the server to the client.\n */\nexport interface OptionalVersionedTextDocumentIdentifier extends TextDocumentIdentifier {\n  /**\n   * The version number of this document. If an optional versioned text document\n   * identifier is sent from the server to the client and the file is not\n   * open in the editor (the server has not received an open notification\n   * before) the server can send `null` to indicate that the version is\n   * known and the content on disk is the master (as specified with document\n   * content ownership).\n   *\n   * The version number of a document will increase after each change,\n   * including undo/redo. The number doesn't need to be consecutive.\n   */\n  version: integer | null;\n}\n\n/**\n * Was `TextDocumentPosition` in 1.0 with inlined parameters.\n *\n * A parameter literal used in requests to pass a text document and a position inside that document.\n * It is up to the client to decide how a selection is converted into a position when issuing a\n * request for a text document. The client can for example honor or ignore the selection direction\n * to make LSP request consistent with features implemented internally.\n */\nexport type TextDocumentPositionParams = {\n  /**\n   * The text document.\n   */\n  textDocument: TextDocumentIdentifier;\n\n  /**\n   * The position inside the text document.\n   */\n  position: Position;\n};\n\n/* eslint-disable no-irregular-whitespace */\n/**\n * A document filter denotes a document through properties like `language`, `scheme` or `pattern`.\n * An example is a filter that applies to TypeScript files on disk. Another example is a filter that\n * applies to JSON files with name `package.json`:\n *\n * ```javascript\n * { language: 'typescript', scheme: 'file' }\n * { language: 'json', pattern: '**​/package.json' }\n * ```\n *\n * Please note that for a document filter to be valid at least one of the properties for `language`,\n * `scheme`, or `pattern` must be set. To keep the type definition simple all properties are marked\n * as optional.\n */\n/* eslint-enable no-irregular-whitespace */\nexport type DocumentFilter = {\n  /**\n   * A language id, like `typescript`.\n   */\n  language?: string;\n\n  /**\n   * A Uri [scheme](#Uri.scheme), like `file` or `untitled`.\n   */\n  scheme?: string;\n\n  /* eslint-disable no-irregular-whitespace */\n  /**\n   * A glob pattern, like `*.{ts,js}`.\n   *\n   * Glob patterns can have the following syntax:\n   * - `*` to match one or more characters in a path segment\n   * - `?` to match on one character in a path segment\n   * - `**` to match any number of path segments, including none\n   * - `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}`\n   *   matches all TypeScript and JavaScript files)\n   * - `[]` to declare a range of characters to match in a path segment\n   *   (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)\n   * - `[!...]` to negate a range of characters to match in a path segment\n   *   (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but\n   *   not `example.0`)\n   */\n  /* eslint-enable no-irregular-whitespace */\n  pattern?: string;\n};\n\n/**\n * A document selector is the combination of one or more document filters.\n */\nexport type DocumentSelector = readonly DocumentFilter[];\n\n/**\n * A textual edit applicable to a text document.\n */\nexport type TextEdit = {\n  /**\n   * The range of the text document to be manipulated. To insert\n   * text into a document create a range where start === end.\n   */\n  range: Range;\n\n  /**\n   * The string to be inserted. For delete operations use an\n   * empty string.\n   */\n  newText: string;\n};\n\n/**\n * Additional information that describes document changes.\n *\n * @since 3.16.0\n */\nexport type ChangeAnnotation = {\n  /**\n   * A human-readable string describing the actual change. The string\n   * is rendered prominent in the user interface.\n   */\n  label: string;\n\n  /**\n   * A flag which indicates that user confirmation is needed\n   * before applying the change.\n   */\n  needsConfirmation?: boolean;\n\n  /**\n   * A human-readable string which is rendered less prominent in\n   * the user interface.\n   */\n  description?: string;\n};\n\n/**\n * An identifier referring to a change annotation managed by a workspace\n * edit.\n *\n * @since 3.16.0.\n */\nexport type ChangeAnnotationIdentifier = string;\n\n/**\n * A special text edit with an additional change annotation.\n *\n * @since 3.16.0.\n */\nexport interface AnnotatedTextEdit extends TextEdit {\n  /**\n   * The actual annotation identifier.\n   */\n  annotationId: ChangeAnnotationIdentifier;\n}\n\n/**\n * Describes textual changes on a single text document. The text document is referred to as a\n * {@link OptionalVersionedTextDocumentIdentifier} to allow clients to check the text document\n * version before an edit is applied. A {@link TextDocumentEdit} describes all changes on a version\n * Si and after they are applied move the document to version Si+1. So the creator of a\n * {@link TextDocumentEdit} doesn’t need to sort the array of edits or do any kind of ordering.\n * However the edits must be non overlapping.\n */\nexport type TextDocumentEdit = {\n  /**\n   * The text document to change.\n   */\n  textDocument: OptionalVersionedTextDocumentIdentifier;\n\n  /**\n   * The edits to be applied.\n   *\n   * @since 3.16.0 - support for AnnotatedTextEdit. This is guarded by the\n   * client capability `workspace.workspaceEdit.changeAnnotationSupport`\n   */\n  edits: (TextEdit | AnnotatedTextEdit)[];\n};\n\n/**\n * Represents a location inside a resource, such as a line inside a text file.\n */\nexport type Location = {\n  uri: DocumentUri;\n  range: Range;\n};\n\n/**\n * Represents a link between a source and a target location.\n */\nexport type LocationLink = {\n  /**\n   * Span of the origin of this link.\n   *\n   * Used as the underlined span for mouse interaction. Defaults to the word\n   * range at the mouse position.\n   */\n  originSelectionRange?: Range;\n\n  /**\n   * The target resource identifier of this link.\n   */\n  targetUri: DocumentUri;\n\n  /**\n   * The full target range of this link. If the target for example is a symbol\n   * then target range is the range enclosing this symbol not including\n   * leading/trailing whitespace but everything else like comments. This\n   * information is typically used to highlight the range in the editor.\n   */\n  targetRange: Range;\n\n  /**\n   * The range that should be selected and revealed when this link is being\n   * followed, e.g the name of a function. Must be contained by the\n   * `targetRange`. See also `DocumentSymbol#range`\n   */\n  targetSelectionRange: Range;\n};\n\n/**\n * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects are only valid\n * in the scope of a resource.\n */\nexport type Diagnostic = {\n  /**\n   * The range at which the message applies.\n   */\n  range: Range;\n\n  /**\n   * The diagnostic's severity. Can be omitted. If omitted it is up to the\n   * client to interpret diagnostics as error, warning, info or hint.\n   */\n  severity?: DiagnosticSeverity;\n\n  /**\n   * The diagnostic's code, which might appear in the user interface.\n   */\n  code?: integer | string;\n\n  /**\n   * An optional property to describe the error code.\n   *\n   * @since 3.16.0\n   */\n  codeDescription?: CodeDescription;\n\n  /**\n   * A human-readable string describing the source of this\n   * diagnostic, e.g. 'typescript' or 'super lint'.\n   */\n  source?: string;\n\n  /**\n   * The diagnostic's message.\n   */\n  message: string;\n\n  /**\n   * Additional metadata about the diagnostic.\n   *\n   * @since 3.15.0\n   */\n  tags?: DiagnosticTag[];\n\n  /**\n   * An array of related diagnostic information, e.g. when symbol-names within\n   * a scope collide all definitions can be marked via this property.\n   */\n  relatedInformation?: DiagnosticRelatedInformation[];\n\n  /**\n   * A data entry field that is preserved between a\n   * `textDocument/publishDiagnostics` notification and\n   * `textDocument/codeAction` request.\n   *\n   * @since 3.16.0\n   */\n  data?: unknown;\n};\n\n/**\n * Diagnostic severities and tags supported by the protocol.\n */\nexport type DiagnosticSeverity = (typeof DiagnosticSeverity)[keyof typeof DiagnosticSeverity];\n/**\n * Diagnostic severities and tags supported by the protocol.\n */\n\nexport namespace DiagnosticSeverity {\n  /**\n   * Reports an error.\n   */\n  export const Error = 1;\n\n  /**\n   * Reports a warning.\n   */\n  export const Warning = 2;\n\n  /**\n   * Reports an information.\n   */\n  export const Information = 3;\n\n  /**\n   * Reports a hint.\n   */\n  export const Hint = 4;\n}\n\n/**\n * The diagnostic tags.\n *\n * @since 3.15.0\n */\nexport type DiagnosticTag = (typeof DiagnosticTag)[keyof typeof DiagnosticTag];\n/**\n * The diagnostic tags.\n *\n * @since 3.15.0\n */\n\nexport namespace DiagnosticTag {\n  /**\n   * Unused or unnecessary code.\n   *\n   * Clients are allowed to render diagnostics with this tag faded out\n   * instead of having an error squiggle.\n   */\n  export const Unnecessary = 1;\n\n  /**\n   * Deprecated or obsolete code.\n   *\n   * Clients are allowed to rendered diagnostics with this tag strike through.\n   */\n  export const Deprecated = 2;\n}\n\n/**\n * Represents a related message and source code location for a diagnostic.\n * This should be used to point to code locations that cause or are related to\n * a diagnostics, e.g when duplicating a symbol in a scope.\n */\nexport type DiagnosticRelatedInformation = {\n  /**\n   * The location of this related diagnostic information.\n   */\n  location: Location;\n\n  /**\n   * The message of this related diagnostic information.\n   */\n  message: string;\n};\n\n/**\n * Structure to capture a description for an error code.\n *\n * @since 3.16.0\n */\nexport type CodeDescription = {\n  /**\n   * An URI to open with more information about the diagnostic error.\n   */\n  href: URI;\n};\n\n/**\n * Represents a reference to a command. Provides a title which will be used to represent a command\n * in the UI. Commands are identified by a string identifier. The recommended way to handle Commands\n * is to implement their execution on the server side if the client and server provides the\n * corresponding capabilities. Alternatively the tool extension code could handle the command. The\n * protocol currently doesn’t specify a set of well-known commands.\n */\nexport type Command = {\n  /**\n   * Title of the command, like `save`.\n   */\n  title: string;\n\n  /**\n   * The identifier of the actual command handler.\n   */\n  command: string;\n\n  /**\n   * Arguments that the command handler should be\n   * invoked with.\n   */\n  arguments?: LSPAny[];\n};\n\n/**\n * Describes the content type that a client supports in various result literals like `Hover`,\n * `ParameterInfo` or `CompletionItem`.\n *\n * Please note that `MarkupKinds` must not start with a `$`. This kinds are reserved for internal\n * usage.\n */\nexport type MarkupKind = (typeof MarkupKind)[keyof typeof MarkupKind];\n/**\n * Describes the content type that a client supports in various result literals like `Hover`,\n * `ParameterInfo` or `CompletionItem`.\n *\n * Please note that `MarkupKinds` must not start with a `$`. This kinds are reserved for internal\n * usage.\n */\n\nexport namespace MarkupKind {\n  /**\n   * Plain text is supported as a content format\n   */\n  export const PlainText = \"plaintext\";\n\n  /**\n   * Markdown is supported as a content format\n   */\n  export const Markdown = \"markdown\";\n}\n\n/**\n * A `MarkupContent` literal represents a string value which content is\n * interpreted base on its kind flag. Currently the protocol supports\n * `plaintext` and `markdown` as markup kinds.\n *\n * If the kind is `markdown` then the value can contain fenced code blocks like\n * in GitHub issues.\n *\n * Here is an example how such a string can be constructed using\n * JavaScript / TypeScript:\n *\n * ```typescript\n * let markdown: MarkdownContent = {\n *   kind: MarkupKind.Markdown,\n *   value: [\n *     '# Header',\n *     'Some text',\n *     '```typescript',\n *     'someCode();',\n *     '```'\n *   ].join('\\n')\n * };\n * ```\n *\n * _Please Note_ that clients might sanitize the return markdown. A client could\n * decide to remove HTML from the markdown to avoid script execution.\n */\nexport type MarkupContent = {\n  /**\n   * The type of the Markup\n   */\n  kind: MarkupKind;\n\n  /**\n   * The content itself\n   */\n  value: string;\n};\n\n/**\n * Client capabilities specific to the used markdown parser.\n *\n * Known markdown parsers used by clients right now are:\n *\n * | Name | Version | Documentation |\n * | ---- | ------- | ------------- |\n * | marked | 1.1.0 | [Marked Documentation](https://marked.js.org) |\n * | Python-Markdown | 3.2.2 | [Python-Markdown Documentation](https://python-markdown.github.io) |\n *\n * @since 3.16.0\n */\nexport type MarkdownClientCapabilities = {\n  /**\n   * The name of the parser.\n   */\n  parser: string;\n\n  /**\n   * The version of the parser.\n   */\n  version?: string;\n\n  /**\n   * A list of HTML tags that the client allows / supports in\n   * Markdown.\n   *\n   * @since 3.17.0\n   */\n  allowedTags?: readonly string[];\n};\n\n/**\n * Options to create a file.\n */\nexport type CreateFileOptions = {\n  /**\n   * Overwrite existing file. Overwrite wins over `ignoreIfExists`\n   */\n  overwrite?: boolean;\n\n  /**\n   * Ignore if exists.\n   */\n  ignoreIfExists?: boolean;\n};\n\n/**\n * Create file operation\n */\nexport type CreateFile = {\n  /**\n   * A create\n   */\n  kind: \"create\";\n\n  /**\n   * The resource to create.\n   */\n  uri: DocumentUri;\n\n  /**\n   * Additional options\n   */\n  options?: CreateFileOptions;\n\n  /**\n   * An optional annotation identifier describing the operation.\n   *\n   * @since 3.16.0\n   */\n  annotationId?: ChangeAnnotationIdentifier;\n};\n\n/**\n * Rename file options\n */\nexport type RenameFileOptions = {\n  /**\n   * Overwrite target if existing. Overwrite wins over `ignoreIfExists`\n   */\n  overwrite?: boolean;\n\n  /**\n   * Ignores if target exists.\n   */\n  ignoreIfExists?: boolean;\n};\n\n/**\n * Rename file operation\n */\nexport type RenameFile = {\n  /**\n   * A rename\n   */\n  kind: \"rename\";\n\n  /**\n   * The old (existing) location.\n   */\n  oldUri: DocumentUri;\n\n  /**\n   * The new location.\n   */\n  newUri: DocumentUri;\n\n  /**\n   * Rename options.\n   */\n  options?: RenameFileOptions;\n\n  /**\n   * An optional annotation identifier describing the operation.\n   *\n   * @since 3.16.0\n   */\n  annotationId?: ChangeAnnotationIdentifier;\n};\n\n/**\n * Delete file options\n */\nexport type DeleteFileOptions = {\n  /**\n   * Delete the content recursively if a folder is denoted.\n   */\n  recursive?: boolean;\n\n  /**\n   * Ignore the operation if the file doesn't exist.\n   */\n  ignoreIfNotExists?: boolean;\n};\n\n/**\n * Delete file operation\n */\nexport type DeleteFile = {\n  /**\n   * A delete\n   */\n  kind: \"delete\";\n\n  /**\n   * The file to delete.\n   */\n  uri: DocumentUri;\n\n  /**\n   * Delete options.\n   */\n  options?: DeleteFileOptions;\n\n  /**\n   * An optional annotation identifier describing the operation.\n   *\n   * @since 3.16.0\n   */\n  annotationId?: ChangeAnnotationIdentifier;\n};\n\n/**\n * A workspace edit represents changes to many resources managed in the workspace. The edit should\n * either provide `changes` or `documentChanges`. If the client can handle versioned document edits\n * and if `documentChanges` are present, the latter are preferred over `changes`.\n *\n * Since version 3.13.0 a workspace edit can contain resource operations (create, delete or rename\n * files and folders) as well. If resource operations are present clients need to execute the\n * operations in the order in which they are provided. So a workspace edit for example can consist\n * of the following two changes: (1) create file a.txt and (2) a text document edit which insert\n * text into file a.txt. An invalid sequence (e.g. (1) delete file a.txt and (2) insert text into\n * file a.txt) will cause failure of the operation. How the client recovers from the failure is\n * described by the client capability: `workspace.workspaceEdit.failureHandling`\n */\nexport type WorkspaceEdit = {\n  /**\n   * Holds changes to existing resources.\n   */\n  changes?: { [uri: DocumentUri]: TextEdit[] };\n\n  /**\n   * Depending on the client capability\n   * `workspace.workspaceEdit.resourceOperations` document changes are either\n   * an array of `TextDocumentEdit`s to express changes to n different text\n   * documents where each text document edit addresses a specific version of\n   * a text document. Or it can contain above `TextDocumentEdit`s mixed with\n   * create, rename and delete file / folder operations.\n   *\n   * Whether a client supports versioned document edits is expressed via\n   * `workspace.workspaceEdit.documentChanges` client capability.\n   *\n   * If a client neither supports `documentChanges` nor\n   * `workspace.workspaceEdit.resourceOperations` then only plain `TextEdit`s\n   * using the `changes` property are supported.\n   */\n  documentChanges?:\n    | readonly TextDocumentEdit[]\n    | readonly (TextDocumentEdit | CreateFile | RenameFile | DeleteFile)[];\n\n  /**\n   * A map of change annotations that can be referenced in\n   * `AnnotatedTextEdit`s or create, rename and delete file / folder\n   * operations.\n   *\n   * Whether clients honor this property depends on the client capability\n   * `workspace.changeAnnotationSupport`.\n   *\n   * @since 3.16.0\n   */\n  changeAnnotations?: { [id: ChangeAnnotationIdentifier]: ChangeAnnotation };\n};\n\n/**\n * The capabilities of a workspace edit has evolved over the time. Clients can describe their\n * support using the following client capability:\n *\n * _Client Capability_:\n *\n * - property path (optional): `workspace.workspaceEdit`\n * - property type: {@link WorkspaceEditClientCapabilities} defined as follows.\n */\nexport type WorkspaceEditClientCapabilities = {\n  /**\n   * The client supports versioned document changes in `WorkspaceEdit`s\n   */\n  documentChanges?: boolean;\n\n  /**\n   * The resource operations the client supports. Clients should at least\n   * support 'create', 'rename' and 'delete' files and folders.\n   *\n   * @since 3.13.0\n   */\n  resourceOperations?: readonly ResourceOperationKind[];\n\n  /**\n   * The failure handling strategy of a client if applying the workspace edit\n   * fails.\n   *\n   * @since 3.13.0\n   */\n  failureHandling?: FailureHandlingKind;\n\n  /**\n   * Whether the client normalizes line endings to the client specific\n   * setting.\n   * If set to `true` the client will normalize line ending characters\n   * in a workspace edit to the client specific new line character(s).\n   *\n   * @since 3.16.0\n   */\n  normalizesLineEndings?: boolean;\n\n  /**\n   * Whether the client in general supports change annotations on text edits,\n   * create file, rename file and delete file changes.\n   *\n   * @since 3.16.0\n   */\n  changeAnnotationSupport?: {\n    /**\n     * Whether the client groups edits with equal labels into tree nodes,\n     * for instance all edits labelled with \"Changes in Strings\" would\n     * be a tree node.\n     */\n    groupsOnLabel?: boolean;\n  };\n};\n\n/**\n * The kind of resource operations supported by the client.\n */\nexport type ResourceOperationKind =\n  (typeof ResourceOperationKind)[keyof typeof ResourceOperationKind];\n/**\n * The kind of resource operations supported by the client.\n */\n\nexport namespace ResourceOperationKind {\n  /**\n   * Supports creating new files and folders.\n   */\n  export const Create = \"create\";\n\n  /**\n   * Supports renaming existing files and folders.\n   */\n  export const Rename = \"rename\";\n\n  /**\n   * Supports deleting existing files and folders.\n   */\n  export const DeleteFileOptions = \"delete\";\n}\n\n/**\n * The failure handling strategy of a client if applying the workspace edit.\n */\nexport type FailureHandlingKind = (typeof FailureHandlingKind)[keyof typeof FailureHandlingKind];\n/**\n * The failure handling strategy of a client if applying the workspace edit.\n */\n\nexport namespace FailureHandlingKind {\n  /**\n   * Applying the workspace change is simply aborted if one of the changes\n   * provided fails. All operations executed before the failing operation\n   * stay executed.\n   */\n  export const Abort = \"abort\";\n\n  /**\n   * All operations are executed transactional. That means they either all\n   * succeed or no changes at all are applied to the workspace.\n   */\n  export const Transactional = \"transactional\";\n\n  /**\n   * If the workspace edit contains only textual file changes they are\n   * executed transactional. If resource changes (create, rename or delete\n   * file) are part of the change the failure handling strategy is abort.\n   */\n  export const TextOnlyTransactional = \"textOnlyTransactional\";\n\n  /**\n   * The client tries to undo the operations already executed. But there is no\n   * guarantee that this is succeeding.\n   */\n  export const Undo = \"undo\";\n}\n\n/**\n * The payload to be sent in a `$/progress` notification.\n */\nexport type WorkDoneProgressPayload =\n  | WorkDoneProgressBegin\n  | WorkDoneProgressReport\n  | WorkDoneProgressEnd;\n\n/**\n * The payload to be sent in a `$/progress` notification to start progress reporting.\n */\nexport type WorkDoneProgressBegin = {\n  kind: \"begin\";\n\n  /**\n   * Mandatory title of the progress operation. Used to briefly inform about\n   * the kind of operation being performed.\n   *\n   * Examples: \"Indexing\" or \"Linking dependencies\".\n   */\n  title: string;\n\n  /**\n   * Controls if a cancel button should show to allow the user to cancel the\n   * long running operation. Clients that don't support cancellation are\n   * allowed to ignore the setting.\n   */\n  cancellable?: boolean;\n\n  /**\n   * Optional, more detailed associated progress message. Contains\n   * complementary information to the `title`.\n   *\n   * Examples: \"3/25 files\", \"project/src/module2\", \"node_modules/some_dep\".\n   * If unset, the previous progress message (if any) is still valid.\n   */\n  message?: string;\n\n  /**\n   * Optional progress percentage to display (value 100 is considered 100%).\n   * If not provided infinite progress is assumed and clients are allowed\n   * to ignore the `percentage` value in subsequent in report notifications.\n   *\n   * The value should be steadily rising. Clients are free to ignore values\n   * that are not following this rule. The value range is [0, 100]\n   */\n  percentage?: uinteger;\n};\n\n/**\n * The payload to be sent in a `$/progress` notification to report progress is done.\n */\nexport type WorkDoneProgressReport = {\n  kind: \"report\";\n\n  /**\n   * Controls enablement state of a cancel button. This property is only valid\n   * if a cancel button got requested in the `WorkDoneProgressBegin` payload.\n   *\n   * Clients that don't support cancellation or don't support control the\n   * button's enablement state are allowed to ignore the setting.\n   */\n  cancellable?: boolean;\n\n  /**\n   * Optional, more detailed associated progress message. Contains\n   * complementary information to the `title`.\n   *\n   * Examples: \"3/25 files\", \"project/src/module2\", \"node_modules/some_dep\".\n   * If unset, the previous progress message (if any) is still valid.\n   */\n  message?: string;\n\n  /**\n   * Optional progress percentage to display (value 100 is considered 100%).\n   * If not provided infinite progress is assumed and clients are allowed\n   * to ignore the `percentage` value in subsequent in report notifications.\n   *\n   * The value should be steadily rising. Clients are free to ignore values\n   * that are not following this rule. The value range is [0, 100]\n   */\n  percentage?: uinteger;\n};\n\n/**\n * The payload to be sent in a `$/progress` notification to signal the end of a progress.\n */\nexport type WorkDoneProgressEnd = {\n  kind: \"end\";\n\n  /**\n   * Optional, a final message indicating to for example indicate the outcome\n   * of the operation.\n   */\n  message?: string;\n};\n\n/**\n * Work done progress params for the client.\n */\nexport type WorkDoneProgressParams = {\n  /**\n   * An optional token that a server can use to report work done progress.\n   */\n  workDoneToken?: ProgressToken;\n};\n\n/**\n * Work done progress options.\n */\nexport type WorkDoneProgressOptions = {\n  workDoneProgress?: boolean;\n};\n\n/**\n * A parameter literal used to pass a partial result token.\n */\nexport type PartialResultParams = {\n  /**\n   * An optional token that a server can use to report partial results (e.g.\n   * streaming) to the client.\n   */\n  partialResultToken?: ProgressToken;\n};\n\n/**\n * A TraceValue represents the level of verbosity with which the server systematically reports its\n * execution trace using `$/logTrace` notifications. The initial trace value is set by the client at\n * initialization and can be modified later using the `$/setTrace` notification.\n */\nexport type TraceValue = \"off\" | \"messages\" | \"verbose\";\n\n/**********************\n * Lifecycle Messages *\n **********************/\n/**\n * Params to be sent with an `initialize` request, which is sent as the first request from the\n * client to the server.\n */\nexport interface InitializeParams extends WorkDoneProgressParams {\n  /**\n   * The process Id of the parent process that started the server. Is null if\n   * the process has not been started by another process. If the parent\n   * process is not alive then the server should exit (see exit notification)\n   * its process.\n   */\n  processId: integer | null;\n\n  /**\n   * Information about the client\n   *\n   * @since 3.15.0\n   */\n  clientInfo?: {\n    /**\n     * The name of the client as defined by the client.\n     */\n    name: string;\n\n    /**\n     * The client's version as defined by the client.\n     */\n    version?: string;\n  };\n\n  /**\n   * The locale the client is currently showing the user interface\n   * in. This must not necessarily be the locale of the operating\n   * system.\n   *\n   * Uses IETF language tags as the value's syntax\n   * (See https://en.wikipedia.org/wiki/IETF_language_tag)\n   *\n   * @since 3.16.0\n   */\n  locale?: string;\n\n  /**\n   * The rootPath of the workspace. Is null\n   * if no folder is open.\n   *\n   * @deprecated in favour of `rootUri`.\n   */\n  rootPath?: string | null;\n\n  /**\n   * The rootUri of the workspace. Is null if no\n   * folder is open. If both `rootPath` and `rootUri` are set\n   * `rootUri` wins.\n   *\n   * @deprecated in favour of `workspaceFolders`\n   */\n  rootUri: DocumentUri | null;\n\n  /**\n   * User provided initialization options.\n   */\n  initializationOptions?: LSPAny;\n\n  /**\n   * The capabilities provided by the client (editor or tool)\n   */\n  capabilities: ClientCapabilities;\n\n  /**\n   * The initial trace setting. If omitted trace is disabled ('off').\n   */\n  trace?: TraceValue;\n\n  /**\n   * The workspace folders configured in the client when the server starts.\n   * This property is only available if the client supports workspace folders.\n   * It can be `null` if the client supports workspace folders but none are\n   * configured.\n   *\n   * @since 3.6.0\n   */\n  workspaceFolders?: readonly WorkspaceFolder[] | null;\n}\n\n/**\n * Text document specific client capabilities.\n */\nexport type TextDocumentClientCapabilities = {\n  synchronization?: TextDocumentSyncClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/completion` request.\n   */\n  completion?: CompletionClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/hover` request.\n   */\n  hover?: HoverClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/signatureHelp` request.\n   */\n  signatureHelp?: SignatureHelpClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/declaration` request.\n   *\n   * @since 3.14.0\n   */\n  declaration?: DeclarationClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/definition` request.\n   */\n  definition?: DefinitionClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/typeDefinition` request.\n   *\n   * @since 3.6.0\n   */\n  typeDefinition?: TypeDefinitionClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/implementation` request.\n   *\n   * @since 3.6.0\n   */\n  implementation?: ImplementationClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/references` request.\n   */\n  references?: ReferenceClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/documentHighlight` request.\n   */\n  documentHighlight?: DocumentHighlightClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/documentSymbol` request.\n   */\n  documentSymbol?: DocumentSymbolClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/codeAction` request.\n   */\n  codeAction?: CodeActionClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/codeLens` request.\n   */\n  codeLens?: CodeLensClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/documentLink` request.\n   */\n  documentLink?: DocumentLinkClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/documentColor` and the\n   * `textDocument/colorPresentation` request.\n   *\n   * @since 3.6.0\n   */\n  colorProvider?: DocumentColorClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/formatting` request.\n   */\n  formatting?: DocumentFormattingClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/rangeFormatting` request.\n   */\n  rangeFormatting?: DocumentRangeFormattingClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/onTypeFormatting` request.\n   */\n  onTypeFormatting?: DocumentOnTypeFormattingClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/rename` request.\n   */\n  rename?: RenameClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/publishDiagnostics`\n   * notification.\n   */\n  publishDiagnostics?: PublishDiagnosticsClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/foldingRange` request.\n   *\n   * @since 3.10.0\n   */\n  foldingRange?: FoldingRangeClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/selectionRange` request.\n   *\n   * @since 3.15.0\n   */\n  selectionRange?: SelectionRangeClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/linkedEditingRange` request.\n   *\n   * @since 3.16.0\n   */\n  linkedEditingRange?: LinkedEditingRangeClientCapabilities;\n\n  /**\n   * Capabilities specific to the various call hierarchy requests.\n   *\n   * @since 3.16.0\n   */\n  callHierarchy?: CallHierarchyClientCapabilities;\n\n  /**\n   * Capabilities specific to the various semantic token requests.\n   *\n   * @since 3.16.0\n   */\n  semanticTokens?: SemanticTokensClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/moniker` request.\n   *\n   * @since 3.16.0\n   */\n  moniker?: MonikerClientCapabilities;\n\n  /**\n   * Capabilities specific to the various type hierarchy requests.\n   *\n   * @since 3.17.0\n   */\n  typeHierarchy?: TypeHierarchyClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/inlineValue` request.\n   *\n   * @since 3.17.0\n   */\n  inlineValue?: InlineValueClientCapabilities;\n\n  /**\n   * Capabilities specific to the `textDocument/inlayHint` request.\n   *\n   * @since 3.17.0\n   */\n  inlayHint?: InlayHintClientCapabilities;\n\n  /**\n   * Capabilities specific to the diagnostic pull model.\n   *\n   * @since 3.17.0\n   */\n  diagnostic?: DiagnosticClientCapabilities;\n};\n\n/**\n * Capabilities specific to the notebook document support.\n *\n * @since 3.17.0\n */\nexport type NotebookDocumentClientCapabilities = {\n  /**\n   * Capabilities specific to notebook document synchronization\n   *\n   * @since 3.17.0\n   */\n  synchronization: NotebookDocumentSyncClientCapabilities;\n};\n\ntype ClientCapabilities = {\n  /**\n   * Workspace specific client capabilities.\n   */\n  workspace?: {\n    /**\n     * The client supports applying batch edits\n     * to the workspace by supporting the request\n     * 'workspace/applyEdit'\n     */\n    applyEdit?: boolean;\n\n    /**\n     * Capabilities specific to `WorkspaceEdit`s\n     */\n    workspaceEdit?: WorkspaceEditClientCapabilities;\n\n    /**\n     * Capabilities specific to the `workspace/didChangeConfiguration`\n     * notification.\n     */\n    didChangeConfiguration?: DidChangeConfigurationClientCapabilities;\n\n    /**\n     * Capabilities specific to the `workspace/didChangeWatchedFiles`\n     * notification.\n     */\n    didChangeWatchedFiles?: DidChangeWatchedFilesClientCapabilities;\n\n    /**\n     * Capabilities specific to the `workspace/symbol` request.\n     */\n    symbol?: WorkspaceSymbolClientCapabilities;\n\n    /**\n     * Capabilities specific to the `workspace/executeCommand` request.\n     */\n    executeCommand?: ExecuteCommandClientCapabilities;\n\n    /**\n     * The client has support for workspace folders.\n     *\n     * @since 3.6.0\n     */\n    workspaceFolders?: boolean;\n\n    /**\n     * The client supports `workspace/configuration` requests.\n     *\n     * @since 3.6.0\n     */\n    configuration?: boolean;\n\n    /**\n     * Capabilities specific to the semantic token requests scoped to the\n     * workspace.\n     *\n     * @since 3.16.0\n     */\n    semanticTokens?: SemanticTokensWorkspaceClientCapabilities;\n\n    /**\n     * Capabilities specific to the code lens requests scoped to the\n     * workspace.\n     *\n     * @since 3.16.0\n     */\n    codeLens?: CodeLensWorkspaceClientCapabilities;\n\n    /**\n     * The client has support for file requests/notifications.\n     *\n     * @since 3.16.0\n     */\n    fileOperations?: {\n      /**\n       * Whether the client supports dynamic registration for file\n       * requests/notifications.\n       */\n      dynamicRegistration?: boolean;\n\n      /**\n       * The client has support for sending didCreateFiles notifications.\n       */\n      didCreate?: boolean;\n\n      /**\n       * The client has support for sending willCreateFiles requests.\n       */\n      willCreate?: boolean;\n\n      /**\n       * The client has support for sending didRenameFiles notifications.\n       */\n      didRename?: boolean;\n\n      /**\n       * The client has support for sending willRenameFiles requests.\n       */\n      willRename?: boolean;\n\n      /**\n       * The client has support for sending didDeleteFiles notifications.\n       */\n      didDelete?: boolean;\n\n      /**\n       * The client has support for sending willDeleteFiles requests.\n       */\n      willDelete?: boolean;\n    };\n\n    /**\n     * Client workspace capabilities specific to inline values.\n     *\n     * @since 3.17.0\n     */\n    inlineValue?: InlineValueWorkspaceClientCapabilities;\n\n    /**\n     * Client workspace capabilities specific to inlay hints.\n     *\n     * @since 3.17.0\n     */\n    inlayHint?: InlayHintWorkspaceClientCapabilities;\n\n    /**\n     * Client workspace capabilities specific to diagnostics.\n     *\n     * @since 3.17.0.\n     */\n    diagnostics?: DiagnosticWorkspaceClientCapabilities;\n  };\n\n  /**\n   * Text document specific client capabilities.\n   */\n  textDocument?: TextDocumentClientCapabilities;\n\n  /**\n   * Capabilities specific to the notebook document support.\n   *\n   * @since 3.17.0\n   */\n  notebookDocument?: NotebookDocumentClientCapabilities;\n\n  /**\n   * Window specific client capabilities.\n   */\n  window?: {\n    /**\n     * It indicates whether the client supports server initiated\n     * progress using the `window/workDoneProgress/create` request.\n     *\n     * The capability also controls Whether client supports handling\n     * of progress notifications. If set servers are allowed to report a\n     * `workDoneProgress` property in the request specific server\n     * capabilities.\n     *\n     * @since 3.15.0\n     */\n    workDoneProgress?: boolean;\n\n    /**\n     * Capabilities specific to the showMessage request\n     *\n     * @since 3.16.0\n     */\n    showMessage?: ShowMessageRequestClientCapabilities;\n\n    /**\n     * Client capabilities for the show document request.\n     *\n     * @since 3.16.0\n     */\n    showDocument?: ShowDocumentClientCapabilities;\n  };\n\n  /**\n   * General client capabilities.\n   *\n   * @since 3.16.0\n   */\n  general?: {\n    /**\n     * Client capability that signals how the client\n     * handles stale requests (e.g. a request\n     * for which the client will not process the response\n     * anymore since the information is outdated).\n     *\n     * @since 3.17.0\n     */\n    staleRequestSupport?: {\n      /**\n       * The client will actively cancel the request.\n       */\n      cancel: boolean;\n\n      /**\n       * The list of requests for which the client\n       * will retry the request if it receives a\n       * response with error code `ContentModified``\n       */\n      retryOnContentModified: string[];\n    };\n\n    /**\n     * Client capabilities specific to regular expressions.\n     *\n     * @since 3.16.0\n     */\n    regularExpressions?: RegularExpressionsClientCapabilities;\n\n    /**\n     * Client capabilities specific to the client's markdown parser.\n     *\n     * @since 3.16.0\n     */\n    markdown?: MarkdownClientCapabilities;\n\n    /**\n     * The position encodings supported by the client. Client and server\n     * have to agree on the same position encoding to ensure that offsets\n     * (e.g. character position in a line) are interpreted the same on both\n     * side.\n     *\n     * To keep the protocol backwards compatible the following applies: if\n     * the value 'utf-16' is missing from the array of position encodings\n     * servers can assume that the client supports UTF-16. UTF-16 is\n     * therefore a mandatory encoding.\n     *\n     * If omitted it defaults to ['utf-16'].\n     *\n     * Implementation considerations: since the conversion from one encoding\n     * into another requires the content of the file / line the conversion\n     * is best done where the file is read which is usually on the server\n     * side.\n     *\n     * @since 3.17.0\n     */\n    positionEncodings?: PositionEncodingKind[];\n  };\n\n  /**\n   * Experimental client capabilities.\n   */\n  experimental?: LSPAny;\n};\n\n/**\n * Result of the `initialize` request.\n */\nexport type InitializeResult = {\n  /**\n   * The capabilities the language server provides.\n   */\n  capabilities: ServerCapabilities;\n\n  /**\n   * Information about the server.\n   *\n   * @since 3.15.0\n   */\n  serverInfo?: {\n    /**\n     * The name of the server as defined by the server.\n     */\n    name: string;\n\n    /**\n     * The server's version as defined by the server.\n     */\n    version?: string;\n  };\n};\n\n/**\n * Known error codes for an `InitializeErrorCodes`.\n */\nexport type InitializeErrorCodes = (typeof InitializeErrorCodes)[keyof typeof InitializeErrorCodes];\n/**\n * Known error codes for an `InitializeErrorCodes`.\n */\n\nexport namespace InitializeErrorCodes {\n  /**\n   * If the protocol version provided by the client can't be handled by\n   * the server.\n   *\n   * @deprecated This initialize error got replaced by client capabilities.\n   * There is no version handshake in version 3.0x\n   */\n  export const unknownProtocolVersion = 1;\n}\n\n/**\n * Error returned if the `initialize` request failed.\n */\nexport type InitializeError = {\n  /**\n   * Indicates whether the client execute the following retry logic:\n   * (1) show the message provided by the ResponseError to the user\n   * (2) user selects retry or cancel\n   * (3) if user selected retry the initialize method is sent again.\n   */\n  retry: boolean;\n};\n\n/**\n * Server capabilities.\n */\ntype ServerCapabilities = {\n  /**\n   * The position encoding the server picked from the encodings offered\n   * by the client via the client capability `general.positionEncodings`.\n   *\n   * If the client didn't provide any position encodings the only valid\n   * value that a server can return is 'utf-16'.\n   *\n   * If omitted it defaults to 'utf-16'.\n   *\n   * @since 3.17.0\n   */\n  positionEncoding?: PositionEncodingKind;\n\n  /**\n   * Defines how text documents are synced. Is either a detailed structure\n   * defining each notification or for backwards compatibility the\n   * TextDocumentSyncKind number. If omitted it defaults to\n   * `TextDocumentSyncKind.None`.\n   */\n  textDocumentSync?: TextDocumentSyncOptions | TextDocumentSyncKind;\n\n  /**\n   * Defines how notebook documents are synced.\n   *\n   * @since 3.17.0\n   */\n  notebookDocumentSync?: NotebookDocumentSyncOptions | NotebookDocumentSyncRegistrationOptions;\n\n  /**\n   * The server provides completion support.\n   */\n  completionProvider?: CompletionOptions;\n\n  /**\n   * The server provides hover support.\n   */\n  hoverProvider?: boolean | HoverOptions;\n\n  /**\n   * The server provides signature help support.\n   */\n  signatureHelpProvider?: SignatureHelpOptions;\n\n  /**\n   * The server provides go to declaration support.\n   *\n   * @since 3.14.0\n   */\n  declarationProvider?: boolean | DeclarationOptions | DeclarationRegistrationOptions;\n\n  /**\n   * The server provides goto definition support.\n   */\n  definitionProvider?: boolean | DefinitionOptions;\n\n  /**\n   * The server provides goto type definition support.\n   *\n   * @since 3.6.0\n   */\n  typeDefinitionProvider?: boolean | TypeDefinitionOptions | TypeDefinitionRegistrationOptions;\n\n  /**\n   * The server provides goto implementation support.\n   *\n   * @since 3.6.0\n   */\n  implementationProvider?: boolean | ImplementationOptions | ImplementationRegistrationOptions;\n\n  /**\n   * The server provides find references support.\n   */\n  referencesProvider?: boolean | ReferenceOptions;\n\n  /**\n   * The server provides document highlight support.\n   */\n  documentHighlightProvider?: boolean | DocumentHighlightOptions;\n\n  /**\n   * The server provides document symbol support.\n   */\n  documentSymbolProvider?: boolean | DocumentSymbolOptions;\n\n  /**\n   * The server provides code actions. The `CodeActionOptions` return type is\n   * only valid if the client signals code action literal support via the\n   * property `textDocument.codeAction.codeActionLiteralSupport`.\n   */\n  codeActionProvider?: boolean | CodeActionOptions;\n\n  /**\n   * The server provides code lens.\n   */\n  codeLensProvider?: CodeLensOptions;\n\n  /**\n   * The server provides document link support.\n   */\n  documentLinkProvider?: DocumentLinkOptions;\n\n  /**\n   * The server provides color provider support.\n   *\n   * @since 3.6.0\n   */\n  colorProvider?: boolean | DocumentColorOptions | DocumentColorRegistrationOptions;\n\n  /**\n   * The server provides document formatting.\n   */\n  documentFormattingProvider?: boolean | DocumentFormattingOptions;\n\n  /**\n   * The server provides document range formatting.\n   */\n  documentRangeFormattingProvider?: boolean | DocumentRangeFormattingOptions;\n\n  /**\n   * The server provides document formatting on typing.\n   */\n  documentOnTypeFormattingProvider?: DocumentOnTypeFormattingOptions;\n\n  /**\n   * The server provides rename support. RenameOptions may only be\n   * specified if the client states that it supports\n   * `prepareSupport` in its initial `initialize` request.\n   */\n  renameProvider?: boolean | RenameOptions;\n\n  /**\n   * The server provides folding provider support.\n   *\n   * @since 3.10.0\n   */\n  foldingRangeProvider?: boolean | FoldingRangeOptions | FoldingRangeRegistrationOptions;\n\n  /**\n   * The server provides execute command support.\n   */\n  executeCommandProvider?: ExecuteCommandOptions;\n\n  /**\n   * The server provides selection range support.\n   *\n   * @since 3.15.0\n   */\n  selectionRangeProvider?: boolean | SelectionRangeOptions | SelectionRangeRegistrationOptions;\n\n  /**\n   * The server provides linked editing range support.\n   *\n   * @since 3.16.0\n   */\n  linkedEditingRangeProvider?:\n    | boolean\n    | LinkedEditingRangeOptions\n    | LinkedEditingRangeRegistrationOptions;\n\n  /**\n   * The server provides call hierarchy support.\n   *\n   * @since 3.16.0\n   */\n  callHierarchyProvider?: boolean | CallHierarchyOptions | CallHierarchyRegistrationOptions;\n\n  /**\n   * The server provides semantic tokens support.\n   *\n   * @since 3.16.0\n   */\n  semanticTokensProvider?: SemanticTokensOptions | SemanticTokensRegistrationOptions;\n\n  /**\n   * Whether server provides moniker support.\n   *\n   * @since 3.16.0\n   */\n  monikerProvider?: boolean | MonikerOptions | MonikerRegistrationOptions;\n\n  /**\n   * The server provides type hierarchy support.\n   *\n   * @since 3.17.0\n   */\n  typeHierarchyProvider?: boolean | TypeHierarchyOptions | TypeHierarchyRegistrationOptions;\n\n  /**\n   * The server provides inline values.\n   *\n   * @since 3.17.0\n   */\n  inlineValueProvider?: boolean | InlineValueOptions | InlineValueRegistrationOptions;\n\n  /**\n   * The server provides inlay hints.\n   *\n   * @since 3.17.0\n   */\n  inlayHintProvider?: boolean | InlayHintOptions | InlayHintRegistrationOptions;\n\n  /**\n   * The server has support for pull model diagnostics.\n   *\n   * @since 3.17.0\n   */\n  diagnosticProvider?: DiagnosticOptions | DiagnosticRegistrationOptions;\n\n  /**\n   * The server provides workspace symbol support.\n   */\n  workspaceSymbolProvider?: boolean | WorkspaceSymbolOptions;\n\n  /**\n   * Workspace specific server capabilities\n   */\n  workspace?: {\n    /**\n     * The server supports workspace folder.\n     *\n     * @since 3.6.0\n     */\n    workspaceFolders?: WorkspaceFoldersServerCapabilities;\n\n    /**\n     * The server is interested in file notifications/requests.\n     *\n     * @since 3.16.0\n     */\n    fileOperations?: {\n      /**\n       * The server is interested in receiving didCreateFiles\n       * notifications.\n       */\n      didCreate?: FileOperationRegistrationOptions;\n\n      /**\n       * The server is interested in receiving willCreateFiles requests.\n       */\n      willCreate?: FileOperationRegistrationOptions;\n\n      /**\n       * The server is interested in receiving didRenameFiles\n       * notifications.\n       */\n      didRename?: FileOperationRegistrationOptions;\n\n      /**\n       * The server is interested in receiving willRenameFiles requests.\n       */\n      willRename?: FileOperationRegistrationOptions;\n\n      /**\n       * The server is interested in receiving didDeleteFiles file\n       * notifications.\n       */\n      didDelete?: FileOperationRegistrationOptions;\n\n      /**\n       * The server is interested in receiving willDeleteFiles file\n       * requests.\n       */\n      willDelete?: FileOperationRegistrationOptions;\n    };\n  };\n\n  /**\n   * Experimental server capabilities.\n   */\n  experimental?: LSPAny;\n};\n\n/**\n * Params of the `initialized` notification.\n */\nexport interface InitializedParams {}\n\n/**\n * General parameters to register for a capability.\n */\nexport type Registration = {\n  /**\n   * The id used to register the request. The id can be used to deregister\n   * the request again.\n   */\n  id: string;\n\n  /**\n   * The method / capability to register for.\n   */\n  method: string;\n\n  /**\n   * Options necessary for the registration.\n   */\n  registerOptions?: LSPAny;\n};\n\n/**\n * Params of the `client/registerCapability` request.\n */\nexport type RegistrationParams = {\n  registrations: readonly Registration[];\n};\n\n/**\n * Static registration options to be returned in the initialize request.\n */\nexport type StaticRegistrationOptions = {\n  /**\n   * The id used to register the request. The id can be used to deregister\n   * the request again. See also Registration#id.\n   */\n  id?: string;\n};\n\n/**\n * General text document registration options.\n */\nexport type TextDocumentRegistrationOptions = {\n  /**\n   * A document selector to identify the scope of the registration. If set to\n   * null the document selector provided on the client side will be used.\n   */\n  documentSelector: DocumentSelector | null;\n};\n\n/**\n * General parameters to unregister a capability.\n */\nexport type Unregistration = {\n  /**\n   * The id used to unregister the request or notification. Usually an id\n   * provided during the register request.\n   */\n  id: string;\n\n  /**\n   * The method / capability to unregister for.\n   */\n  method: string;\n};\n\n/**\n * Params of the `client/unregisterCapability` request.\n */\nexport type UnregistrationParams = {\n  // This should correctly be named `unregistrations`. However changing this\n  // is a breaking change and needs to wait until we deliver a 4.x version\n  // of the specification.\n  unregisterations: readonly Unregistration[];\n};\n\n/**\n * Params of the `$/setTrace` request.\n */\nexport type SetTraceParams = {\n  /**\n   * The new value that should be assigned to the trace setting.\n   */\n  value: TraceValue;\n};\n\n/**\n * Params of the `$/logTrace` request.\n */\nexport type LogTraceParams = {\n  /**\n   * The message to be logged.\n   */\n  message: string;\n  /**\n   * Additional information that can be computed if the `trace` configuration\n   * is set to `'verbose'`\n   */\n  verbose?: string;\n};\n\n/*********************************\n * Text Document Synchronization *\n *********************************/\n/**\n * Defines how the host (editor) should sync document changes to the language server.\n */\nexport type TextDocumentSyncKind = (typeof TextDocumentSyncKind)[keyof typeof TextDocumentSyncKind];\n/**\n * Defines how the host (editor) should sync document changes to the language server.\n */\n\nexport namespace TextDocumentSyncKind {\n  /**\n   * Documents should not be synced at all.\n   */\n  export const None = 0;\n\n  /**\n   * Documents are synced by always sending the full content\n   * of the document.\n   */\n  export const Full = 1;\n\n  /**\n   * Documents are synced by sending the full content on open.\n   * After that only incremental updates to the document are\n   * sent.\n   */\n  export const Incremental = 2;\n}\n\n/**\n * Text document sync options.\n */\nexport type TextDocumentSyncOptions = {\n  /**\n   * Open and close notifications are sent to the server. If omitted open\n   * close notification should not be sent.\n   */\n  openClose?: boolean;\n\n  /**\n   * Change notifications are sent to the server. See\n   * TextDocumentSyncKind.None, TextDocumentSyncKind.Full and\n   * TextDocumentSyncKind.Incremental. If omitted it defaults to\n   * TextDocumentSyncKind.None.\n   */\n  change?: TextDocumentSyncKind;\n\n  /**\n   * If present will save notifications are sent to the server. If omitted\n   * the notification should not be sent.\n   */\n  willSave?: boolean;\n\n  /**\n   * If present will save wait until requests are sent to the server. If\n   * omitted the request should not be sent.\n   */\n  willSaveWaitUntil?: boolean;\n\n  /**\n   * If present save notifications are sent to the server. If omitted the\n   * notification should not be sent.\n   */\n  save?: boolean | SaveOptions;\n};\n\n/**\n * Params of the `textDocument/didOpen` notification.\n */\nexport type DidOpenTextDocumentParams = {\n  /**\n   * The document that was opened.\n   */\n  textDocument: TextDocumentItem;\n};\n\n/**\n * Describe options to be used when registering for text document change events.\n */\nexport interface TextDocumentChangeRegistrationOptions extends TextDocumentRegistrationOptions {\n  /**\n   * How documents are synced to the server. See TextDocumentSyncKind.Full\n   * and TextDocumentSyncKind.Incremental.\n   */\n  syncKind: TextDocumentSyncKind;\n}\n\n/**\n * Params of the `textDocument/didChange` notification.\n */\nexport type DidChangeTextDocumentParams = {\n  /**\n   * The document that did change. The version number points\n   * to the version after all provided content changes have\n   * been applied.\n   */\n  textDocument: VersionedTextDocumentIdentifier;\n\n  /**\n   * The actual content changes. The content changes describe single state\n   * changes to the document. So if there are two content changes c1 (at\n   * array index 0) and c2 (at array index 1) for a document in state S then\n   * c1 moves the document from S to S' and c2 from S' to S''. So c1 is\n   * computed on the state S and c2 is computed on the state S'.\n   *\n   * To mirror the content of a document using change events use the following approach:\n   *\n   * - start with the same initial content\n   * - apply the 'textDocument/didChange' notifications in the order you\n   *   receive them.\n   * - apply the `TextDocumentContentChangeEvent`s in a single notification\n   *   in the order you receive them.\n   */\n  contentChanges: readonly TextDocumentContentChangeEvent[];\n};\n\n/**\n * An event describing a change to a text document. If only a text is provided\n * it is considered to be the full content of the document.\n */\nexport type TextDocumentContentChangeEvent =\n  | {\n      /**\n       * The range of the document that changed.\n       */\n      range: Range;\n\n      /**\n       * The optional length of the range that got replaced.\n       *\n       * @deprecated use range instead.\n       */\n      rangeLength?: uinteger;\n\n      /**\n       * The new text for the provided range.\n       */\n      text: string;\n    }\n  | {\n      /**\n       * The new text of the whole document.\n       */\n      text: string;\n    };\n\n/**\n * Params of the `textDocument/willSave` notification and the `textDocument/willSaveWaitUntil`\n * request.\n */\nexport type WillSaveTextDocumentParams = {\n  /**\n   * The document that will be saved.\n   */\n  textDocument: TextDocumentIdentifier;\n\n  /**\n   * The 'TextDocumentSaveReason'.\n   */\n  reason: TextDocumentSaveReason;\n};\n\n/**\n * Represents reasons why a text document is saved.\n */\nexport type TextDocumentSaveReason =\n  (typeof TextDocumentSaveReason)[keyof typeof TextDocumentSaveReason];\n/**\n * Represents reasons why a text document is saved.\n */\n\nexport namespace TextDocumentSaveReason {\n  /**\n   * Manually triggered, e.g. by the user pressing save, by starting debugging, or by an API call.\n   */\n  export const Manual = 1;\n\n  /**\n   * Automatic after a delay.\n   */\n  export const AfterDelay = 2;\n\n  /**\n   * When the editor lost focus.\n   */\n  export const FocusOut = 3;\n}\n\n/**\n * Save options.\n */\nexport type SaveOptions = {\n  /**\n   * The client is supposed to include the content on save.\n   */\n  includeText?: boolean;\n};\n\n/**\n * Text document save registration options.\n */\nexport interface TextDocumentSaveRegistrationOptions extends TextDocumentRegistrationOptions {\n  /**\n   * The client is supposed to include the content on save.\n   */\n  includeText?: boolean;\n}\n\n/**\n * Params of the `document/didSave` notification.\n */\nexport type DidSaveTextDocumentParams = {\n  /**\n   * The document that was saved.\n   */\n  textDocument: TextDocumentIdentifier;\n\n  /**\n   * Optional the content when saved. Depends on the includeText value\n   * when the save notification was requested.\n   */\n  text?: string;\n};\n\n/**\n * Params of the `document/didClose` notification.\n */\nexport type DidCloseTextDocumentParams = {\n  /**\n   * The document that was closed.\n   */\n  textDocument: TextDocumentIdentifier;\n};\n\nexport type TextDocumentSyncClientCapabilities = {\n  /**\n   * Whether text document synchronization supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * The client supports sending will save notifications.\n   */\n  willSave?: boolean;\n\n  /**\n   * The client supports sending a will save request and\n   * waits for a response providing text edits which will\n   * be applied to the document before it is saved.\n   */\n  willSaveWaitUntil?: boolean;\n\n  /**\n   * The client supports did save notifications.\n   */\n  didSave?: boolean;\n};\n\n/**\n * A notebook document.\n *\n * @since 3.17.0\n */\nexport type NotebookDocument = {\n  /**\n   * The notebook document's URI.\n   */\n  uri: URI;\n\n  /**\n   * The type of the notebook.\n   */\n  notebookType: string;\n\n  /**\n   * The version number of this document (it will increase after each\n   * change, including undo/redo).\n   */\n  version: integer;\n\n  /**\n   * Additional metadata stored with the notebook\n   * document.\n   */\n  metadata?: LSPObject;\n\n  /**\n   * The cells of a notebook.\n   */\n  cells: readonly NotebookCell[];\n};\n\n/**\n * A notebook cell.\n *\n * A cell's document URI must be unique across ALL notebook\n * cells and can therefore be used to uniquely identify a\n * notebook cell or the cell's text document.\n *\n * @since 3.17.0\n */\nexport type NotebookCell = {\n  /**\n   * The cell's kind\n   */\n  kind: NotebookCellKind;\n\n  /**\n   * The URI of the cell's text document\n   * content.\n   */\n  document: DocumentUri;\n\n  /**\n   * Additional metadata stored with the cell.\n   */\n  metadata?: LSPObject;\n\n  /**\n   * Additional execution summary information\n   * if supported by the client.\n   */\n  executionSummary?: ExecutionSummary;\n};\n\n/**\n * A notebook cell kind.\n *\n * @since 3.17.0\n */\nexport type NotebookCellKind = (typeof NotebookCellKind)[keyof typeof NotebookCellKind];\n/**\n * A notebook cell kind.\n *\n * @since 3.17.0\n */\n\nexport namespace NotebookCellKind {\n  /**\n   * A markup-cell is formatted source that is used for display.\n   */\n  export const Markup = 1;\n\n  /**\n   * A code-cell is source code.\n   */\n  export const Code = 2;\n}\n\n/**\n * Notebook execution summary.\n */\nexport type ExecutionSummary = {\n  /**\n   * A strict monotonically increasing value\n   * indicating the execution order of a cell\n   * inside a notebook.\n   */\n  executionOrder: uinteger;\n\n  /**\n   * Whether the execution was successful or\n   * not if known by the client.\n   */\n  success?: boolean;\n};\n\n/**\n * A notebook cell text document filter denotes a cell text\n * document by different properties.\n *\n * @since 3.17.0\n */\nexport type NotebookCellTextDocumentFilter = {\n  /**\n   * A filter that matches against the notebook\n   * containing the notebook cell. If a string\n   * value is provided it matches against the\n   * notebook type. '*' matches every notebook.\n   */\n  notebook: string | NotebookDocumentFilter;\n\n  /**\n   * A language id like `python`.\n   *\n   * Will be matched against the language id of the\n   * notebook cell document. '*' matches every language.\n   */\n  language?: string;\n};\n\n/**\n * A notebook document filter denotes a notebook document by\n * different properties.\n *\n * @since 3.17.0\n */\nexport type NotebookDocumentFilter =\n  | {\n      /** The type of the enclosing notebook. */\n      notebookType: string;\n\n      /** A Uri [scheme](#Uri.scheme), like `file` or `untitled`. */\n      scheme?: string;\n\n      /** A glob pattern. */\n      pattern?: string;\n    }\n  | {\n      /** The type of the enclosing notebook. */\n      notebookType?: string;\n\n      /** A Uri [scheme](#Uri.scheme), like `file` or `untitled`.*/\n      scheme: string;\n\n      /** A glob pattern. */\n      pattern?: string;\n    }\n  | {\n      /** The type of the enclosing notebook. */\n      notebookType?: string;\n\n      /** A Uri [scheme](#Uri.scheme), like `file` or `untitled`. */\n      scheme?: string;\n\n      /** A glob pattern. */\n      pattern: string;\n    };\n\n/**\n * Notebook specific client capabilities.\n *\n * @since 3.17.0\n */\nexport type NotebookDocumentSyncClientCapabilities = {\n  /**\n   * Whether implementation supports dynamic registration. If this is\n   * set to `true` the client supports the new\n   * `(NotebookDocumentSyncRegistrationOptions & NotebookDocumentSyncOptions)`\n   * return value for the corresponding server capability as well.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * The client supports sending execution summary data per cell.\n   */\n  executionSummarySupport?: boolean;\n};\n\n/**\n * Options specific to a notebook plus its cells\n * to be synced to the server.\n *\n * If a selector provides a notebook document\n * filter but no cell selector all cells of a\n * matching notebook document will be synced.\n *\n * If a selector provides no notebook document\n * filter but only a cell selector all notebook\n * documents that contain at least one matching\n * cell will be synced.\n *\n * @since 3.17.0\n */\nexport type NotebookDocumentSyncOptions = {\n  /**\n   * The notebooks to be synced\n   */\n  notebookSelector: (\n    | {\n        /**\n         * The notebook to be synced. If a string\n         * value is provided it matches against the\n         * notebook type. '*' matches every notebook.\n         */\n        notebook: string | NotebookDocumentFilter;\n\n        /**\n         * The cells of the matching notebook to be synced.\n         */\n        cells?: { language: string }[];\n      }\n    | {\n        /**\n         * The notebook to be synced. If a string\n         * value is provided it matches against the\n         * notebook type. '*' matches every notebook.\n         */\n        notebook?: string | NotebookDocumentFilter;\n\n        /**\n         * The cells of the matching notebook to be synced.\n         */\n        cells: { language: string }[];\n      }\n  )[];\n\n  /**\n   * Whether save notification should be forwarded to\n   * the server. Will only be honored if mode === `notebook`.\n   */\n  save?: boolean;\n};\n\n/**\n * Registration options specific to a notebook.\n *\n * @since 3.17.0\n */\nexport interface NotebookDocumentSyncRegistrationOptions\n  extends NotebookDocumentSyncOptions, StaticRegistrationOptions {}\n\n/**\n * Params of the `notebookDocument/didOpen` notification.\n *\n * @since 3.17.0\n */\nexport type DidOpenNotebookDocumentParams = {\n  /**\n   * The notebook document that got opened.\n   */\n  notebookDocument: NotebookDocument;\n\n  /**\n   * The text documents that represent the content\n   * of a notebook cell.\n   */\n  cellTextDocuments: TextDocumentItem[];\n};\n\n/**\n * Params of the `notebookDocument/didChange` notification.\n *\n * @since 3.17.0\n */\nexport type DidChangeNotebookDocumentParams = {\n  /**\n   * The notebook document that did change. The version number points\n   * to the version after all provided changes have been applied.\n   */\n  notebookDocument: VersionedNotebookDocumentIdentifier;\n\n  /**\n   * The actual changes to the notebook document.\n   *\n   * The change describes single state change to the notebook document.\n   * So it moves a notebook document, its cells and its cell text document\n   * contents from state S to S'.\n   *\n   * To mirror the content of a notebook using change events use the\n   * following approach:\n   * - start with the same initial content\n   * - apply the 'notebookDocument/didChange' notifications in the order\n   *   you receive them.\n   */\n  change: NotebookDocumentChangeEvent;\n};\n\n/**\n * A versioned notebook document identifier.\n *\n * @since 3.17.0\n */\nexport type VersionedNotebookDocumentIdentifier = {\n  /**\n   * The version number of this notebook document.\n   */\n  version: integer;\n\n  /**\n   * The notebook document's URI.\n   */\n  uri: URI;\n};\n\n/**\n * A change event for a notebook document.\n *\n * @since 3.17.0\n */\nexport type NotebookDocumentChangeEvent = {\n  /**\n   * The changed meta data if any.\n   */\n  metadata?: LSPObject;\n\n  /**\n   * Changes to cells\n   */\n  cells?: {\n    /**\n     * Changes to the cell structure to add or\n     * remove cells.\n     */\n    structure?: {\n      /**\n       * The change to the cell array.\n       */\n      array: NotebookCellArrayChange;\n\n      /**\n       * Additional opened cell text documents.\n       */\n      didOpen?: readonly TextDocumentItem[];\n\n      /**\n       * Additional closed cell text documents.\n       */\n      didClose?: readonly TextDocumentIdentifier[];\n    };\n\n    /**\n     * Changes to notebook cells properties like its\n     * kind, execution summary or metadata.\n     */\n    data?: readonly NotebookCell[];\n\n    /**\n     * Changes to the text content of notebook cells.\n     */\n    textContent?: readonly {\n      document: VersionedTextDocumentIdentifier;\n      changes: readonly TextDocumentContentChangeEvent[];\n    }[];\n  };\n};\n\n/**\n * A change describing how to move a `NotebookCell`\n * array from state S to S'.\n *\n * @since 3.17.0\n */\nexport type NotebookCellArrayChange = {\n  /**\n   * The start offset of the cell that changed.\n   */\n  start: uinteger;\n\n  /**\n   * The deleted cells\n   */\n  deleteCount: uinteger;\n\n  /**\n   * The new cells, if any\n   */\n  cells?: readonly NotebookCell[];\n};\n\n/**\n * Params of the `notebookDocument/didSave` notification.\n *\n * @since 3.17.0\n */\nexport type DidSaveNotebookDocumentParams = {\n  /**\n   * The notebook document that got saved.\n   */\n  notebookDocument: NotebookDocumentIdentifier;\n};\n\n/**\n * Params of the `notebookDocument/didClose` notification.\n *\n * @since 3.17.0\n */\nexport type DidCloseNotebookDocumentParams = {\n  /**\n   * The notebook document that got closed.\n   */\n  notebookDocument: NotebookDocumentIdentifier;\n\n  /**\n   * The text documents that represent the content\n   * of a notebook cell that got closed.\n   */\n  cellTextDocuments: TextDocumentIdentifier[];\n};\n\n/**\n * A literal to identify a notebook document in the client.\n *\n * @since 3.17.0\n */\nexport type NotebookDocumentIdentifier = {\n  /**\n   * The notebook document's URI.\n   */\n  uri: URI;\n};\n\n/*********************\n * Language Features *\n *********************/\nexport type DeclarationClientCapabilities = {\n  /**\n   * Whether declaration supports dynamic registration. If this is set to\n   * `true` the client supports the new `DeclarationRegistrationOptions`\n   * return value for the corresponding server capability as well.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * The client supports additional metadata in the form of declaration links.\n   */\n  linkSupport?: boolean;\n};\n\nexport interface DeclarationOptions extends WorkDoneProgressOptions {}\n\nexport interface DeclarationRegistrationOptions\n  extends DeclarationOptions, TextDocumentRegistrationOptions, StaticRegistrationOptions {}\n\n/**\n * Params of the `textDocument/declaration` request.\n */\nexport interface DeclarationParams\n  extends TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams {}\n\nexport type DefinitionClientCapabilities = {\n  /**\n   * Whether definition supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * The client supports additional metadata in the form of definition links.\n   *\n   * @since 3.14.0\n   */\n  linkSupport?: boolean;\n};\n\nexport interface DefinitionOptions extends WorkDoneProgressOptions {}\n\nexport interface DefinitionRegistrationOptions\n  extends TextDocumentRegistrationOptions, DefinitionOptions {}\n\n/**\n * Params of the `textDocument/definition` request.\n */\nexport interface DefinitionParams\n  extends TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams {}\n\nexport type TypeDefinitionClientCapabilities = {\n  /**\n   * Whether implementation supports dynamic registration. If this is set to\n   * `true` the client supports the new `TypeDefinitionRegistrationOptions`\n   * return value for the corresponding server capability as well.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * The client supports additional metadata in the form of definition links.\n   *\n   * @since 3.14.0\n   */\n  linkSupport?: boolean;\n};\n\nexport interface TypeDefinitionOptions extends WorkDoneProgressOptions {}\n\nexport interface TypeDefinitionRegistrationOptions\n  extends TextDocumentRegistrationOptions, TypeDefinitionOptions, StaticRegistrationOptions {}\n\n/**\n * Params of the `textDocument/typeDefinition` request.\n */\nexport interface TypeDefinitionParams\n  extends TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams {}\n\nexport type ImplementationClientCapabilities = {\n  /**\n   * Whether implementation supports dynamic registration. If this is set to\n   * `true` the client supports the new `ImplementationRegistrationOptions`\n   * return value for the corresponding server capability as well.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * The client supports additional metadata in the form of definition links.\n   *\n   * @since 3.14.0\n   */\n  linkSupport?: boolean;\n};\n\nexport interface ImplementationOptions extends WorkDoneProgressOptions {}\n\nexport interface ImplementationRegistrationOptions\n  extends TextDocumentRegistrationOptions, ImplementationOptions, StaticRegistrationOptions {}\n\n/**\n * Params of the `textDocument/implementation` request.\n */\nexport interface ImplementationParams\n  extends TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams {}\n\nexport type ReferenceClientCapabilities = {\n  /**\n   * Whether references supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n};\n\nexport interface ReferenceOptions extends WorkDoneProgressOptions {}\n\nexport interface ReferenceRegistrationOptions\n  extends TextDocumentRegistrationOptions, ReferenceOptions {}\n\n/**\n * Params of the `textDocument/references` request.\n */\nexport interface ReferenceParams\n  extends TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams {\n  context: ReferenceContext;\n}\n\nexport type ReferenceContext = {\n  /**\n   * Include the declaration of the current symbol.\n   */\n  includeDeclaration: boolean;\n};\n\nexport type CallHierarchyClientCapabilities = {\n  /**\n   * Whether implementation supports dynamic registration. If this is set to\n   * `true` the client supports the new `(TextDocumentRegistrationOptions &\n   * StaticRegistrationOptions)` return value for the corresponding server\n   * capability as well.\n   */\n  dynamicRegistration?: boolean;\n};\n\nexport interface CallHierarchyOptions extends WorkDoneProgressOptions {}\n\nexport interface CallHierarchyRegistrationOptions\n  extends TextDocumentRegistrationOptions, CallHierarchyOptions, StaticRegistrationOptions {}\n\n/**\n * Params of the `textDocument/prepareCallHierarchy` request.\n */\nexport interface CallHierarchyPrepareParams\n  extends TextDocumentPositionParams, WorkDoneProgressParams {}\n\nexport type CallHierarchyItem = {\n  /**\n   * The name of this item.\n   */\n  name: string;\n\n  /**\n   * The kind of this item.\n   */\n  kind: SymbolKind;\n\n  /**\n   * Tags for this item.\n   */\n  tags?: readonly SymbolTag[];\n\n  /**\n   * More detail for this item, e.g. the signature of a function.\n   */\n  detail?: string;\n\n  /**\n   * The resource identifier of this item.\n   */\n  uri: DocumentUri;\n\n  /**\n   * The range enclosing this symbol not including leading/trailing whitespace\n   * but everything else, e.g. comments and code.\n   */\n  range: Range;\n\n  /**\n   * The range that should be selected and revealed when this symbol is being\n   * picked, e.g. the name of a function. Must be contained by the\n   * [`range`](#CallHierarchyItem.range).\n   */\n  selectionRange: Range;\n\n  /**\n   * A data entry field that is preserved between a call hierarchy prepare and\n   * incoming calls or outgoing calls requests.\n   */\n  data?: unknown;\n};\n\n/**\n * Params of the `callHierarchy/incomingCalls` request.\n */\nexport interface CallHierarchyIncomingCallsParams\n  extends WorkDoneProgressParams, PartialResultParams {\n  item: CallHierarchyItem;\n}\n\nexport type CallHierarchyIncomingCall = {\n  /**\n   * The item that makes the call.\n   */\n  from: CallHierarchyItem;\n\n  /**\n   * The ranges at which the calls appear. This is relative to the caller\n   * denoted by [`this.from`](#CallHierarchyIncomingCall.from).\n   */\n  fromRanges: readonly Range[];\n};\n\n/**\n * Params of the `callHierarchy/outgoingCalls` request.\n */\nexport interface CallHierarchyOutgoingCallsParams\n  extends WorkDoneProgressParams, PartialResultParams {\n  item: CallHierarchyItem;\n}\n\nexport type CallHierarchyOutgoingCall = {\n  /**\n   * The item that is called.\n   */\n  to: CallHierarchyItem;\n\n  /**\n   * The range at which this item is called. This is the range relative to\n   * the caller, e.g the item passed to `callHierarchy/outgoingCalls` request.\n   */\n  fromRanges: readonly Range[];\n};\n\nexport type TypeHierarchyClientCapabilities = {\n  /**\n   * Whether implementation supports dynamic registration. If this is set to\n   * `true` the client supports the new `(TextDocumentRegistrationOptions &\n   * StaticRegistrationOptions)` return value for the corresponding server\n   * capability as well.\n   */\n  dynamicRegistration?: boolean;\n};\n\nexport interface TypeHierarchyOptions extends WorkDoneProgressOptions {}\n\nexport interface TypeHierarchyRegistrationOptions\n  extends TextDocumentRegistrationOptions, TypeHierarchyOptions, StaticRegistrationOptions {}\n\n/**\n * Params of the `textDocument/prepareTypeHierarchy` request.\n */\nexport interface TypeHierarchyPrepareParams\n  extends TextDocumentPositionParams, WorkDoneProgressParams {}\n\nexport type TypeHierarchyItem = {\n  /**\n   * The name of this item.\n   */\n  name: string;\n\n  /**\n   * The kind of this item.\n   */\n  kind: SymbolKind;\n\n  /**\n   * Tags for this item.\n   */\n  tags?: SymbolTag[];\n\n  /**\n   * More detail for this item, e.g. the signature of a function.\n   */\n  detail?: string;\n\n  /**\n   * The resource identifier of this item.\n   */\n  uri: DocumentUri;\n\n  /**\n   * The range enclosing this symbol not including leading/trailing whitespace\n   * but everything else, e.g. comments and code.\n   */\n  range: Range;\n\n  /**\n   * The range that should be selected and revealed when this symbol is being\n   * picked, e.g. the name of a function. Must be contained by the\n   * [`range`](#TypeHierarchyItem.range).\n   */\n  selectionRange: Range;\n\n  /**\n   * A data entry field that is preserved between a type hierarchy prepare and\n   * supertypes or subtypes requests. It could also be used to identify the\n   * type hierarchy in the server, helping improve the performance on\n   * resolving supertypes and subtypes.\n   */\n  data?: LSPAny;\n};\n\n/**\n * Params of the `typeHierarchy/supertypes` request.\n */\nexport interface TypeHierarchySupertypesParams extends WorkDoneProgressParams, PartialResultParams {\n  item: TypeHierarchyItem;\n}\n\n/**\n * Params of the `typeHierarchy/subtypes` request.\n */\nexport interface TypeHierarchySubtypesParams extends WorkDoneProgressParams, PartialResultParams {\n  item: TypeHierarchyItem;\n}\n\nexport type DocumentHighlightClientCapabilities = {\n  /**\n   * Whether document highlight supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n};\n\nexport interface DocumentHighlightOptions extends WorkDoneProgressOptions {}\n\nexport interface DocumentHighlightRegistrationOptions\n  extends TextDocumentRegistrationOptions, DocumentHighlightOptions {}\n\n/**\n * Params of the `textDocument/documentHighlight` request.\n */\nexport interface DocumentHighlightParams\n  extends TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams {}\n\n/**\n * A document highlight is a range inside a text document which deserves\n * special attention. Usually a document highlight is visualized by changing\n * the background color of its range.\n *\n */\nexport type DocumentHighlight = {\n  /**\n   * The range this highlight applies to.\n   */\n  range: Range;\n\n  /**\n   * The highlight kind, default is DocumentHighlightKind.Text.\n   */\n  kind?: DocumentHighlightKind;\n};\n\n/**\n * A document highlight kind.\n */\nexport type DocumentHighlightKind =\n  (typeof DocumentHighlightKind)[keyof typeof DocumentHighlightKind];\n/**\n * A document highlight kind.\n */\n\nexport namespace DocumentHighlightKind {\n  /**\n   * A textual occurrence.\n   */\n  export const Text = 1;\n\n  /**\n   * Read-access of a symbol, like reading a variable.\n   */\n  export const Read = 2;\n\n  /**\n   * Write-access of a symbol, like writing to a variable.\n   */\n  export const Write = 3;\n}\n\nexport type DocumentLinkClientCapabilities = {\n  /**\n   * Whether document link supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * Whether the client supports the `tooltip` property on `DocumentLink`.\n   *\n   * @since 3.15.0\n   */\n  tooltipSupport?: boolean;\n};\n\nexport interface DocumentLinkOptions extends WorkDoneProgressOptions {\n  /**\n   * Document links have a resolve provider as well.\n   */\n  resolveProvider?: boolean;\n}\n\nexport interface DocumentLinkRegistrationOptions\n  extends TextDocumentRegistrationOptions, DocumentLinkOptions {}\n\n/**\n * Params of the `textDocument/documentLink` request.\n */\nexport interface DocumentLinkParams extends WorkDoneProgressParams, PartialResultParams {\n  /**\n   * The document to provide document links for.\n   */\n  textDocument: TextDocumentIdentifier;\n}\n\n/**\n * A document link is a range in a text document that links to an internal or\n * external resource, like another text document or a web site.\n */\nexport type DocumentLink = {\n  /**\n   * The range this link applies to.\n   */\n  range: Range;\n\n  /**\n   * The uri this link points to. If missing a resolve request is sent later.\n   */\n  target?: URI;\n\n  /**\n   * The tooltip text when you hover over this link.\n   *\n   * If a tooltip is provided, is will be displayed in a string that includes\n   * instructions on how to trigger the link, such as `{0} (ctrl + click)`.\n   * The specific instructions vary depending on OS, user settings, and\n   * localization.\n   *\n   * @since 3.15.0\n   */\n  tooltip?: string;\n\n  /**\n   * A data entry field that is preserved on a document link between a\n   * DocumentLinkRequest and a DocumentLinkResolveRequest.\n   */\n  data?: LSPAny;\n};\n\nexport type HoverClientCapabilities = {\n  /**\n   * Whether hover supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * Client supports the follow content formats if the content\n   * property refers to a `literal of type MarkupContent`.\n   * The order describes the preferred format of the client.\n   */\n  contentFormat?: MarkupKind[];\n};\n\nexport interface HoverOptions extends WorkDoneProgressOptions {}\n\nexport interface HoverRegistrationOptions extends TextDocumentRegistrationOptions, HoverOptions {}\n\n/**\n * Params of the `textDocument/hover` request.\n */\nexport interface HoverParams extends TextDocumentPositionParams, WorkDoneProgressParams {}\n\n/**\n * The result of a hover request.\n */\nexport type Hover = {\n  /**\n   * The hover's content\n   */\n  contents: MarkedString | readonly MarkedString[] | MarkupContent;\n\n  /**\n   * An optional range is a range inside a text document\n   * that is used to visualize a hover, e.g. by changing the background color.\n   */\n  range?: Range;\n};\n\n/**\n * MarkedString can be used to render human readable text. It is either a\n * markdown string or a code-block that provides a language and a code snippet.\n * The language identifier is semantically equal to the optional language\n * identifier in fenced code blocks in GitHub issues.\n *\n * The pair of a language and a value is an equivalent to markdown:\n *\n * ```${language}\n * ${value}\n * ```\n *\n * Note that markdown strings will be sanitized - that means html will be\n * escaped.\n *\n * @deprecated use MarkupContent instead.\n */\ntype MarkedString = string | { language: string; value: string };\n\nexport type CodeLensClientCapabilities = {\n  /**\n   * Whether code lens supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n};\n\nexport interface CodeLensOptions extends WorkDoneProgressOptions {\n  /**\n   * Code lens has a resolve provider as well.\n   */\n  resolveProvider?: boolean;\n}\n\nexport interface CodeLensRegistrationOptions\n  extends TextDocumentRegistrationOptions, CodeLensOptions {}\n\n/**\n * Params of the `textDocument/codeLens` request.\n */\nexport interface CodeLensParams extends WorkDoneProgressParams, PartialResultParams {\n  /**\n   * The document to request code lens for.\n   */\n  textDocument: TextDocumentIdentifier;\n}\n\n/**\n * A code lens represents a command that should be shown along with\n * source text, like the number of references, a way to run tests, etc.\n *\n * A code lens is _unresolved_ when no command is associated to it. For\n * performance reasons the creation of a code lens and resolving should be done\n * in two stages.\n */\nexport type CodeLens = {\n  /**\n   * The range in which this code lens is valid. Should only span a single\n   * line.\n   */\n  range: Range;\n\n  /**\n   * The command this code lens represents.\n   */\n  command?: Command;\n\n  /**\n   * A data entry field that is preserved on a code lens item between\n   * a code lens and a code lens resolve request.\n   */\n  data?: LSPAny;\n};\n\nexport type CodeLensWorkspaceClientCapabilities = {\n  /**\n   * Whether the client implementation supports a refresh request sent from the\n   * server to the client.\n   *\n   * Note that this event is global and will force the client to refresh all\n   * code lenses currently shown. It should be used with absolute care and is\n   * useful for situation where a server for example detect a project wide\n   * change that requires such a calculation.\n   */\n  refreshSupport?: boolean;\n};\n\nexport type FoldingRangeClientCapabilities = {\n  /**\n   * Whether implementation supports dynamic registration for folding range\n   * providers. If this is set to `true` the client supports the new\n   * `FoldingRangeRegistrationOptions` return value for the corresponding\n   * server capability as well.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * The maximum number of folding ranges that the client prefers to receive\n   * per document. The value serves as a hint, servers are free to follow the\n   * limit.\n   */\n  rangeLimit?: uinteger;\n\n  /**\n   * If set, the client signals that it only supports folding complete lines.\n   * If set, client will ignore specified `startCharacter` and `endCharacter`\n   * properties in a FoldingRange.\n   */\n  lineFoldingOnly?: boolean;\n\n  /**\n   * Specific options for the folding range kind.\n   *\n   * @since 3.17.0\n   */\n  foldingRangeKind?: {\n    /**\n     * The folding range kind values the client supports. When this\n     * property exists the client also guarantees that it will\n     * handle values outside its set gracefully and falls back\n     * to a default value when unknown.\n     */\n    valueSet?: readonly FoldingRangeKind[];\n  };\n\n  /**\n   * Specific options for the folding range.\n   * @since 3.17.0\n   */\n  foldingRange?: {\n    /**\n     * If set, the client signals that it supports setting collapsedText on\n     * folding ranges to display custom labels instead of the default text.\n     *\n     * @since 3.17.0\n     */\n    collapsedText?: boolean;\n  };\n};\n\nexport interface FoldingRangeOptions extends WorkDoneProgressOptions {}\n\nexport interface FoldingRangeRegistrationOptions\n  extends TextDocumentRegistrationOptions, FoldingRangeOptions, StaticRegistrationOptions {}\n\n/**\n * Params of the `textDocument/foldingRange` request.\n */\nexport interface FoldingRangeParams extends WorkDoneProgressParams, PartialResultParams {\n  /**\n   * The text document.\n   */\n  textDocument: TextDocumentIdentifier;\n}\n\n/**\n * A set of predefined range kinds.\n *\n * The type is a string since the value set is extensible\n */\nexport type FoldingRangeKind =\n  | (typeof FoldingRangeKind)[keyof typeof FoldingRangeKind]\n  | (string & NonNullable<unknown>);\n/**\n * A set of predefined range kinds.\n */\n\nexport namespace FoldingRangeKind {\n  /**\n   * Folding range for a comment\n   */\n  export const Comment = \"comment\";\n\n  /**\n   * Folding range for imports or includes\n   */\n  export const Imports = \"imports\";\n\n  /**\n   * Folding range for a region (e.g. `#region`)\n   */\n  export const Region = \"region\";\n}\n\n/**\n * Represents a folding range. To be valid, start and end line must be bigger\n * than zero and smaller than the number of lines in the document. Clients\n * are free to ignore invalid ranges.\n */\nexport type FoldingRange = {\n  /**\n   * The zero-based start line of the range to fold. The folded area starts\n   * after the line's last character. To be valid, the end must be zero or\n   * larger and smaller than the number of lines in the document.\n   */\n  startLine: uinteger;\n\n  /**\n   * The zero-based character offset from where the folded range starts. If\n   * not defined, defaults to the length of the start line.\n   */\n  startCharacter?: uinteger;\n\n  /**\n   * The zero-based end line of the range to fold. The folded area ends with\n   * the line's last character. To be valid, the end must be zero or larger\n   * and smaller than the number of lines in the document.\n   */\n  endLine: uinteger;\n\n  /**\n   * The zero-based character offset before the folded range ends. If not\n   * defined, defaults to the length of the end line.\n   */\n  endCharacter?: uinteger;\n\n  /**\n   * Describes the kind of the folding range such as `comment` or `region`.\n   * The kind is used to categorize folding ranges and used by commands like\n   * 'Fold all comments'. See [FoldingRangeKind](#FoldingRangeKind) for an\n   * enumeration of standardized kinds.\n   */\n  kind?: FoldingRangeKind;\n\n  /**\n   * The text that the client should show when the specified range is\n   * collapsed. If not defined or not supported by the client, a default\n   * will be chosen by the client.\n   *\n   * @since 3.17.0 - proposed\n   */\n  collapsedText?: string;\n};\n\nexport type SelectionRangeClientCapabilities = {\n  /**\n   * Whether implementation supports dynamic registration for selection range\n   * providers. If this is set to `true` the client supports the new\n   * `SelectionRangeRegistrationOptions` return value for the corresponding\n   * server capability as well.\n   */\n  dynamicRegistration?: boolean;\n};\n\nexport interface SelectionRangeOptions extends WorkDoneProgressOptions {}\n\nexport interface SelectionRangeRegistrationOptions\n  extends SelectionRangeOptions, TextDocumentRegistrationOptions, StaticRegistrationOptions {}\n\n/**\n * Params of the `textDocument/selectionRange` request.\n */\nexport interface SelectionRangeParams extends WorkDoneProgressParams, PartialResultParams {\n  /**\n   * The text document.\n   */\n  textDocument: TextDocumentIdentifier;\n\n  /**\n   * The positions inside the text document.\n   */\n  positions: readonly Position[];\n}\n\nexport type SelectionRange = {\n  /**\n   * The [range](#Range) of this selection range.\n   */\n  range: Range;\n\n  /**\n   * The parent selection range containing this range. Therefore\n   * `parent.range` must contain `this.range`.\n   */\n  parent?: SelectionRange;\n};\n\nexport type DocumentSymbolClientCapabilities = {\n  /**\n   * Whether document symbol supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * Specific capabilities for the `SymbolKind` in the\n   * `textDocument/documentSymbol` request.\n   */\n  symbolKind?: {\n    /**\n     * The symbol kind values the client supports. When this\n     * property exists the client also guarantees that it will\n     * handle values outside its set gracefully and falls back\n     * to a default value when unknown.\n     *\n     * If this property is not present the client only supports\n     * the symbol kinds from `File` to `Array` as defined in\n     * the initial version of the protocol.\n     */\n    valueSet?: readonly SymbolKind[];\n  };\n\n  /**\n   * The client supports hierarchical document symbols.\n   */\n  hierarchicalDocumentSymbolSupport?: boolean;\n\n  /**\n   * The client supports tags on `SymbolInformation`. Tags are supported on\n   * `DocumentSymbol` if `hierarchicalDocumentSymbolSupport` is set to true.\n   * Clients supporting tags have to handle unknown tags gracefully.\n   *\n   * @since 3.16.0\n   */\n  tagSupport?: {\n    /**\n     * The tags supported by the client.\n     */\n    valueSet: readonly SymbolTag[];\n  };\n\n  /**\n   * The client supports an additional label presented in the UI when\n   * registering a document symbol provider.\n   *\n   * @since 3.16.0\n   */\n  labelSupport?: boolean;\n};\n\nexport interface DocumentSymbolOptions extends WorkDoneProgressOptions {\n  /**\n   * A human-readable string that is shown when multiple outlines trees\n   * are shown for the same document.\n   *\n   * @since 3.16.0\n   */\n  label?: string;\n}\n\nexport interface DocumentSymbolRegistrationOptions\n  extends TextDocumentRegistrationOptions, DocumentSymbolOptions {}\n\n/**\n * Params of the `textDocument/documentSymbol` request.\n */\nexport interface DocumentSymbolParams extends WorkDoneProgressParams, PartialResultParams {\n  /**\n   * The text document.\n   */\n  textDocument: TextDocumentIdentifier;\n}\n\n/**\n * A symbol kind.\n */\nexport type SymbolKind = (typeof SymbolKind)[keyof typeof SymbolKind];\n/**\n * Symbol kinds.\n */\n\nexport namespace SymbolKind {\n  export const File = 1;\n  export const Module = 2;\n  export const Namespace = 3;\n  export const Package = 4;\n  export const Class = 5;\n  export const Method = 6;\n  export const Property = 7;\n  export const Field = 8;\n  export const Constructor = 9;\n  export const Enum = 10;\n  export const Interface = 11;\n  export const Function = 12;\n  export const Variable = 13;\n  export const Constant = 14;\n  export const String = 15;\n  export const Number = 16;\n  export const Boolean = 17;\n  export const Array = 18;\n  export const Object = 19;\n  export const Key = 20;\n  export const Null = 21;\n  export const EnumMember = 22;\n  export const Struct = 23;\n  export const Event = 24;\n  export const Operator = 25;\n  export const TypeParameter = 26;\n}\n\n/**\n * A symbol tag.\n */\nexport type SymbolTag = (typeof SymbolTag)[keyof typeof SymbolTag];\n/**\n * Symbol tags are extra annotations that tweak the rendering of a symbol.\n *\n * @since 3.16\n */\n\nexport namespace SymbolTag {\n  /**\n   * Render a symbol as obsolete, usually using a strike-out.\n   */\n  export const Deprecated = 1;\n}\n\n/**\n * Represents programming constructs like variables, classes, interfaces etc.\n * that appear in a document. Document symbols can be hierarchical and they\n * have two ranges: one that encloses its definition and one that points to its\n * most interesting range, e.g. the range of an identifier.\n */\nexport type DocumentSymbol = {\n  /**\n   * The name of this symbol. Will be displayed in the user interface and\n   * therefore must not be an empty string or a string only consisting of\n   * white spaces.\n   */\n  name: string;\n\n  /**\n   * More detail for this symbol, e.g the signature of a function.\n   */\n  detail?: string;\n\n  /**\n   * The kind of this symbol.\n   */\n  kind: SymbolKind;\n\n  /**\n   * Tags for this document symbol.\n   *\n   * @since 3.16.0\n   */\n  tags?: readonly SymbolTag[];\n\n  /**\n   * Indicates if this symbol is deprecated.\n   *\n   * @deprecated Use tags instead\n   */\n  deprecated?: boolean;\n\n  /**\n   * The range enclosing this symbol not including leading/trailing whitespace\n   * but everything else like comments. This information is typically used to\n   * determine if the clients cursor is inside the symbol to reveal in the\n   * symbol in the UI.\n   */\n  range: Range;\n\n  /**\n   * The range that should be selected and revealed when this symbol is being\n   * picked, e.g. the name of a function. Must be contained by the `range`.\n   */\n  selectionRange: Range;\n\n  /**\n   * Children of this symbol, e.g. properties of a class.\n   */\n  children?: readonly DocumentSymbol[];\n};\n\n/**\n * Represents information about programming constructs like variables, classes,\n * interfaces etc.\n *\n * @deprecated use DocumentSymbol or WorkspaceSymbol instead.\n */\nexport type SymbolInformation = {\n  /**\n   * The name of this symbol.\n   */\n  name: string;\n\n  /**\n   * The kind of this symbol.\n   */\n  kind: SymbolKind;\n\n  /**\n   * Tags for this symbol.\n   *\n   * @since 3.16.0\n   */\n  tags?: readonly SymbolTag[];\n\n  /**\n   * Indicates if this symbol is deprecated.\n   *\n   * @deprecated Use tags instead\n   */\n  deprecated?: boolean;\n\n  /**\n   * The location of this symbol. The location's range is used by a tool\n   * to reveal the location in the editor. If the symbol is selected in the\n   * tool the range's start information is used to position the cursor. So\n   * the range usually spans more then the actual symbol's name and does\n   * normally include things like visibility modifiers.\n   *\n   * The range doesn't have to denote a node range in the sense of an abstract\n   * syntax tree. It can therefore not be used to re-construct a hierarchy of\n   * the symbols.\n   */\n  location: Location;\n\n  /**\n   * The name of the symbol containing this symbol. This information is for\n   * user interface purposes (e.g. to render a qualifier in the user interface\n   * if necessary). It can't be used to re-infer a hierarchy for the document\n   * symbols.\n   */\n  containerName?: string;\n};\n\nexport enum SemanticTokenTypes {\n  namespace = \"namespace\",\n  /**\n   * Represents a generic type. Acts as a fallback for types which\n   * can't be mapped to a specific type like class or enum.\n   */\n  type = \"type\",\n  class = \"class\",\n  enum = \"enum\",\n  interface = \"interface\",\n  struct = \"struct\",\n  typeParameter = \"typeParameter\",\n  parameter = \"parameter\",\n  variable = \"variable\",\n  property = \"property\",\n  enumMember = \"enumMember\",\n  event = \"event\",\n  function = \"function\",\n  method = \"method\",\n  macro = \"macro\",\n  keyword = \"keyword\",\n  modifier = \"modifier\",\n  comment = \"comment\",\n  string = \"string\",\n  number = \"number\",\n  regexp = \"regexp\",\n  operator = \"operator\",\n  /**\n   * @since 3.17.0\n   */\n  decorator = \"decorator\",\n}\n\nexport enum SemanticTokenModifiers {\n  declaration = \"declaration\",\n  definition = \"definition\",\n  readonly = \"readonly\",\n  static = \"static\",\n  deprecated = \"deprecated\",\n  abstract = \"abstract\",\n  async = \"async\",\n  modification = \"modification\",\n  documentation = \"documentation\",\n  defaultLibrary = \"defaultLibrary\",\n}\n\nexport type TokenFormat = (typeof TokenFormat)[keyof typeof TokenFormat];\n\nexport namespace TokenFormat {\n  export const Relative = \"relative\";\n}\n\nexport type SemanticTokensLegend = {\n  /**\n   * The token types a server uses.\n   */\n  tokenTypes: readonly string[];\n\n  /**\n   * The token modifiers a server uses.\n   */\n  tokenModifiers: readonly string[];\n};\n\ntype SemanticTokensClientCapabilities = {\n  /**\n   * Whether implementation supports dynamic registration. If this is set to\n   * `true` the client supports the new `(TextDocumentRegistrationOptions &\n   * StaticRegistrationOptions)` return value for the corresponding server\n   * capability as well.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * Which requests the client supports and might send to the server\n   * depending on the server's capability. Please note that clients might not\n   * show semantic tokens or degrade some of the user experience if a range\n   * or full request is advertised by the client but not provided by the\n   * server. If for example the client capability `requests.full` and\n   * `request.range` are both set to true but the server only provides a\n   * range provider the client might not render a minimap correctly or might\n   * even decide to not show any semantic tokens at all.\n   */\n  requests: {\n    /**\n     * The client will send the `textDocument/semanticTokens/range` request\n     * if the server provides a corresponding handler.\n     */\n    range?: boolean | NonNullable<unknown>;\n\n    /**\n     * The client will send the `textDocument/semanticTokens/full` request\n     * if the server provides a corresponding handler.\n     */\n    full?:\n      | boolean\n      | {\n          /**\n           * The client will send the `textDocument/semanticTokens/full/delta`\n           * request if the server provides a corresponding handler.\n           */\n          delta?: boolean;\n        };\n  };\n\n  /**\n   * The token types that the client supports.\n   */\n  tokenTypes: readonly string[];\n\n  /**\n   * The token modifiers that the client supports.\n   */\n  tokenModifiers: readonly string[];\n\n  /**\n   * The formats the clients supports.\n   */\n  formats: readonly TokenFormat[];\n\n  /**\n   * Whether the client supports tokens that can overlap each other.\n   */\n  overlappingTokenSupport?: boolean;\n\n  /**\n   * Whether the client supports tokens that can span multiple lines.\n   */\n  multilineTokenSupport?: boolean;\n\n  /**\n   * Whether the client allows the server to actively cancel a\n   * semantic token request, e.g. supports returning\n   * ErrorCodes.ServerCancelled. If a server does the client\n   * needs to retrigger the request.\n   *\n   * @since 3.17.0\n   */\n  serverCancelSupport?: boolean;\n\n  /**\n   * Whether the client uses semantic tokens to augment existing\n   * syntax tokens. If set to `true` client side created syntax\n   * tokens and semantic tokens are both used for colorization. If\n   * set to `false` the client only uses the returned semantic tokens\n   * for colorization.\n   *\n   * If the value is `undefined` then the client behavior is not\n   * specified.\n   *\n   * @since 3.17.0\n   */\n  augmentsSyntaxTokens?: boolean;\n};\n\nexport interface SemanticTokensOptions extends WorkDoneProgressOptions {\n  /**\n   * The legend used by the server\n   */\n  legend: SemanticTokensLegend;\n\n  /**\n   * Server supports providing semantic tokens for a specific range\n   * of a document.\n   */\n  range?: boolean | NonNullable<unknown>;\n\n  /**\n   * Server supports providing semantic tokens for a full document.\n   */\n  full?:\n    | boolean\n    | {\n        /**\n         * The server supports deltas for full documents.\n         */\n        delta?: boolean;\n      };\n}\n\nexport interface SemanticTokensRegistrationOptions\n  extends TextDocumentRegistrationOptions, SemanticTokensOptions, StaticRegistrationOptions {}\n\n/**\n * Params of the `textDocument/semanticTokens/full` request.\n */\nexport interface SemanticTokensParams extends WorkDoneProgressParams, PartialResultParams {\n  /**\n   * The text document.\n   */\n  textDocument: TextDocumentIdentifier;\n}\n\nexport type SemanticTokens = {\n  /**\n   * An optional result id. If provided and clients support delta updating\n   * the client will include the result id in the next semantic token request.\n   * A server can then instead of computing all semantic tokens again simply\n   * send a delta.\n   */\n  resultId?: string;\n\n  /**\n   * The actual tokens.\n   */\n  data: readonly uinteger[];\n};\n\nexport type SemanticTokensPartialResult = {\n  data: readonly uinteger[];\n};\n\n/**\n * Params of the `textDocument/semanticTokens/full/delta` request.\n */\nexport interface SemanticTokensDeltaParams extends WorkDoneProgressParams, PartialResultParams {\n  /**\n   * The text document.\n   */\n  textDocument: TextDocumentIdentifier;\n\n  /**\n   * The result id of a previous response. The result Id can either point to\n   * a full response or a delta response depending on what was received last.\n   */\n  previousResultId: string;\n}\n\nexport type SemanticTokensDelta = {\n  readonly resultId?: string;\n  /**\n   * The semantic token edits to transform a previous result into a new\n   * result.\n   */\n  edits: readonly SemanticTokensEdit[];\n};\n\nexport type SemanticTokensEdit = {\n  /**\n   * The start offset of the edit.\n   */\n  start: uinteger;\n\n  /**\n   * The count of elements to remove.\n   */\n  deleteCount: uinteger;\n\n  /**\n   * The elements to insert.\n   */\n  data?: readonly uinteger[];\n};\n\nexport type SemanticTokensDeltaPartialResult = {\n  edits: readonly SemanticTokensEdit[];\n};\n\n/**\n * Params of the `textDocument/semanticTokens/range` request.\n */\nexport interface SemanticTokensRangeParams extends WorkDoneProgressParams, PartialResultParams {\n  /**\n   * The text document.\n   */\n  textDocument: TextDocumentIdentifier;\n\n  /**\n   * The range the semantic tokens are requested for.\n   */\n  range: Range;\n}\n\nexport type SemanticTokensWorkspaceClientCapabilities = {\n  /**\n   * Whether the client implementation supports a refresh request sent from\n   * the server to the client.\n   *\n   * Note that this event is global and will force the client to refresh all\n   * semantic tokens currently shown. It should be used with absolute care\n   * and is useful for situation where a server for example detect a project\n   * wide change that requires such a calculation.\n   */\n  refreshSupport?: boolean;\n};\n\n/**\n * Inlay hint client capabilities.\n *\n * @since 3.17.0\n */\nexport type InlayHintClientCapabilities = {\n  /**\n   * Whether inlay hints support dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * Indicates which properties a client can resolve lazily on an inlay\n   * hint.\n   */\n  resolveSupport?: {\n    /**\n     * The properties that a client can resolve lazily.\n     */\n    properties: readonly string[];\n  };\n};\n\n/**\n * Inlay hint options used during static registration.\n *\n * @since 3.17.0\n */\nexport interface InlayHintOptions extends WorkDoneProgressOptions {\n  /**\n   * The server provides support to resolve additional\n   * information for an inlay hint item.\n   */\n  resolveProvider?: boolean;\n}\n\n/**\n * Inlay hint options used during static or dynamic registration.\n *\n * @since 3.17.0\n */\nexport interface InlayHintRegistrationOptions\n  extends InlayHintOptions, TextDocumentRegistrationOptions, StaticRegistrationOptions {}\n\n/**\n * Params of the `textDocument/inlayHint` request.\n *\n * @since 3.17.0\n */\nexport interface InlayHintParams extends WorkDoneProgressParams {\n  /**\n   * The text document.\n   */\n  textDocument: TextDocumentIdentifier;\n\n  /**\n   * The visible document range for which inlay hints should be computed.\n   */\n  range: Range;\n}\n\n/**\n * Inlay hint information.\n *\n * @since 3.17.0\n */\nexport type InlayHint = {\n  /**\n   * The position of this hint.\n   *\n   * If multiple hints have the same position, they will be shown in the order\n   * they appear in the response.\n   */\n  position: Position;\n\n  /**\n   * The label of this hint. A human readable string or an array of\n   * InlayHintLabelPart label parts.\n   *\n   * _Note_ that neither the string nor the label part can be empty.\n   */\n  label: string | readonly InlayHintLabelPart[];\n\n  /**\n   * The kind of this hint. Can be omitted in which case the client\n   * should fall back to a reasonable default.\n   */\n  kind?: InlayHintKind;\n\n  /**\n   * Optional text edits that are performed when accepting this inlay hint.\n   *\n   * _Note_ that edits are expected to change the document so that the inlay\n   * hint (or its nearest variant) is now part of the document and the inlay\n   * hint itself is now obsolete.\n   *\n   * Depending on the client capability `inlayHint.resolveSupport` clients\n   * might resolve this property late using the resolve request.\n   */\n  textEdits?: readonly TextEdit[];\n\n  /**\n   * The tooltip text when you hover over this item.\n   *\n   * Depending on the client capability `inlayHint.resolveSupport` clients\n   * might resolve this property late using the resolve request.\n   */\n  tooltip?: string | MarkupContent;\n\n  /**\n   * Render padding before the hint.\n   *\n   * Note: Padding should use the editor's background color, not the\n   * background color of the hint itself. That means padding can be used\n   * to visually align/separate an inlay hint.\n   */\n  paddingLeft?: boolean;\n\n  /**\n   * Render padding after the hint.\n   *\n   * Note: Padding should use the editor's background color, not the\n   * background color of the hint itself. That means padding can be used\n   * to visually align/separate an inlay hint.\n   */\n  paddingRight?: boolean;\n\n  /**\n   * A data entry field that is preserved on an inlay hint between\n   * a `textDocument/inlayHint` and a `inlayHint/resolve` request.\n   */\n  data?: LSPAny;\n};\n\n/**\n * An inlay hint label part allows for interactive and composite labels of inlay hints.\n *\n * @since 3.17.0\n */\nexport type InlayHintLabelPart = {\n  /**\n   * The value of this label part.\n   */\n  value: string;\n\n  /**\n   * The tooltip text when you hover over this label part. Depending on\n   * the client capability `inlayHint.resolveSupport` clients might resolve\n   * this property late using the resolve request.\n   */\n  tooltip?: string | MarkupContent;\n\n  /**\n   * An optional source code location that represents this\n   * label part.\n   *\n   * The editor will use this location for the hover and for code navigation\n   * features: This part will become a clickable link that resolves to the\n   * definition of the symbol at the given location (not necessarily the\n   * location itself), it shows the hover that shows at the given location,\n   * and it shows a context menu with further code navigation commands.\n   *\n   * Depending on the client capability `inlayHint.resolveSupport` clients\n   * might resolve this property late using the resolve request.\n   */\n  location?: Location;\n\n  /**\n   * An optional command for this label part.\n   *\n   * Depending on the client capability `inlayHint.resolveSupport` clients\n   * might resolve this property late using the resolve request.\n   */\n  command?: Command;\n};\n\n/**\n * Inlay hint kind.\n *\n * @since 3.17.0\n */\nexport type InlayHintKind = (typeof InlayHintKind)[keyof typeof InlayHintKind];\n/**\n * Inlay hint kinds.\n *\n * @since 3.17.0\n */\n\nexport namespace InlayHintKind {\n  /**\n   * An inlay hint that for a type annotation.\n   */\n  export const Type = 1;\n\n  /**\n   * An inlay hint that is for a parameter.\n   */\n  export const Parameter = 2;\n}\n\n/**\n * Client workspace capabilities specific to inlay hints.\n *\n * @since 3.17.0\n */\nexport type InlayHintWorkspaceClientCapabilities = {\n  /**\n   * Whether the client implementation supports a refresh request sent from\n   * the server to the client.\n   *\n   * Note that this event is global and will force the client to refresh all\n   * inlay hints currently shown. It should be used with absolute care and\n   * is useful for situation where a server for example detects a project wide\n   * change that requires such a calculation.\n   */\n  refreshSupport?: boolean;\n};\n\n/**\n * Client capabilities specific to inline values.\n *\n * @since 3.17.0\n */\nexport type InlineValueClientCapabilities = {\n  /**\n   * Whether implementation supports dynamic registration for inline\n   * value providers.\n   */\n  dynamicRegistration?: boolean;\n};\n\n/**\n * Inline value options used during static registration.\n *\n * @since 3.17.0\n */\nexport interface InlineValueOptions extends WorkDoneProgressOptions {}\n\n/**\n * Inline value options used during static or dynamic registration.\n *\n * @since 3.17.0\n */\nexport interface InlineValueRegistrationOptions\n  extends InlineValueOptions, TextDocumentRegistrationOptions, StaticRegistrationOptions {}\n\n/**\n * A parameter literal used in inline value requests.\n *\n * @since 3.17.0\n */\nexport interface InlineValueParams extends WorkDoneProgressParams {\n  /**\n   * The text document.\n   */\n  textDocument: TextDocumentIdentifier;\n\n  /**\n   * The document range for which inline values should be computed.\n   */\n  range: Range;\n\n  /**\n   * Additional information about the context in which inline values were\n   * requested.\n   */\n  context: InlineValueContext;\n}\n\n/**\n * @since 3.17.0\n */\nexport type InlineValueContext = {\n  /**\n   * The stack frame (as a DAP Id) where the execution has stopped.\n   */\n  frameId: integer;\n\n  /**\n   * The document range where execution has stopped.\n   * Typically the end position of the range denotes the line where the\n   * inline values are shown.\n   */\n  stoppedLocation: Range;\n};\n\n/**\n * Provide inline value as text.\n *\n * @since 3.17.0\n */\nexport type InlineValueText = {\n  /**\n   * The document range for which the inline value applies.\n   */\n  range: Range;\n\n  /**\n   * The text of the inline value.\n   */\n  text: string;\n};\n\n/**\n * Provide inline value through a variable lookup.\n *\n * If only a range is specified, the variable name will be extracted from\n * the underlying document.\n *\n * An optional variable name can be used to override the extracted name.\n *\n * @since 3.17.0\n */\nexport type InlineValueVariableLookup = {\n  /**\n   * The document range for which the inline value applies.\n   * The range is used to extract the variable name from the underlying\n   * document.\n   */\n  range: Range;\n\n  /**\n   * If specified the name of the variable to look up.\n   */\n  variableName?: string;\n\n  /**\n   * How to perform the lookup.\n   */\n  caseSensitiveLookup: boolean;\n};\n\n/**\n * Provide an inline value through an expression evaluation.\n *\n * If only a range is specified, the expression will be extracted from the\n * underlying document.\n *\n * An optional expression can be used to override the extracted expression.\n *\n * @since 3.17.0\n */\nexport type InlineValueEvaluatableExpression = {\n  /**\n   * The document range for which the inline value applies.\n   * The range is used to extract the evaluatable expression from the\n   * underlying document.\n   */\n  range: Range;\n\n  /**\n   * If specified the expression overrides the extracted expression.\n   */\n  expression?: string;\n};\n\n/**\n * Inline value information can be provided by different means:\n * - directly as a text value (class InlineValueText).\n * - as a name to use for a variable lookup (class InlineValueVariableLookup)\n * - as an evaluatable expression (class InlineValueEvaluatableExpression)\n * The InlineValue types combines all inline value types into one type.\n *\n * @since 3.17.0\n */\nexport type InlineValue =\n  | InlineValueText\n  | InlineValueVariableLookup\n  | InlineValueEvaluatableExpression;\n\n/**\n * Client workspace capabilities specific to inline values.\n *\n * @since 3.17.0\n */\nexport type InlineValueWorkspaceClientCapabilities = {\n  /**\n   * Whether the client implementation supports a refresh request sent from\n   * the server to the client.\n   *\n   * Note that this event is global and will force the client to refresh all\n   * inline values currently shown. It should be used with absolute care and\n   * is useful for situation where a server for example detect a project wide\n   * change that requires such a calculation.\n   */\n  refreshSupport?: boolean;\n};\n\ntype MonikerClientCapabilities = {\n  /**\n   * Whether implementation supports dynamic registration. If this is set to\n   * `true` the client supports the new `(TextDocumentRegistrationOptions &\n   * StaticRegistrationOptions)` return value for the corresponding server\n   * capability as well.\n   */\n  dynamicRegistration?: boolean;\n};\n\nexport interface MonikerOptions extends WorkDoneProgressOptions {}\n\nexport interface MonikerRegistrationOptions\n  extends TextDocumentRegistrationOptions, MonikerOptions {}\n\nexport interface MonikerParams\n  extends TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams {}\n\n/**\n * Moniker uniqueness level to define scope of the moniker.\n */\nexport enum UniquenessLevel {\n  /**\n   * The moniker is only unique inside a document\n   */\n  document = \"document\",\n\n  /**\n   * The moniker is unique inside a project for which a dump got created\n   */\n  project = \"project\",\n\n  /**\n   * The moniker is unique inside the group to which a project belongs\n   */\n  group = \"group\",\n\n  /**\n   * The moniker is unique inside the moniker scheme.\n   */\n  scheme = \"scheme\",\n\n  /**\n   * The moniker is globally unique\n   */\n  global = \"global\",\n}\n\n/**\n * The moniker kind.\n */\nexport enum MonikerKind {\n  /**\n   * The moniker represent a symbol that is imported into a project\n   */\n  import = \"import\",\n\n  /**\n   * The moniker represents a symbol that is exported from a project\n   */\n  export = \"export\",\n\n  /**\n   * The moniker represents a symbol that is local to a project (e.g. a local\n   * variable of a function, a class not visible outside the project, ...)\n   */\n  local = \"local\",\n}\n\n/**\n * Moniker definition to match LSIF 0.5 moniker definition.\n */\nexport type Moniker = {\n  /**\n   * The scheme of the moniker. For example tsc or .Net\n   */\n  scheme: string;\n\n  /**\n   * The identifier of the moniker. The value is opaque in LSIF however\n   * schema owners are allowed to define the structure if they want.\n   */\n  identifier: string;\n\n  /**\n   * The scope in which the moniker is unique\n   */\n  unique: UniquenessLevel;\n\n  /**\n   * The moniker kind if known.\n   */\n  kind?: MonikerKind;\n};\n\nexport type CompletionClientCapabilities = {\n  /**\n   * Whether completion supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * The client supports the following `CompletionItem` specific\n   * capabilities.\n   */\n  completionItem?: {\n    /**\n     * Client supports snippets as insert text.\n     *\n     * A snippet can define tab stops and placeholders with `$1`, `$2`\n     * and `${3:foo}`. `$0` defines the final tab stop, it defaults to\n     * the end of the snippet. Placeholders with equal identifiers are\n     * linked, that is typing in one will update others too.\n     */\n    snippetSupport?: boolean;\n\n    /**\n     * Client supports commit characters on a completion item.\n     */\n    commitCharactersSupport?: boolean;\n\n    /**\n     * Client supports the follow content formats for the documentation\n     * property. The order describes the preferred format of the client.\n     */\n    documentationFormat?: readonly MarkupKind[];\n\n    /**\n     * Client supports the deprecated property on a completion item.\n     */\n    deprecatedSupport?: boolean;\n\n    /**\n     * Client supports the preselect property on a completion item.\n     */\n    preselectSupport?: boolean;\n\n    /**\n     * Client supports the tag property on a completion item. Clients\n     * supporting tags have to handle unknown tags gracefully. Clients\n     * especially need to preserve unknown tags when sending a completion\n     * item back to the server in a resolve call.\n     *\n     * @since 3.15.0\n     */\n    tagSupport?: {\n      /**\n       * The tags supported by the client.\n       */\n      valueSet: readonly CompletionItemTag[];\n    };\n\n    /**\n     * Client supports insert replace edit to control different behavior if\n     * a completion item is inserted in the text or should replace text.\n     *\n     * @since 3.16.0\n     */\n    insertReplaceSupport?: boolean;\n\n    /**\n     * Indicates which properties a client can resolve lazily on a\n     * completion item. Before version 3.16.0 only the predefined properties\n     * `documentation` and `detail` could be resolved lazily.\n     *\n     * @since 3.16.0\n     */\n    resolveSupport?: {\n      /**\n       * The properties that a client can resolve lazily.\n       */\n      properties: readonly string[];\n    };\n\n    /**\n     * The client supports the `insertTextMode` property on\n     * a completion item to override the whitespace handling mode\n     * as defined by the client (see `insertTextMode`).\n     *\n     * @since 3.16.0\n     */\n    insertTextModeSupport?: {\n      valueSet: readonly InsertTextMode[];\n    };\n\n    /**\n     * The client has support for completion item label\n     * details (see also `CompletionItemLabelDetails`).\n     *\n     * @since 3.17.0\n     */\n    labelDetailsSupport?: boolean;\n  };\n\n  completionItemKind?: {\n    /**\n     * The completion item kind values the client supports. When this\n     * property exists the client also guarantees that it will\n     * handle values outside its set gracefully and falls back\n     * to a default value when unknown.\n     *\n     * If this property is not present the client only supports\n     * the completion items kinds from `Text` to `Reference` as defined in\n     * the initial version of the protocol.\n     */\n    valueSet?: readonly CompletionItemKind[];\n  };\n\n  /**\n   * The client supports to send additional context information for a\n   * `textDocument/completion` request.\n   */\n  contextSupport?: boolean;\n\n  /**\n   * The client's default when the completion item doesn't provide a\n   * `insertTextMode` property.\n   *\n   * @since 3.17.0\n   */\n  insertTextMode?: InsertTextMode;\n\n  /**\n   * The client supports the following `CompletionList` specific\n   * capabilities.\n   *\n   * @since 3.17.0\n   */\n  completionList?: {\n    /**\n     * The client supports the following itemDefaults on\n     * a completion list.\n     *\n     * The value lists the supported property names of the\n     * `CompletionList.itemDefaults` object. If omitted\n     * no properties are supported.\n     *\n     * @since 3.17.0\n     */\n    itemDefaults?: readonly string[];\n  };\n};\n\n/**\n * Completion options.\n */\nexport interface CompletionOptions extends WorkDoneProgressOptions {\n  /**\n   * The additional characters, beyond the defaults provided by the client (typically\n   * [a-zA-Z]), that should automatically trigger a completion request. For example\n   * `.` in JavaScript represents the beginning of an object property or method and is\n   * thus a good candidate for triggering a completion request.\n   *\n   * Most tools trigger a completion request automatically without explicitly\n   * requesting it using a keyboard shortcut (e.g. Ctrl+Space). Typically they\n   * do so when the user starts to type an identifier. For example if the user\n   * types `c` in a JavaScript file code complete will automatically pop up\n   * present `console` besides others as a completion item. Characters that\n   * make up identifiers don't need to be listed here.\n   */\n  triggerCharacters?: readonly string[];\n\n  /**\n   * The list of all possible characters that commit a completion. This field\n   * can be used if clients don't support individual commit characters per\n   * completion item. See client capability\n   * `completion.completionItem.commitCharactersSupport`.\n   *\n   * If a server provides both `allCommitCharacters` and commit characters on\n   * an individual completion item the ones on the completion item win.\n   *\n   * @since 3.2.0\n   */\n  allCommitCharacters?: readonly string[];\n\n  /**\n   * The server provides support to resolve additional\n   * information for a completion item.\n   */\n  resolveProvider?: boolean;\n\n  /**\n   * The server supports the following `CompletionItem` specific\n   * capabilities.\n   *\n   * @since 3.17.0\n   */\n  completionItem?: {\n    /**\n     * The server has support for completion item label\n     * details (see also `CompletionItemLabelDetails`) when receiving\n     * a completion item in a resolve call.\n     *\n     * @since 3.17.0\n     */\n    labelDetailsSupport?: boolean;\n  };\n}\n\nexport interface CompletionRegistrationOptions\n  extends TextDocumentRegistrationOptions, CompletionOptions {}\n\n/**\n * Params of the `textDocument/completion` request.\n */\nexport interface CompletionParams\n  extends TextDocumentPositionParams, WorkDoneProgressParams, PartialResultParams {\n  /**\n   * The completion context. This is only available if the client specifies\n   * to send this using the client capability\n   * `completion.contextSupport === true`\n   */\n  context?: CompletionContext;\n}\n\n/**\n * How a completion was triggered.\n */\nexport type CompletionTriggerKind =\n  (typeof CompletionTriggerKind)[keyof typeof CompletionTriggerKind];\n/**\n * How a completion was triggered.\n */\n\nexport namespace CompletionTriggerKind {\n  /**\n   * Completion was triggered by typing an identifier (24x7 code\n   * complete), manual invocation (e.g Ctrl+Space) or via API.\n   */\n  export const Invoked = 1;\n\n  /**\n   * Completion was triggered by a trigger character specified by\n   * the `triggerCharacters` properties of the\n   * `CompletionRegistrationOptions`.\n   */\n  export const TriggerCharacter = 2;\n\n  /**\n   * Completion was re-triggered as the current completion list is incomplete.\n   */\n  export const TriggerForIncompleteCompletions = 3;\n}\n\n/**\n * Contains additional information about the context in which a completion\n * request is triggered.\n */\nexport type CompletionContext = {\n  /**\n   * How the completion was triggered.\n   */\n  triggerKind: CompletionTriggerKind;\n\n  /**\n   * The trigger character (a single character) that has trigger code\n   * complete. Is undefined if\n   * `triggerKind !== CompletionTriggerKind.TriggerCharacter`\n   */\n  triggerCharacter?: string;\n};\n\n/**\n * Represents a collection of [completion items](#CompletionItem) to be\n * presented in the editor.\n */\nexport type CompletionList = {\n  /**\n   * This list is not complete. Further typing should result in recomputing\n   * this list.\n   *\n   * Recomputed lists have all their items replaced (not appended) in the\n   * incomplete completion sessions.\n   */\n  isIncomplete: boolean;\n\n  /**\n   * In many cases the items of an actual completion result share the same\n   * value for properties like `commitCharacters` or the range of a text\n   * edit. A completion list can therefore define item defaults which will\n   * be used if a completion item itself doesn't specify the value.\n   *\n   * If a completion list specifies a default value and a completion item\n   * also specifies a corresponding value the one from the item is used.\n   *\n   * Servers are only allowed to return default values if the client\n   * signals support for this via the `completionList.itemDefaults`\n   * capability.\n   *\n   * @since 3.17.0\n   */\n  itemDefaults?: {\n    /**\n     * A default commit character set.\n     *\n     * @since 3.17.0\n     */\n    commitCharacters?: readonly string[];\n\n    /**\n     * A default edit range\n     *\n     * @since 3.17.0\n     */\n    editRange?:\n      | Range\n      | {\n          insert: Range;\n          replace: Range;\n        };\n\n    /**\n     * A default insert text format\n     *\n     * @since 3.17.0\n     */\n    insertTextFormat?: InsertTextFormat;\n\n    /**\n     * A default insert text mode\n     *\n     * @since 3.17.0\n     */\n    insertTextMode?: InsertTextMode;\n\n    /**\n     * A default data value.\n     *\n     * @since 3.17.0\n     */\n    data?: LSPAny;\n  };\n\n  /**\n   * The completion items.\n   */\n  items: readonly CompletionItem[];\n};\n\n/**\n * Defines whether the insert text in a completion item should be interpreted as\n * plain text or a snippet.\n */\nexport type InsertTextFormat = (typeof InsertTextFormat)[keyof typeof InsertTextFormat];\n/**\n * Defines whether the insert text in a completion item should be interpreted as\n * plain text or a snippet.\n */\n\nexport namespace InsertTextFormat {\n  /**\n   * The primary text to be inserted is treated as a plain string.\n   */\n  export const PlainText = 1;\n\n  /**\n   * The primary text to be inserted is treated as a snippet.\n   *\n   * A snippet can define tab stops and placeholders with `$1`, `$2`\n   * and `${3:foo}`. `$0` defines the final tab stop, it defaults to\n   * the end of the snippet. Placeholders with equal identifiers are linked,\n   * that is typing in one will update others too.\n   */\n  export const Snippet = 2;\n}\n\n/**\n * Completion item tags are extra annotations that tweak the rendering of a\n * completion item.\n *\n * @since 3.15.0\n */\nexport type CompletionItemTag = (typeof CompletionItemTag)[keyof typeof CompletionItemTag];\n/**\n * Completion item tags are extra annotations that tweak the rendering of a\n * completion item.\n *\n * @since 3.15.0\n */\n\nexport namespace CompletionItemTag {\n  /**\n   * Render a completion as obsolete, usually using a strike-out.\n   */\n  export const Deprecated = 1;\n}\n\n/**\n * A special text edit to provide an insert and a replace operation.\n *\n * @since 3.16.0\n */\nexport type InsertReplaceEdit = {\n  /**\n   * The string to be inserted.\n   */\n  newText: string;\n\n  /**\n   * The range if the insert is requested\n   */\n  insert: Range;\n\n  /**\n   * The range if the replace is requested.\n   */\n  replace: Range;\n};\n\n/**\n * How whitespace and indentation is handled during completion item insertion.\n *\n * @since 3.16.0\n */\nexport type InsertTextMode = (typeof InsertTextMode)[keyof typeof InsertTextMode];\n/**\n * How whitespace and indentation is handled during completion item insertion.\n *\n * @since 3.16.0\n */\n\nexport namespace InsertTextMode {\n  /**\n   * The insertion or replace strings is taken as it is. If the\n   * value is multi line the lines below the cursor will be\n   * inserted using the indentation defined in the string value.\n   * The client will not apply any kind of adjustments to the\n   * string.\n   */\n  export const asIs = 1;\n\n  /**\n   * The editor adjusts leading whitespace of new lines so that\n   * they match the indentation up to the cursor of the line for\n   * which the item is accepted.\n   *\n   * Consider a line like this: <2tabs><cursor><3tabs>foo. Accepting a\n   * multi line completion item is indented using 2 tabs and all\n   * following lines inserted will be indented using 2 tabs as well.\n   */\n  export const adjustIndentation = 2;\n}\n\n/**\n * Additional details for a completion item label.\n *\n * @since 3.17.0\n */\nexport type CompletionItemLabelDetails = {\n  /**\n   * An optional string which is rendered less prominently directly after\n   * {@link CompletionItem.label label}, without any spacing. Should be\n   * used for function signatures or type annotations.\n   */\n  detail?: string;\n\n  /**\n   * An optional string which is rendered less prominently after\n   * {@link CompletionItemLabelDetails.detail}. Should be used for fully qualified\n   * names or file path.\n   */\n  description?: string;\n};\n\nexport type CompletionItem = {\n  /**\n   * The label of this completion item.\n   *\n   * The label property is also by default the text that\n   * is inserted when selecting this completion.\n   *\n   * If label details are provided the label itself should\n   * be an unqualified name of the completion item.\n   */\n  label: string;\n\n  /**\n   * Additional details for the label\n   *\n   * @since 3.17.0\n   */\n  labelDetails?: CompletionItemLabelDetails;\n\n  /**\n   * The kind of this completion item. Based of the kind\n   * an icon is chosen by the editor. The standardized set\n   * of available values is defined in `CompletionItemKind`.\n   */\n  kind?: CompletionItemKind;\n\n  /**\n   * Tags for this completion item.\n   *\n   * @since 3.15.0\n   */\n  tags?: readonly CompletionItemTag[];\n\n  /**\n   * A human-readable string with additional information\n   * about this item, like type or symbol information.\n   */\n  detail?: string;\n\n  /**\n   * A human-readable string that represents a doc-comment.\n   */\n  documentation?: string | MarkupContent;\n\n  /**\n   * Indicates if this item is deprecated.\n   *\n   * @deprecated Use `tags` instead if supported.\n   */\n  deprecated?: boolean;\n\n  /**\n   * Select this item when showing.\n   *\n   * _Note_ that only one completion item can be selected and that the\n   * tool / client decides which item that is. The rule is that the *first*\n   * item of those that match best is selected.\n   */\n  preselect?: boolean;\n\n  /**\n   * A string that should be used when comparing this item\n   * with other items. When omitted the label is used\n   * as the sort text for this item.\n   */\n  sortText?: string;\n\n  /**\n   * A string that should be used when filtering a set of\n   * completion items. When omitted the label is used as the\n   * filter text for this item.\n   */\n  filterText?: string;\n\n  /**\n   * A string that should be inserted into a document when selecting\n   * this completion. When omitted the label is used as the insert text\n   * for this item.\n   *\n   * The `insertText` is subject to interpretation by the client side.\n   * Some tools might not take the string literally. For example\n   * VS Code when code complete is requested in this example\n   * `con<cursor position>` and a completion item with an `insertText` of\n   * `console` is provided it will only insert `sole`. Therefore it is\n   * recommended to use `textEdit` instead since it avoids additional client\n   * side interpretation.\n   */\n  insertText?: string;\n\n  /**\n   * The format of the insert text. The format applies to both the\n   * `insertText` property and the `newText` property of a provided\n   * `textEdit`. If omitted defaults to `InsertTextFormat.PlainText`.\n   *\n   * Please note that the insertTextFormat doesn't apply to\n   * `additionalTextEdits`.\n   */\n  insertTextFormat?: InsertTextFormat;\n\n  /**\n   * How whitespace and indentation is handled during completion\n   * item insertion. If not provided the client's default value depends on\n   * the `textDocument.completion.insertTextMode` client capability.\n   *\n   * @since 3.16.0\n   * @since 3.17.0 - support for `textDocument.completion.insertTextMode`\n   */\n  insertTextMode?: InsertTextMode;\n\n  /**\n   * An edit which is applied to a document when selecting this completion.\n   * When an edit is provided the value of `insertText` is ignored.\n   *\n   * _Note:_ The range of the edit must be a single line range and it must\n   * contain the position at which completion has been requested.\n   *\n   * Most editors support two different operations when accepting a completion\n   * item. One is to insert a completion text and the other is to replace an\n   * existing text with a completion text. Since this can usually not be\n   * predetermined by a server it can report both ranges. Clients need to\n   * signal support for `InsertReplaceEdit`s via the\n   * `textDocument.completion.completionItem.insertReplaceSupport` client\n   * capability property.\n   *\n   * _Note 1:_ The text edit's range as well as both ranges from an insert\n   * replace edit must be a [single line] and they must contain the position\n   * at which completion has been requested.\n   * _Note 2:_ If an `InsertReplaceEdit` is returned the edit's insert range\n   * must be a prefix of the edit's replace range, that means it must be\n   * contained and starting at the same position.\n   *\n   * @since 3.16.0 additional type `InsertReplaceEdit`\n   */\n  textEdit?: TextEdit | InsertReplaceEdit;\n\n  /**\n   * The edit text used if the completion item is part of a CompletionList and\n   * CompletionList defines an item default for the text edit range.\n   *\n   * Clients will only honor this property if they opt into completion list\n   * item defaults using the capability `completionList.itemDefaults`.\n   *\n   * If not provided and a list's default range is provided the label\n   * property is used as a text.\n   *\n   * @since 3.17.0\n   */\n  textEditText?: string;\n\n  /**\n   * An optional array of additional text edits that are applied when\n   * selecting this completion. Edits must not overlap (including the same\n   * insert position) with the main edit nor with themselves.\n   *\n   * Additional text edits should be used to change text unrelated to the\n   * current cursor position (for example adding an import statement at the\n   * top of the file if the completion item will insert an unqualified type).\n   */\n  additionalTextEdits?: readonly TextEdit[];\n\n  /**\n   * An optional set of characters that when pressed while this completion is\n   * active will accept it first and then type that character. *Note* that all\n   * commit characters should have `length=1` and that superfluous characters\n   * will be ignored.\n   */\n  commitCharacters?: readonly string[];\n\n  /**\n   * An optional command that is executed *after* inserting this completion.\n   * _Note_ that additional modifications to the current document should be\n   * described with the additionalTextEdits-property.\n   */\n  command?: Command;\n\n  /**\n   * A data entry field that is preserved on a completion item between\n   * a completion and a completion resolve request.\n   */\n  data?: LSPAny;\n};\n\n/**\n * The kind of a completion entry.\n */\nexport type CompletionItemKind = (typeof CompletionItemKind)[keyof typeof CompletionItemKind];\n/**\n * The kind of a completion entry.\n */\n\nexport namespace CompletionItemKind {\n  export const Text = 1;\n  export const Method = 2;\n  export const Function = 3;\n  export const Constructor = 4;\n  export const Field = 5;\n  export const Variable = 6;\n  export const Class = 7;\n  export const Interface = 8;\n  export const Module = 9;\n  export const Property = 10;\n  export const Unit = 11;\n  export const Value = 12;\n  export const Enum = 13;\n  export const Keyword = 14;\n  export const Snippet = 15;\n  export const Color = 16;\n  export const File = 17;\n  export const Reference = 18;\n  export const Folder = 19;\n  export const EnumMember = 20;\n  export const Constant = 21;\n  export const Struct = 22;\n  export const Event = 23;\n  export const Operator = 24;\n  export const TypeParameter = 25;\n}\n\nexport type PublishDiagnosticsClientCapabilities = {\n  /**\n   * Whether the clients accepts diagnostics with related information.\n   */\n  relatedInformation?: boolean;\n\n  /**\n   * Client supports the tag property to provide meta data about a diagnostic.\n   * Clients supporting tags have to handle unknown tags gracefully.\n   *\n   * @since 3.15.0\n   */\n  tagSupport?: {\n    /**\n     * The tags supported by the client.\n     */\n    valueSet: readonly DiagnosticTag[];\n  };\n\n  /**\n   * Whether the client interprets the version property of the\n   * `textDocument/publishDiagnostics` notification's parameter.\n   *\n   * @since 3.15.0\n   */\n  versionSupport?: boolean;\n\n  /**\n   * Client supports a codeDescription property\n   *\n   * @since 3.16.0\n   */\n  codeDescriptionSupport?: boolean;\n\n  /**\n   * Whether code action supports the `data` property which is\n   * preserved between a `textDocument/publishDiagnostics` and\n   * `textDocument/codeAction` request.\n   *\n   * @since 3.16.0\n   */\n  dataSupport?: boolean;\n};\n\n/**\n * Params of the `textDocument/publishDiagnostics` notification.\n */\nexport type PublishDiagnosticsParams = {\n  /**\n   * The URI for which diagnostic information is reported.\n   */\n  uri: DocumentUri;\n\n  /**\n   * Optional the version number of the document the diagnostics are published\n   * for.\n   *\n   * @since 3.15.0\n   */\n  version?: integer;\n\n  /**\n   * An array of diagnostic information items.\n   */\n  diagnostics: Diagnostic[];\n};\n\n/**\n * Client capabilities specific to diagnostic pull requests.\n *\n * @since 3.17.0\n */\nexport type DiagnosticClientCapabilities = {\n  /**\n   * Whether implementation supports dynamic registration. If this is set to\n   * `true` the client supports the new\n   * `(TextDocumentRegistrationOptions & StaticRegistrationOptions)`\n   * return value for the corresponding server capability as well.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * Whether the clients supports related documents for document diagnostic\n   * pulls.\n   */\n  relatedDocumentSupport?: boolean;\n};\n\n/**\n * Diagnostic options.\n *\n * @since 3.17.0\n */\nexport interface DiagnosticOptions extends WorkDoneProgressOptions {\n  /**\n   * An optional identifier under which the diagnostics are\n   * managed by the client.\n   */\n  identifier?: string;\n\n  /**\n   * Whether the language has inter file dependencies meaning that\n   * editing code in one file can result in a different diagnostic\n   * set in another file. Inter file dependencies are common for\n   * most programming languages and typically uncommon for linters.\n   */\n  interFileDependencies: boolean;\n\n  /**\n   * The server provides support for workspace diagnostics as well.\n   */\n  workspaceDiagnostics: boolean;\n}\n\n/**\n * Diagnostic registration options.\n *\n * @since 3.17.0\n */\nexport interface DiagnosticRegistrationOptions\n  extends TextDocumentRegistrationOptions, DiagnosticOptions, StaticRegistrationOptions {}\n\n/**\n * Params of the `textDocument/diagnostic` request.\n *\n * @since 3.17.0\n */\nexport interface DocumentDiagnosticParams extends WorkDoneProgressParams, PartialResultParams {\n  /**\n   * The text document.\n   */\n  textDocument: TextDocumentIdentifier;\n\n  /**\n   * The additional identifier  provided during registration.\n   */\n  identifier?: string;\n\n  /**\n   * The result id of a previous response if provided.\n   */\n  previousResultId?: string;\n}\n\n/**\n * The result of a document diagnostic pull request. A report can\n * either be a full report containing all diagnostics for the\n * requested document or a unchanged report indicating that nothing\n * has changed in terms of diagnostics in comparison to the last\n * pull request.\n *\n * @since 3.17.0\n */\nexport type DocumentDiagnosticReport =\n  | RelatedFullDocumentDiagnosticReport\n  | RelatedUnchangedDocumentDiagnosticReport;\n\n/**\n * The document diagnostic report kind.\n *\n * @since 3.17.0\n */\nexport type DocumentDiagnosticReportKind =\n  (typeof DocumentDiagnosticReportKind)[keyof typeof DocumentDiagnosticReportKind];\n/**\n * The document diagnostic report kinds.\n *\n * @since 3.17.0\n */\n\nexport namespace DocumentDiagnosticReportKind {\n  /**\n   * A diagnostic report with a full\n   * set of problems.\n   */\n  export const Full = \"full\";\n\n  /**\n   * A report indicating that the last\n   * returned report is still accurate.\n   */\n  export const Unchanged = \"unchanged\";\n}\n\n/**\n * A diagnostic report with a full set of problems.\n *\n * @since 3.17.0\n */\nexport type FullDocumentDiagnosticReport = {\n  /**\n   * A full document diagnostic report.\n   */\n  kind: typeof DocumentDiagnosticReportKind.Full;\n\n  /**\n   * An optional result id. If provided it will\n   * be sent on the next diagnostic request for the\n   * same document.\n   */\n  resultId?: string;\n\n  /**\n   * The actual items.\n   */\n  items: readonly Diagnostic[];\n};\n\n/**\n * A diagnostic report indicating that the last returned\n * report is still accurate.\n *\n * @since 3.17.0\n */\nexport type UnchangedDocumentDiagnosticReport = {\n  /**\n   * A document diagnostic report indicating\n   * no changes to the last result. A server can\n   * only return `unchanged` if result ids are\n   * provided.\n   */\n  kind: typeof DocumentDiagnosticReportKind.Unchanged;\n\n  /**\n   * A result id which will be sent on the next\n   * diagnostic request for the same document.\n   */\n  resultId: string;\n};\n\n/**\n * A full diagnostic report with a set of related documents.\n *\n * @since 3.17.0\n */\nexport interface RelatedFullDocumentDiagnosticReport extends FullDocumentDiagnosticReport {\n  /**\n   * Diagnostics of related documents. This information is useful\n   * in programming languages where code in a file A can generate\n   * diagnostics in a file B which A depends on. An example of\n   * such a language is C/C++ where marco definitions in a file\n   * a.cpp and result in errors in a header file b.hpp.\n   *\n   * @since 3.17.0\n   */\n  relatedDocuments?: {\n    [uri: DocumentUri]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport;\n  };\n}\n\n/**\n * An unchanged diagnostic report with a set of related documents.\n *\n * @since 3.17.0\n */\nexport interface RelatedUnchangedDocumentDiagnosticReport extends UnchangedDocumentDiagnosticReport {\n  /**\n   * Diagnostics of related documents. This information is useful\n   * in programming languages where code in a file A can generate\n   * diagnostics in a file B which A depends on. An example of\n   * such a language is C/C++ where marco definitions in a file\n   * a.cpp and result in errors in a header file b.hpp.\n   *\n   * @since 3.17.0\n   */\n  relatedDocuments?: {\n    [uri: DocumentUri]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport;\n  };\n}\n\n/**\n * A partial result for a document diagnostic report.\n *\n * @since 3.17.0\n */\nexport type DocumentDiagnosticReportPartialResult = {\n  relatedDocuments: {\n    [uri: DocumentUri]: FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport;\n  };\n};\n\n/**\n * Cancellation data returned from a diagnostic request.\n *\n * @since 3.17.0\n */\nexport type DiagnosticServerCancellationData = {\n  retriggerRequest: boolean;\n};\n\n/**\n * Params of the `workspace/diagnostic` request.\n *\n * @since 3.17.0\n */\nexport interface WorkspaceDiagnosticParams extends WorkDoneProgressParams, PartialResultParams {\n  /**\n   * The additional identifier provided during registration.\n   */\n  identifier?: string;\n\n  /**\n   * The currently known diagnostic reports with their\n   * previous result ids.\n   */\n  previousResultIds: readonly PreviousResultId[];\n}\n\n/**\n * A previous result id in a workspace pull request.\n *\n * @since 3.17.0\n */\nexport type PreviousResultId = {\n  /**\n   * The URI for which the client knows a\n   * result id.\n   */\n  uri: DocumentUri;\n\n  /**\n   * The value of the previous result id.\n   */\n  value: string;\n};\n\n/**\n * A workspace diagnostic report.\n *\n * @since 3.17.0\n */\nexport type WorkspaceDiagnosticReport = {\n  items: readonly WorkspaceDocumentDiagnosticReport[];\n};\n\n/**\n * A full document diagnostic report for a workspace diagnostic result.\n *\n * @since 3.17.0\n */\nexport interface WorkspaceFullDocumentDiagnosticReport extends FullDocumentDiagnosticReport {\n  /**\n   * The URI for which diagnostic information is reported.\n   */\n  uri: DocumentUri;\n\n  /**\n   * The version number for which the diagnostics are reported.\n   * If the document is not marked as open `null` can be provided.\n   */\n  version: integer | null;\n}\n\n/**\n * An unchanged document diagnostic report for a workspace diagnostic result.\n *\n * @since 3.17.0\n */\nexport interface WorkspaceUnchangedDocumentDiagnosticReport extends UnchangedDocumentDiagnosticReport {\n  /**\n   * The URI for which diagnostic information is reported.\n   */\n  uri: DocumentUri;\n\n  /**\n   * The version number for which the diagnostics are reported.\n   * If the document is not marked as open `null` can be provided.\n   */\n  version: integer | null;\n}\n\n/**\n * A workspace diagnostic document report.\n *\n * @since 3.17.0\n */\nexport type WorkspaceDocumentDiagnosticReport =\n  | WorkspaceFullDocumentDiagnosticReport\n  | WorkspaceUnchangedDocumentDiagnosticReport;\n\n/**\n * A partial result for a workspace diagnostic report.\n *\n * @since 3.17.0\n */\nexport type WorkspaceDiagnosticReportPartialResult = {\n  items: readonly WorkspaceDocumentDiagnosticReport[];\n};\n\n/**\n * Workspace client capabilities specific to diagnostic pull requests.\n *\n * @since 3.17.0\n */\nexport type DiagnosticWorkspaceClientCapabilities = {\n  /**\n   * Whether the client implementation supports a refresh request sent from\n   * the server to the client.\n   *\n   * Note that this event is global and will force the client to refresh all\n   * pulled diagnostics currently shown. It should be used with absolute care\n   * and is useful for situation where a server for example detects a project\n   * wide change that requires such a calculation.\n   */\n  refreshSupport?: boolean;\n};\n\nexport type SignatureHelpClientCapabilities = {\n  /**\n   * Whether signature help supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * The client supports the following `SignatureInformation`\n   * specific properties.\n   */\n  signatureInformation?: {\n    /**\n     * Client supports the follow content formats for the documentation\n     * property. The order describes the preferred format of the client.\n     */\n    documentationFormat?: MarkupKind[];\n\n    /**\n     * Client capabilities specific to parameter information.\n     */\n    parameterInformation?: {\n      /**\n       * The client supports processing label offsets instead of a\n       * simple label string.\n       *\n       * @since 3.14.0\n       */\n      labelOffsetSupport?: boolean;\n    };\n\n    /**\n     * The client supports the `activeParameter` property on\n     * `SignatureInformation` literal.\n     *\n     * @since 3.16.0\n     */\n    activeParameterSupport?: boolean;\n  };\n\n  /**\n   * The client supports to send additional context information for a\n   * `textDocument/signatureHelp` request. A client that opts into\n   * contextSupport will also support the `retriggerCharacters` on\n   * `SignatureHelpOptions`.\n   *\n   * @since 3.15.0\n   */\n  contextSupport?: boolean;\n};\n\nexport interface SignatureHelpOptions extends WorkDoneProgressOptions {\n  /**\n   * The characters that trigger signature help\n   * automatically.\n   */\n  triggerCharacters?: readonly string[];\n\n  /**\n   * List of characters that re-trigger signature help.\n   *\n   * These trigger characters are only active when signature help is already\n   * showing. All trigger characters are also counted as re-trigger\n   * characters.\n   *\n   * @since 3.15.0\n   */\n  retriggerCharacters?: readonly string[];\n}\n\nexport interface SignatureHelpRegistrationOptions\n  extends TextDocumentRegistrationOptions, SignatureHelpOptions {}\n\nexport interface SignatureHelpParams extends TextDocumentPositionParams, WorkDoneProgressParams {\n  /**\n   * The signature help context. This is only available if the client\n   * specifies to send this using the client capability\n   * `textDocument.signatureHelp.contextSupport === true`\n   *\n   * @since 3.15.0\n   */\n  context?: SignatureHelpContext;\n}\n\n/**\n * How a signature help was triggered.\n *\n * @since 3.15.0\n */\nexport type SignatureHelpTriggerKind =\n  (typeof SignatureHelpTriggerKind)[keyof typeof SignatureHelpTriggerKind];\n/**\n * How a signature help was triggered.\n *\n * @since 3.15.0\n */\n\nexport namespace SignatureHelpTriggerKind {\n  /**\n   * Signature help was invoked manually by the user or by a command.\n   */\n  export const Invoked = 1;\n  /**\n   * Signature help was triggered by a trigger character.\n   */\n  export const TriggerCharacter = 2;\n  /**\n   * Signature help was triggered by the cursor moving or by the document\n   * content changing.\n   */\n  export const ContentChange = 3;\n}\n\n/**\n * Additional information about the context in which a signature help request\n * was triggered.\n *\n * @since 3.15.0\n */\nexport type SignatureHelpContext = {\n  /**\n   * Action that caused signature help to be triggered.\n   */\n  triggerKind: SignatureHelpTriggerKind;\n\n  /**\n   * Character that caused signature help to be triggered.\n   *\n   * This is undefined when triggerKind !==\n   * SignatureHelpTriggerKind.TriggerCharacter\n   */\n  triggerCharacter?: string;\n\n  /**\n   * `true` if signature help was already showing when it was triggered.\n   *\n   * Retriggers occur when the signature help is already active and can be\n   * caused by actions such as typing a trigger character, a cursor move, or\n   * document content changes.\n   */\n  isRetrigger: boolean;\n\n  /**\n   * The currently active `SignatureHelp`.\n   *\n   * The `activeSignatureHelp` has its `SignatureHelp.activeSignature` field\n   * updated based on the user navigating through available signatures.\n   */\n  activeSignatureHelp?: SignatureHelp;\n};\n\n/**\n * Signature help represents the signature of something callable. There can be multiple signature\n * but only one active and only one active parameter.\n */\nexport type SignatureHelp = {\n  /**\n   * One or more signatures. If no signatures are available the signature help\n   * request should return `null`.\n   */\n  signatures: readonly SignatureInformation[];\n\n  /**\n   * The active signature. If omitted or the value lies outside the\n   * range of `signatures` the value defaults to zero or is ignore if\n   * the `SignatureHelp` as no signatures.\n   *\n   * Whenever possible implementors should make an active decision about\n   * the active signature and shouldn't rely on a default value.\n   *\n   * In future version of the protocol this property might become\n   * mandatory to better express this.\n   */\n  activeSignature?: uinteger;\n\n  /**\n   * The active parameter of the active signature. If omitted or the value\n   * lies outside the range of `signatures[activeSignature].parameters`\n   * defaults to 0 if the active signature has parameters. If\n   * the active signature has no parameters it is ignored.\n   * In future version of the protocol this property might become\n   * mandatory to better express the active parameter if the\n   * active signature does have any.\n   */\n  activeParameter?: uinteger;\n};\n\n/**\n * Represents the signature of something callable. A signature\n * can have a label, like a function-name, a doc-comment, and\n * a set of parameters.\n */\nexport type SignatureInformation = {\n  /**\n   * The label of this signature. Will be shown in\n   * the UI.\n   */\n  label: string;\n\n  /**\n   * The human-readable doc-comment of this signature. Will be shown\n   * in the UI but can be omitted.\n   */\n  documentation?: string | MarkupContent;\n\n  /**\n   * The parameters of this signature.\n   */\n  parameters?: readonly ParameterInformation[];\n\n  /**\n   * The index of the active parameter.\n   *\n   * If provided, this is used in place of `SignatureHelp.activeParameter`.\n   *\n   * @since 3.16.0\n   */\n  activeParameter?: uinteger;\n};\n\n/**\n * Represents a parameter of a callable-signature. A parameter can\n * have a label and a doc-comment.\n */\nexport type ParameterInformation = {\n  /**\n   * The label of this parameter information.\n   *\n   * Either a string or an inclusive start and exclusive end offsets within\n   * its containing signature label. (see SignatureInformation.label). The\n   * offsets are based on a UTF-16 string representation as `Position` and\n   * `Range` does.\n   *\n   * _Note_: a label of type string should be a substring of its containing\n   * signature label. Its intended use case is to highlight the parameter\n   * label part in the `SignatureInformation.label`.\n   */\n  label: string | readonly [uinteger, uinteger];\n\n  /**\n   * The human-readable doc-comment of this parameter. Will be shown\n   * in the UI but can be omitted.\n   */\n  documentation?: string | MarkupContent;\n};\n\nexport type CodeActionClientCapabilities = {\n  /**\n   * Whether code action supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * The client supports code action literals as a valid\n   * response of the `textDocument/codeAction` request.\n   *\n   * @since 3.8.0\n   */\n  codeActionLiteralSupport?: {\n    /**\n     * The code action kind is supported with the following value\n     * set.\n     */\n    codeActionKind: {\n      /**\n       * The code action kind values the client supports. When this\n       * property exists the client also guarantees that it will\n       * handle values outside its set gracefully and falls back\n       * to a default value when unknown.\n       */\n      valueSet: readonly CodeActionKind[];\n    };\n  };\n\n  /**\n   * Whether code action supports the `isPreferred` property.\n   *\n   * @since 3.15.0\n   */\n  isPreferredSupport?: boolean;\n\n  /**\n   * Whether code action supports the `disabled` property.\n   *\n   * @since 3.16.0\n   */\n  disabledSupport?: boolean;\n\n  /**\n   * Whether code action supports the `data` property which is\n   * preserved between a `textDocument/codeAction` and a\n   * `codeAction/resolve` request.\n   *\n   * @since 3.16.0\n   */\n  dataSupport?: boolean;\n\n  /**\n   * Whether the client supports resolving additional code action\n   * properties via a separate `codeAction/resolve` request.\n   *\n   * @since 3.16.0\n   */\n  resolveSupport?: {\n    /**\n     * The properties that a client can resolve lazily.\n     */\n    properties: readonly string[];\n  };\n\n  /**\n   * Whether the client honors the change annotations in\n   * text edits and resource operations returned via the\n   * `CodeAction#edit` property by for example presenting\n   * the workspace edit in the user interface and asking\n   * for confirmation.\n   *\n   * @since 3.16.0\n   */\n  honorsChangeAnnotations?: boolean;\n};\n\nexport interface CodeActionOptions extends WorkDoneProgressOptions {\n  /**\n   * CodeActionKinds that this server may return.\n   *\n   * The list of kinds may be generic, such as `CodeActionKind.Refactor`,\n   * or the server may list out every specific kind they provide.\n   */\n  codeActionKinds?: readonly CodeActionKind[];\n\n  /**\n   * The server provides support to resolve additional\n   * information for a code action.\n   *\n   * @since 3.16.0\n   */\n  resolveProvider?: boolean;\n}\n\nexport interface CodeActionRegistrationOptions\n  extends TextDocumentRegistrationOptions, CodeActionOptions {}\n\n/**\n * Params for the CodeActionRequest\n */\nexport interface CodeActionParams extends WorkDoneProgressParams, PartialResultParams {\n  /**\n   * The document in which the command was invoked.\n   */\n  textDocument: TextDocumentIdentifier;\n\n  /**\n   * The range for which the command was invoked.\n   */\n  range: Range;\n\n  /**\n   * Context carrying additional information.\n   */\n  context: CodeActionContext;\n}\n\n/**\n * The kind of a code action.\n *\n * Kinds are a hierarchical list of identifiers separated by `.`,\n * e.g. `\"refactor.extract.function\"`.\n *\n * The set of kinds is open and client needs to announce the kinds it supports\n * to the server during initialization.\n */\nexport type CodeActionKind = string;\n/**\n * A set of predefined code action kinds.\n */\n\nexport namespace CodeActionKind {\n  /**\n   * Empty kind.\n   */\n  export const Empty = \"\";\n\n  /**\n   * Base kind for quickfix actions: 'quickfix'.\n   */\n  export const QuickFix = \"quickfix\";\n\n  /**\n   * Base kind for refactoring actions: 'refactor'.\n   */\n  export const Refactor = \"refactor\";\n\n  /**\n   * Base kind for refactoring extraction actions: 'refactor.extract'.\n   *\n   * Example extract actions:\n   *\n   * - Extract method\n   * - Extract function\n   * - Extract variable\n   * - Extract interface from class\n   * - ...\n   */\n  export const RefactorExtract = \"refactor.extract\";\n\n  /**\n   * Base kind for refactoring inline actions: 'refactor.inline'.\n   *\n   * Example inline actions:\n   *\n   * - Inline function\n   * - Inline variable\n   * - Inline constant\n   * - ...\n   */\n  export const RefactorInline = \"refactor.inline\";\n\n  /**\n   * Base kind for refactoring rewrite actions: 'refactor.rewrite'.\n   *\n   * Example rewrite actions:\n   *\n   * - Convert JavaScript function to class\n   * - Add or remove parameter\n   * - Encapsulate field\n   * - Make method static\n   * - Move method to base class\n   * - ...\n   */\n  export const RefactorRewrite = \"refactor.rewrite\";\n\n  /**\n   * Base kind for source actions: `source`.\n   *\n   * Source code actions apply to the entire file.\n   */\n  export const Source = \"source\";\n\n  /**\n   * Base kind for an organize imports source action:\n   * `source.organizeImports`.\n   */\n  export const SourceOrganizeImports = \"source.organizeImports\";\n\n  /**\n   * Base kind for a 'fix all' source action: `source.fixAll`.\n   *\n   * 'Fix all' actions automatically fix errors that have a clear fix that\n   * do not require user input. They should not suppress errors or perform\n   * unsafe fixes such as generating new types or classes.\n   *\n   * @since 3.17.0\n   */\n  export const SourceFixAll = \"source.fixAll\";\n}\n\n/**\n * Contains additional diagnostic information about the context in which\n * a code action is run.\n */\nexport type CodeActionContext = {\n  /**\n   * An array of diagnostics known on the client side overlapping the range\n   * provided to the `textDocument/codeAction` request. They are provided so\n   * that the server knows which errors are currently presented to the user\n   * for the given range. There is no guarantee that these accurately reflect\n   * the error state of the resource. The primary parameter\n   * to compute code actions is the provided range.\n   */\n  diagnostics: readonly Diagnostic[];\n\n  /**\n   * Requested kind of actions to return.\n   *\n   * Actions not of this kind are filtered out by the client before being\n   * shown. So servers can omit computing them.\n   */\n  only?: readonly CodeActionKind[];\n\n  /**\n   * The reason why code actions were requested.\n   *\n   * @since 3.17.0\n   */\n  triggerKind?: CodeActionTriggerKind;\n};\n\n/**\n * The reason why code actions were requested.\n *\n * @since 3.17.0\n */\nexport type CodeActionTriggerKind =\n  (typeof CodeActionTriggerKind)[keyof typeof CodeActionTriggerKind];\n/**\n * The reason why code actions were requested.\n *\n * @since 3.17.0\n */\n\nexport namespace CodeActionTriggerKind {\n  /**\n   * Code actions were explicitly requested by the user or by an extension.\n   */\n  export const Invoked = 1;\n\n  /**\n   * Code actions were requested automatically.\n   *\n   * This typically happens when current selection in a file changes, but can\n   * also be triggered when file content changes.\n   */\n  export const Automatic = 2;\n}\n\n/**\n * A code action represents a change that can be performed in code, e.g. to fix\n * a problem or to refactor code.\n *\n * A CodeAction must set either `edit` and/or a `command`. If both are supplied,\n * the `edit` is applied first, then the `command` is executed.\n */\nexport type CodeAction = {\n  /**\n   * A short, human-readable, title for this code action.\n   */\n  title: string;\n\n  /**\n   * The kind of the code action.\n   *\n   * Used to filter code actions.\n   */\n  kind?: CodeActionKind;\n\n  /**\n   * The diagnostics that this code action resolves.\n   */\n  diagnostics?: readonly Diagnostic[];\n\n  /**\n   * Marks this as a preferred action. Preferred actions are used by the\n   * `auto fix` command and can be targeted by keybindings.\n   *\n   * A quick fix should be marked preferred if it properly addresses the\n   * underlying error. A refactoring should be marked preferred if it is the\n   * most reasonable choice of actions to take.\n   *\n   * @since 3.15.0\n   */\n  isPreferred?: boolean;\n\n  /**\n   * Marks that the code action cannot currently be applied.\n   *\n   * Clients should follow the following guidelines regarding disabled code\n   * actions:\n   *\n   * - Disabled code actions are not shown in automatic lightbulbs code\n   *   action menus.\n   *\n   * - Disabled actions are shown as faded out in the code action menu when\n   *   the user request a more specific type of code action, such as\n   *   refactorings.\n   *\n   * - If the user has a keybinding that auto applies a code action and only\n   *   a disabled code actions are returned, the client should show the user\n   *   an error message with `reason` in the editor.\n   *\n   * @since 3.16.0\n   */\n  disabled?: {\n    /**\n     * Human readable description of why the code action is currently\n     * disabled.\n     *\n     * This is displayed in the code actions UI.\n     */\n    reason: string;\n  };\n\n  /**\n   * The workspace edit this code action performs.\n   */\n  edit?: WorkspaceEdit;\n\n  /**\n   * A command this code action executes. If a code action\n   * provides an edit and a command, first the edit is\n   * executed and then the command.\n   */\n  command?: Command;\n\n  /**\n   * A data entry field that is preserved on a code action between\n   * a `textDocument/codeAction` and a `codeAction/resolve` request.\n   *\n   * @since 3.16.0\n   */\n  data?: LSPAny;\n};\n\nexport type DocumentColorClientCapabilities = {\n  /**\n   * Whether document color supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n};\n\nexport interface DocumentColorOptions extends WorkDoneProgressOptions {}\n\nexport interface DocumentColorRegistrationOptions\n  extends TextDocumentRegistrationOptions, StaticRegistrationOptions, DocumentColorOptions {}\n\n/**\n * Params of the `textDocument/documentColor` request.\n */\nexport interface DocumentColorParams extends WorkDoneProgressParams, PartialResultParams {\n  /**\n   * The text document.\n   */\n  textDocument: TextDocumentIdentifier;\n}\n\nexport type ColorInformation = {\n  /**\n   * The range in the document where this color appears.\n   */\n  range: Range;\n\n  /**\n   * The actual color value for this color range.\n   */\n  color: Color;\n};\n\n/**\n * Represents a color in RGBA space.\n */\nexport type Color = {\n  /**\n   * The red component of this color in the range [0-1].\n   */\n  readonly red: decimal;\n\n  /**\n   * The green component of this color in the range [0-1].\n   */\n  readonly green: decimal;\n\n  /**\n   * The blue component of this color in the range [0-1].\n   */\n  readonly blue: decimal;\n\n  /**\n   * The alpha component of this color in the range [0-1].\n   */\n  readonly alpha: decimal;\n};\n\n/**\n * Params of the `textDocument/colorPresentation` request.\n */\nexport interface ColorPresentationParams extends WorkDoneProgressParams, PartialResultParams {\n  /**\n   * The text document.\n   */\n  textDocument: TextDocumentIdentifier;\n\n  /**\n   * The color information to request presentations for.\n   */\n  color: Color;\n\n  /**\n   * The range where the color would be inserted. Serves as a context.\n   */\n  range: Range;\n}\n\nexport type ColorPresentation = {\n  /**\n   * The label of this color presentation. It will be shown on the color\n   * picker header. By default this is also the text that is inserted when\n   * selecting this color presentation.\n   */\n  label: string;\n  /**\n   * An [edit](#TextEdit) which is applied to a document when selecting\n   * this presentation for the color. When omitted the\n   * [label](#ColorPresentation.label) is used.\n   */\n  textEdit?: TextEdit;\n  /**\n   * An optional array of additional [text edits](#TextEdit) that are applied\n   * when selecting this color presentation. Edits must not overlap with the\n   * main [edit](#ColorPresentation.textEdit) nor with themselves.\n   */\n  additionalTextEdits?: readonly TextEdit[];\n};\n\nexport type DocumentFormattingClientCapabilities = {\n  /**\n   * Whether formatting supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n};\n\nexport interface DocumentFormattingOptions extends WorkDoneProgressOptions {}\n\nexport interface DocumentFormattingRegistrationOptions\n  extends TextDocumentRegistrationOptions, DocumentFormattingOptions {}\n\n/**\n * Params of the `textDocument/formatting` request.\n */\nexport interface DocumentFormattingParams extends WorkDoneProgressParams {\n  /**\n   * The document to format.\n   */\n  textDocument: TextDocumentIdentifier;\n\n  /**\n   * The format options.\n   */\n  options: FormattingOptions;\n}\n\n/**\n * Value-object describing what options formatting should use.\n */\nexport type FormattingOptions = {\n  /**\n   * Size of a tab in spaces.\n   */\n  tabSize: uinteger;\n\n  /**\n   * Prefer spaces over tabs.\n   */\n  insertSpaces: boolean;\n\n  /**\n   * Trim trailing whitespace on a line.\n   *\n   * @since 3.15.0\n   */\n  // @ts-expect-error - Index type overridden\n  trimTrailingWhitespace?: boolean;\n\n  /**\n   * Insert a newline character at the end of the file if one does not exist.\n   *\n   * @since 3.15.0\n   */\n  // @ts-expect-error - Index type overridden\n  insertFinalNewline?: boolean;\n\n  /**\n   * Trim all newlines after the final newline at the end of the file.\n   *\n   * @since 3.15.0\n   */\n  // @ts-expect-error - Index type overridden\n  trimFinalNewlines?: boolean;\n\n  /**\n   * Signature for further properties.\n   */\n  [key: string]: boolean | integer | string;\n};\n\nexport type DocumentRangeFormattingClientCapabilities = {\n  /**\n   * Whether formatting supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n};\n\nexport interface DocumentRangeFormattingOptions extends WorkDoneProgressOptions {}\n\nexport interface DocumentRangeFormattingRegistrationOptions\n  extends TextDocumentRegistrationOptions, DocumentRangeFormattingOptions {}\n\n/**\n * Params of the `textDocument/rangeFormatting` request.\n */\nexport interface DocumentRangeFormattingParams extends WorkDoneProgressParams {\n  /**\n   * The document to format.\n   */\n  textDocument: TextDocumentIdentifier;\n\n  /**\n   * The range to format\n   */\n  range: Range;\n\n  /**\n   * The format options\n   */\n  options: FormattingOptions;\n}\n\nexport type DocumentOnTypeFormattingClientCapabilities = {\n  /**\n   * Whether on type formatting supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n};\n\nexport type DocumentOnTypeFormattingOptions = {\n  /**\n   * A character on which formatting should be triggered, like `{`.\n   */\n  firstTriggerCharacter: string;\n\n  /**\n   * More trigger characters.\n   */\n  moreTriggerCharacter?: readonly string[];\n};\n\nexport interface DocumentOnTypeFormattingRegistrationOptions\n  extends TextDocumentRegistrationOptions, DocumentOnTypeFormattingOptions {}\n\n/**\n * Params of the `textDocument/onTypeFormatting` request.\n */\nexport type DocumentOnTypeFormattingParams = {\n  /**\n   * The document to format.\n   */\n  textDocument: TextDocumentIdentifier;\n\n  /**\n   * The position around which the on type formatting should happen.\n   * This is not necessarily the exact position where the character denoted\n   * by the property `ch` got typed.\n   */\n  position: Position;\n\n  /**\n   * The character that has been typed that triggered the formatting\n   * on type request. That is not necessarily the last character that\n   * got inserted into the document since the client could auto insert\n   * characters as well (e.g. like automatic brace completion).\n   */\n  ch: string;\n\n  /**\n   * The formatting options.\n   */\n  options: FormattingOptions;\n};\n\nexport type PrepareSupportDefaultBehavior =\n  (typeof PrepareSupportDefaultBehavior)[keyof typeof PrepareSupportDefaultBehavior];\n\nexport namespace PrepareSupportDefaultBehavior {\n  /**\n   * The client's default behavior is to select the identifier\n   * according to the language's syntax rule.\n   */\n  export const Identifier = 1;\n}\n\nexport type RenameClientCapabilities = {\n  /**\n   * Whether rename supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * Client supports testing for validity of rename operations\n   * before execution.\n   *\n   * @since version 3.12.0\n   */\n  prepareSupport?: boolean;\n\n  /**\n   * Client supports the default behavior result\n   * (`{ defaultBehavior: boolean }`).\n   *\n   * The value indicates the default behavior used by the\n   * client.\n   *\n   * @since version 3.16.0\n   */\n  prepareSupportDefaultBehavior?: PrepareSupportDefaultBehavior;\n\n  /**\n   * Whether the client honors the change annotations in\n   * text edits and resource operations returned via the\n   * rename request's workspace edit by for example presenting\n   * the workspace edit in the user interface and asking\n   * for confirmation.\n   *\n   * @since 3.16.0\n   */\n  honorsChangeAnnotations?: boolean;\n};\n\nexport interface RenameOptions extends WorkDoneProgressOptions {\n  /**\n   * Renames should be checked and tested before being executed.\n   */\n  prepareProvider?: boolean;\n}\n\nexport interface RenameRegistrationOptions extends TextDocumentRegistrationOptions, RenameOptions {}\n\n/**\n * Params of the `textDocument/rename` request.\n */\nexport interface RenameParams extends TextDocumentPositionParams, WorkDoneProgressParams {\n  /**\n   * The new name of the symbol. If the given name is not valid the\n   * request must return a [ResponseError](#ResponseError) with an\n   * appropriate message set.\n   */\n  newName: string;\n}\n\n/**\n * Params of the `textDocument/prepareRename` request.\n */\nexport interface PrepareRenameParams extends TextDocumentPositionParams, WorkDoneProgressParams {}\n\nexport interface LinkedEditingRangeOptions extends WorkDoneProgressOptions {}\n\nexport interface LinkedEditingRangeRegistrationOptions\n  extends TextDocumentRegistrationOptions, LinkedEditingRangeOptions, StaticRegistrationOptions {}\n\nexport type LinkedEditingRangeClientCapabilities = {\n  /**\n   * Whether the implementation supports dynamic registration.\n   * If this is set to `true` the client supports the new\n   * `(TextDocumentRegistrationOptions & StaticRegistrationOptions)`\n   * return value for the corresponding server capability as well.\n   */\n  dynamicRegistration?: boolean;\n};\n\n/**\n * Params of the `textDocument/linkedEditingRange` request.\n */\nexport interface LinkedEditingRangeParams\n  extends TextDocumentPositionParams, WorkDoneProgressParams {}\n\nexport type LinkedEditingRanges = {\n  /**\n   * A list of ranges that can be renamed together. The ranges must have\n   * identical length and contain identical text content. The ranges cannot\n   * overlap.\n   */\n  ranges: readonly Range[];\n\n  /**\n   * An optional word pattern (regular expression) that describes valid\n   * contents for the given ranges. If no pattern is provided, the client\n   * configuration's word pattern will be used.\n   */\n  wordPattern?: string;\n};\n\n/**********************\n * Workspace Features *\n **********************/\n/**\n * Workspace symbol client capabilities.\n */\nexport type WorkspaceSymbolClientCapabilities = {\n  /**\n   * Symbol request supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * Specific capabilities for the `SymbolKind` in the `workspace/symbol`\n   * request.\n   */\n  symbolKind?: {\n    /**\n     * The symbol kind values the client supports. When this\n     * property exists the client also guarantees that it will\n     * handle values outside its set gracefully and falls back\n     * to a default value when unknown.\n     *\n     * If this property is not present the client only supports\n     * the symbol kinds from `File` to `Array` as defined in\n     * the initial version of the protocol.\n     */\n    valueSet?: readonly SymbolKind[];\n  };\n\n  /**\n   * The client supports tags on `SymbolInformation` and `WorkspaceSymbol`.\n   * Clients supporting tags have to handle unknown tags gracefully.\n   *\n   * @since 3.16.0\n   */\n  tagSupport?: {\n    /**\n     * The tags supported by the client.\n     */\n    valueSet: readonly SymbolTag[];\n  };\n\n  /**\n   * The client support partial workspace symbols. The client will send the\n   * request `workspaceSymbol/resolve` to the server to resolve additional\n   * properties.\n   *\n   * @since 3.17.0 - proposedState\n   */\n  resolveSupport?: {\n    /**\n     * The properties that a client can resolve lazily. Usually\n     * `location.range`\n     */\n    properties: readonly string[];\n  };\n};\n\nexport interface WorkspaceSymbolOptions extends WorkDoneProgressOptions {\n  /**\n   * The server provides support to resolve additional\n   * information for a workspace symbol.\n   *\n   * @since 3.17.0\n   */\n  resolveProvider?: boolean;\n}\n\nexport interface WorkspaceSymbolRegistrationOptions extends WorkspaceSymbolOptions {}\n\n/**\n * Params of the `workspace/symbol` request.\n */\nexport interface WorkspaceSymbolParams extends WorkDoneProgressParams, PartialResultParams {\n  /**\n   * A query string to filter symbols by. Clients may send an empty\n   * string here to request all symbols.\n   */\n  query: string;\n}\n\n/**\n * A special workspace symbol that supports locations without a range\n *\n * @since 3.17.0\n */\nexport type WorkspaceSymbol = {\n  /**\n   * The name of this symbol.\n   */\n  name: string;\n\n  /**\n   * The kind of this symbol.\n   */\n  kind: SymbolKind;\n\n  /**\n   * Tags for this completion item.\n   */\n  tags?: readonly SymbolTag[];\n\n  /**\n   * The name of the symbol containing this symbol. This information is for\n   * user interface purposes (e.g. to render a qualifier in the user interface\n   * if necessary). It can't be used to re-infer a hierarchy for the document\n   * symbols.\n   */\n  containerName?: string;\n\n  /**\n   * The location of this symbol. Whether a server is allowed to\n   * return a location without a range depends on the client\n   * capability `workspace.symbol.resolveSupport`.\n   *\n   * See also `SymbolInformation.location`.\n   */\n  location: Location | { uri: DocumentUri };\n\n  /**\n   * A data entry field that is preserved on a workspace symbol between a\n   * workspace symbol request and a workspace symbol resolve request.\n   */\n  data?: LSPAny;\n};\n\n/**\n * Params of the `workspace/configuration` request.\n */\nexport type ConfigurationParams = {\n  items: readonly ConfigurationItem[];\n};\n\nexport type ConfigurationItem = {\n  /**\n   * The scope to get the configuration section for.\n   */\n  scopeUri?: URI;\n\n  /**\n   * The configuration section asked for.\n   */\n  section?: string;\n};\n\nexport type DidChangeConfigurationClientCapabilities = {\n  /**\n   * Did change configuration notification supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n};\n\n/**\n * Params of the `workspace/didChangeConfiguration` notification.\n */\nexport type DidChangeConfigurationParams = {\n  /**\n   * The actual changed settings\n   */\n  settings: LSPAny;\n};\n\nexport type WorkspaceFoldersServerCapabilities = {\n  /**\n   * The server has support for workspace folders\n   */\n  supported?: boolean;\n\n  /**\n   * Whether the server wants to receive workspace folder\n   * change notifications.\n   *\n   * If a string is provided, the string is treated as an ID\n   * under which the notification is registered on the client\n   * side. The ID can be used to unregister for these events\n   * using the `client/unregisterCapability` request.\n   */\n  changeNotifications?: string | boolean;\n};\n\nexport type WorkspaceFolder = {\n  /**\n   * The associated URI for this workspace folder.\n   */\n  uri: URI;\n\n  /**\n   * The name of the workspace folder. Used to refer to this\n   * workspace folder in the user interface.\n   */\n  name: string;\n};\n\n/**\n * Params of the `workspace/didChangeWorkspaceFolders` notification.\n */\nexport type DidChangeWorkspaceFoldersParams = {\n  /**\n   * The actual workspace folder change event.\n   */\n  event: WorkspaceFoldersChangeEvent;\n};\n\n/**\n * The workspace folder change event.\n */\nexport type WorkspaceFoldersChangeEvent = {\n  /**\n   * The array of added workspace folders\n   */\n  added: readonly WorkspaceFolder[];\n\n  /**\n   * The array of the removed workspace folders\n   */\n  removed: readonly WorkspaceFolder[];\n};\n\n/**\n * The options to register for file operations.\n *\n * @since 3.16.0\n */\nexport type FileOperationRegistrationOptions = {\n  /**\n   * The actual filters.\n   */\n  filters: readonly FileOperationFilter[];\n};\n\n/**\n * A pattern kind describing if a glob pattern matches a file a folder or both.\n *\n * @since 3.16.0\n */\nexport type FileOperationPatternKind =\n  (typeof FileOperationPatternKind)[keyof typeof FileOperationPatternKind];\n/**\n * A pattern kind describing if a glob pattern matches a file a folder or both.\n *\n * @since 3.16.0\n */\n\nexport namespace FileOperationPatternKind {\n  /**\n   * The pattern matches a file only.\n   */\n  export const file = \"file\";\n\n  /**\n   * The pattern matches a folder only.\n   */\n  export const folder = \"folder\";\n}\n\n/**\n * Matching options for the file operation pattern.\n *\n * @since 3.16.0\n */\nexport type FileOperationPatternOptions = {\n  /**\n   * The pattern should be matched ignoring casing.\n   */\n  ignoreCase?: boolean;\n};\n\n/**\n * A pattern to describe in which file operation requests or notifications\n * the server is interested in.\n *\n * @since 3.16.0\n */\nexport type FileOperationPattern = {\n  /* eslint-disable no-irregular-whitespace */\n  /**\n   * The glob pattern to match. Glob patterns can have the following syntax:\n   * - `*` to match one or more characters in a path segment\n   * - `?` to match on one character in a path segment\n   * - `**` to match any number of path segments, including none\n   * - `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}`\n   *   matches all TypeScript and JavaScript files)\n   * - `[]` to declare a range of characters to match in a path segment\n   *   (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)\n   * - `[!...]` to negate a range of characters to match in a path segment\n   *   (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but\n   *   not `example.0`)\n   */\n  /* eslint-enable no-irregular-whitespace */\n  glob: string;\n\n  /**\n   * Whether to match files or folders with this pattern.\n   *\n   * Matches both if undefined.\n   */\n  matches?: FileOperationPatternKind;\n\n  /**\n   * Additional options used during matching.\n   */\n  options?: FileOperationPatternOptions;\n};\n\n/**\n * A filter to describe in which file operation requests or notifications\n * the server is interested in.\n *\n * @since 3.16.0\n */\nexport type FileOperationFilter = {\n  /**\n   * A Uri like `file` or `untitled`.\n   */\n  scheme?: string;\n\n  /**\n   * The actual file operation pattern.\n   */\n  pattern: FileOperationPattern;\n};\n\n/**\n * The parameters sent in notifications/requests for user-initiated creation of files.\n *\n * @since 3.16.0\n */\nexport type CreateFilesParams = {\n  /**\n   * An array of all files/folders created in this operation.\n   */\n  files: readonly FileCreate[];\n};\n\n/**\n * Represents information on a file/folder create.\n *\n * @since 3.16.0\n */\nexport type FileCreate = {\n  /**\n   * A file:// URI for the location of the file/folder being created.\n   */\n  uri: string;\n};\n\n/**\n * The parameters sent in notifications/requests for user-initiated renames of files.\n *\n * @since 3.16.0\n */\nexport type RenameFilesParams = {\n  /**\n   * An array of all files/folders renamed in this operation. When a folder\n   * is renamed, only the folder will be included, and not its children.\n   */\n  files: readonly FileRename[];\n};\n\n/**\n * Represents information on a file/folder rename.\n *\n * @since 3.16.0\n */\nexport type FileRename = {\n  /**\n   * A file:// URI for the original location of the file/folder being renamed.\n   */\n  oldUri: string;\n\n  /**\n   * A file:// URI for the new location of the file/folder being renamed.\n   */\n  newUri: string;\n};\n\n/**\n * The parameters sent in notifications/requests for user-initiated deletes of files.\n *\n * @since 3.16.0\n */\nexport type DeleteFilesParams = {\n  /**\n   * An array of all files/folders deleted in this operation.\n   */\n  files: readonly FileDelete[];\n};\n\n/**\n * Represents information on a file/folder delete.\n *\n * @since 3.16.0\n */\nexport type FileDelete = {\n  /**\n   * A file:// URI for the location of the file/folder being deleted.\n   */\n  uri: string;\n};\n\nexport type DidChangeWatchedFilesClientCapabilities = {\n  /**\n   * Did change watched files notification supports dynamic registration.\n   * Please note that the current protocol doesn't support static\n   * configuration for file changes from the server side.\n   */\n  dynamicRegistration?: boolean;\n\n  /**\n   * Whether the client has support for relative patterns\n   * or not.\n   *\n   * @since 3.17.0\n   */\n  relativePatternSupport?: boolean;\n};\n\n/**\n * Describe options to be used when registering for file system change events.\n */\nexport type DidChangeWatchedFilesRegistrationOptions = {\n  /**\n   * The watchers to register.\n   */\n  watchers: readonly FileSystemWatcher[];\n};\n\n/* eslint-disable no-irregular-whitespace */\n/**\n * The glob pattern to watch relative to the base path. Glob patterns can have\n * the following syntax:\n * - `*` to match one or more characters in a path segment\n * - `?` to match on one character in a path segment\n * - `**` to match any number of path segments, including none\n * - `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript\n *   and JavaScript files)\n * - `[]` to declare a range of characters to match in a path segment\n *   (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)\n * - `[!...]` to negate a range of characters to match in a path segment\n *   (e.g., `example.[!0-9]` to match on `example.a`, `example.b`,\n *   but not `example.0`)\n *\n * @since 3.17.0\n */\n/* eslint-enable no-irregular-whitespace */\nexport type Pattern = string;\n\n/**\n * A relative pattern is a helper to construct glob patterns that are matched\n * relatively to a base URI. The common value for a `baseUri` is a workspace\n * folder root, but it can be another absolute URI as well.\n *\n * @since 3.17.0\n */\nexport type RelativePattern = {\n  /**\n   * A workspace folder or a base URI to which this pattern will be matched\n   * against relatively.\n   */\n  baseUri: WorkspaceFolder | URI;\n\n  /**\n   * The actual glob pattern;\n   */\n  pattern: Pattern;\n};\n\n/**\n * The glob pattern. Either a string pattern or a relative pattern.\n *\n * @since 3.17.0\n */\nexport type GlobPattern = Pattern | RelativePattern;\n\nexport type FileSystemWatcher = {\n  /**\n   * The glob pattern to watch. See {@link GlobPattern glob pattern}\n   * for more detail.\n   *\n   * @since 3.17.0 support for relative patterns.\n   */\n  globPattern: GlobPattern;\n\n  /**\n   * The kind of events of interest. If omitted it defaults\n   * to WatchKind.Create | WatchKind.Change | WatchKind.Delete\n   * which is 7.\n   */\n  kind?: WatchKind;\n};\n\nexport type WatchKind = (typeof WatchKind)[keyof typeof WatchKind];\n\nexport namespace WatchKind {\n  /**\n   * Interested in create events.\n   */\n  export const Create = 1;\n\n  /**\n   * Interested in change events\n   */\n  export const Change = 2;\n\n  /**\n   * Interested in delete events\n   */\n  export const Delete = 4;\n}\n\n/**\n * Params of the `workspace/didChangeWatchedFiles` notification.\n */\nexport type DidChangeWatchedFilesParams = {\n  /**\n   * The actual file events.\n   */\n  changes: readonly FileEvent[];\n};\n\n/**\n * An event describing a file change.\n */\ntype FileEvent = {\n  /**\n   * The file's URI.\n   */\n  uri: DocumentUri;\n  /**\n   * The change type.\n   */\n  type: FileChangeType;\n};\n\n/**\n * The file event type.\n */\nexport type FileChangeType = (typeof FileChangeType)[keyof typeof FileChangeType];\n/**\n * The file event type.\n */\n\nexport namespace FileChangeType {\n  /**\n   * The file got created.\n   */\n  export const Created = 1;\n  /**\n   * The file got changed.\n   */\n  export const Changed = 2;\n  /**\n   * The file got deleted.\n   */\n  export const Deleted = 3;\n}\n\nexport type ExecuteCommandClientCapabilities = {\n  /**\n   * Execute command supports dynamic registration.\n   */\n  dynamicRegistration?: boolean;\n};\nexport interface ExecuteCommandOptions extends WorkDoneProgressOptions {\n  /**\n   * The commands to be executed on the server\n   */\n  commands: readonly string[];\n}\n\n/**\n * Execute command registration options.\n */\nexport interface ExecuteCommandRegistrationOptions extends ExecuteCommandOptions {}\n\n/**\n * Params of the `workspace/executeCommand` request.\n */\nexport interface ExecuteCommandParams extends WorkDoneProgressParams {\n  /**\n   * The identifier of the actual command handler.\n   */\n  command: string;\n  /**\n   * Arguments that the command should be invoked with.\n   */\n  arguments?: readonly LSPAny[];\n}\n\n/**\n * Params of the `workspace/applyEdit` request.\n */\nexport type ApplyWorkspaceEditParams = {\n  /**\n   * An optional label of the workspace edit. This label is\n   * presented in the user interface for example on an undo\n   * stack to undo the workspace edit.\n   */\n  label?: string;\n\n  /**\n   * The edits to apply.\n   */\n  edit: WorkspaceEdit;\n};\n\n/**\n * Result of the `workspace/applyEdit` request.\n */\nexport type ApplyWorkspaceEditResult = {\n  /**\n   * Indicates whether the edit was applied or not.\n   */\n  applied: boolean;\n\n  /**\n   * An optional textual description for why the edit was not applied.\n   * This may be used by the server for diagnostic logging or to provide\n   * a suitable error for a request that triggered the edit.\n   */\n  failureReason?: string;\n\n  /**\n   * Depending on the client's failure handling strategy `failedChange`\n   * might contain the index of the change that failed. This property is\n   * only available if the client signals a `failureHandling` strategy\n   * in its client capabilities.\n   */\n  failedChange?: uinteger;\n};\n\n/*******************\n * Window Features *\n *******************/\n/**\n * Params of the `window/showMessage` notification.\n */\nexport type ShowMessageParams = {\n  /**\n   * The message type. See {@link MessageType}.\n   */\n  type: MessageType;\n\n  /**\n   * The actual message.\n   */\n  message: string;\n};\n\n/**\n * Message type of the `window/showMessage`, `window/showMessageRequest` and `window/logMessage`\n * request.\n */\nexport type MessageType = (typeof MessageType)[keyof typeof MessageType];\n/**\n * Message type of the `window/showMessage`, `window/showMessageRequest` and `window/logMessage`\n * request.\n */\n\nexport namespace MessageType {\n  /**\n   * An error message.\n   */\n  export const Error = 1;\n  /**\n   * A warning message.\n   */\n  export const Warning = 2;\n  /**\n   * An information message.\n   */\n  export const Info = 3;\n  /**\n   * A log message.\n   */\n  export const Log = 4;\n  /**\n   * A debug message.\n   *\n   * @since 3.18.0\n   * @proposed\n   */\n  export const Debug = 5;\n}\n\n/**\n * Show message request client capabilities\n */\nexport type ShowMessageRequestClientCapabilities = {\n  /**\n   * Capabilities specific to the `MessageActionItem` type.\n   */\n  messageActionItem?: {\n    /**\n     * Whether the client supports additional attributes which\n     * are preserved and sent back to the server in the\n     * request's response.\n     */\n    additionalPropertiesSupport?: boolean;\n  };\n};\n\n/**\n * Params of the `window/showMessageRequest` request.\n */\nexport type ShowMessageRequestParams = {\n  /**\n   * The message type. See {@link MessageType}.\n   */\n  type: MessageType;\n\n  /**\n   * The actual message\n   */\n  message: string;\n\n  /**\n   * The message action items to present.\n   */\n  actions?: readonly MessageActionItem[];\n};\n\ntype MessageActionItem = {\n  /**\n   * A short title like 'Retry', 'Open Log' etc.\n   */\n  title: string;\n};\n\n/**\n * Client capabilities for the show document request.\n *\n * @since 3.16.0\n */\nexport type ShowDocumentClientCapabilities = {\n  /**\n   * The client has support for the show document\n   * request.\n   */\n  support: boolean;\n};\n\n/**\n * Params of the `window/showDocument` request.\n *\n * @since 3.16.0\n */\nexport type ShowDocumentParams = {\n  /**\n   * The uri to show.\n   */\n  uri: URI;\n\n  /**\n   * Indicates to show the resource in an external program.\n   * To show, for example, `https://code.visualstudio.com/`\n   * in the default WEB browser set `external` to `true`.\n   */\n  external?: boolean;\n\n  /**\n   * An optional property to indicate whether the editor\n   * showing the document should take focus or not.\n   * Clients might ignore this property if an external\n   * program is started.\n   */\n  takeFocus?: boolean;\n\n  /**\n   * An optional selection range if the document is a text\n   * document. Clients might ignore the property if an\n   * external program is started or the file is not a text\n   * file.\n   */\n  selection?: Range;\n};\n\n/**\n * Result of the `window/showDocument` request.\n *\n * @since 3.16.0\n */\nexport type ShowDocumentResult = {\n  /**\n   * A boolean indicating if the show was successful.\n   */\n  success: boolean;\n};\n\n/**\n * Params of the `window/logMessage` notification.\n */\nexport type LogMessageParams = {\n  /**\n   * The message type. See {@link MessageType}.\n   */\n  type: MessageType;\n\n  /**\n   * The actual message\n   */\n  message: string;\n};\n\n/**\n * Params of the `window/workDoneProgress/create` request.\n */\nexport type WorkDoneProgressCreateParams = {\n  /**\n   * The token to be used to report progress.\n   */\n  token: ProgressToken;\n};\n\n/**\n * Params of the `window/workDoneProgress/cancel` notification.\n */\nexport type WorkDoneProgressCancelParams = {\n  /**\n   * The token to be used to report progress.\n   */\n  token: ProgressToken;\n};\n"
  },
  {
    "path": "src/types/tools.ts",
    "content": "/**\n * Construct a type with a set of readonly properties `K` of type `T`.\n */\nexport type ReadonlyRecord<K extends PropertyKey, T> = { readonly [P in K]: T };\n\n/**\n * Check if two types are equal.\n */\nexport type Equals<T, U> =\n  (<G>() => G extends T ? 1 : 2) extends <G>() => G extends U ? 1 : 2 ? true : false;\n\n/**\n * Tell TS to evaluate an object type immediately. Actually does nothing, but\n * it's useful for debugging or make type information more readable.\n *\n * Sometimes strange things happen when you try to use it with a _generic type_,\n * so avoid that if possible.\n */\nexport type _Id<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;\n\nexport type _IdDeep<T> =\n  T extends infer U ?\n    U extends object ?\n      { [K in keyof U]: _IdDeep<U[K]> }\n    : U\n  : never;\n\n/**\n * Merge two object types, preferring the second type when keys overlap.\n *\n * @example\n * ```typescript\n * type A = { a: number; b: number; c?: number; d?: number; e?: number; }\n * type B = { b: string; c: string; d?: string; f: string; g?: string; }\n * type R = Merge<A, B>;\n * //   ^ { a: number; b: string; c: string; d?: string; e?: number; f: string; g?: string; }\n * ```\n */\nexport type Merge<L, R> = _Id<\n  Pick<L, Exclude<keyof L, keyof R>> &\n    Pick<R, Exclude<keyof R, _OptionalPropertyNames<R>>> &\n    Pick<R, Exclude<_OptionalPropertyNames<R>, keyof L>> &\n    _SpreadProperties<L, R, _OptionalPropertyNames<R> & keyof L>\n>;\ntype _OptionalPropertyNames<T> = {\n  [K in keyof T]-?: NonNullable<unknown> extends { [P in K]: T[K] } ? K : never;\n}[keyof T];\ntype _SpreadProperties<L, R, K extends keyof L & keyof R> = {\n  [P in K]: L[P] | Exclude<R[P], undefined>;\n};\n"
  },
  {
    "path": "src/typora-utils.ts",
    "content": "/* eslint-disable @typescript-eslint/unbound-method */\n\nimport * as path from \"@modules/path\";\nimport { fileURLToPath } from \"@modules/url\";\n\n/*************\n * Constants *\n *************/\n/**\n * Typora version.\n */\nexport const TYPORA_VERSION = window._options.appVersion;\n\n/**\n * Typora resource directory.\n */\nexport const TYPORA_RESOURCE_DIR: string = (() => {\n  let result = \"\";\n  if (\"dirname\" in window && window.dirname) result = window.dirname as string;\n  else if (\"__dirname\" in window && window.__dirname) result = window.__dirname;\n  else if (\"appPath\" in _options && _options.appPath) result = _options.appPath as string;\n\n  if (!result) throw new Error(\"Cannot determine Typora resource directory.\");\n\n  if (result.startsWith(\"file://\")) result = fileURLToPath(result);\n\n  let lastResult = \"\";\n  while (![\"resources\", \"Resources\"].includes(path.basename(result))) {\n    lastResult = result;\n    result = path.dirname(result);\n    if (result === lastResult) throw new Error(\"Cannot determine Typora resource directory.\");\n  }\n\n  return Files.isMac ? path.join(result, \"TypeMark\") : result;\n})();\n\n/*********************\n * Prototype patches *\n *********************/\nlet editorEnhanced = false;\n/**\n * Enhance Typora `Editor` class (by manipulating its prototype) to provide the following methods:\n *\n * ```typescript\n * type EnhancedEditor = Typora.Editor & Typora.EditorExtensions;\n * interface Typora.EditorExtensions {\n *   on(event: \"change\", handler: (editor: EnhancedEditor, ev: ChangeEvent) => void | Promise<void>): void;\n *   off(event: \"change\", handler: (editor: EnhancedEditor, ev: ChangeEvent) => void | Promise<void>): void;\n * }\n * interface ChangeEvent {\n *   oldMarkdown: string;\n *   newMarkdown: string;\n * }\n * ```\n */\nconst enhanceEditor = () => {\n  if (editorEnhanced) return;\n\n  editorEnhanced = true;\n\n  const rawEditor = Files.editor!;\n\n  const editorPrototype: Typora.EnhancedEditor = rawEditor.constructor.prototype;\n\n  const handlersMap = new Map<\n    string,\n    ((editor: Typora.EnhancedEditor, ...args: any[]) => unknown)[]\n  >();\n\n  editorPrototype.on = (event, handler) => {\n    if (!handlersMap.has(event)) handlersMap.set(event, []);\n    const handlers = handlersMap.get(event)!;\n    handlers.push(handler);\n    handlersMap.set(event, handlers);\n  };\n  editorPrototype.off = (event, handler) => {\n    if (!handlersMap.has(event)) return;\n    const handlers = handlersMap.get(event)!;\n    handlers.splice(handlers.indexOf(handler), 1);\n    handlersMap.set(event, handlers);\n  };\n\n  // Temporarily suppress change event when opening a file to avoid too many events\n  let isOpeningFile = false;\n  let onOpenFileComplete: (() => void | Promise<void>) | null = null;\n  const originalOpenFile = Files.editor!.library!.openFile;\n  Files.editor!.library!.openFile = function openFile(this: any, pathname: string, cb: () => void) {\n    isOpeningFile = true;\n    originalOpenFile.call(this, pathname, (...args) => {\n      isOpeningFile = false;\n      void onOpenFileComplete?.();\n      onOpenFileComplete = null;\n      return cb(...args);\n    });\n  };\n\n  const temporarilySuppressConsoleError = () => {\n    const originalConsoleError = console.error;\n    console.error = () => {};\n    return () => {\n      console.error = originalConsoleError;\n    };\n  };\n\n  let oldMarkdown = rawEditor.getMarkdown();\n  let scheduledTriggerChangeTask: (() => void | Promise<void>) | null = null;\n  const scheduleTriggerChange = () => {\n    scheduledTriggerChangeTask = () => {\n      const restoreConsoleError = temporarilySuppressConsoleError();\n      let newMarkdown: string;\n      try {\n        newMarkdown = rawEditor.getMarkdown();\n      } catch (e) {\n        restoreConsoleError();\n        return;\n      }\n      restoreConsoleError();\n      if (!newMarkdown) return;\n      if (oldMarkdown === newMarkdown) return;\n\n      const tmp = oldMarkdown;\n      oldMarkdown = newMarkdown;\n      scheduledTriggerChangeTask = null;\n      const handlers = handlersMap.get(\"change\") ?? [];\n      for (const handler of handlers)\n        handler(rawEditor as Typora.EnhancedEditor, {\n          oldMarkdown: tmp,\n          newMarkdown,\n        });\n    };\n    if (isOpeningFile) onOpenFileComplete = scheduledTriggerChangeTask;\n    else\n      void Promise.resolve().then(() => {\n        void scheduledTriggerChangeTask?.();\n      });\n  };\n\n  /* Proxy set on `editor.nodeMap` */\n  let rawNodeMap = rawEditor.nodeMap;\n  Object.defineProperty(rawEditor, \"nodeMap\", {\n    get() {\n      return rawNodeMap;\n    },\n    set(value) {\n      rawNodeMap = value;\n      scheduleTriggerChange();\n    },\n  });\n\n  /* Proxy `nodeMap.constructor.prototype` */\n  const nodeMapCollectionPrototype = rawNodeMap.constructor.prototype;\n\n  const originalNodeMapCollectionReset = nodeMapCollectionPrototype.reset;\n  nodeMapCollectionPrototype.reset = function (...args: any) {\n    const result = originalNodeMapCollectionReset.apply(this, args);\n    scheduleTriggerChange();\n    return result;\n  };\n\n  const originalRestoreFromJson = nodeMapCollectionPrototype.restoreFromJson;\n  nodeMapCollectionPrototype.restoreFromJson = function (...args: any) {\n    const result = originalRestoreFromJson.apply(this, args);\n    scheduleTriggerChange();\n    return result;\n  };\n\n  /* Proxy `nodeMap.allNodes.constructor.prototype` */\n  const nodeMapPrototype: Typora.NodeMap = rawNodeMap.allNodes.constructor.prototype;\n\n  const originalAdd = nodeMapPrototype.add;\n  nodeMapPrototype.add = function (...args) {\n    const result = originalAdd.apply(this, args);\n    scheduleTriggerChange();\n    return result;\n  };\n\n  const originalRemove = nodeMapPrototype.remove;\n  nodeMapPrototype.remove = function (...args) {\n    const result = originalRemove.apply(this, args);\n    scheduleTriggerChange();\n    return result;\n  };\n\n  const originalNodeMapReset = nodeMapPrototype.reset;\n  nodeMapPrototype.reset = function (...args) {\n    const result = originalNodeMapReset.apply(this, args);\n    scheduleTriggerChange();\n    return result;\n  };\n\n  const originalUpdate = nodeMapPrototype.update;\n  nodeMapPrototype.update = function (...args) {\n    const result = originalUpdate.apply(this, args);\n    scheduleTriggerChange();\n    return result;\n  };\n\n  /* Watch for `editor.writingArea` input */\n  const writingArea = rawEditor.writingArea;\n  writingArea.addEventListener(\"input\", () => {\n    scheduleTriggerChange();\n  });\n\n  /**\n   * Watch an object and trigger `scheduleTriggerChange` when any of its methods\n   * except those starting with `get`, `copy`, `is`, `has` is called.\n   * @param o The object to watch.\n   * @param key The key of the object to watch.\n   */\n  const watchObj = <O>(o: O, key: keyof O) => {\n    if (\n      typeof key === \"string\" &&\n      (key.startsWith(\"get\") ||\n        key.startsWith(\"copy\") ||\n        key.startsWith(\"is\") ||\n        key.startsWith(\"has\"))\n    )\n      return;\n    const value = o[key];\n    if (value === null) return;\n    if (typeof value === \"function\") {\n      o[key] = function (this: any, ...args: any[]) {\n        const result = value.apply(this, args);\n        scheduleTriggerChange();\n        return result;\n      } as never;\n      return;\n    }\n    if (typeof value === \"object\") {\n      for (const key of Object.getOwnPropertyNames(value))\n        watchObj(value, key as keyof typeof value);\n    }\n  };\n\n  /* Watch for all methods in `editor.UserOp` */\n  const userOp = rawEditor.UserOp;\n  for (const key of Object.getOwnPropertyNames(userOp))\n    watchObj(userOp, key as keyof typeof userOp);\n\n  /* Watch for all methods on `editor.undo.constructor.prototype` */\n  const historyManagerPrototype = rawEditor.undo.constructor.prototype;\n  for (const key of Object.getOwnPropertyNames(historyManagerPrototype))\n    watchObj(historyManagerPrototype, key as keyof typeof historyManagerPrototype);\n};\n\nlet sourceViewEnhanced = false;\n/**\n * Enhance Typora `SourceView` class (by manipulating its prototype) to provide the following methods:\n *\n * @example\n * ```typescript\n * type EnhancedSourceView = Typora.SourceView & Typora.SourceViewExtensions;\n * interface Typora.SourceViewExtensions {\n *   on(event: \"beforeToggle\", handler: (sv: EnhancedSourceView, on: boolean) => void | Promise<void>): void;\n *   on(event: \"toggle\", handler: (sv: EnhancedSourceView, on: boolean) => void | Promise<void>): void;\n *   on(event: \"beforeShow\", handler: (sv: EnhancedSourceView) => void | Promise<void>): void;\n *   on(event: \"show\", handler: (sv: EnhancedSourceView) => void | Promise<void>): void;\n *   on(event: \"beforeHide\", handler: (sv: EnhancedSourceView) => void | Promise<void>): void;\n *   on(event: \"hide\", handler: (sv: EnhancedSourceView) => void | Promise<void>): void;\n *\n *   off(event: \"beforeToggle\", handler: (sv: EnhancedSourceView, on: boolean) => void | Promise<void>): void;\n *   off(event: \"toggle\", handler: (sv: EnhancedSourceView, on: boolean) => void | Promise<void>): void;\n *   off(event: \"beforeShow\", handler: (sv: EnhancedSourceView) => void | Promise<void>): void;\n *   off(event: \"show\", handler: (sv: EnhancedSourceView) => void | Promise<void>): void;\n *   off(event: \"beforeHide\", handler: (sv: EnhancedSourceView) => void | Promise<void>): void;\n *   off(event: \"hide\", handler: (sv: EnhancedSourceView) => void | Promise<void>): void;\n * }\n * ```\n */\nconst enhanceSourceView = () => {\n  if (sourceViewEnhanced) return;\n\n  sourceViewEnhanced = true;\n\n  const sourceViewPrototype: Typora.EnhancedSourceView =\n    Files.editor!.sourceView.constructor.prototype;\n\n  const handlersMap = new Map<string, ((sv: Typora.SourceView, ...args: any[]) => unknown)[]>();\n\n  sourceViewPrototype.on = (event, handler) => {\n    if (!handlersMap.has(event)) handlersMap.set(event, []);\n    const handlers = handlersMap.get(event)!;\n    handlers.push(handler);\n    handlersMap.set(event, handlers);\n  };\n  sourceViewPrototype.off = (event, handler) => {\n    if (!handlersMap.has(event)) return;\n    const handlers = handlersMap.get(event)!;\n    handlers.splice(handlers.indexOf(handler), 1);\n    handlersMap.set(event, handlers);\n  };\n\n  const originalHide = sourceViewPrototype.hide;\n  sourceViewPrototype.hide = function () {\n    const beforeHideHandlers = handlersMap.get(\"beforeHide\") ?? [];\n    for (const handler of beforeHideHandlers) handler(this);\n    const beforeToggleHandlers = handlersMap.get(\"beforeToggle\") ?? [];\n    for (const handler of beforeToggleHandlers) handler(this, false);\n    originalHide.call(this);\n    const hideHandlers = handlersMap.get(\"hide\") ?? [];\n    for (const handler of hideHandlers) handler(this);\n    const toggleHandlers = handlersMap.get(\"toggle\") ?? [];\n    for (const handler of toggleHandlers) handler(this, false);\n  };\n\n  const originalShow = sourceViewPrototype.show;\n  sourceViewPrototype.show = function () {\n    const beforeShowHandlers = handlersMap.get(\"beforeShow\") ?? [];\n    for (const handler of beforeShowHandlers) handler(this);\n    const beforeToggleHandlers = handlersMap.get(\"beforeToggle\") ?? [];\n    for (const handler of beforeToggleHandlers) handler(this, true);\n    originalShow.call(this);\n    const showHandlers = handlersMap.get(\"show\") ?? [];\n    for (const handler of showHandlers) handler(this);\n    const toggleHandlers = handlersMap.get(\"toggle\") ?? [];\n    for (const handler of toggleHandlers) handler(this, true);\n  };\n};\n\n/*********************\n * Utility functions *\n *********************/\n/**\n * Wait until Typora Editor is initialized.\n * @returns\n */\nexport const waitUntilEditorInitialized = (): Promise<void> =>\n  new Promise((resolve) => {\n    const interval = setInterval(() => {\n      if (Files.editor) {\n        clearInterval(interval);\n\n        // Apply patches\n        enhanceEditor();\n        enhanceSourceView();\n\n        resolve(undefined);\n      }\n    }, 100);\n  });\n\n/**\n * Get workspace folder path.\n *\n * **⚠️ Warning:** This function assumes {@link Files.editor} is initialized, otherwise an error will\n * be thrown. To ensure {@link Files.editor} is initialized, use {@link waitUntilEditorInitialized}\n * before this function.\n * @throws {TypeError} If {@link Files.editor} is not initialized.\n * @returns\n */\nexport const getWorkspaceFolder = (): string | null => Files.editor!.library?.watchedFolder ?? null;\n\n/**\n * Get active file pathname.\n * @returns\n */\nexport const getActiveFilePathname = (): string | null =>\n  (Files.filePath ?? Files.bundle?.filePath) || null;\n\n/**\n * Get closest CodeMirror instance from an element, if any.\n * @param element Element to search.\n * @returns CodeMirror instance if found, otherwise `null`.\n */\nexport const getCodeMirror = (element: Element): CodeMirror.Editor | null => {\n  const cms = $(element).closest(\".CodeMirror\");\n  if (!cms.length) return null;\n  return (cms[0] as unknown as { CodeMirror: CodeMirror.Editor }).CodeMirror;\n};\n"
  },
  {
    "path": "src/utils/cli-tools.ts",
    "content": "import { CommandError, NoFreePortError, PlatformError } from \"@/errors\";\nimport type { ReadonlyRecord } from \"@/types/tools\";\n\n/**\n * Run a command from shell and return its output.\n * @param command Command to run.\n * @returns\n * @throws {CommandError} If the command fails.\n */\nexport const runCommand = (() => {\n  if (Files.isNode) {\n    const { exec } = window.reqnode!(\"child_process\");\n\n    return async function runCommand(command: string, options?: { cwd?: string }): Promise<string> {\n      const { cwd } = options ?? {};\n\n      return new Promise((resolve, reject) => {\n        exec(command, { cwd }, (error, stdout, stderr) => {\n          if (error) reject(error);\n          else if (stderr) reject(new CommandError(stderr));\n          else resolve(stdout);\n        });\n      });\n    };\n  }\n\n  if (Files.isMac)\n    return async function runCommand(command: string, options?: { cwd?: string }): Promise<string> {\n      const { cwd } = options ?? {};\n\n      return new Promise((resolve, reject) => {\n        window.bridge!.callHandler(\n          \"controller.runCommand\",\n          { args: command, ...(cwd ? { cwd } : {}) },\n          ([success, stdout, stderr]) => {\n            if (success) resolve(stdout);\n            else reject(new CommandError(stderr));\n          },\n        );\n      });\n    };\n\n  throw new PlatformError(\"Unsupported platform for `runCommand`\");\n})();\n\n/**\n * Get the environment variables.\n * @returns\n */\nexport const getEnv: () => Promise<ReadonlyRecord<string, string | undefined>> = (() => {\n  if (Files.isNode)\n    // eslint-disable-next-line @typescript-eslint/require-await\n    return async function getEnv() {\n      return process.env;\n    };\n\n  if (Files.isMac) {\n    let cache:\n      | ReadonlyRecord<string, string | undefined>\n      | Promise<ReadonlyRecord<string, string | undefined>>\n      | null = null;\n    return async function getEnv() {\n      if (cache) return cache;\n      cache = runCommand(\"printenv\")\n        .then((output) =>\n          output\n            .trim()\n            .split(\"\\n\")\n            .map((line) => line.trim())\n            .filter(Boolean),\n        )\n        .then((lines) => {\n          const env: Record<string, string | undefined> = {};\n          for (const line of lines) {\n            const [key, value] = line.split(\"=\");\n            env[key!] = value;\n          }\n          return env;\n        })\n        .catch(() => ({}));\n      return cache;\n    };\n  }\n\n  throw new PlatformError(\"Unsupported platform for `getEnv`\");\n})();\n\n/**\n * Find a free localhost port.\n *\n * **⚠️ Warning:** This function only works on macOS and Linux.\n * @throws {NoFreePortError} If no free port is found.\n * @returns\n */\nexport const findFreePort = async (startAt = 6190): Promise<number> => {\n  const command = /* sh */ `\n    for port in {${startAt}..${startAt + 100 > 65535 ? 65535 : startAt + 100}}; do\n      nc -z localhost $port &>/dev/null || { echo $port; break; }\n    done\n  `;\n  const output = await runCommand(command);\n  const port = Number.parseInt(output.trim());\n  if (Number.isNaN(port)) throw new NoFreePortError(\"Cannot find free port\");\n  return port;\n};\n"
  },
  {
    "path": "src/utils/diff.ts",
    "content": "import diff from \"fast-diff\";\n\nimport type { Range } from \"@/types/lsp\";\n\nexport const computeTextChanges = (\n  oldStr: string,\n  newStr: string,\n  lastCaretPosition?: { line: number; character: number } | null,\n): { range: Range; text: string }[] => {\n  const result: { range: Range; text: string }[] = [];\n\n  const diffs = diff(\n    oldStr,\n    newStr,\n    lastCaretPosition ?\n      oldStr\n        .split(Files.useCRLF ? \"\\r\\n\" : \"\\n\")\n        .slice(0, lastCaretPosition.line)\n        .reduce((acc, line) => acc + line.length + (Files.useCRLF ? 2 : 1), 0) +\n        lastCaretPosition.character\n    : 0,\n    true,\n  ).reverse();\n\n  let line = 0;\n  let character = 0;\n\n  let change: { range: Range; text: string } | null = null;\n\n  let part: diff.Diff | undefined;\n  while ((part = diffs.pop())) {\n    const [operation, text] = part;\n\n    const linesToAdd = text.split(Files.useCRLF ? \"\\r\\n\" : \"\\n\").length - 1;\n\n    switch (operation) {\n      case diff.EQUAL:\n        if (change !== null) {\n          result.push(change);\n          change = null;\n        }\n        line += linesToAdd;\n        character =\n          linesToAdd === 0 ?\n            character + text.length\n          : text.length - text.lastIndexOf(Files.useCRLF ? \"\\r\\n\" : \"\\n\") - 1;\n        break;\n\n      case diff.DELETE:\n        if (change === null) {\n          change = {\n            range: {\n              start: { line, character },\n              end: {\n                line: line + linesToAdd,\n                character:\n                  linesToAdd === 0 ?\n                    character + text.length\n                  : text.length - text.lastIndexOf(Files.useCRLF ? \"\\r\\n\" : \"\\n\") - 1,\n              },\n            },\n            text: \"\",\n          };\n        } else {\n          change.range.end.line += linesToAdd;\n          change.range.end.character =\n            linesToAdd === 0 ?\n              character + text.length\n            : text.length - text.lastIndexOf(Files.useCRLF ? \"\\r\\n\" : \"\\n\") - 1;\n        }\n        line += linesToAdd;\n        character =\n          linesToAdd === 0 ?\n            character + text.length\n          : text.length - text.lastIndexOf(Files.useCRLF ? \"\\r\\n\" : \"\\n\") - 1;\n        break;\n\n      case diff.INSERT:\n        if (change === null) {\n          change = {\n            range: {\n              start: { line, character },\n              end: { line, character },\n            },\n            text,\n          };\n        } else {\n          change.text += text;\n        }\n        break;\n    }\n  }\n\n  if (change !== null) result.push(change);\n\n  return result;\n};\n"
  },
  {
    "path": "src/utils/dom.ts",
    "content": "/**\n * Get coordinates of the caret.\n * @returns\n */\nexport const getCaretCoordinate = (): { x: number; y: number } | null => {\n  const sel = window.getSelection();\n  if (sel?.rangeCount) {\n    const range = sel.getRangeAt(0).cloneRange();\n    const caret = document.createElement(\"span\");\n    range.insertNode(caret);\n    const rect = caret.getBoundingClientRect();\n    if (caret.parentNode) caret.parentNode.removeChild(caret);\n    return { x: rect.left, y: rect.top };\n  }\n  return null;\n};\n\n/**\n * Get the CSS styles of the given class names.\n * @param classNames The CSS class names.\n * @returns\n */\nexport const getCSSClassStyles = (() => {\n  const cachedElements = new Map<string, HTMLDivElement>();\n\n  const createElementWithClasses = (classNames: string[]): HTMLDivElement => {\n    const element = document.createElement(\"div\");\n    element.style.height = \"0\";\n    element.style.width = \"0\";\n    element.style.position = \"absolute\";\n    element.style.left = \"0\";\n    element.style.top = \"0\";\n    element.classList.add(...classNames);\n    return element;\n  };\n\n  return (...classNames: string[]): CSSStyleDeclaration => {\n    const key = classNames.join(\" \");\n    if (cachedElements.has(key)) return window.getComputedStyle(cachedElements.get(key)!);\n    const element = createElementWithClasses(classNames);\n    document.body.appendChild(element);\n    cachedElements.set(key, element);\n    return window.getComputedStyle(element);\n  };\n})();\n"
  },
  {
    "path": "src/utils/function.ts",
    "content": "/**\n * Cache the result of a function.\n * @param fn The function to cache.\n * @returns\n */\nexport const cache = <F extends (...args: any) => unknown>(fn: F): F => {\n  const cache = new Map();\n  const result = ((...args) => {\n    const key = JSON.stringify(args);\n    if (cache.has(key)) return cache.get(key);\n    const result = fn(...args);\n    cache.set(key, result);\n    return result;\n  }) as F;\n  Object.defineProperty(result, \"name\", {\n    value: fn.name,\n    writable: false,\n    enumerable: false,\n    configurable: true,\n  });\n  return result;\n};\n"
  },
  {
    "path": "src/utils/logging.ts",
    "content": "import type { integer } from \"@/types/lsp\";\nimport type { Merge, _Id } from \"@/types/tools\";\n\nimport { getErrorCodeName } from \"./lsp\";\nimport { omit } from \"./tools\";\n\n/**\n * Options for block logger.\n */\nexport interface SimpleLoggerOptions {\n  /**\n   * Prefix to display before the text.\n   */\n  prefix?: string;\n  /**\n   * Styles applied to the text (the count of styles should match the count of `%c`).\n   */\n  styles?: readonly string[];\n}\n\n/**\n * Create a simple logger that logs a message to the console.\n * @param options Options.\n * @returns\n */\nexport const createSimpleLogger = (options?: SimpleLoggerOptions) => {\n  const { prefix = \"\", styles } = options ?? {};\n\n  return {\n    /**\n     * Log a debug message to the console.\n     * @param data Data to log.\n     * @returns\n     */\n    debug: (...data: unknown[]) =>\n      console.debug(\n        ...(data.length > 0 && typeof data[0] === \"string\" ? [prefix + data[0]] : []),\n        ...(styles ?? []),\n        ...(data.length > 1 ? data.slice(1) : []),\n      ),\n    /**\n     * Log a message to the console.\n     * @param data Data to log.\n     * @returns\n     */\n    info: (...data: unknown[]) =>\n      console.log(\n        ...(data.length > 0 && typeof data[0] === \"string\" ? [prefix + data[0]] : []),\n        ...(styles ?? []),\n        ...(data.length > 1 ? data.slice(1) : []),\n      ),\n    /**\n     * Log a warning message to the console.\n     * @param data Data to log.\n     * @returns\n     */\n    warn: (...data: unknown[]) =>\n      console.warn(\n        ...(data.length > 0 && typeof data[0] === \"string\" ? [prefix + data[0]] : []),\n        ...(styles ?? []),\n        ...(data.length > 1 ? data.slice(1) : []),\n      ),\n    /**\n     * Log an error message to the console.\n     * @param data Data to log.\n     * @returns\n     */\n    error: (...data: unknown[]) =>\n      console.error(\n        ...(data.length > 0 && typeof data[0] === \"string\" ? [prefix + data[0]] : []),\n        ...(styles ?? []),\n        ...(data.length > 1 ? data.slice(1) : []),\n      ),\n\n    /**\n     * Overwrite `options` with new options.\n     * @param overwriteOptions New options.\n     * @returns\n     */\n    overwrite: (overwriteOptions: SimpleLoggerOptions) =>\n      createSimpleLogger({ ...options, ...overwriteOptions }),\n  };\n};\n\n/**\n * Prepare console block parameters.\n * @param options Options.\n * @returns\n */\nconst _prepareConsoleBlockParams = (options: {\n  /**\n   * Text to display.\n   */\n  text: string;\n  /**\n   * Prefix to display before the text.\n   */\n  prefix?: string;\n  /**\n   * Block background color.\n   */\n  color?: string;\n  /**\n   * Block text color.\n   */\n  textColor?: string;\n}) => {\n  const { color: backgroundColor = \"gray\", prefix = \"\", text, textColor = \"white\" } = options;\n\n  return [\n    \"%c\" + prefix + text,\n    `background-color: ${backgroundColor}; color: ${textColor}; padding: 5px 10px; font-family: ''; font-weight: bold; width: 100%`,\n  ];\n};\n\n/**\n * Options for block logger.\n */\ntype BlockLoggerOptions =\n  Omit<Parameters<typeof _prepareConsoleBlockParams>[0], \"text\"> extends infer U ? _Id<U> : never;\n\n/**\n * Create a logger that logs a block-formatted message to the console.\n * @param options Options.\n * @returns\n */\nexport const createBlockLogger = (options?: BlockLoggerOptions) => ({\n  /**\n   * Log a block-formatted debug message to the console.\n   * @param text Text to display.\n   * @param data Data to log.\n   * @returns\n   */\n  debug: (text: string, ...data: unknown[]) =>\n    console.debug(..._prepareConsoleBlockParams({ text, ...options }), ...data),\n\n  /**\n   * Log a block-formatted message to the console.\n   * @param text Text to display.\n   * @param data Data to log.\n   * @returns\n   */\n  info: (text: string, ...data: unknown[]) =>\n    console.log(..._prepareConsoleBlockParams({ text, ...options }), ...data),\n\n  /**\n   * Log a block-formatted warning message to the console.\n   * @param text Text to display.\n   * @param data Data to log.\n   * @returns\n   */\n  warn: (text: string, ...data: unknown[]) =>\n    console.warn(..._prepareConsoleBlockParams({ text, ...options }), ...data),\n\n  /**\n   * Log a block-formatted error message to the console.\n   * @param text Text to display.\n   * @param data Data to log.\n   * @returns\n   */\n  error: (text: string, ...data: unknown[]) =>\n    console.error(..._prepareConsoleBlockParams({ text, ...options }), ...data),\n\n  /**\n   * Overwrite `options` with new options.\n   * @param overwriteOptions New options.\n   * @returns\n   */\n  overwrite: (overwriteOptions: BlockLoggerOptions) =>\n    createBlockLogger({ ...options, ...overwriteOptions }),\n});\n\n/**\n * A logger that logs a message to the console.\n */\nexport type Logger = ReturnType<typeof createLogger>;\n\n/**\n * Options for logger.\n */\nexport type LoggerOptions = Merge<SimpleLoggerOptions, { block?: BlockLoggerOptions }>;\n\n/**\n * Create a logger that logs a message to the console.\n * @param options The options.\n * @returns\n */\nexport const createLogger = (options?: LoggerOptions) =>\n  Object.assign(createSimpleLogger(options), {\n    block: createBlockLogger({ ...(options ? omit(options, \"block\") : {}), ...options?.block }),\n  });\n\n/**\n * Format request ID for logging.\n * @param id The request ID.\n * @returns\n *\n * @example\n * ```javascript\n * formatId(1); // => \"[1] \"\n * formatId(\"abc\"); // => \"[abc] \"\n * formatId(null); // => \"\"\n * ```\n */\nexport const formatId = (id: (integer | string) | null) => (id !== null ? `[${id}] ` : \"\");\n/**\n * Format method name for logging.\n * @param method The method name.\n * @returns\n *\n * @example\n * ```javascript\n * formatMethod(\"initialize\"); // => \"initialize\"\n * formatMethod(null); // => \"Anonymous\"\n * ```\n */\nexport const formatMethod = (method: string | null) => method ?? \"Anonymous\";\n/**\n * Format error code for logging.\n * @param code The error code.\n * @returns\n *\n * @example\n * ```javascript\n * formatErrorCode(-32601); // => \"-32601 (MethodNotFound)\"\n * formatErrorCode(123); // => \"123\"\n * ```\n */\nexport const formatErrorCode = (code: integer) => {\n  const name = getErrorCodeName(code) ?? \"\";\n  return code + (name ? ` (${name})` : \"\");\n};\n"
  },
  {
    "path": "src/utils/lsp.ts",
    "content": "import type {\n  LSPAny,\n  LSPArray,\n  LSPObject,\n  Message,\n  NotificationMessage,\n  RequestMessage,\n  ResponseError,\n  ResponseMessage,\n  decimal,\n  integer,\n  uinteger,\n} from \"@/types/lsp\";\nimport { ErrorCodes } from \"@/types/lsp\";\n\n/**************\n * Base types *\n **************/\n/**\n * Check if a value is an integer.\n * @param value The value to check.\n * @returns\n *\n * @see {@link integer}\n */\nexport const isInteger = (value: unknown): value is integer => Number.isInteger(value);\n/**\n * Check if a value is an unsigned integer.\n * @param value The value to check.\n * @returns\n *\n * @see {@link uinteger}\n */\nexport const isUInteger = (value: unknown): value is uinteger => isInteger(value) && value >= 0;\n/**\n * Check if a value is a decimal.\n * @param value The value to check.\n * @returns\n *\n * @see {@link decimal}\n */\nexport const isDecimal = (value: unknown): value is decimal =>\n  typeof value === \"number\" && !isNaN(value);\n\n/**\n * Check if a value is an LSP any.\n * @param value The value to check.\n * @returns\n *\n * @see {@link LSPAny}\n */\nexport const isLSPAny = (value: unknown): value is LSPAny =>\n  typeof value === \"string\" ||\n  typeof value === \"boolean\" ||\n  value === null ||\n  isInteger(value) ||\n  isDecimal(value) ||\n  isLSPArray(value) ||\n  isLSPObject(value);\n\n/**\n * Check if a value is an LSP object.\n * @param value The value to check.\n * @returns\n *\n * @see {@link LSPObject}\n */\nexport const isLSPObject = (value: unknown): value is LSPObject => {\n  if (typeof value !== \"object\" || value === null) return false;\n  for (const key in value) if (!isLSPAny(value[key as keyof typeof value])) return false;\n  return true;\n};\n\n/**\n * Check if a value is an LSP array.\n * @param value The value to check.\n * @returns\n *\n * @see {@link LSPArray}\n */\nexport const isLSPArray = (value: unknown): value is LSPArray =>\n  Array.isArray(value) && value.every(isLSPAny);\n\n/*****************\n * Base Protocol *\n *****************/\n/**\n * Check if a value is a message.\n * @param value The value to check.\n * @returns\n */\nexport const isMessage = (value: unknown): value is Message => {\n  if (typeof value !== \"object\" || value === null) return false;\n  return !(!(\"jsonrpc\" in value) || typeof value.jsonrpc !== \"string\");\n};\n\n/**\n * Check if a value is a request message.\n * @param value The value to check.\n * @returns\n */\nexport const isRequestMessage = (value: unknown): value is RequestMessage => {\n  if (!isMessage(value)) return false;\n  if (\n    !(\"id\" in value) ||\n    (!isInteger(value.id) && typeof value.id !== \"string\" && value.id !== null)\n  )\n    return false;\n  if (!(\"method\" in value) || typeof value.method !== \"string\") return false;\n  return !(\"params\" in value && !isLSPArray(value.params) && !isLSPObject(value.params));\n};\n\n/**\n * Check if a value is a response message.\n * @param value The value to check.\n * @returns\n */\nexport const isResponseMessage = (value: unknown): value is ResponseMessage => {\n  if (!isMessage(value)) return false;\n  if (\n    !(\"id\" in value) ||\n    (!isInteger(value.id) && typeof value.id !== \"string\" && value.id !== null)\n  )\n    return false;\n  if (\n    \"result\" in value &&\n    (\"error\" in value ||\n      (typeof value.result !== \"string\" &&\n        typeof value.result !== \"number\" &&\n        typeof value.result !== \"boolean\" &&\n        !isLSPArray(value.result) &&\n        !isLSPObject(value.result) &&\n        value.result !== null))\n  )\n    return false;\n  if (\"error\" in value && !isResponseError(value.error)) return false;\n  return !(!(\"result\" in value) && !(\"error\" in value));\n};\n\n/**\n * Check if a value is a response error.\n * @param value The value to check.\n * @returns\n */\nexport const isResponseError = (value: unknown): value is ResponseError => {\n  if (typeof value !== \"object\" || value === null) return false;\n  if (!(\"code\" in value) || !isInteger(value.code)) return false;\n  if (!(\"message\" in value) || typeof value.message !== \"string\") return false;\n  return !(\n    \"data\" in value &&\n    typeof value.data !== \"string\" &&\n    typeof value.data !== \"number\" &&\n    typeof value.data !== \"boolean\" &&\n    !isLSPArray(value.data) &&\n    !isLSPObject(value.data) &&\n    value.data !== null\n  );\n};\n\nexport const toJSError = (error: ResponseError) => {\n  const ErrorClass = class extends Error {};\n  let errorName = getErrorCodeName(error.code) ?? \"UnknownError\";\n  if (!errorName.endsWith(\"Error\")) errorName += \"Error\";\n  Object.defineProperty(ErrorClass, \"name\", {\n    value: errorName,\n    writable: false,\n    enumerable: false,\n    configurable: true,\n  });\n  Object.defineProperty(ErrorClass.prototype, \"name\", {\n    value: errorName,\n    writable: true,\n    enumerable: false,\n    configurable: true,\n  });\n  return Object.assign(new ErrorClass(error.message), { code: error.code, data: error.data });\n};\n\n/**\n * Get the name of an error code.\n * @param errorCode The error code.\n * @returns\n */\nexport const getErrorCodeName = (errorCode: integer) =>\n  Object.entries(ErrorCodes).find(([, v]) => v === errorCode)?.[0] ?? null;\n\n/**\n * Check if a value is a notification message.\n * @param value The value to check.\n * @returns\n */\nexport const isNotificationMessage = (value: unknown): value is NotificationMessage => {\n  if (!isMessage(value)) return false;\n  if (\"id\" in value && value.id !== null) return false;\n  if (!(\"method\" in value) || typeof value.method !== \"string\") return false;\n  return !(\"params\" in value && !isLSPArray(value.params) && !isLSPObject(value.params));\n};\n"
  },
  {
    "path": "src/utils/node-bridge.ts",
    "content": "import type { ChildProcessWithoutNullStreams } from \"node:child_process\";\n\nimport {\n  accessDir,\n  accessFile,\n  lookApp,\n  lookAppFirst,\n  lookPath,\n  lookPathFirst,\n  readDir,\n  realpath,\n  trueCasePath,\n} from \"@modules/fs\";\nimport * as path from \"@modules/path\";\n\nimport { unique } from \"radash\";\nimport semverGte from \"semver/functions/gte\";\nimport semverLt from \"semver/functions/lt\";\nimport semverRCompare from \"semver/functions/rcompare\";\nimport semverValid from \"semver/functions/valid\";\n\nimport { PLUGIN_DIR } from \"@/constants\";\nimport { TYPORA_VERSION } from \"@/typora-utils\";\n\nimport { findFreePort, getEnv, runCommand } from \"./cli-tools\";\nimport { cache } from \"./function\";\n\nexport abstract class NodeServer {\n  abstract readonly pid: number;\n\n  abstract send(message: string): void;\n  abstract onMessage(listener: (message: string) => void): void;\n\n  static async start(nodePath: string, modulePath: string): Promise<NodeServer> {\n    if (Files.isNode) return ElectronNodeServer.start(nodePath, modulePath);\n    return await MacOSNodeServer.start(nodePath, modulePath);\n  }\n\n  static getMock(): NodeServer {\n    return {\n      pid: -1,\n      send() {},\n      onMessage() {},\n    };\n  }\n}\n\nclass ElectronNodeServer implements NodeServer {\n  private readonly childProcess: ChildProcessWithoutNullStreams;\n\n  private constructor(nodePath: string, modulePath: string) {\n    if (nodePath === \"bundled\")\n      this.childProcess = window.reqnode!(\"child_process\").fork(modulePath, [\"--stdio\", \"true\"], {\n        silent: true,\n      }) as ChildProcessWithoutNullStreams;\n    else\n      this.childProcess = window.reqnode!(\"child_process\").spawn(nodePath, [\n        modulePath,\n        \"--stdio\",\n        \"true\",\n      ]);\n  }\n\n  get pid(): number {\n    return this.childProcess.pid!;\n  }\n\n  send(message: string): void {\n    this.childProcess.stdin.write(message);\n  }\n\n  onMessage(listener: (message: string) => void): void {\n    this.childProcess.stdout.on(\"data\", (data) => {\n      const message: string = data.toString(\"utf-8\");\n      listener(message);\n    });\n  }\n\n  static start(nodePath: string, modulePath: string): NodeServer {\n    return new ElectronNodeServer(nodePath, modulePath);\n  }\n}\n\nclass MacOSNodeServer implements NodeServer {\n  private readonly wsClient: WebSocket;\n  private readonly listeners: ((message: string) => void)[] = [];\n\n  private constructor(\n    public readonly pid: number,\n    port: number,\n  ) {\n    this.wsClient = new WebSocket(`ws://localhost:${port}`);\n    this.wsClient.onmessage = (event) => {\n      this.listeners.forEach((listener) => listener(event.data as string));\n    };\n  }\n\n  send(message: string): void {\n    this.wsClient.send(message);\n  }\n\n  onMessage(listener: (message: string) => void): void {\n    this.listeners.push(listener);\n  }\n\n  static async start(nodePath: string, modulePath: string): Promise<NodeServer> {\n    const port = await findFreePort();\n\n    const serverExecPath = path.join(PLUGIN_DIR, \"mac-server.cjs\");\n    const logFileName = \".typora-copilot-lsp-sever-output.log\";\n    const command = `nohup ${nodePath} '${serverExecPath}' ${port} '${modulePath}' > ~/${logFileName} 2>&1 &`;\n    void runCommand(command);\n\n    const pid = await MacOSNodeServer.getPortPID(port);\n    if (pid === -1) throw new Error(\"Failed to start Node LSP server\");\n\n    return new MacOSNodeServer(pid, port);\n  }\n\n  private static getPortPID(port: number): Promise<number> {\n    return new Promise<number>((resolve) => {\n      let times = 0;\n      const go = async () => {\n        const pid = Number.parseInt(await runCommand(`lsof -t -i:${port} | tail -n 1`));\n        if (Number.isNaN(pid)) {\n          if (times > 50) {\n            resolve(-1);\n          } else {\n            setTimeout(() => void go(), 100);\n            times++;\n          }\n        } else resolve(pid);\n      };\n      void go();\n    });\n  }\n}\n\n// The following 2 variables are used as global state in the application and are not persisted.\n// They should be set manually by other parts of the codebase.\nlet currentNodeRuntime: NodeRuntime = { path: \"not found\", version: \"unknown\" };\nexport const getCurrentNodeRuntime = () => currentNodeRuntime;\nexport const setCurrentNodeRuntime = (runtime: NodeRuntime) => {\n  currentNodeRuntime = runtime;\n};\n\nlet allAvailableNodeRuntimes: readonly NodeRuntime[] = [];\nexport const getAllAvailableNodeRuntimes = () => allAvailableNodeRuntimes;\nexport const setAllAvailableNodeRuntimes = (runtimes: readonly NodeRuntime[]) => {\n  allAvailableNodeRuntimes = runtimes;\n};\n\nexport interface NodeRuntime {\n  readonly path: string;\n  readonly version: string;\n}\n\n/**\n * Detect all available Node.js runtimes.\n * @returns\n */\nexport const detectAvailableNodeRuntimes = async ({\n  onFirstResolved = () => {},\n}: { onFirstResolved?: (runtime: NodeRuntime) => void } = {}): Promise<readonly NodeRuntime[]> => {\n  // The language server of GitHub Copilot requires at least Node.js >= 20 to run, so we need to\n  // find a Node.js runtime that meets this requirement.\n  const promises: Promise<\n    ((NodeRuntime | string | null)[] | NodeRuntime | string | null)[] | NodeRuntime | string | null\n  >[] = [];\n\n  // On Windows and Linux, Typora is built as an Electron app, and since Typora 1.9, the bundled\n  // Node.js version is >= 20.0.0. However, since Typora 1.10, the Electron `runAsNode` fuse is\n  // disabled, so the Typora executable can no longer be used as a Node.js runtime.\n  // Thus, for 1.9 <= Typora < 1.10.0 on Windows/Linux, we can use the bundled Node.js runtime.\n  if (Files.isNode && semverLt(TYPORA_VERSION, \"1.10.0\") && semverGte(process.version, \"20.0.0\"))\n    promises.push(Promise.resolve({ path: \"bundled\", version: process.version }));\n\n  /* Detect Node.js runtimes from the system */\n  const baseNodeNames = [\"node\", \"nodejs\"];\n  promises.push(Promise.defer(() => lookPath(\"node\")));\n  baseNodeNames.forEach((name) => promises.push(Promise.defer(() => lookApp(name, \"node\"))));\n  // When written this code, the latest Node.js version is v23, but for future compatibility, we\n  // search for Node.js runtimes from v28 to v20.\n  if (Files.isNode) {\n    Array.from({ length: 28 - 20 + 1 }, (_, i) => i + 20)\n      .reverse()\n      .flatMap((i) =>\n        baseNodeNames.flatMap((name) => [`${name}${i}`, `${name}-${i}`, `${name}-v${i}`]),\n      )\n      .forEach((name) => {\n        promises.push(Promise.defer(() => lookPath(name)));\n        promises.push(Promise.defer(() => lookApp(name, \"node\")));\n      });\n  } else {\n    // Optimization for macOS. Since all CLI functions on macOS are polyfilled simply by running\n    // command in the shell, we have to merge these operations into one command to reduce the number\n    // of shell calls to prevent performance issues.\n    const nodeMatchRegEx = \"^node(([1][8-9]|2[0-6])|(-v?([1][8-9]|2[0-6])))$\";\n    const script = /* sh */ `\n      regex='${nodeMatchRegEx}'\n      echo \"$PATH\" | tr ':' '\\n' | while read -r dir; do\n        find \"$dir\" -maxdepth 1 -type f -executable 2>/dev/null | awk -F/ -v regex=\"$regex\" '\n          {\n            # Extract basename and match against regex\n            file = $NF\n            if (file ~ regex) {\n              print $0\n            }\n          }\n        '\n      done\n    `;\n    promises.push(\n      Promise.defer(() => runCommand(script)).then((output) =>\n        output\n          .trim()\n          .split(\"\\n\")\n          .map((path) => path.trim())\n          .filter(Boolean),\n      ),\n    );\n  }\n\n  /* (Windows) Detect Node.js installed by Scoop: https://scoop.sh/ */\n  const lookScoop = cache(async function lookScoop() {\n    const scoopPath = (await lookPathFirst(\"scoop\")) || (await lookAppFirst(\"scoop\"));\n    if (!scoopPath) return null;\n\n    const scoopRoot = path.dirname(path.dirname(scoopPath));\n    const appsDir = path.join(scoopRoot, \"apps\");\n\n    const appDirs = await readDir(appsDir, \"dirsOnly\");\n\n    return { appsDir, appDirs };\n  });\n\n  if (Files.isWin)\n    promises.push(\n      Promise.defer(async () => {\n        const scoop = await lookScoop();\n        if (!scoop) return [];\n\n        const { appDirs, appsDir } = scoop;\n\n        const runtimes: NodeRuntime[] = [];\n        await Promise.all(\n          appDirs\n            .filter((dir) => dir.startsWith(\"nodejs\"))\n            .map(async (dir) => {\n              const versionDirs = await readDir(path.join(appsDir, dir), \"dirsOnly\");\n              for (const versionDir of versionDirs)\n                runtimes.push({\n                  path: path.join(appsDir, dir, versionDir, \"node.exe\"),\n                  version: versionDir,\n                });\n            })\n            .map((p) => p.catch(() => {})),\n        );\n        return runtimes.sort((a, b) => semverRCompare(a.version, b.version));\n      }),\n    );\n\n  /* (Windows) Detect Node.js installed by Chocolatey: https://chocolatey.org/ */\n  const lookChocolatey = cache(async function lookChocolatey() {\n    const chocoPath = (await lookPathFirst(\"choco\")) || (await lookAppFirst(\"chocolatey\", \"choco\"));\n    if (!chocoPath) return null;\n\n    const chocoRoot = path.dirname(path.dirname(chocoPath));\n    const libDir = path.join(chocoRoot, \"lib\");\n\n    const libDirs = await readDir(libDir, \"dirsOnly\");\n\n    return { libDir, libDirs };\n  });\n\n  if (Files.isWin) {\n    // It doesn't make sense to specifically support Chocolatey.\n    // Chocolatey doesn't provide `nodejs.portable` or support multiple versions of Node.js,\n    // `nodejs` and `nodejs-lts` are simply installed to `C:\\Program Files\\`, which should be\n    // already detected by the common directories lookup.\n    // `nvm.install` and `nvm.portable` are both installed to `C:\\ProgramData\\nvm\\`, which should be\n    // detected by the common directories lookup as well.\n    // The only exception is `fnm` installed to `C:\\ProgramData\\chocolatey\\lib\\fnm\\tools\\fnm.exe`,\n    // we'll detect it in the fnm section.\n  }\n\n  /* (macOS/Linux) Detect Node.js installed by Homebrew: https://brew.sh/ */\n  const lookHomebrew = cache(async function lookHomebrew() {\n    const brewPath =\n      Files.isMac ?\n        await (async () => {\n          const brewPath1 = await lookPathFirst(\"brew\");\n          if (!brewPath1) return await lookAppFirst(\"homebrew\", \"brew\");\n          // On macOS M1/M2/M3/M4, two versions of Homebrew may be installed: one for x86 simulating\n          // and one for ARM. We need to choose the latter if it exists.\n          if (/* Intel */ brewPath1 === \"/usr/local/bin/brew\") {\n            const brewPath2 = await lookAppFirst(\"homebrew\", \"brew\");\n            if (brewPath2) return brewPath2;\n            return brewPath1;\n          }\n          return brewPath1;\n        })()\n      : (await lookPathFirst(\"brew\")) || (await lookAppFirst(\"linuxbrew\", \"brew\"));\n    if (!brewPath) return null;\n\n    const cellarDir = (await runCommand(`\"${brewPath}\" --cellar`)).trim();\n    const cellarDirs = await readDir(cellarDir, \"dirsOnly\");\n\n    return { cellarDir, cellarDirs };\n  });\n\n  if (Files.isMac || Files.isLinux)\n    promises.push(\n      Promise.defer(async () => {\n        const homebrew = await lookHomebrew();\n        if (!homebrew) return [];\n\n        const { cellarDir, cellarDirs } = homebrew;\n\n        const runtimes: NodeRuntime[] = [];\n        await Promise.all(\n          cellarDirs\n            .filter((dir) => dir === \"node\" || dir.startsWith(\"node@\"))\n            .map(async (dir) => {\n              const versionDirs = (await readDir(path.join(cellarDir, dir), \"dirsOnly\")).filter(\n                (dir) => semverValid(dir),\n              );\n              for (const versionDir of versionDirs)\n                runtimes.push({\n                  path: path.join(cellarDir, dir, versionDir, \"bin\", \"node\"),\n                  version: versionDir,\n                });\n            })\n            .map((p) => p.catch(() => {})),\n        );\n        return runtimes.sort((a, b) => semverRCompare(a.version, b.version));\n      }),\n    );\n\n  /* (macOS) Detect Node.js installed by MacPorts: https://www.macports.org/ */\n  const lookMacPorts = cache(async function lookMacPorts() {\n    const portPath = (await lookPathFirst(\"port\")) || (await lookAppFirst(\"port\"));\n    if (!portPath) return null;\n\n    return { portPath };\n  });\n\n  if (Files.isMac)\n    promises.push(\n      Promise.defer(async () => {\n        const macPorts = await lookMacPorts();\n        if (!macPorts) return [];\n\n        const { portPath } = macPorts;\n\n        const output = await runCommand(\n          `\"${portPath}\" info installed | grep --color=never ^nodejs`,\n        );\n\n        const nodePaths = await Promise.all(\n          output\n            .trim()\n            .split(\"\\n\")\n            .map((line) => line.trim().split(\" \", 2)[0]!)\n            .map((name) =>\n              runCommand(`\"${portPath}\" contents ${name} | head -n 2 | tail -n 1`)\n                .then((path) => path.trim())\n                .catch(() => null),\n            ),\n        );\n        return nodePaths.filter(Boolean);\n      }),\n    );\n\n  /* (macOS/Linux) Detect Node.js installed by Devbox: https://www.jetify.com/docs/devbox/ */\n  if (Files.isMac || Files.isLinux)\n    promises.push(\n      Promise.defer(() =>\n        // Devbox is typically installed to `/usr/local/bin/devbox`, which is usually in the PATH,\n        // so invoke `devbox` directly should be enough\n        runCommand('eval \"$(devbox global shellenv)\" && echo \"$(which node)\\n$(node -v)\"')\n          .then(\n            (output) =>\n              output\n                .trim()\n                .split(\"\\n\")\n                .map((line) => line.trim()) as [string, string],\n          )\n          .then(([path, version]) => ({ path, version })),\n      ),\n    );\n\n  /* (Windows) Detect NVM for Windows: https://github.com/coreybutler/nvm-windows */\n  if (Files.isWin)\n    promises.push(\n      Promise.defer(async () => {\n        const nvmPath =\n          (await lookPathFirst(\"nvm\")) ||\n          (await lookAppFirst(\"nvm\")) ||\n          (await (async () => {\n            const scoop = await lookScoop();\n            if (!scoop) return null;\n            const { appDirs, appsDir } = scoop;\n            if (!appDirs.includes(\"nvm\")) return null;\n            return await accessFile(path.join(appsDir, \"nvm\", \"current\", \"nvm.exe\"));\n          })());\n        // NOTE: No need to detect NVM installed by Chocolatey, because Chocolatey installs NVM to\n        // `C:\\ProgramData\\nvm\\`, and it should be detected by the common directories lookup.\n        // NOTE: No need to detect NVM installed by Homebrew or MacPorts, because both create a\n        // symlink `~/.nvm/nvm.sh` to the actual NVM installation directory, and it should be\n        // detected by the common directories lookup.\n        if (!nvmPath) return [];\n\n        const nvmRoot = (await runCommand(`\"${nvmPath}\" root`))\n          .trim()\n          .replace(/^Current Root:/i, \"\")\n          .trim();\n        const dirs = await readDir(nvmRoot, \"dirsOnly\");\n\n        const runtimes: NodeRuntime[] = [];\n        for (const dir of dirs)\n          if (dir.startsWith(\"v\")) {\n            const version = dir.slice(1);\n            runtimes.push({ path: path.join(nvmRoot, dir, \"node.exe\"), version });\n          }\n        return runtimes.sort((a, b) => semverRCompare(a.version, b.version));\n      }),\n    );\n\n  /* (macOS/Linux) Detect NVM: https://github.com/nvm-sh/nvm */\n  if (Files.isMac || Files.isLinux)\n    promises.push(\n      Promise.defer(async () => {\n        // If found `NVM_DIR` in env, use it\n        // https://stackoverflow.com/a/45139064/21418758\n        // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n        let nvmRoot = (window.process ? process.env : await getEnv())[\"NVM_DIR\"];\n\n        if (!nvmRoot) {\n          // Lookup NVM from common directories\n          const nvmPath = await lookAppFirst(\"nvm\");\n          if (!nvmPath) return [];\n          // `NVM_DIR` is set to the parent directory of `nvm.sh` by default. See:\n          // https://github.com/nvm-sh/nvm/blob/9659af6c164ef5258751f17c0c2e73dc7b491210/nvm.sh#L429\n          nvmRoot = path.dirname(nvmPath);\n        }\n\n        const runtimes: NodeRuntime[] = [];\n        await Promise.all(\n          [\n            accessDir(path.join(nvmRoot, \"versions\", \"node\")).then((path) =>\n              path ?\n                readDir(path, \"dirsOnly\")\n                  .then((dirs) => ({ basePath: path, dirs }))\n                  .catch(() => ({ basePath: path, dirs: [] }))\n              : { basePath: \"\", dirs: [] },\n            ),\n            readDir(nvmRoot, \"dirsOnly\")\n              .then((dirs) => ({ basePath: nvmRoot, dirs }))\n              .catch(() => ({ basePath: nvmRoot, dirs: [] })),\n          ].map((p) =>\n            p.then(({ basePath, dirs }) => {\n              for (const dir of dirs) {\n                const match = /v\\d+\\.\\d+\\.\\d+/.exec(dir);\n                if (match) {\n                  const version = match[0];\n                  runtimes.push({ path: path.join(basePath, dir, \"bin\", \"node\"), version });\n                }\n              }\n            }),\n          ),\n        );\n        return runtimes.sort((a, b) => semverRCompare(a.version, b.version));\n      }),\n    );\n\n  /* Detect Node.js installed by fnm: https://github.com/Schniz/fnm */\n  promises.push(\n    Promise.defer(async () => {\n      const fnmPath =\n        (await lookPathFirst(\"fnm\")) ||\n        (await lookAppFirst(\"fnm\")) ||\n        (Files.isWin &&\n          (await (async () => {\n            const scoop = await lookScoop();\n            if (!scoop) return null;\n            const { appDirs, appsDir } = scoop;\n            if (!appDirs.includes(\"fnm\")) return null;\n            return await accessFile(path.join(appsDir, \"fnm\", \"current\", \"fnm.exe\"));\n          })())) ||\n        (Files.isWin &&\n          (await (async () => {\n            const chocolatey = await lookChocolatey();\n            if (!chocolatey) return null;\n            const { libDir, libDirs } = chocolatey;\n            if (!libDirs.includes(\"fnm\")) return null;\n            return await accessFile(path.join(libDir, \"fnm\", \"tools\", \"fnm.exe\"));\n          })())) ||\n        ((Files.isMac || Files.isLinux) &&\n          (await (async () => {\n            const homebrew = await lookHomebrew();\n            if (!homebrew) return null;\n            const { cellarDir, cellarDirs } = homebrew;\n            if (!cellarDirs.includes(\"fnm\")) return null;\n            const versionDirs = await readDir(path.join(cellarDir, \"fnm\"), \"dirsOnly\");\n            if (versionDirs.length === 0) return null;\n            const maxVersion = versionDirs.reduce((a, b) => (semverGte(a, b) ? a : b));\n            return await accessFile(path.join(cellarDir, \"fnm\", maxVersion, \"bin\", \"fnm\"));\n          })())) ||\n        (Files.isMac &&\n          (await (async () => {\n            const macPorts = await lookMacPorts();\n            if (!macPorts) return null;\n            const { portPath } = macPorts;\n            const output = (\n              await runCommand(`\"${portPath}\" contents fnm | head -n 2 | tail -n 1`).catch(() => \"\")\n            ).trim();\n            if (!output || output.startsWith(\"Error:\")) return null;\n            return output;\n          })())) ||\n        ((Files.isMac || Files.isLinux) &&\n          (await (async () => {\n            const output = (\n              await runCommand('eval \"$(devbox global shellenv)\" && which fnm').catch(() => \"\")\n            ).trim();\n            if (!output) return null;\n            return output;\n          })()));\n      if (!fnmPath) return [];\n\n      const fnmDirEnvRegEx = /^(?:export\\s+)?FNM_DIR=(?:['\"])?(.+?)(?:['\"])?$/;\n      const fnmDir = (await runCommand(`\"${fnmPath}\" env --shell bash`))\n        .trim()\n        .split(\"\\n\")\n        .map((line) => line.trim())\n        .filter(Boolean)\n        .find((line) => fnmDirEnvRegEx.test(line))\n        ?.match(fnmDirEnvRegEx)?.[1];\n      if (!fnmDir) return [];\n\n      const versionsDir = await accessDir(path.join(fnmDir, \"node-versions\"));\n      if (!versionsDir) return [];\n\n      const versionDirs = await readDir(versionsDir, \"dirsOnly\").catch(() => []);\n      if (versionDirs.length === 0) return [];\n\n      const runtimes: NodeRuntime[] = [];\n      for (const dir of versionDirs) {\n        const match = /v\\d+\\.\\d+\\.\\d+/.exec(dir);\n        if (match) {\n          const version = match[0];\n          runtimes.push({\n            path: path.join(\n              versionsDir,\n              dir,\n              \"installation\",\n              ...(Files.isWin ? [\"node.exe\"] : [\"bin\", \"node\"]),\n            ),\n            version,\n          });\n        }\n      }\n      return runtimes.sort((a, b) => semverRCompare(a.version, b.version));\n    }),\n  );\n\n  const filterVersion = (runtime: NodeRuntime | null) =>\n    runtime && semverValid(runtime.version) && semverGte(runtime.version, \"20.0.0\") ?\n      runtime\n    : null;\n\n  const resolveRuntime = (path: string) =>\n    runCommand(`\"${path}\" -v`)\n      .then((version) => ({ path, version: version.trim() }))\n      .catch(() => null);\n\n  const mayContainDuplicatesRuntimeFetcherPromises = promises.map((p) =>\n    p\n      .catch(() => null)\n      .then((r) =>\n        // Promisify all paths\n        r === null ? r\n        : typeof r === \"string\" ? resolveRuntime(r).then(filterVersion)\n        : Array.isArray(r) ?\n          r.map((r) =>\n            r === null ? r\n            : typeof r === \"string\" ? resolveRuntime(r).then(filterVersion)\n            : Array.isArray(r) ?\n              r.map((r) =>\n                r === null ? r\n                : typeof r === \"string\" ? resolveRuntime(r).then(filterVersion)\n                : filterVersion(r),\n              )\n            : filterVersion(r),\n          )\n        : filterVersion(r),\n      )\n      .then((r) =>\n        // Flatten 1\n        r === null ? [r]\n          // eslint-disable-next-line @typescript-eslint/await-thenable\n        : Array.isArray(r) ? Promise.all(r)\n        : [r],\n      )\n      .then((r) =>\n        // Flatten 2\n        // eslint-disable-next-line @typescript-eslint/await-thenable\n        Promise.all(r.filter(Boolean).flatMap((r) => (Array.isArray(r) ? r.filter(Boolean) : [r]))),\n      )\n      .then((r) =>\n        // Resolve true case path\n        Promise.all(\n          // eslint-disable-next-line @typescript-eslint/await-thenable\n          r.filter(Boolean).map((runtime) =>\n            runtime.path === \"bundled\" ?\n              runtime\n            : trueCasePath(runtime.path)\n                .then((path) => ({ path, version: runtime.version }))\n                .catch(() => null),\n          ),\n        ).then((r) => r.filter(Boolean)),\n      ),\n  );\n\n  Promise.orderedFirstResolved(\n    mayContainDuplicatesRuntimeFetcherPromises.map((p) =>\n      p.then((r) => (r.length === 0 ? Promise.reject(new Error(\"No runtime found\")) : r[0]!)),\n    ),\n  )\n    .then(onFirstResolved)\n    .catch(() => {});\n\n  const runtimes = await Promise.all(mayContainDuplicatesRuntimeFetcherPromises).then((runtimes) =>\n    unique(runtimes.flat(), (runtime) => runtime.path),\n  );\n  // Filter out runtimes with same realpath\n  return unique(\n    await Promise.all(\n      // eslint-disable-next-line @typescript-eslint/await-thenable\n      runtimes.map((runtime) =>\n        runtime.path === \"bundled\" ?\n          { ...runtime, realPath: runtime.path }\n        : realpath(runtime.path)\n            .then((realPath) => realPath && { ...runtime, realPath })\n            .catch(() => null),\n      ),\n    ).then((runtimes) => runtimes.filter(Boolean)),\n    (runtime) => runtime.realPath,\n  ).map(({ path, version }) => ({ path, version }));\n};\n"
  },
  {
    "path": "src/utils/observable.ts",
    "content": "/**\n * A lightweight observable implementation.\n */\nexport class Observable<T> {\n  private observers: ((value: T) => void)[] = [];\n\n  subscribe(observer: (value: T) => void): () => void {\n    this.observers.push(observer);\n    return () => {\n      this.observers = this.observers.filter((o) => o !== observer);\n    };\n  }\n\n  subscribeOnce(observer: (value: T) => void): void {\n    const unsubscribe = this.subscribe((value) => {\n      unsubscribe();\n      observer(value);\n    });\n  }\n\n  next(value: T): void {\n    this.observers.forEach((observer) => observer(value));\n  }\n}\n"
  },
  {
    "path": "src/utils/random.ts",
    "content": "/**\n * Generate a UUID.\n * @returns\n */\nexport function generateUUID(): string {\n  return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, function (c) {\n    // eslint-disable-next-line sonarjs/pseudo-random\n    const r = (Math.random() * 16) | 0;\n    const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n    return v.toString(16);\n  });\n}\n"
  },
  {
    "path": "src/utils/stream.ts",
    "content": "/**\n * Parse an SSE (Server-Sent Events) stream and handle messages.\n * @param stream The ReadableStream to read from.\n * @param onMessage The callback to handle each message.\n * @param onError The callback to handle errors for each message (optional).\n */\nexport async function parseSSEStream<T>(\n  stream: ReadableStream,\n  onMessage: (data: T) => void,\n  onError?: (error: Error, rawData?: string) => void,\n  signal?: AbortSignal,\n): Promise<void> {\n  const reader = stream.getReader();\n  const decoder = new TextDecoder();\n  let buffer = \"\";\n\n  try {\n    if (signal?.aborted) {\n      await reader.cancel();\n      throw new DOMException(\"Stream reading was aborted\", \"AbortError\");\n    }\n\n    const abortController = new AbortController();\n    const localSignal = abortController.signal;\n\n    if (signal) {\n      const abortListener = () => {\n        abortController.abort();\n        void reader.cancel();\n      };\n\n      signal.addEventListener(\"abort\", abortListener, { once: true });\n\n      localSignal.addEventListener(\n        \"abort\",\n        () => {\n          signal.removeEventListener(\"abort\", abortListener);\n        },\n        { once: true },\n      );\n    }\n\n    while (!localSignal.aborted) {\n      if (signal?.aborted) {\n        await reader.cancel();\n        break;\n      }\n\n      const { done, value } = await reader.read();\n      if (done) break;\n\n      buffer += decoder.decode(value, { stream: true });\n\n      let processBuffer = true;\n      while (processBuffer) {\n        const messageEnd = buffer.indexOf(\"\\n\\n\");\n        if (messageEnd === -1) {\n          processBuffer = false;\n          continue;\n        }\n\n        const message = buffer.substring(0, messageEnd).trim();\n        buffer = buffer.substring(messageEnd + 2);\n\n        if (message && !message.includes(\"[DONE]\")) {\n          const dataPrefix = \"data: \";\n          if (message.startsWith(dataPrefix)) {\n            const jsonStr = message.substring(dataPrefix.length).trim();\n            try {\n              const data = JSON.parse(jsonStr) as T;\n              onMessage(data);\n            } catch (e) {\n              onError?.(e instanceof Error ? e : new Error(String(e)), jsonStr);\n            }\n          }\n        }\n      }\n\n      if (!signal?.aborted && buffer.trim() && buffer.trim() !== \"[DONE]\") {\n        try {\n          const data = JSON.parse(buffer.trim()) as T;\n          onMessage(data);\n        } catch (e) {\n          onError?.(e instanceof Error ? e : new Error(String(e)), buffer.trim());\n        }\n      }\n    }\n\n    // Process any remaining buffer content\n    if (buffer.trim() && buffer.trim() !== \"[DONE]\")\n      try {\n        const data = JSON.parse(buffer.trim()) as T;\n        onMessage(data);\n      } catch (e) {\n        onError?.(e instanceof Error ? e : new Error(String(e)), buffer.trim());\n      }\n  } catch (error) {\n    if (signal?.aborted && error instanceof DOMException && error.name === \"AbortError\") return;\n\n    if (onError) onError(error instanceof Error ? error : new Error(String(error)));\n    else throw error;\n  } finally {\n    decoder.decode();\n\n    try {\n      await reader.cancel();\n    } catch (e) {\n      // Ignore\n    }\n  }\n}\n"
  },
  {
    "path": "src/utils/tools.proof.ts",
    "content": "import { describe, equal, expect, it } from \"typroof\";\n\nimport { omit } from \"./tools\";\n\ndescribe(\"omit\", () => {\n  it(\"should omit keys from an object\", () => {\n    expect(omit({ a: 1, b: 2, c: 3 }, \"a\")).to(equal<{ b: number; c: number }>);\n    expect(omit({ a: 1, b: 2, c: 3 }, \"a\", \"c\")).to(equal<{ b: number }>);\n  });\n});\n"
  },
  {
    "path": "src/utils/tools.ts",
    "content": "import type { EOL, Range } from \"@/types/lsp\";\nimport type { ReadonlyRecord } from \"@/types/tools\";\n\n/**\n * Assert that the value is never (i.e., this statement should never be reached).\n * @param value The value to assert.\n */\nexport const assertNever = (value: never): never => {\n  throw new Error(`Unexpected value: ${JSON.stringify(value)}`);\n};\n\n/**\n * Omit keys from an object\n * @param obj The object to omit keys from.\n * @param keys The keys to omit.\n * @returns\n *\n * @example\n * ```javascript\n * omit({ a: 1, b: 2, c: 3 }, \"a\"); // => { b: 2, c: 3 }\n * omit({ a: 1, b: 2, c: 3 }, \"a\", \"c\"); // => { b: 2 }\n * ```\n */\nexport const omit = <O extends ReadonlyRecord<PropertyKey, unknown>, KS extends (keyof O)[]>(\n  obj: O,\n  ...keys: KS\n): Omit<O, KS[number]> extends infer U ? { [K in keyof U]: U[K] } : never => {\n  const result: Record<PropertyKey, unknown> = {};\n  for (const key in obj) if (!keys.includes(key)) result[key] = obj[key];\n  return result as never;\n};\n\n/**\n * A stricter version of {@linkcode Object.keys} that makes TS happy.\n * @param obj The object to get keys from.\n * @returns\n */\nexport const keysOf = <O extends object>(obj: O): readonly `${keyof O & (string | number)}`[] =>\n  Object.keys(obj) as readonly `${keyof O & (string | number)}`[];\n\n/**\n * A stricter version of {@linkcode Object.values} that makes TS happy.\n * @param obj The object to get values from.\n * @returns\n */\nexport const valuesOf = <O extends object>(obj: O): readonly O[keyof O][] =>\n  Object.values(obj) as readonly O[keyof O][];\n\n/**\n * A stricter version of {@linkcode Object.entries} that makes TS happy.\n * @param obj The object to get entries from.\n * @returns\n */\nexport const entriesOf = <O extends object>(\n  obj: O,\n): O extends O ? readonly (readonly [`${keyof O & (string | number)}`, O[keyof O]])[] : never =>\n  Object.entries(obj) as unknown as O extends O ?\n    readonly (readonly [`${keyof O & (string | number)}`, O[keyof O]])[]\n  : never;\n\nexport const isKeyOf = <O extends object>(obj: O, key: PropertyKey): key is keyof O => key in obj;\n\n/**\n * Get a global variable.\n * @param name The name of the global variable.\n * @returns The value of the global variable.\n */\nexport const getGlobalVar = <K extends keyof typeof globalThis | (string & NonNullable<unknown>)>(\n  name: K,\n): K extends keyof typeof globalThis ? (typeof globalThis)[K] : unknown => {\n  try {\n    return global[name as keyof typeof globalThis];\n  } catch {\n    return globalThis[name as keyof typeof globalThis];\n  }\n};\n/**\n * Set a global variable.\n * @param name The name of the global variable.\n * @param value The value of the global variable to set.\n */\nexport const setGlobalVar = <K extends keyof typeof globalThis | (string & NonNullable<unknown>)>(\n  name: K,\n  value: K extends keyof typeof globalThis ? (typeof globalThis)[K] : unknown,\n) => {\n  try {\n    Object.defineProperty(global, name, { value });\n  } catch {\n    Object.defineProperty(globalThis, name, { value });\n  }\n};\n\n/**\n * Replace `text` in `range` with `newText`.\n * @param text The text to replace.\n * @param range The range to replace.\n * @param newText The new text.\n * @param eol The end of line character. Defaults to `\"\\n\"`.\n * @returns\n */\nexport const replaceTextByRange = (\n  text: string,\n  range: Range,\n  newText: string,\n  eol: EOL = \"\\n\",\n) => {\n  const { end, start } = range;\n  const lines = text.split(eol);\n  const startLine = lines[start.line]!;\n\n  if (start.line === end.line)\n    return [\n      ...lines.slice(0, start.line),\n      startLine.slice(0, start.character) + newText + startLine.slice(end.character),\n      ...lines.slice(end.line + 1),\n    ].join(eol);\n\n  const endLine = lines[end.line]!;\n  return [\n    ...lines.slice(0, start.line),\n    startLine.slice(0, start.character) + newText,\n    ...lines.slice(start.line + 1, end.line),\n    endLine.slice(end.character),\n  ].join(eol);\n};\n"
  },
  {
    "path": "stylelint.config.js",
    "content": "// @ts-check\n\n/** @satisfies {import(\"stylelint\").Config} */\nconst config = {\n  extends: \"stylelint-config-standard-scss\",\n  rules: {\n    \"color-function-alias-notation\": \"with-alpha\",\n    \"color-function-notation\": \"legacy\",\n  },\n};\n\nexport default config;\n"
  },
  {
    "path": "test/setup.ts",
    "content": "import { Window } from \"happy-dom\";\n\nimport \"@/patches/typora\";\n\nconst window = new Window();\nObject.assign(window, {\n  isWin: false,\n  dirname: \"/usr/share/typora/resources\",\n  _options: {\n    appLocale: \"en\",\n    appVersion: \"1.7.6\",\n  },\n});\nglobal.window = window as unknown as typeof global.window;\n"
  },
  {
    "path": "tsconfig.build.json",
    "content": "{\n  \"extends\": \"./tsconfig.json\",\n  \"compilerOptions\": {\n    \"noEmit\": false,\n    \"outDir\": \"dist\",\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"src\"],\n  \"exclude\": [\"src/**/*.spec.ts\", \"src/**/*.proof.ts\"]\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"module\": \"ES2020\",\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n\n    /* Bundler mode */\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n\n    /* JSX */\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"preact\",\n\n    /* Linting */\n    \"strict\": true,\n    \"checkJs\": true,\n    \"allowUmdGlobalAccess\": true,\n    \"noUnusedLocals\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUncheckedIndexedAccess\": true,\n\n    /* Path aliases */\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"],\n      \"@modules/*\": [\"src/modules/*\"],\n      \"@test/*\": [\"test/*\"]\n    }\n  },\n  \"include\": [\"src\", \"test\", \"rollup.config.ts\", \"pre-commit.ts\", \"vitest.config.ts\"]\n}\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import path from \"node:path\";\n\nimport { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  resolve: {\n    alias: {\n      \"@modules\": path.resolve(__dirname, \"src/modules\"),\n      \"@\": path.resolve(__dirname, \"src\"),\n      \"@test\": path.resolve(__dirname, \"test\"),\n    },\n  },\n  test: {\n    setupFiles: [\"./test/setup.ts\"],\n    environment: \"happy-dom\",\n  },\n});\n"
  }
]