├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.yml └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── LICENSE ├── README.md ├── build-settings.lua ├── buildClient.bat ├── buildClient.sh ├── changelog.md ├── client ├── .eslintrc.json ├── .prettierrc.yml ├── package-lock.json ├── package.json ├── src │ ├── addon_manager │ │ ├── commands │ │ │ ├── disable.ts │ │ │ ├── enable.ts │ │ │ ├── getAddons.ts │ │ │ ├── index.ts │ │ │ ├── open.ts │ │ │ ├── openLog.ts │ │ │ ├── refreshAddons.ts │ │ │ ├── setVersion.ts │ │ │ ├── uninstall.ts │ │ │ └── update.ts │ │ ├── config.ts │ │ ├── models │ │ │ └── addon.ts │ │ ├── panels │ │ │ └── WebVue.ts │ │ ├── registration.ts │ │ ├── services │ │ │ ├── addonManager.service.ts │ │ │ ├── filesystem.service.ts │ │ │ ├── git.service.ts │ │ │ ├── logging.service.ts │ │ │ ├── logging │ │ │ │ ├── vsCodeLogFileTransport.ts │ │ │ │ └── vsCodeOutputTransport.ts │ │ │ ├── settings.service.ts │ │ │ └── string.service.ts │ │ └── types │ │ │ ├── addon.d.ts │ │ │ └── webvue.ts │ ├── extension.ts │ ├── languageserver.ts │ ├── psi │ │ └── psiViewer.ts │ └── vscode.proposed │ │ └── editorHoverVerbosityLevel.d.ts ├── tsconfig.json └── web │ └── dist │ ├── 3rdpartylicenses.txt │ ├── assets │ ├── fonts │ │ └── mat-icon-font.woff2 │ └── scss │ │ └── mat-icon.scss │ ├── favicon.ico │ ├── index.html │ ├── main.0ac5708dde926afc.js │ ├── mat-icon-font.d36bf6bfd46ff3bb.woff2 │ ├── polyfills.fdaa14aa9967abe5.js │ ├── runtime.16fa3418f03cd751.js │ └── styles.4321c6214ef1a9a7.css ├── images ├── Auto Completion.gif ├── Diagnostics.gif ├── Emmy Annotation.gif ├── Find All References.gif ├── Goto Definition.gif ├── Hover.gif ├── Install In VSCode.gif ├── Rename.gif ├── Signature Help.gif ├── icon.ico ├── logo.png ├── plugin-diff.gif ├── setting-without-vscode.gif ├── tokens │ ├── comment.block.lua.jpg │ ├── comment.line.double-dash.lua.jpg │ ├── constant.character.escape.byte.lua.jpg │ ├── constant.character.escape.lua.jpg │ ├── constant.character.escape.unicode.lua.jpg │ ├── constant.language.lua.jpg │ ├── constant.numeric.float.hexadecimal.lua.jpg │ ├── constant.numeric.float.lua.jpg │ ├── constant.numeric.integer.hexadecimal.lua.jpg │ ├── constant.numeric.integer.lua.jpg │ ├── entity.name.class.lua.jpg │ ├── entity.name.function.lua.jpg │ ├── interface.declaration.jpg │ ├── invalid.illegal.character.escape.lua.jpg │ ├── keyword.control.goto.lua.jpg │ ├── keyword.control.lua.jpg │ ├── keyword.local.lua.jpg │ ├── keyword.operator.lua.jpg │ ├── namespace.deprecated.jpg │ ├── namespace.readonly.jpg │ ├── namespace.static.jpg │ ├── parameter.declaration.jpg │ ├── property.declaration.jpg │ ├── punctuation.definition.comment.begin.lua.jpg │ ├── punctuation.definition.comment.end.lua.jpg │ ├── punctuation.definition.comment.lua.jpg │ ├── punctuation.definition.parameters.begin.lua.jpg │ ├── punctuation.definition.parameters.finish.lua.jpg │ ├── punctuation.definition.string.begin.lua.jpg │ ├── punctuation.definition.string.end.lua.jpg │ ├── punctuation.section.embedded.begin.lua.jpg │ ├── punctuation.section.embedded.end.lua.jpg │ ├── punctuation.separator.arguments.lua.jpg │ ├── string.quoted.double.lua.jpg │ ├── string.quoted.other.multiline.lua.jpg │ ├── string.quoted.single.lua.jpg │ ├── string.tag.lua.jpg │ ├── support.function.library.lua.jpg │ ├── support.function.lua.jpg │ ├── variable.jpg │ ├── variable.language.self.lua.jpg │ ├── variable.other.lua.jpg │ └── variable.parameter.function.lua.jpg └── wiki-workspace.png ├── make └── copy.lua ├── package-lock.json ├── package.json ├── package.nls.es-419.json ├── package.nls.ja-jp.json ├── package.nls.json ├── package.nls.pt-br.json ├── package.nls.zh-cn.json ├── package.nls.zh-tw.json ├── package ├── build.lua └── semanticTokenScope.lua ├── publish.lua ├── setting ├── schema-es-419.json ├── schema-ja-jp.json ├── schema-pt-br.json ├── schema-zh-cn.json ├── schema-zh-tw.json ├── schema.json └── setting.json └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ["https://paypal.me/sumneko", "https://github.com/LuaLS/lua-language-server/issues/484"] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report something behaving in an unexpected manor. 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: > 7 | **Please check for similar issues before continuing!** 8 | - type: dropdown 9 | id: OS 10 | attributes: 11 | label: Which OS are you using? 12 | options: 13 | - Windows 14 | - Linux 15 | - MacOS 16 | - Windows WSL 17 | - Other 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: expected 22 | attributes: 23 | label: Expected Behaviour 24 | description: What is the expected behaviour? 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: actual 29 | attributes: 30 | label: Actual Behaviour 31 | description: What is actually happening that is unexpected? 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: reproduction 36 | attributes: 37 | label: Reproduction steps 38 | description: > 39 | Please provide detailed steps to reproduce the error. This will help us 40 | to diagnose, test fixes, and fix the issue. 41 | value: | 42 | 1. Go to '...' 43 | 2. Click '...' 44 | 3. See error '...' 45 | validations: 46 | required: true 47 | - type: textarea 48 | id: additional-notes 49 | attributes: 50 | label: Additional Notes 51 | description: > 52 | Please provide any additional notes, context, and media you have. 53 | - type: textarea 54 | id: log 55 | attributes: 56 | label: Log 57 | description: > 58 | Please provide your log. The log can be found in VS Code by opening the 59 | `OUTPUT` panel and selecting `Lua Addon Manager` from the dropdown. 60 | - type: markdown 61 | attributes: 62 | value: | 63 | Thank you very much for helping us improve! ❤️ 64 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | tags: 11 | - "*" 12 | pull_request: 13 | branches: 14 | - master 15 | 16 | env: 17 | PROJECT: vscode-lua 18 | BIN_DIR: server/bin 19 | PKG_SUFFIX: vsix 20 | 21 | jobs: 22 | compile: 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | include: 27 | - { os: ubuntu-20.04, target: linux, platform: linux-x64 } 28 | - { os: ubuntu-20.04, target: linux, platform: linux-arm64 } 29 | - { os: macos-latest, target: darwin, platform: darwin-x64 } 30 | - { os: macos-latest, target: darwin, platform: darwin-arm64 } 31 | #- { os: windows-latest, target: windows, platform: win32-ia32 } # 不再支持32位 32 | - { os: windows-latest, target: windows, platform: win32-x64 } 33 | runs-on: ${{ matrix.os }} 34 | steps: 35 | - name: Install aarch64-linux-gnu 36 | if: ${{ matrix.platform == 'linux-arm64' }} 37 | run: | 38 | sudo apt-get update 39 | sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu 40 | 41 | - uses: actions/checkout@v4 42 | with: 43 | submodules: recursive 44 | 45 | - name: Set up Node.js 46 | uses: actions/setup-node@v2 47 | with: 48 | node-version: '22' # 指定要安装的 Node.js 版本 49 | 50 | - name: Build for Windows 51 | if: ${{ matrix.target == 'windows' }} 52 | working-directory: ./server 53 | run: | 54 | .\make.bat ${{ matrix.platform }} 55 | rm -r ./build 56 | 57 | - name: Build for Linux 58 | if: ${{ matrix.target == 'linux' }} 59 | working-directory: ./server 60 | run: | 61 | sudo apt update 62 | sudo apt install ninja-build 63 | ./make.sh ${{ matrix.platform }} 64 | rm -r ./build 65 | 66 | - name: Build for macOS 67 | if: ${{ matrix.target == 'darwin' }} 68 | working-directory: ./server 69 | run: | 70 | brew install ninja 71 | ./make.sh ${{ matrix.platform }} 72 | rm -r ./build 73 | 74 | - name: Setting up workflow variables 75 | id: vars 76 | shell: bash 77 | run: | 78 | # Package version 79 | if [[ $GITHUB_REF = refs/tags/* ]]; then 80 | PKG_VERSION=${GITHUB_REF##*/} 81 | else 82 | PKG_VERSION=${GITHUB_SHA:0:7} 83 | fi 84 | 85 | # Package name w/ version 86 | PKG_BASENAME="${{ env.PROJECT }}-${PKG_VERSION}-${{ matrix.platform }}" 87 | 88 | # Full name of the tarball asset 89 | PKG_NAME="${PKG_BASENAME}.${PKG_SUFFIX}" 90 | 91 | echo PKG_BASENAME=${PKG_BASENAME} >> $GITHUB_OUTPUT 92 | echo PKG_NAME=${PKG_NAME} >> $GITHUB_OUTPUT 93 | 94 | - name: Compile client 95 | shell: bash 96 | run: | 97 | npm install -g typescript 98 | cd client 99 | npm ci 100 | npm run build 101 | cd .. 102 | 103 | - name: Build Addon Manager WebVue 104 | shell: bash 105 | run: | 106 | cd client/webvue 107 | npm ci 108 | npm run build 109 | cd ../.. 110 | 111 | - name: Pack vsix 112 | id: pack 113 | shell: bash 114 | run: | 115 | npm install -g @vscode/vsce 116 | vsce package -o ${{ steps.vars.outputs.PKG_NAME }} -t ${{ matrix.platform }} 117 | 118 | - uses: actions/upload-artifact@v4 119 | with: 120 | name: ${{ steps.vars.outputs.PKG_BASENAME }} 121 | path: ${{ steps.vars.outputs.PKG_NAME }} 122 | 123 | - name: Publish release assets 124 | uses: softprops/action-gh-release@v1 125 | if: startsWith(github.ref, 'refs/tags/') 126 | with: 127 | generate_release_notes: true 128 | files: | 129 | ${{ steps.vars.outputs.PKG_NAME }} 130 | 131 | - name: Publish to VSCode Market 132 | if: startsWith(github.ref, 'refs/tags/') 133 | run: vsce publish -i ${{ steps.vars.outputs.PKG_NAME }} -p ${{ secrets.VSCE_TOKEN }} 134 | 135 | - name: Publish to Open VSX Registry 136 | if: startsWith(github.ref, 'refs/tags/') 137 | run: | 138 | npm install -g ovsx 139 | ovsx publish -i ${{ steps.vars.outputs.PKG_NAME }} -p ${{ secrets.OVSX_TOKEN }} 140 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /client/node_modules 2 | /client/out/ 3 | /publish/ 4 | /luadoc/out/ 5 | /ovsx-token 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "server"] 2 | path = server 3 | url = https://github.com/LuaLS/lua-language-server 4 | [submodule "client/3rd/vscode-lua-doc"] 5 | path = client/3rd/vscode-lua-doc 6 | url = https://github.com/LuaLS/vscode-lua-doc 7 | [submodule "client/webvue"] 8 | path = client/webvue 9 | url = https://github.com/LuaLS/vscode-lua-webvue 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "发布", 7 | "type": "lua", 8 | "request": "launch", 9 | "stopOnEntry": false, 10 | "program": "${workspaceRoot}/publish.lua", 11 | "arg": [ 12 | ], 13 | "sourceCoding": "utf8", 14 | "luaexe": "${workspaceFolder}/server/bin/lua-language-server.exe", 15 | "outputCapture": [ 16 | "print", 17 | "stderr", 18 | ], 19 | }, 20 | { 21 | "name": "导出配置文件", 22 | "type": "lua", 23 | "request": "launch", 24 | "stopOnEntry": false, 25 | "program": "${workspaceRoot}/build-settings.lua", 26 | "arg": [ 27 | ], 28 | "luaexe": "${workspaceFolder}/server/bin/lua-language-server.exe", 29 | "sourceCoding": "utf8", 30 | "outputCapture": [ 31 | "print", 32 | "stderr", 33 | ], 34 | }, 35 | { 36 | "type": "extensionHost", 37 | "request": "launch", 38 | "name": "Launch Client", 39 | "runtimeExecutable": "${execPath}", 40 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 41 | "outFiles": ["${workspaceRoot}/client/out/**/*.js"], 42 | }, 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Lua.runtime.version": "Lua 5.4", 3 | "Lua.workspace.library": { 4 | "server/script-beta": true 5 | }, 6 | "Lua.workspace.checkThirdParty": false 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "shell", 6 | "command": "./buildClient.sh", 7 | "windows": { 8 | "command": ".\\buildClient.bat" 9 | }, 10 | "group": { 11 | "kind": "build", 12 | "isDefault": true 13 | }, 14 | "presentation": { 15 | "echo": true, 16 | "reveal": "always", 17 | "focus": false, 18 | "panel": "dedicated", 19 | "showReuseMessage": false, 20 | "clear": true 21 | }, 22 | "icon": { "color": "terminal.ansiCyan", "id": "server-process" }, 23 | "label": "Build Client" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | **/* 2 | 3 | !client/node_modules 4 | !client/out 5 | !client/package.json 6 | !client/3rd/vscode-lua-doc/doc 7 | !client/3rd/vscode-lua-doc/extension.js 8 | !client/webvue/build 9 | 10 | !server/bin 11 | !server/locale 12 | !server/script 13 | !server/main.lua 14 | !server/main.lua 15 | !server/debugger.lua 16 | !server/changelog.md 17 | !server/LICENSE 18 | !server/meta/3rd 19 | !server/meta/template 20 | !server/meta/spell 21 | 22 | !images/logo.png 23 | 24 | !setting 25 | !syntaxes 26 | !package.json 27 | !README.md 28 | !changelog.md 29 | !package.nls.json 30 | !package.nls.*.json 31 | !LICENSE 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 最萌小汐 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-language-server 2 | 3 | ![build](https://img.shields.io/github/actions/workflow/status/LuaLS/lua-language-server/.github%2Fworkflows%2Fbuild.yml) 4 | ![Version (including pre-releases)](https://img.shields.io/visual-studio-marketplace/v/sumneko.lua) 5 | ![Installs](https://img.shields.io/visual-studio-marketplace/i/sumneko.lua) 6 | ![Downloads](https://img.shields.io/visual-studio-marketplace/d/sumneko.lua) 7 | 8 | 9 | ***Lua development just got a whole lot better*** 🧠 10 | 11 | The Lua language server provides various language features for Lua to make development easier and faster. With nearly a million installs in Visual Studio Code, it is the most popular extension for Lua language support. 12 | 13 | [See our website for more info](https://luals.github.io). 14 | 15 | ## Features 16 | 17 | - ⚙️ Supports `Lua 5.4`, `Lua 5.3`, `Lua 5.2`, `Lua 5.1`, and `LuaJIT` 18 | - 📄 Over 20 supported [annotations](https://luals.github.io/wiki/annotations/) for documenting your code 19 | - ↪ Go to definition 20 | - 🦺 Dynamic [type checking](https://luals.github.io/wiki/type-checking/) 21 | - 🔍 Find references 22 | - ⚠️ [Diagnostics/Warnings](https://luals.github.io/wiki/diagnostics/) 23 | - 🕵️ [Syntax checking](https://luals.github.io/wiki/syntax-errors/) 24 | - 📝 Element renaming 25 | - 🗨️ Hover to view details on variables, functions, and more 26 | - 🖊️ Autocompletion 27 | - 📚 Support for [libraries](https://luals.github.io/wiki/settings/#workspacelibrary) 28 | - 💅 [Code formatting](https://luals.github.io/wiki/formatter/) 29 | - 💬 [Spell checking](https://luals.github.io/wiki/diagnostics/#spell-check) 30 | - 🛠️ Custom [plugins](https://luals.github.io/wiki/plugins/) 31 | - 📖 [Documentation Generation](https://luals.github.io/wiki/export-docs/) 32 | 33 | ## Install 34 | The language server can be installed for use in Visual Studio Code, NeoVim, and any [other clients](https://microsoft.github.io/language-server-protocol/implementors/tools/) that support the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/). 35 | 36 | See [installation instructions on our website](https://luals.github.io/#install). 37 | 38 | [![Install in VS Code](https://img.shields.io/badge/VS%20Code-Install-blue?style=for-the-badge&logo=visualstudiocode "Install in VS Code")](https://luals.github.io/#vscode-install) 39 | [![Install for NeoVim](https://img.shields.io/badge/NeoVim-Install-blue?style=for-the-badge&logo=neovim "Install for NeoVim")](https://luals.github.io/#neovim-install) 40 | [![Other](https://img.shields.io/badge/Other-Install-blue?style=for-the-badge&logo=windowsterminal "Install for command line")](https://luals.github.io/#other-install) 41 | 42 | ### Community Install Methods 43 | The install methods below are maintained by community members. 44 | 45 | [asdf plugin](https://github.com/bellini666/asdf-lua-language-server) 46 | 47 | ## Links 48 | - [Changelog](https://github.com/LuaLS/lua-language-server/blob/master/changelog.md) 49 | - [Wiki](https://luals.github.io/wiki) 50 | - [FAQ](https://luals.github.io/wiki/faq) 51 | - [Report an issue][issues] 52 | - [Suggest a feature][issues] 53 | - [Discuss](https://github.com/LuaLS/lua-language-server/discussions) 54 | 55 | > If you find any mistakes, please [report it][issues] or open a [pull request][pulls] if you have a fix of your own ❤️ 56 | > 57 | > 如果你发现了任何错误,请[告诉我][issues]或使用[Pull Requests][pulls]来直接修复。❤️ 58 | 59 | [issues]: https://github.com/LuaLS/lua-language-server/issues 60 | [pulls]: https://github.com/LuaLS/lua-language-server/pulls 61 | 62 | ## Available Languages 63 | 64 | - `en-us` 🇺🇸 65 | - `zh-cn` 🇨🇳 66 | - `zh-tw` 🇹🇼 67 | - `pt-br` 🇧🇷 68 | 69 | 70 | > **Note** 71 | > All translations are provided and collaborated on by the community. If you find an inappropriate or harmful translation, [please report it immediately](https://github.com/LuaLS/lua-language-server/issues). 72 | 73 | Are you able to [provide a translation](https://luals.github.io/wiki/translations)? It would be greatly appreciated! 74 | 75 | Thank you to [all contributors of translations](https://github.com/LuaLS/lua-language-server/commits/master/locale)! 76 | 77 | 78 | ## Privacy 79 | The language server had **opt-in** telemetry that collected usage data and sent it to the development team to help improve the extension. Read our [privacy policy](https://luals.github.io/privacy#language-server) to learn more. Telemetry was removed in `v3.6.5` and is no longer part of the language server. 80 | 81 | 82 | ## Contributors 83 | ![GitHub Contributors Image](https://contrib.rocks/image?repo=sumneko/lua-language-server) 84 | 85 | ## Credit 86 | Software that the language server (or the development of it) uses: 87 | 88 | * [bee.lua](https://github.com/actboy168/bee.lua) 89 | * [luamake](https://github.com/actboy168/luamake) 90 | * [LPegLabel](https://github.com/sqmedeiros/lpeglabel) 91 | * [LuaParser](https://github.com/LuaLS/LuaParser) 92 | * [ScreenToGif](https://github.com/NickeManarin/ScreenToGif) 93 | * [vscode-languageclient](https://github.com/microsoft/vscode-languageserver-node) 94 | * [lua.tmbundle](https://github.com/textmate/lua.tmbundle) 95 | * [EmmyLua](https://emmylua.github.io) 96 | * [lua-glob](https://github.com/LuaLS/lua-glob) 97 | * [utility](https://github.com/LuaLS/utility) 98 | * [vscode-lua-doc](https://github.com/actboy168/vscode-lua-doc) 99 | * [json.lua](https://github.com/actboy168/json.lua) 100 | * [EmmyLuaCodeStyle](https://github.com/CppCXY/EmmyLuaCodeStyle) 101 | * [inspect.lua](https://github.com/kikito/inspect.lua) 102 | -------------------------------------------------------------------------------- /build-settings.lua: -------------------------------------------------------------------------------- 1 | local fs = require 'bee.filesystem' 2 | 3 | local currentPath = debug.getinfo(1, 'S').source:sub(2) 4 | local rootPath = currentPath:gsub('[^/\\]-$', '') 5 | package.path = package.path 6 | .. ';' .. rootPath .. '?.lua' 7 | .. ';' .. rootPath .. 'server/script/?.lua' 8 | 9 | local json = require 'json-beautify' 10 | local configuration = require 'server.tools.configuration' 11 | local fsu = require 'fs-utility' 12 | local lloader = require 'locale-loader' 13 | local diagd = require 'proto.diagnostic' 14 | local util = require 'utility' 15 | 16 | local function addSplited(t, key, value) 17 | t[key] = value 18 | for pos in key:gmatch '()%.' do 19 | local left = key:sub(1, pos - 1) 20 | local right = key:sub(pos + 1) 21 | local nt = t[left] or { 22 | properties = {} 23 | } 24 | t[left] = nt 25 | addSplited(nt.properties, right, value) 26 | end 27 | end 28 | 29 | local function copyWithNLS(t, callback) 30 | local nt = {} 31 | local mt = getmetatable(t) 32 | if mt then 33 | setmetatable(nt, mt) 34 | end 35 | for k, v in pairs(t) do 36 | if type(v) == 'string' then 37 | v = callback(v) or v 38 | elseif type(v) == 'table' then 39 | v = copyWithNLS(v, callback) 40 | end 41 | nt[k] = v 42 | if type(k) == 'string' and k:sub(1, #'Lua.') == 'Lua.' then 43 | local shortKey = k:sub(#'Lua.' + 1) 44 | local ref = { 45 | ['$ref'] = '#/properties/' .. shortKey 46 | } 47 | addSplited(nt, shortKey, ref) 48 | nt[k] = nil 49 | nt[shortKey] = v 50 | end 51 | end 52 | return nt 53 | end 54 | 55 | local encodeOption = { 56 | newline = '\r\n', 57 | indent = ' ', 58 | } 59 | local function mergeDiagnosticGroupLocale(locale) 60 | for groupName, names in pairs(diagd.diagnosticGroups) do 61 | local key = ('config.diagnostics.%s'):format(groupName) 62 | local list = {} 63 | for name in util.sortPairs(names) do 64 | list[#list+1] = ('* %s'):format(name) 65 | end 66 | local desc = table.concat(list, '\n') 67 | locale[key] = desc 68 | end 69 | end 70 | 71 | for dirPath in fs.pairs(fs.path 'server/locale') do 72 | local lang = dirPath:filename():string() 73 | local nlsPath = dirPath / 'setting.lua' 74 | local text = fsu.loadFile(nlsPath) 75 | if not text then 76 | goto CONTINUE 77 | end 78 | local nls = lloader(text, nlsPath:string()) 79 | -- add `config.diagnostics.XXX` 80 | mergeDiagnosticGroupLocale(nls) 81 | 82 | local setting = { 83 | title = 'setting', 84 | description = 'Setting of sumneko.lua', 85 | type = 'object', 86 | properties = copyWithNLS(configuration, function (str) 87 | return str:gsub('^%%(.+)%%$', function (key) 88 | if not nls[key] then 89 | nls[key] = "TODO: Needs documentation" 90 | end 91 | return nls[key] 92 | end) 93 | end), 94 | } 95 | 96 | local schemaName, nlsName 97 | if lang == 'en-us' then 98 | schemaName = 'setting/schema.json' 99 | nlsName = 'package.nls.json' 100 | else 101 | schemaName = 'setting/schema-' .. lang .. '.json' 102 | nlsName = 'package.nls.' .. lang .. '.json' 103 | end 104 | 105 | fsu.saveFile(fs.path(schemaName), json.beautify(setting, encodeOption)) 106 | fsu.saveFile(fs.path(nlsName), json.beautify(nls, encodeOption)) 107 | ::CONTINUE:: 108 | end 109 | -------------------------------------------------------------------------------- /buildClient.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | echo Building VS Code Extension Client... 4 | 5 | echo Compiling TypeScript... 6 | cd client 7 | call npm i 8 | call npm run build 9 | 10 | echo Building Addon Manager WebVue... 11 | cd webvue 12 | call npm i 13 | call npm run build 14 | 15 | echo Build complete! 16 | -------------------------------------------------------------------------------- /buildClient.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | bold=$(tput bold) 6 | normal=$(tput sgr0) 7 | 8 | black='\e[0;30m' 9 | red='\e[0;31m' 10 | green='\e[0;32m' 11 | cyan='\e[0;36m' 12 | 13 | echo -e "${red}${bold}Building VS Code Extension Client..." 14 | 15 | echo -e "${cyan}${bold}Compiling TypeScript...${black}${normal}" 16 | cd client 17 | npm i 18 | npm run build 19 | 20 | echo -e "${green}${bold}Building Addon Manager WebVue...${black}${normal}" 21 | cd webvue 22 | npm i 23 | npm run build 24 | 25 | echo -e "${green}${bold}Build complete!${black}${normal}" 26 | -------------------------------------------------------------------------------- /client/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 6 | "rules": { 7 | "@typescript-eslint/no-namespace": "off", 8 | "linebreak-style": "off", 9 | "no-duplicate-imports": "warn", 10 | "semi": "error", 11 | "default-case": "error", 12 | "default-case-last": "error", 13 | "eqeqeq": "error" 14 | }, 15 | "ignorePatterns": ["out", "dist", "**/*.d.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /client/.prettierrc.yml: -------------------------------------------------------------------------------- 1 | tabWidth: 4 2 | endOfLine: auto 3 | -------------------------------------------------------------------------------- /client/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lsp-sample-client", 3 | "version": "0.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "lsp-sample-client", 9 | "version": "0.0.1", 10 | "license": "MIT", 11 | "dependencies": { 12 | "axios": "^1.7.4", 13 | "dayjs": "^1.11.7", 14 | "simple-git": "^3.16.0", 15 | "triple-beam": "^1.3.0", 16 | "vscode-languageclient": "9.0.1", 17 | "winston": "^3.8.2" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^18.19.18", 21 | "@types/vscode": "1.85.0", 22 | "typescript": "^5.5.0" 23 | }, 24 | "engines": { 25 | "vscode": "^1.85.0" 26 | } 27 | }, 28 | "node_modules/@colors/colors": { 29 | "version": "1.6.0", 30 | "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", 31 | "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", 32 | "engines": { 33 | "node": ">=0.1.90" 34 | } 35 | }, 36 | "node_modules/@dabh/diagnostics": { 37 | "version": "2.0.3", 38 | "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", 39 | "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", 40 | "dependencies": { 41 | "colorspace": "1.1.x", 42 | "enabled": "2.0.x", 43 | "kuler": "^2.0.0" 44 | } 45 | }, 46 | "node_modules/@kwsites/file-exists": { 47 | "version": "1.1.1", 48 | "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", 49 | "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", 50 | "dependencies": { 51 | "debug": "^4.1.1" 52 | } 53 | }, 54 | "node_modules/@kwsites/promise-deferred": { 55 | "version": "1.1.1", 56 | "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", 57 | "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==" 58 | }, 59 | "node_modules/@types/node": { 60 | "version": "18.19.42", 61 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.42.tgz", 62 | "integrity": "sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg==", 63 | "dev": true, 64 | "dependencies": { 65 | "undici-types": "~5.26.4" 66 | } 67 | }, 68 | "node_modules/@types/triple-beam": { 69 | "version": "1.3.5", 70 | "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", 71 | "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" 72 | }, 73 | "node_modules/@types/vscode": { 74 | "version": "1.85.0", 75 | "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.85.0.tgz", 76 | "integrity": "sha512-CF/RBon/GXwdfmnjZj0WTUMZN5H6YITOfBCP4iEZlOtVQXuzw6t7Le7+cR+7JzdMrnlm7Mfp49Oj2TuSXIWo3g==", 77 | "dev": true 78 | }, 79 | "node_modules/async": { 80 | "version": "3.2.5", 81 | "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", 82 | "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" 83 | }, 84 | "node_modules/asynckit": { 85 | "version": "0.4.0", 86 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 87 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 88 | }, 89 | "node_modules/axios": { 90 | "version": "1.7.4", 91 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", 92 | "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", 93 | "dependencies": { 94 | "follow-redirects": "^1.15.6", 95 | "form-data": "^4.0.0", 96 | "proxy-from-env": "^1.1.0" 97 | } 98 | }, 99 | "node_modules/balanced-match": { 100 | "version": "1.0.2", 101 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 102 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 103 | }, 104 | "node_modules/brace-expansion": { 105 | "version": "2.0.1", 106 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 107 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 108 | "dependencies": { 109 | "balanced-match": "^1.0.0" 110 | } 111 | }, 112 | "node_modules/color": { 113 | "version": "3.2.1", 114 | "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", 115 | "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", 116 | "dependencies": { 117 | "color-convert": "^1.9.3", 118 | "color-string": "^1.6.0" 119 | } 120 | }, 121 | "node_modules/color-convert": { 122 | "version": "1.9.3", 123 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 124 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 125 | "dependencies": { 126 | "color-name": "1.1.3" 127 | } 128 | }, 129 | "node_modules/color-name": { 130 | "version": "1.1.3", 131 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 132 | "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" 133 | }, 134 | "node_modules/color-string": { 135 | "version": "1.9.1", 136 | "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", 137 | "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 138 | "dependencies": { 139 | "color-name": "^1.0.0", 140 | "simple-swizzle": "^0.2.2" 141 | } 142 | }, 143 | "node_modules/colorspace": { 144 | "version": "1.1.4", 145 | "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", 146 | "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", 147 | "dependencies": { 148 | "color": "^3.1.3", 149 | "text-hex": "1.0.x" 150 | } 151 | }, 152 | "node_modules/combined-stream": { 153 | "version": "1.0.8", 154 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 155 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 156 | "dependencies": { 157 | "delayed-stream": "~1.0.0" 158 | }, 159 | "engines": { 160 | "node": ">= 0.8" 161 | } 162 | }, 163 | "node_modules/dayjs": { 164 | "version": "1.11.10", 165 | "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", 166 | "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" 167 | }, 168 | "node_modules/debug": { 169 | "version": "4.3.4", 170 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 171 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 172 | "dependencies": { 173 | "ms": "2.1.2" 174 | }, 175 | "engines": { 176 | "node": ">=6.0" 177 | }, 178 | "peerDependenciesMeta": { 179 | "supports-color": { 180 | "optional": true 181 | } 182 | } 183 | }, 184 | "node_modules/delayed-stream": { 185 | "version": "1.0.0", 186 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 187 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 188 | "engines": { 189 | "node": ">=0.4.0" 190 | } 191 | }, 192 | "node_modules/enabled": { 193 | "version": "2.0.0", 194 | "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", 195 | "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" 196 | }, 197 | "node_modules/fecha": { 198 | "version": "4.2.3", 199 | "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", 200 | "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" 201 | }, 202 | "node_modules/fn.name": { 203 | "version": "1.1.0", 204 | "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", 205 | "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" 206 | }, 207 | "node_modules/follow-redirects": { 208 | "version": "1.15.6", 209 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", 210 | "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", 211 | "funding": [ 212 | { 213 | "type": "individual", 214 | "url": "https://github.com/sponsors/RubenVerborgh" 215 | } 216 | ], 217 | "engines": { 218 | "node": ">=4.0" 219 | }, 220 | "peerDependenciesMeta": { 221 | "debug": { 222 | "optional": true 223 | } 224 | } 225 | }, 226 | "node_modules/form-data": { 227 | "version": "4.0.0", 228 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 229 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 230 | "dependencies": { 231 | "asynckit": "^0.4.0", 232 | "combined-stream": "^1.0.8", 233 | "mime-types": "^2.1.12" 234 | }, 235 | "engines": { 236 | "node": ">= 6" 237 | } 238 | }, 239 | "node_modules/inherits": { 240 | "version": "2.0.4", 241 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 242 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 243 | }, 244 | "node_modules/is-arrayish": { 245 | "version": "0.3.2", 246 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 247 | "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" 248 | }, 249 | "node_modules/is-stream": { 250 | "version": "2.0.1", 251 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", 252 | "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", 253 | "engines": { 254 | "node": ">=8" 255 | }, 256 | "funding": { 257 | "url": "https://github.com/sponsors/sindresorhus" 258 | } 259 | }, 260 | "node_modules/kuler": { 261 | "version": "2.0.0", 262 | "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", 263 | "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" 264 | }, 265 | "node_modules/logform": { 266 | "version": "2.6.0", 267 | "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", 268 | "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", 269 | "dependencies": { 270 | "@colors/colors": "1.6.0", 271 | "@types/triple-beam": "^1.3.2", 272 | "fecha": "^4.2.0", 273 | "ms": "^2.1.1", 274 | "safe-stable-stringify": "^2.3.1", 275 | "triple-beam": "^1.3.0" 276 | }, 277 | "engines": { 278 | "node": ">= 12.0.0" 279 | } 280 | }, 281 | "node_modules/lru-cache": { 282 | "version": "6.0.0", 283 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 284 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 285 | "dependencies": { 286 | "yallist": "^4.0.0" 287 | }, 288 | "engines": { 289 | "node": ">=10" 290 | } 291 | }, 292 | "node_modules/mime-db": { 293 | "version": "1.52.0", 294 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 295 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 296 | "engines": { 297 | "node": ">= 0.6" 298 | } 299 | }, 300 | "node_modules/mime-types": { 301 | "version": "2.1.35", 302 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 303 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 304 | "dependencies": { 305 | "mime-db": "1.52.0" 306 | }, 307 | "engines": { 308 | "node": ">= 0.6" 309 | } 310 | }, 311 | "node_modules/minimatch": { 312 | "version": "5.1.6", 313 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", 314 | "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", 315 | "dependencies": { 316 | "brace-expansion": "^2.0.1" 317 | }, 318 | "engines": { 319 | "node": ">=10" 320 | } 321 | }, 322 | "node_modules/ms": { 323 | "version": "2.1.2", 324 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 325 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 326 | }, 327 | "node_modules/one-time": { 328 | "version": "1.0.0", 329 | "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", 330 | "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", 331 | "dependencies": { 332 | "fn.name": "1.x.x" 333 | } 334 | }, 335 | "node_modules/proxy-from-env": { 336 | "version": "1.1.0", 337 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 338 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" 339 | }, 340 | "node_modules/readable-stream": { 341 | "version": "3.6.2", 342 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 343 | "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 344 | "dependencies": { 345 | "inherits": "^2.0.3", 346 | "string_decoder": "^1.1.1", 347 | "util-deprecate": "^1.0.1" 348 | }, 349 | "engines": { 350 | "node": ">= 6" 351 | } 352 | }, 353 | "node_modules/safe-buffer": { 354 | "version": "5.2.1", 355 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 356 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 357 | "funding": [ 358 | { 359 | "type": "github", 360 | "url": "https://github.com/sponsors/feross" 361 | }, 362 | { 363 | "type": "patreon", 364 | "url": "https://www.patreon.com/feross" 365 | }, 366 | { 367 | "type": "consulting", 368 | "url": "https://feross.org/support" 369 | } 370 | ] 371 | }, 372 | "node_modules/safe-stable-stringify": { 373 | "version": "2.4.3", 374 | "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", 375 | "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", 376 | "engines": { 377 | "node": ">=10" 378 | } 379 | }, 380 | "node_modules/semver": { 381 | "version": "7.5.4", 382 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", 383 | "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", 384 | "dependencies": { 385 | "lru-cache": "^6.0.0" 386 | }, 387 | "bin": { 388 | "semver": "bin/semver.js" 389 | }, 390 | "engines": { 391 | "node": ">=10" 392 | } 393 | }, 394 | "node_modules/simple-git": { 395 | "version": "3.20.0", 396 | "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.20.0.tgz", 397 | "integrity": "sha512-ozK8tl2hvLts8ijTs18iFruE+RoqmC/mqZhjs/+V7gS5W68JpJ3+FCTmLVqmR59MaUQ52MfGQuWsIqfsTbbJ0Q==", 398 | "dependencies": { 399 | "@kwsites/file-exists": "^1.1.1", 400 | "@kwsites/promise-deferred": "^1.1.1", 401 | "debug": "^4.3.4" 402 | }, 403 | "funding": { 404 | "type": "github", 405 | "url": "https://github.com/steveukx/git-js?sponsor=1" 406 | } 407 | }, 408 | "node_modules/simple-swizzle": { 409 | "version": "0.2.2", 410 | "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 411 | "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 412 | "dependencies": { 413 | "is-arrayish": "^0.3.1" 414 | } 415 | }, 416 | "node_modules/stack-trace": { 417 | "version": "0.0.10", 418 | "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", 419 | "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", 420 | "engines": { 421 | "node": "*" 422 | } 423 | }, 424 | "node_modules/string_decoder": { 425 | "version": "1.3.0", 426 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 427 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 428 | "dependencies": { 429 | "safe-buffer": "~5.2.0" 430 | } 431 | }, 432 | "node_modules/text-hex": { 433 | "version": "1.0.0", 434 | "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", 435 | "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" 436 | }, 437 | "node_modules/triple-beam": { 438 | "version": "1.4.1", 439 | "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", 440 | "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", 441 | "engines": { 442 | "node": ">= 14.0.0" 443 | } 444 | }, 445 | "node_modules/typescript": { 446 | "version": "5.5.4", 447 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", 448 | "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", 449 | "dev": true, 450 | "bin": { 451 | "tsc": "bin/tsc", 452 | "tsserver": "bin/tsserver" 453 | }, 454 | "engines": { 455 | "node": ">=14.17" 456 | } 457 | }, 458 | "node_modules/undici-types": { 459 | "version": "5.26.5", 460 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 461 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 462 | "dev": true 463 | }, 464 | "node_modules/util-deprecate": { 465 | "version": "1.0.2", 466 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 467 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 468 | }, 469 | "node_modules/vscode-jsonrpc": { 470 | "version": "8.2.0", 471 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", 472 | "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", 473 | "engines": { 474 | "node": ">=14.0.0" 475 | } 476 | }, 477 | "node_modules/vscode-languageclient": { 478 | "version": "9.0.1", 479 | "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-9.0.1.tgz", 480 | "integrity": "sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==", 481 | "dependencies": { 482 | "minimatch": "^5.1.0", 483 | "semver": "^7.3.7", 484 | "vscode-languageserver-protocol": "3.17.5" 485 | }, 486 | "engines": { 487 | "vscode": "^1.82.0" 488 | } 489 | }, 490 | "node_modules/vscode-languageserver-protocol": { 491 | "version": "3.17.5", 492 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", 493 | "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", 494 | "dependencies": { 495 | "vscode-jsonrpc": "8.2.0", 496 | "vscode-languageserver-types": "3.17.5" 497 | } 498 | }, 499 | "node_modules/vscode-languageserver-types": { 500 | "version": "3.17.5", 501 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", 502 | "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" 503 | }, 504 | "node_modules/winston": { 505 | "version": "3.11.0", 506 | "resolved": "https://registry.npmjs.org/winston/-/winston-3.11.0.tgz", 507 | "integrity": "sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==", 508 | "dependencies": { 509 | "@colors/colors": "^1.6.0", 510 | "@dabh/diagnostics": "^2.0.2", 511 | "async": "^3.2.3", 512 | "is-stream": "^2.0.0", 513 | "logform": "^2.4.0", 514 | "one-time": "^1.0.0", 515 | "readable-stream": "^3.4.0", 516 | "safe-stable-stringify": "^2.3.1", 517 | "stack-trace": "0.0.x", 518 | "triple-beam": "^1.3.0", 519 | "winston-transport": "^4.5.0" 520 | }, 521 | "engines": { 522 | "node": ">= 12.0.0" 523 | } 524 | }, 525 | "node_modules/winston-transport": { 526 | "version": "4.6.0", 527 | "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.6.0.tgz", 528 | "integrity": "sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==", 529 | "dependencies": { 530 | "logform": "^2.3.2", 531 | "readable-stream": "^3.6.0", 532 | "triple-beam": "^1.3.0" 533 | }, 534 | "engines": { 535 | "node": ">= 12.0.0" 536 | } 537 | }, 538 | "node_modules/yallist": { 539 | "version": "4.0.0", 540 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 541 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 542 | } 543 | } 544 | } 545 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lsp-sample-client", 3 | "description": "VSCode part of a language server", 4 | "author": "Microsoft Corporation", 5 | "license": "MIT", 6 | "version": "0.0.1", 7 | "publisher": "vscode", 8 | "scripts": { 9 | "lint": "npx eslint src/", 10 | "build": "tsc" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/Microsoft/vscode-extension-samples" 15 | }, 16 | "engines": { 17 | "vscode": "^1.85.0" 18 | }, 19 | "dependencies": { 20 | "axios": "^1.7.4", 21 | "dayjs": "^1.11.7", 22 | "simple-git": "^3.16.0", 23 | "triple-beam": "^1.3.0", 24 | "vscode-languageclient": "9.0.1", 25 | "winston": "^3.8.2" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^18.19.18", 29 | "@types/vscode": "1.85.0", 30 | "typescript": "^5.5.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/src/addon_manager/commands/disable.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import addonManager from "../services/addonManager.service"; 3 | import { createChildLogger } from "../services/logging.service"; 4 | import { setConfig } from "../../languageserver"; 5 | 6 | type Message = { 7 | data: { 8 | name: string; 9 | }; 10 | }; 11 | 12 | const localLogger = createChildLogger("Disable Addon"); 13 | 14 | export default async (context: vscode.ExtensionContext, message: Message) => { 15 | const addon = addonManager.addons.get(message.data.name); 16 | const workspaceFolders = vscode.workspace.workspaceFolders; 17 | 18 | if (!addon || !workspaceFolders) { 19 | return; 20 | } 21 | 22 | let selectedFolders: vscode.WorkspaceFolder[]; 23 | 24 | if (workspaceFolders && workspaceFolders.length === 1) { 25 | selectedFolders = [workspaceFolders[0]]; 26 | } else { 27 | const folderOptions = await addon.getQuickPickerOptions(true); 28 | const pickResult = await vscode.window.showQuickPick(folderOptions, { 29 | canPickMany: true, 30 | ignoreFocusOut: true, 31 | title: `Disable ${addon.name} in which folders?`, 32 | }); 33 | if (!pickResult) { 34 | localLogger.warn("User did not pick workspace folder"); 35 | await addon.setLock(false); 36 | return; 37 | } 38 | selectedFolders = pickResult.map((selection) => { 39 | return workspaceFolders.find( 40 | (folder) => folder.name === selection.label 41 | ); 42 | }).filter((folder) => !!folder); 43 | } 44 | 45 | for (const folder of selectedFolders) { 46 | await addon.disable(folder); 47 | await setConfig([ 48 | { 49 | action: "set", 50 | key: "Lua.workspace.checkThirdParty", 51 | value: false, 52 | uri: folder.uri, 53 | }, 54 | ]); 55 | } 56 | 57 | return addon.setLock(false); 58 | }; 59 | -------------------------------------------------------------------------------- /client/src/addon_manager/commands/enable.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import addonManager from "../services/addonManager.service"; 3 | import { createChildLogger } from "../services/logging.service"; 4 | import { setConfig } from "../../languageserver"; 5 | import { WebVue } from "../panels/WebVue"; 6 | import { NotificationLevels } from "../types/webvue"; 7 | import { ADDONS_DIRECTORY, getStorageUri } from "../config"; 8 | 9 | type Message = { 10 | data: { 11 | name: string; 12 | }; 13 | }; 14 | 15 | const localLogger = createChildLogger("Enable Addon"); 16 | 17 | export default async (context: vscode.ExtensionContext, message: Message) => { 18 | const addon = addonManager.addons.get(message.data.name); 19 | const workspaceFolders = vscode.workspace.workspaceFolders; 20 | 21 | if (!addon || !workspaceFolders) { 22 | return; 23 | } 24 | 25 | let selectedFolders: vscode.WorkspaceFolder[]; 26 | 27 | if (workspaceFolders && workspaceFolders.length === 1) { 28 | selectedFolders = [workspaceFolders[0]]; 29 | } else { 30 | const folderOptions = await addon.getQuickPickerOptions(false); 31 | 32 | const pickResult = await vscode.window.showQuickPick(folderOptions, { 33 | canPickMany: true, 34 | ignoreFocusOut: true, 35 | title: `Enable ${addon.name} in which folders?`, 36 | }); 37 | if (!pickResult) { 38 | localLogger.warn("User did not pick workspace folder"); 39 | await addon.setLock(false); 40 | return; 41 | } 42 | selectedFolders = pickResult 43 | .map((selection) => { 44 | return workspaceFolders.find( 45 | (folder) => folder.name === selection.label 46 | ); 47 | }) 48 | .filter((folder) => !!folder); 49 | } 50 | 51 | for (const folder of selectedFolders) { 52 | try { 53 | const installLocation = vscode.Uri.joinPath( 54 | getStorageUri(context), 55 | "addonManager", 56 | ADDONS_DIRECTORY 57 | ); 58 | await addon.enable(folder, installLocation); 59 | } catch (e) { 60 | const message = `Failed to enable ${addon.name}!`; 61 | localLogger.error(message, { report: false }); 62 | localLogger.error(String(e), { report: false }); 63 | WebVue.sendNotification({ 64 | level: NotificationLevels.error, 65 | message, 66 | }); 67 | continue; 68 | } 69 | await setConfig([ 70 | { 71 | action: "set", 72 | key: "Lua.workspace.checkThirdParty", 73 | value: false, 74 | uri: folder.uri, 75 | }, 76 | ]); 77 | } 78 | 79 | return addon.setLock(false); 80 | }; 81 | -------------------------------------------------------------------------------- /client/src/addon_manager/commands/getAddons.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { createChildLogger } from "../services/logging.service"; 3 | import addonManager from "../services/addonManager.service"; 4 | import { WebVue } from "../panels/WebVue"; 5 | import { ADDONS_DIRECTORY, getStorageUri } from "../config"; 6 | 7 | const localLogger = createChildLogger("Get Remote Addons"); 8 | 9 | export default async (context: vscode.ExtensionContext) => { 10 | WebVue.setLoadingState(true); 11 | 12 | const installLocation = vscode.Uri.joinPath( 13 | getStorageUri(context), 14 | "addonManager", 15 | ADDONS_DIRECTORY 16 | ); 17 | 18 | if (addonManager.addons.size < 1) { 19 | await addonManager.fetchAddons(installLocation); 20 | } 21 | 22 | WebVue.sendMessage("addonStore", { 23 | property: "total", 24 | value: addonManager.addons.size, 25 | }); 26 | 27 | if (addonManager.addons.size === 0) { 28 | WebVue.setLoadingState(false); 29 | localLogger.verbose("No remote addons found"); 30 | return; 31 | } 32 | 33 | /** Number of addons to load per chunk */ 34 | const CHUNK_SIZE = 30; 35 | 36 | // Get list of addons and sort them alphabetically 37 | const addonList = Array.from(addonManager.addons.values()); 38 | addonList.sort((a, b) => a.displayName.localeCompare(b.displayName)); 39 | 40 | // Send addons to client in chunks 41 | for (let i = 0; i <= addonList.length / CHUNK_SIZE; i++) { 42 | const chunk = addonList.slice(i * CHUNK_SIZE, i * CHUNK_SIZE + CHUNK_SIZE); 43 | const addons = await Promise.all(chunk.map((addon) => addon.toJSON())); 44 | await WebVue.sendMessage("addAddon", { addons }); 45 | } 46 | 47 | WebVue.setLoadingState(false); 48 | }; 49 | -------------------------------------------------------------------------------- /client/src/addon_manager/commands/index.ts: -------------------------------------------------------------------------------- 1 | import enable from "./enable"; 2 | import disable from "./disable"; 3 | import open from "./open"; 4 | import getAddons from "./getAddons"; 5 | import refreshAddons from "./refreshAddons"; 6 | import openLog from "./openLog"; 7 | import update from "./update"; 8 | import uninstall from "./uninstall"; 9 | import setVersion from "./setVersion"; 10 | 11 | export const commands = { 12 | enable, 13 | disable, 14 | open, 15 | getAddons, 16 | refreshAddons, 17 | openLog, 18 | update, 19 | uninstall, 20 | setVersion, 21 | }; 22 | -------------------------------------------------------------------------------- /client/src/addon_manager/commands/open.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { createChildLogger } from "../services/logging.service"; 3 | import { ADDONS_DIRECTORY, getStorageUri } from "../config"; 4 | 5 | const localLogger = createChildLogger("Open Addon"); 6 | 7 | export default async ( 8 | context: vscode.ExtensionContext, 9 | message: { data: { name: string } } 10 | ) => { 11 | const extensionStorageURI = getStorageUri(context); 12 | const uri = vscode.Uri.joinPath( 13 | extensionStorageURI, 14 | "addonManager", 15 | ADDONS_DIRECTORY, 16 | message.data.name 17 | ); 18 | 19 | localLogger.info(`Opening "${message.data.name}" addon in file explorer`); 20 | vscode.env.openExternal(vscode.Uri.file(uri.fsPath)); 21 | }; 22 | -------------------------------------------------------------------------------- /client/src/addon_manager/commands/openLog.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import VSCodeLogFileTransport from "../services/logging/vsCodeLogFileTransport"; 3 | 4 | export default async () => { 5 | vscode.env.openExternal(VSCodeLogFileTransport.currentLogFile); 6 | }; 7 | -------------------------------------------------------------------------------- /client/src/addon_manager/commands/refreshAddons.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import addonManager from "../services/addonManager.service"; 3 | import { ADDONS_DIRECTORY } from "../config"; 4 | import { WebVue } from "../panels/WebVue"; 5 | import { getStorageUri } from "../config" 6 | 7 | export default async (context: vscode.ExtensionContext) => { 8 | WebVue.setLoadingState(true); 9 | 10 | const installLocation = vscode.Uri.joinPath( 11 | getStorageUri(context), 12 | "addonManager", 13 | ADDONS_DIRECTORY 14 | ); 15 | 16 | await addonManager.fetchAddons(installLocation); 17 | 18 | WebVue.setLoadingState(false); 19 | }; 20 | -------------------------------------------------------------------------------- /client/src/addon_manager/commands/setVersion.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { createChildLogger } from "../services/logging.service"; 3 | import addonManager from "../services/addonManager.service"; 4 | import { NotificationLevels } from "../types/webvue"; 5 | import { WebVue } from "../panels/WebVue"; 6 | 7 | const localLogger = createChildLogger("Set Version"); 8 | 9 | export default async ( 10 | context: vscode.ExtensionContext, 11 | message: { data: { name: string; version: string } } 12 | ) => { 13 | const addon = addonManager.addons.get(message.data.name)!; 14 | 15 | try { 16 | if (message.data.version === "Latest") { 17 | await addon.update(); 18 | } else { 19 | await addon.checkout(message.data.version); 20 | } 21 | } catch (e) { 22 | localLogger.error( 23 | `Failed to checkout version ${message.data.version}: ${e}` 24 | ); 25 | WebVue.sendNotification({ 26 | level: NotificationLevels.error, 27 | message: `Failed to checkout version ${message.data.version}`, 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/addon_manager/commands/uninstall.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import addonManagerService from "../services/addonManager.service"; 3 | 4 | export default async ( 5 | context: vscode.ExtensionContext, 6 | message: { data: { name: string } } 7 | ) => { 8 | const addon = addonManagerService.addons.get(message.data.name); 9 | addon?.uninstall(); 10 | }; 11 | -------------------------------------------------------------------------------- /client/src/addon_manager/commands/update.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import addonManager from "../services/addonManager.service"; 3 | import { git } from "../services/git.service"; 4 | import { DiffResultTextFile } from "simple-git"; 5 | import { WebVue } from "../panels/WebVue"; 6 | import { NotificationLevels } from "../types/webvue"; 7 | import { createChildLogger } from "../services/logging.service"; 8 | 9 | const localLogger = createChildLogger("Update Addon"); 10 | 11 | type Message = { 12 | data: { 13 | name: string; 14 | }; 15 | }; 16 | 17 | export default async (context: vscode.ExtensionContext, message: Message) => { 18 | const addon = addonManager.addons.get(message.data.name); 19 | if (!addon) { 20 | return; 21 | } 22 | try { 23 | await addon.update(); 24 | } catch (e) { 25 | const message = `Failed to update ${addon.name}`; 26 | localLogger.error(message, { report: false }); 27 | localLogger.error(String(e), { report: false }); 28 | WebVue.sendNotification({ 29 | level: NotificationLevels.error, 30 | message, 31 | }); 32 | } 33 | await addon.setLock(false); 34 | 35 | const diff = await git.diffSummary(["HEAD", "origin/main"]); 36 | addon.checkForUpdate(diff.files as DiffResultTextFile[]); 37 | }; 38 | -------------------------------------------------------------------------------- /client/src/addon_manager/config.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | // Development 4 | export const DEVELOPMENT_IFRAME_URL = "http://127.0.0.1:5173"; 5 | 6 | // GitHub Repository Info 7 | export const REPOSITORY = { 8 | PATH: "https://github.com/LuaLS/LLS-Addons.git", 9 | DEFAULT_BRANCH: "main", 10 | } 11 | 12 | export const REPOSITORY_OWNER = "carsakiller"; 13 | export const REPOSITORY_NAME = "LLS-Addons"; 14 | export const REPOSITORY_ISSUES_URL = 15 | "https://github.com/LuaLS/vscode-lua/issues/new?template=bug_report.yml"; 16 | export const ADDONS_DIRECTORY = "addons"; 17 | export const GIT_DOWNLOAD_URL = "https://git-scm.com/downloads"; 18 | 19 | // settings.json file info 20 | export const LIBRARY_SETTING = "Lua.workspace.library"; 21 | 22 | // Addon files 23 | export const PLUGIN_FILENAME = "plugin.lua"; 24 | export const CONFIG_FILENAME = "config.json"; 25 | export const INFO_FILENAME = "info.json"; 26 | 27 | let useGlobal = true 28 | export function getStorageUri(context: vscode.ExtensionContext) { 29 | return useGlobal ? context.globalStorageUri : (context.storageUri ?? context.globalStorageUri) 30 | } 31 | 32 | export function setGlobalStorageUri(use: boolean) { 33 | useGlobal = use 34 | } 35 | -------------------------------------------------------------------------------- /client/src/addon_manager/models/addon.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { createChildLogger } from "../services/logging.service"; 3 | import { CONFIG_FILENAME, INFO_FILENAME, LIBRARY_SETTING } from "../config"; 4 | import { AddonConfig, AddonInfo } from "../types/addon"; 5 | import { WebVue } from "../panels/WebVue"; 6 | import { 7 | applyAddonSettings, 8 | getLibraryPaths, 9 | revokeAddonSettings, 10 | } from "../services/settings.service"; 11 | import { git } from "../services/git.service"; 12 | import filesystem from "../services/filesystem.service"; 13 | import { DiffResultTextFile } from "simple-git"; 14 | import { getConfig, setConfig } from "../../languageserver"; 15 | 16 | const localLogger = createChildLogger("Addon"); 17 | 18 | export class Addon { 19 | readonly name: string; 20 | readonly uri: vscode.Uri; 21 | 22 | #displayName?: string; 23 | /** Whether or not this addon is currently processing an operation. */ 24 | #processing?: boolean; 25 | /** The workspace folders that this addon is enabled in. */ 26 | #enabled: boolean[]; 27 | /** Whether or not this addon has an update available from git. */ 28 | #hasUpdate?: boolean; 29 | /** Whether or not this addon is installed */ 30 | #installed: boolean; 31 | 32 | constructor(name: string, path: vscode.Uri) { 33 | this.name = name; 34 | this.uri = path; 35 | 36 | this.#enabled = []; 37 | this.#hasUpdate = false; 38 | this.#installed = false; 39 | } 40 | 41 | public get displayName() { 42 | return this.#displayName ?? this.name; 43 | } 44 | 45 | /** Fetch addon info from `info.json` */ 46 | public async fetchInfo() { 47 | const infoFilePath = vscode.Uri.joinPath(this.uri, INFO_FILENAME); 48 | const modulePath = vscode.Uri.joinPath(this.uri, "module"); 49 | 50 | const rawInfo = await filesystem.readFile(infoFilePath); 51 | const info = JSON.parse(rawInfo) as AddonInfo; 52 | 53 | this.#displayName = info.name; 54 | 55 | const moduleGit = git.cwd({ path: modulePath.fsPath, root: false }); 56 | 57 | let currentVersion = null; 58 | let tags: string[] = []; 59 | 60 | await this.getEnabled(); 61 | 62 | if (this.#installed) { 63 | await git.fetch(["origin", "--prune", "--prune-tags"]); 64 | tags = ( 65 | await moduleGit.tags([ 66 | "--sort=-taggerdate", 67 | "--merged", 68 | `origin/${await this.getDefaultBranch()}`, 69 | ]) 70 | ).all; 71 | 72 | const currentTag = await moduleGit 73 | .raw(["describe", "--tags", "--exact-match"]) 74 | .catch((err) => { 75 | return null; 76 | }); 77 | const commitsBehindLatest = await moduleGit.raw([ 78 | "rev-list", 79 | `HEAD..origin/${await this.getDefaultBranch()}`, 80 | "--count", 81 | ]); 82 | 83 | if (Number(commitsBehindLatest) < 1) { 84 | currentVersion = "Latest"; 85 | } else if (currentTag != "") { 86 | currentVersion = currentTag; 87 | } else { 88 | currentVersion = await moduleGit 89 | .revparse(["--short", "HEAD"]) 90 | .catch((err) => { 91 | localLogger.warn( 92 | `Failed to get current hash for ${this.name}: ${err}` 93 | ); 94 | return null; 95 | }); 96 | } 97 | } 98 | 99 | return { 100 | name: info.name, 101 | description: info.description, 102 | size: info.size, 103 | hasPlugin: info.hasPlugin, 104 | tags: tags, 105 | version: currentVersion, 106 | }; 107 | } 108 | 109 | /** Get the `config.json` for this addon. */ 110 | public async getConfigurationFile() { 111 | const configURI = vscode.Uri.joinPath( 112 | this.uri, 113 | "module", 114 | CONFIG_FILENAME 115 | ); 116 | 117 | try { 118 | const rawConfig = await filesystem.readFile(configURI); 119 | const config = JSON.parse(rawConfig); 120 | return config as AddonConfig; 121 | } catch (e) { 122 | localLogger.error( 123 | `Failed to read config.json file for ${this.name} (${e})` 124 | ); 125 | throw e; 126 | } 127 | } 128 | 129 | /** Update this addon using git. */ 130 | public async update() { 131 | return git 132 | .submoduleUpdate([this.uri.fsPath]) 133 | .then((message) => localLogger.debug(message)); 134 | } 135 | 136 | public async getDefaultBranch() { 137 | // Get branch from .gitmodules if specified 138 | const targetBranch = await git.raw( 139 | "config", 140 | "-f", 141 | ".gitmodules", 142 | "--get", 143 | `submodule.addons/${this.name}/module.branch` 144 | ); 145 | if (targetBranch) { 146 | return targetBranch; 147 | } 148 | 149 | // Fetch default branch from remote 150 | const modulePath = vscode.Uri.joinPath(this.uri, "module"); 151 | const result = (await git 152 | .cwd({ path: modulePath.fsPath, root: false }) 153 | .remote(["show", "origin"])) as string; 154 | const match = result.match(/HEAD branch: (\w+)/); 155 | 156 | return match![1]; 157 | } 158 | 159 | public async pull() { 160 | const modulePath = vscode.Uri.joinPath(this.uri, "module"); 161 | 162 | return await git.cwd({ path: modulePath.fsPath, root: false }).pull(); 163 | } 164 | 165 | public async checkout(obj: string) { 166 | const modulePath = vscode.Uri.joinPath(this.uri, "module"); 167 | return git 168 | .cwd({ path: modulePath.fsPath, root: false }) 169 | .checkout([obj]); 170 | } 171 | 172 | /** Check whether this addon is enabled, given an array of enabled library paths. 173 | * @param libraryPaths An array of paths from the `Lua.workspace.library` setting. 174 | */ 175 | public checkIfEnabled(libraryPaths: string[]) { 176 | const regex = new RegExp(`${this.name}\/module\/library`, "g"); 177 | 178 | const index = libraryPaths.findIndex((path) => regex.test(path)); 179 | return index !== -1; 180 | } 181 | 182 | /** Get the enabled state for this addon in all opened workspace folders */ 183 | public async getEnabled() { 184 | const folders = await getLibraryPaths(); 185 | 186 | // Check all workspace folders for a path that matches this addon 187 | const folderStates = folders.map((entry) => { 188 | return { 189 | folder: entry.folder, 190 | enabled: this.checkIfEnabled(entry.paths), 191 | }; 192 | }); 193 | 194 | folderStates.forEach( 195 | (entry) => (this.#enabled[entry.folder.index] = entry.enabled) 196 | ); 197 | 198 | const moduleURI = vscode.Uri.joinPath(this.uri, "module"); 199 | 200 | const exists = await filesystem.exists(moduleURI); 201 | const empty = await filesystem.empty(moduleURI); 202 | this.#installed = exists && !empty; 203 | 204 | return folderStates; 205 | } 206 | 207 | public async enable( 208 | folder: vscode.WorkspaceFolder, 209 | installLocation: vscode.Uri 210 | ) { 211 | const librarySetting = ((await getConfig( 212 | LIBRARY_SETTING, 213 | folder.uri 214 | )) ?? []) as string[]; 215 | 216 | const enabled = await this.checkIfEnabled(librarySetting); 217 | if (enabled) { 218 | localLogger.warn(`${this.name} is already enabled`); 219 | this.#enabled[folder.index] = true; 220 | return; 221 | } 222 | 223 | // Init submodule 224 | try { 225 | await git.submoduleInit([this.uri.fsPath]); 226 | localLogger.debug("Initialized submodule"); 227 | } catch (e) { 228 | localLogger.warn(`Unable to initialize submodule for ${this.name}`); 229 | localLogger.warn(e); 230 | throw e; 231 | } 232 | 233 | try { 234 | await git.submoduleUpdate([this.uri.fsPath]); 235 | localLogger.debug("Submodule up to date"); 236 | } catch (e) { 237 | localLogger.warn(`Unable to update submodule for ${this.name}`); 238 | localLogger.warn(e); 239 | throw e; 240 | } 241 | 242 | // Apply addon settings 243 | const libraryPath = vscode.Uri.joinPath( 244 | this.uri, 245 | "module", 246 | "library" 247 | ).path.replace(installLocation.path, "${addons}"); 248 | 249 | const configValues = await this.getConfigurationFile(); 250 | 251 | try { 252 | await setConfig([ 253 | { 254 | action: "add", 255 | key: LIBRARY_SETTING, 256 | value: libraryPath, 257 | uri: folder.uri, 258 | }, 259 | ]); 260 | if (configValues.settings) { 261 | await applyAddonSettings(folder, configValues.settings); 262 | localLogger.info(`Applied addon settings for ${this.name}`); 263 | } 264 | } catch (e) { 265 | localLogger.warn(`Failed to apply settings of "${this.name}"`); 266 | localLogger.warn(e); 267 | return; 268 | } 269 | 270 | this.#enabled[folder.index] = true; 271 | localLogger.info(`Enabled "${this.name}"`); 272 | } 273 | 274 | public async disable(folder: vscode.WorkspaceFolder, silent = false) { 275 | const librarySetting = ((await getConfig( 276 | LIBRARY_SETTING, 277 | folder.uri 278 | )) ?? []) as string[]; 279 | 280 | const regex = new RegExp( 281 | `addons}?[/\\\\]+${this.name}[/\\\\]+module[/\\\\]+library`, 282 | "g" 283 | ); 284 | const index = librarySetting.findIndex((path) => regex.test(path)); 285 | 286 | if (index === -1) { 287 | if (!silent) localLogger.warn(`"${this.name}" is already disabled`); 288 | this.#enabled[folder.index] = false; 289 | return; 290 | } 291 | 292 | // Remove this addon from the library list 293 | librarySetting.splice(index, 1); 294 | const result = await setConfig([ 295 | { 296 | action: "set", 297 | key: LIBRARY_SETTING, 298 | value: librarySetting, 299 | uri: folder.uri, 300 | }, 301 | ]); 302 | if (!result) { 303 | localLogger.error( 304 | `Failed to update ${LIBRARY_SETTING} when disabling ${this.name}` 305 | ); 306 | return; 307 | } 308 | 309 | // Remove addon settings if installed 310 | if (this.#installed) { 311 | const configValues = await this.getConfigurationFile(); 312 | try { 313 | if (configValues.settings) 314 | await revokeAddonSettings(folder, configValues.settings); 315 | } catch (e) { 316 | localLogger.error( 317 | `Failed to revoke settings of "${this.name}"` 318 | ); 319 | return; 320 | } 321 | } 322 | 323 | this.#enabled[folder.index] = false; 324 | localLogger.info(`Disabled "${this.name}"`); 325 | } 326 | 327 | public async uninstall() { 328 | for (const folder of vscode.workspace.workspaceFolders ?? []) { 329 | await this.disable(folder, true); 330 | } 331 | const files = 332 | (await filesystem.readDirectory( 333 | vscode.Uri.joinPath(this.uri, "module"), 334 | { depth: 1 } 335 | )) ?? []; 336 | files.map((f) => { 337 | return filesystem.deleteFile(f.uri, { 338 | recursive: true, 339 | useTrash: false, 340 | }); 341 | }); 342 | await Promise.all(files); 343 | localLogger.info(`Uninstalled ${this.name}`); 344 | this.#installed = false; 345 | this.setLock(false); 346 | } 347 | 348 | /** Convert this addon to an object ready for sending to WebVue. */ 349 | public async toJSON() { 350 | await this.getEnabled(); 351 | 352 | const { name, description, size, hasPlugin, tags, version } = 353 | await this.fetchInfo(); 354 | const enabled = this.#enabled; 355 | const installTimestamp = (await git.log()).latest?.date; 356 | const hasUpdate = this.#hasUpdate; 357 | 358 | return { 359 | name: this.name, 360 | displayName: name, 361 | description, 362 | enabled, 363 | hasPlugin, 364 | installTimestamp, 365 | size, 366 | hasUpdate, 367 | processing: this.#processing, 368 | installed: this.#installed, 369 | tags, 370 | version, 371 | }; 372 | } 373 | 374 | public checkForUpdate(modified: DiffResultTextFile[]) { 375 | this.#hasUpdate = false; 376 | if ( 377 | modified.findIndex((modifiedItem) => 378 | modifiedItem.file.includes(this.name) 379 | ) !== -1 380 | ) { 381 | localLogger.info(`Found update for "${this.name}"`); 382 | this.#hasUpdate = true; 383 | } 384 | return this.#hasUpdate; 385 | } 386 | 387 | /** Get a list of options for a quick picker that lists the workspace 388 | * folders that the addon is enabled/disabled in. 389 | * @param enabledState The state the addon must be in in a folder to be included. 390 | * `true` will only return the folders that the addon is **enabled** in. 391 | * `false` will only return the folders that the addon is **disabled** in 392 | */ 393 | public async getQuickPickerOptions(enabledState: boolean) { 394 | return (await this.getEnabled()) 395 | .filter((entry) => entry.enabled === enabledState) 396 | .map((entry) => { 397 | return { 398 | label: entry.folder.name, 399 | detail: entry.folder.uri.path, 400 | }; 401 | }); 402 | } 403 | 404 | public async setLock(state: boolean) { 405 | this.#processing = state; 406 | return this.sendToWebVue(); 407 | } 408 | 409 | /** Send this addon to WebVue. */ 410 | public async sendToWebVue() { 411 | WebVue.sendMessage("addAddon", { addons: await this.toJSON() }); 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /client/src/addon_manager/panels/WebVue.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { createChildLogger } from "../services/logging.service"; 4 | import { commands } from "../commands"; 5 | import { Notification, WebVueMessage } from "../types/webvue"; 6 | import { DEVELOPMENT_IFRAME_URL } from "../config"; 7 | 8 | const localLogger = createChildLogger("WebVue"); 9 | const commandLogger = createChildLogger("Command"); 10 | 11 | export class WebVue { 12 | public static currentPanel: WebVue | undefined; 13 | private readonly _panel: vscode.WebviewPanel; 14 | private readonly _extensionUri: vscode.Uri; 15 | private _disposables: vscode.Disposable[] = []; 16 | 17 | private constructor( 18 | context: vscode.ExtensionContext, 19 | panel: vscode.WebviewPanel 20 | ) { 21 | const extensionUri = context.extensionUri; 22 | 23 | this._panel = panel; 24 | this._extensionUri = extensionUri; 25 | this._panel.iconPath = { 26 | dark: vscode.Uri.joinPath(extensionUri, "images", "logo.png"), 27 | light: vscode.Uri.joinPath(extensionUri, "images", "logo.png"), 28 | }; 29 | this._disposables.push( 30 | this._panel.onDidDispose(this.dispose, null, this._disposables), 31 | this._setWebviewMessageListener(this._panel.webview, context) 32 | ); 33 | this._panel.webview.html = this._getWebViewContent( 34 | this._panel.webview, 35 | context 36 | ); 37 | } 38 | 39 | /** Convert a standard file uri to a uri usable by this webview. */ 40 | private toWebviewUri(pathList: string[]) { 41 | return this._panel.webview.asWebviewUri( 42 | vscode.Uri.joinPath(this._extensionUri, ...pathList) 43 | ); 44 | } 45 | 46 | /** Send a message to the webview */ 47 | public static sendMessage( 48 | command: string, 49 | data: { [index: string]: unknown } | unknown 50 | ) { 51 | WebVue.currentPanel?._panel.webview.postMessage({ command, data }); 52 | } 53 | 54 | public static sendNotification(message: Notification) { 55 | WebVue.sendMessage("notify", message); 56 | } 57 | 58 | /** Set the loading state of a store in the webview */ 59 | public static setLoadingState(loading: boolean) { 60 | WebVue.sendMessage("addonStore", { 61 | property: "loading", 62 | value: loading, 63 | }); 64 | } 65 | 66 | /** Reveal or create a new panel in VS Code */ 67 | public static render(context: vscode.ExtensionContext) { 68 | const extensionUri = context.extensionUri; 69 | 70 | if (WebVue.currentPanel) { 71 | WebVue.currentPanel._panel.reveal(vscode.ViewColumn.One); 72 | } else { 73 | const panel = vscode.window.createWebviewPanel( 74 | "lua-addon_manager", 75 | "Lua Addon Manager", 76 | vscode.ViewColumn.Active, 77 | { 78 | enableScripts: true, 79 | enableForms: false, 80 | localResourceRoots: [extensionUri], 81 | } 82 | ); 83 | 84 | WebVue.currentPanel = new WebVue(context, panel); 85 | } 86 | 87 | const workspaceOpen = 88 | vscode.workspace.workspaceFolders !== undefined && 89 | vscode.workspace.workspaceFolders.length > 0; 90 | const clientVersion = context.extension.packageJSON.version; 91 | 92 | WebVue.sendMessage("appStore", { 93 | property: "workspaceState", 94 | value: workspaceOpen, 95 | }); 96 | WebVue.sendMessage("appStore", { 97 | property: "clientVersion", 98 | value: clientVersion, 99 | }); 100 | localLogger.debug(`Workspace Open: ${workspaceOpen}`); 101 | } 102 | 103 | /** Dispose of panel to clean up resources when it is closed */ 104 | public dispose() { 105 | WebVue.currentPanel = undefined; 106 | 107 | this._panel?.dispose(); 108 | 109 | while (this._disposables.length) { 110 | const disposable = this._disposables.pop(); 111 | if (disposable) { 112 | disposable.dispose(); 113 | } 114 | } 115 | } 116 | 117 | /** Get the HTML content of the webview */ 118 | private _getWebViewContent( 119 | webview: vscode.Webview, 120 | context: vscode.ExtensionContext 121 | ) { 122 | if (context.extensionMode !== vscode.ExtensionMode.Production) { 123 | return ` 124 | 125 | 126 | 127 | 128 | 129 | Lua Addon Manager 130 | 145 | 146 | 147 | 148 | 176 | 177 | 178 | `; 179 | } else { 180 | const stylesUri = this.toWebviewUri([ 181 | "client", 182 | "webvue", 183 | "build", 184 | "assets", 185 | "index.css", 186 | ]); 187 | const scriptUri = this.toWebviewUri([ 188 | "client", 189 | "webvue", 190 | "build", 191 | "assets", 192 | "index.js", 193 | ]); 194 | const codiconUri = this.toWebviewUri([ 195 | "client", 196 | "webvue", 197 | "build", 198 | "assets", 199 | "codicon.ttf", 200 | ]); 201 | 202 | const inlineStyleNonce = this.getNonce(); 203 | const scriptNonce = this.getNonce(); 204 | 205 | return ` 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | Lua Addon Manager 214 | 220 | 221 | 222 |
223 | 224 | 225 | 226 | `; 227 | } 228 | } 229 | 230 | /** Get a `nonce` (number used once). Used for the content security policy. 231 | * 232 | * [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/nonce) 233 | */ 234 | private getNonce() { 235 | let text = ""; 236 | const possible = 237 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 238 | for (let i = 0; i < 32; i++) { 239 | text += possible.charAt( 240 | Math.floor(Math.random() * possible.length) 241 | ); 242 | } 243 | return text; 244 | } 245 | 246 | /** Sets up event listener for messages sent from webview */ 247 | private _setWebviewMessageListener( 248 | webview: vscode.Webview, 249 | context: vscode.ExtensionContext 250 | ) { 251 | return webview.onDidReceiveMessage((message: WebVueMessage) => { 252 | const command = message.command; 253 | commandLogger.verbose( 254 | `Executing "${command}" (${JSON.stringify(message)})` 255 | ); 256 | 257 | try { 258 | (commands as any)[command](context, message); 259 | } catch (e) { 260 | commandLogger.error(e); 261 | } 262 | }); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /client/src/addon_manager/registration.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | import { WebVue } from "./panels/WebVue"; 4 | import VSCodeLogFileTransport from "./services/logging/vsCodeLogFileTransport"; 5 | import { createChildLogger, logger } from "./services/logging.service"; 6 | import dayjs from "dayjs"; 7 | import RelativeTime from "dayjs/plugin/relativeTime"; 8 | import { git, setupGit } from "./services/git.service"; 9 | import { GIT_DOWNLOAD_URL, REPOSITORY, setGlobalStorageUri } from "./config"; 10 | import { NotificationLevels } from "./types/webvue"; 11 | import * as languageServer from "../languageserver"; 12 | 13 | dayjs.extend(RelativeTime); 14 | 15 | const localLogger = createChildLogger("Registration"); 16 | 17 | /** Set up the addon manager by registering its commands in VS Code */ 18 | export async function activate(context: vscode.ExtensionContext) { 19 | const globalConfig = vscode.workspace.getConfiguration("Lua.addonManager"); 20 | const isEnabled = globalConfig.get("enable") as boolean; 21 | 22 | if (!isEnabled) { 23 | // NOTE: Will only log to OUTPUT, not to log file 24 | localLogger.info("Addon manager is disabled"); 25 | return; 26 | } 27 | 28 | const fileLogger = new VSCodeLogFileTransport(context.logUri, { 29 | level: "debug", 30 | }); 31 | 32 | // update config 33 | const repositoryPath = globalConfig.get("repositoryPath") as string 34 | const repositoryBranch = globalConfig.get("repositoryBranch") as string 35 | if ((repositoryPath || repositoryBranch) && context.storageUri) { 36 | REPOSITORY.PATH = !!repositoryPath ? repositoryPath : REPOSITORY.PATH 37 | REPOSITORY.DEFAULT_BRANCH = !!repositoryBranch ? repositoryBranch : REPOSITORY.DEFAULT_BRANCH 38 | setGlobalStorageUri(false) 39 | } 40 | 41 | 42 | // Register command to open addon manager 43 | context.subscriptions.push( 44 | vscode.commands.registerCommand("lua.addon_manager.open", async () => { 45 | // Set up file logger 46 | if (!fileLogger.initialized) { 47 | const disposable = await fileLogger.init(); 48 | context.subscriptions.push(disposable); 49 | logger.info( 50 | `This session's log file: ${VSCodeLogFileTransport.currentLogFile}` 51 | ); 52 | logger.add(fileLogger); 53 | await fileLogger.logStart(); 54 | } 55 | // Start language server if it is not already 56 | // We depend on it to apply config modifications 57 | if (!languageServer.defaultClient) { 58 | logger.debug("Starting language server"); 59 | await languageServer.createClient(context); 60 | logger.debug("Language server has started"); 61 | } 62 | 63 | // Check if git is installed 64 | if (!(await git.version()).installed) { 65 | logger.error("Git does not appear to be installed!", { 66 | report: false, 67 | }); 68 | vscode.window 69 | .showErrorMessage( 70 | "Git does not appear to be installed. Please install Git to use the addon manager", 71 | "Disable Addon Manager", 72 | "Visit Git Website" 73 | ) 74 | .then((result) => { 75 | switch (result) { 76 | case "Disable Addon Manager": 77 | globalConfig.update( 78 | "enable", 79 | false, 80 | vscode.ConfigurationTarget.Global 81 | ); 82 | break; 83 | case "Visit Git Website": 84 | vscode.env.openExternal( 85 | vscode.Uri.parse(GIT_DOWNLOAD_URL) 86 | ); 87 | break; 88 | default: 89 | break; 90 | } 91 | }); 92 | } 93 | 94 | // Set up git repository for fetching addons 95 | try { 96 | setupGit(context); 97 | } catch (e: any) { 98 | const message = 99 | "Failed to set up Git repository. Please check your connection to GitHub."; 100 | logger.error(message, { report: false }); 101 | logger.error(e, { report: false }); 102 | WebVue.sendNotification({ 103 | level: NotificationLevels.error, 104 | message, 105 | }); 106 | } 107 | 108 | WebVue.render(context); 109 | }) 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /client/src/addon_manager/services/addonManager.service.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import filesystem from "./filesystem.service"; 3 | import { createChildLogger } from "./logging.service"; 4 | import { Addon } from "../models/addon"; 5 | import { git } from "./git.service"; 6 | import { DiffResultTextFile } from "simple-git"; 7 | import { WebVue } from "../panels/WebVue"; 8 | import { NotificationLevels } from "../types/webvue"; 9 | 10 | const localLogger = createChildLogger("Addon Manager"); 11 | 12 | class AddonManager { 13 | readonly addons: Map; 14 | 15 | constructor() { 16 | this.addons = new Map(); 17 | } 18 | 19 | public async fetchAddons(installLocation: vscode.Uri) { 20 | try { 21 | await git.fetch(); 22 | await git.pull(); 23 | } catch (e: any) { 24 | const message = 25 | "Failed to fetch addons! Please check your connection to GitHub."; 26 | localLogger.error(message, { report: false }); 27 | localLogger.error(e, { report: false }); 28 | WebVue.sendNotification({ 29 | level: NotificationLevels.error, 30 | message, 31 | }); 32 | } 33 | 34 | const ignoreList = [".DS_Store"]; 35 | let addons = await filesystem.readDirectory(installLocation); 36 | if (addons) { 37 | addons = addons.filter((a) => !ignoreList.includes(a.name)); 38 | } 39 | if (!addons || addons.length === 0) { 40 | localLogger.warn("No addons found in installation folder"); 41 | return; 42 | } 43 | for (const addon of addons) { 44 | this.addons.set(addon.name, new Addon(addon.name, addon.uri)); 45 | localLogger.verbose(`Found ${addon.name}`); 46 | } 47 | 48 | return await this.checkUpdated(); 49 | } 50 | 51 | public async checkUpdated() { 52 | const diff = await git.diffSummary(["main", "origin/main"]); 53 | this.addons.forEach((addon) => { 54 | addon.checkForUpdate(diff.files as DiffResultTextFile[]); 55 | }); 56 | } 57 | 58 | public unlockAddon(name: string) { 59 | const addon = this.addons.get(name); 60 | return addon?.setLock(false); 61 | } 62 | } 63 | 64 | export default new AddonManager(); 65 | -------------------------------------------------------------------------------- /client/src/addon_manager/services/filesystem.service.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { stringToByteArray } from "./string.service"; 3 | import { createChildLogger } from "./logging.service"; 4 | import { platform } from "os"; 5 | 6 | const localLogger = createChildLogger("Filesystem"); 7 | 8 | type ReadDirectoryOptions = { 9 | recursive?: boolean; 10 | maxDepth?: number; 11 | depth?: number; 12 | }; 13 | 14 | namespace filesystem { 15 | /** Get a string representation of a URI using UNIX separators 16 | * @param uri The URI to get as a UNIX path string 17 | */ 18 | export function unixifyPath(uri: vscode.Uri): string { 19 | if (platform() === "win32") { 20 | return uri.path.substring(1); 21 | } else { 22 | return uri.fsPath; 23 | } 24 | } 25 | 26 | /** Check if a file exists 27 | * @param uri - The URI of the file to check the existence of 28 | */ 29 | export async function exists(uri: vscode.Uri): Promise { 30 | try { 31 | await vscode.workspace.fs.stat(uri); 32 | return true; 33 | } catch (e) { 34 | return false; 35 | } 36 | } 37 | 38 | /** Check if a directory is empty 39 | * @param uri - The URI of the directory to check 40 | */ 41 | export async function empty(uri: vscode.Uri): Promise { 42 | try { 43 | const dirContents = await vscode.workspace.fs.readDirectory(uri); 44 | return dirContents.length < 1; 45 | } catch (e) { 46 | localLogger.error(e); 47 | return false; 48 | } 49 | } 50 | 51 | /** Read from a file 52 | * @param uri - The URI of the file to read from 53 | */ 54 | export async function readFile(uri: vscode.Uri): Promise { 55 | const bytes = await vscode.workspace.fs.readFile(uri); 56 | const str = bytes.toString(); 57 | 58 | return str; 59 | } 60 | 61 | /** Write to a file 62 | * @param uri - The URI of the file to write to 63 | * @param content - The content to write in to the file, overwriting any previous content 64 | */ 65 | export async function writeFile( 66 | uri: vscode.Uri, 67 | content: string 68 | ): Promise { 69 | const byteArray = stringToByteArray(content); 70 | await vscode.workspace.fs.writeFile(uri, byteArray); 71 | 72 | localLogger.debug(`Wrote to "${uri.path}"`); 73 | } 74 | 75 | /** Delete a file 76 | * @param uri - The URI of the file to delete 77 | * @param options - Options to control if deleting a directory should be recursive and if the system's trash should be used 78 | */ 79 | export async function deleteFile( 80 | uri: vscode.Uri, 81 | options?: { recursive?: boolean; useTrash?: boolean } 82 | ): Promise { 83 | await vscode.workspace.fs.delete(uri, { 84 | recursive: options?.recursive ?? false, 85 | useTrash: options?.useTrash ?? true, 86 | }); 87 | 88 | localLogger.debug(`Deleted ${uri.path}`); 89 | } 90 | 91 | export async function createDirectory(uri: vscode.Uri) { 92 | return vscode.workspace.fs 93 | .createDirectory(uri) 94 | .then(() => 95 | localLogger.debug(`Created directory at "${uri.path}"`) 96 | ); 97 | } 98 | 99 | export type DirectoryNode = { 100 | path: string; 101 | name: string; 102 | type: vscode.FileType; 103 | uri: vscode.Uri; 104 | }; 105 | 106 | /** Read a directory, returning an array of all entries 107 | * @param uri - The URI of the directory to read 108 | * @param options - Options for controlling recursion 109 | */ 110 | export async function readDirectory( 111 | uri: vscode.Uri, 112 | options?: ReadDirectoryOptions 113 | ) { 114 | const tree: DirectoryNode[] = []; 115 | 116 | options = options ?? {}; 117 | 118 | options.maxDepth = options.maxDepth ?? 10; 119 | options.depth = options.depth ?? 0; 120 | 121 | if (options.depth > options.maxDepth) { 122 | localLogger.warn( 123 | `Max recursion depth(${options.maxDepth}) reached!` 124 | ); 125 | return; 126 | } 127 | 128 | const dirContents = await vscode.workspace.fs.readDirectory(uri); 129 | 130 | for (const item of dirContents) { 131 | const name = item[0]; 132 | const type = item[1]; 133 | const itemURI = vscode.Uri.joinPath(uri, name); 134 | 135 | const pathSegments = itemURI.path.split("/"); 136 | const path = pathSegments 137 | .slice(pathSegments.length - (options.depth + 1)) 138 | .join("/"); 139 | 140 | switch (type) { 141 | case vscode.FileType.File: 142 | tree.push({ path, name, type, uri: itemURI }); 143 | break; 144 | case vscode.FileType.Directory: 145 | if (!options.recursive) { 146 | tree.push({ path, name, type, uri: itemURI }); 147 | continue; 148 | } 149 | tree.push( 150 | ...(await readDirectory(itemURI, { 151 | recursive: true, 152 | maxDepth: options.maxDepth, 153 | depth: options.depth + 1, 154 | })) ?? [] 155 | ); 156 | break; 157 | default: 158 | localLogger.warn(`Unsupported file type ${itemURI.path}`); 159 | break; 160 | } 161 | } 162 | 163 | return tree; 164 | } 165 | 166 | export async function getDirectorySize( 167 | uri: vscode.Uri, 168 | maxDepth = 10 169 | ): Promise { 170 | const tree = await readDirectory(uri, { 171 | maxDepth, 172 | recursive: true, 173 | }); 174 | 175 | const promises = [] as Promise[]; 176 | for (const node of tree ?? []) { 177 | if (node.type !== vscode.FileType.File) continue; 178 | 179 | promises.push( 180 | new Promise((resolve) => { 181 | vscode.workspace.fs 182 | .stat(node.uri) 183 | .then((stats) => resolve(stats.size)); 184 | }) 185 | ); 186 | } 187 | 188 | return Promise.all(promises).then((results) => { 189 | return results.reduce((previous, result) => previous + result, 0); 190 | }); 191 | } 192 | } 193 | 194 | export default filesystem; 195 | -------------------------------------------------------------------------------- /client/src/addon_manager/services/git.service.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import simpleGit from "simple-git"; 3 | import filesystem from "./filesystem.service"; 4 | import { createChildLogger } from "./logging.service"; 5 | import { REPOSITORY_NAME, REPOSITORY, getStorageUri } from "../config"; 6 | 7 | const localLogger = createChildLogger("Git"); 8 | 9 | export const git = simpleGit({ trimmed: true }); 10 | 11 | export const setupGit = async (context: vscode.ExtensionContext) => { 12 | const storageURI = vscode.Uri.joinPath( 13 | getStorageUri(context), 14 | "addonManager" 15 | ); 16 | await filesystem.createDirectory(storageURI); 17 | 18 | // set working directory 19 | await git.cwd({ path: storageURI.fsPath, root: true }); 20 | 21 | // clone if not already cloned 22 | if (await filesystem.empty(storageURI)) { 23 | try { 24 | localLogger.debug( 25 | `Attempting to clone ${REPOSITORY_NAME} to ${storageURI.fsPath}` 26 | ); 27 | await git.clone(REPOSITORY.PATH, storageURI.fsPath); 28 | localLogger.debug( 29 | `Cloned ${REPOSITORY_NAME} to ${storageURI.fsPath}` 30 | ); 31 | } catch (e) { 32 | localLogger.warn( 33 | `Failed to clone ${REPOSITORY_NAME} to ${storageURI.fsPath}!` 34 | ); 35 | throw e; 36 | } 37 | } 38 | 39 | // pull 40 | try { 41 | await git.fetch(); 42 | await git.pull(); 43 | await git.checkout(REPOSITORY.DEFAULT_BRANCH); 44 | } catch (e) { 45 | localLogger.warn(`Failed to pull ${REPOSITORY_NAME}!`); 46 | throw e; 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /client/src/addon_manager/services/logging.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Logging using WintonJS 3 | * https://github.com/winstonjs/winston 4 | */ 5 | 6 | import winston from "winston"; 7 | import VSCodeOutputTransport from "./logging/vsCodeOutputTransport"; 8 | import axios, { AxiosError } from "axios"; 9 | import { padText } from "./string.service"; 10 | import * as vscode from "vscode"; 11 | import { MESSAGE } from "triple-beam"; 12 | import { REPOSITORY_ISSUES_URL } from "../config"; 13 | import VSCodeLogFileTransport from "./logging/vsCodeLogFileTransport"; 14 | 15 | // Create logger from winston 16 | export const logger = winston.createLogger({ 17 | level: "info", 18 | defaultMeta: { category: "General", report: true }, 19 | format: winston.format.combine( 20 | winston.format.timestamp({ 21 | format: "YYYY-MM-DD HH:mm:ss", 22 | }), 23 | winston.format.errors({ stack: true }), 24 | winston.format.printf((message) => { 25 | const level = padText(message.level, 9); 26 | const category = padText( 27 | message?.defaultMeta?.category ?? "GENERAL", 28 | 18 29 | ); 30 | if (typeof message.message === "object") 31 | return `[${ 32 | message.timestamp 33 | }] | ${level.toUpperCase()} | ${category} | ${JSON.stringify( 34 | message.message 35 | )}`; 36 | return `[${ 37 | message.timestamp 38 | }] | ${level.toUpperCase()} | ${category} | ${message.message}`; 39 | }) 40 | ), 41 | 42 | transports: [new VSCodeOutputTransport({ level: "info" })], 43 | }); 44 | 45 | // When a error is logged, ask user to report error. 46 | logger.on("data", async (info) => { 47 | if (info.level !== "error" || !info.report) return; 48 | 49 | const choice = await vscode.window.showErrorMessage( 50 | `An error occurred with the Lua Addon Manager. Please help us improve by reporting the issue ❤️`, 51 | { modal: false }, 52 | "Report Issue" 53 | ); 54 | 55 | if (choice !== "Report Issue") return; 56 | 57 | // Open log file 58 | await vscode.env.openExternal(VSCodeLogFileTransport.currentLogFile); 59 | 60 | // Read log file and copy to clipboard 61 | const log = await vscode.workspace.fs.readFile( 62 | VSCodeLogFileTransport.currentLogFile 63 | ); 64 | await vscode.env.clipboard.writeText( 65 | "
Retrieved Log\n\n```\n" + 66 | log.toString() + 67 | "\n```\n\n
" 68 | ); 69 | vscode.window.showInformationMessage("Copied log to clipboard"); 70 | 71 | // After a delay, open GitHub issues page 72 | setTimeout(() => { 73 | const base = vscode.Uri.parse(REPOSITORY_ISSUES_URL); 74 | const query = [ 75 | base.query, 76 | `actual=...\n\nI also see the following error:\n\n\`\`\`\n${info[MESSAGE]}\n\`\`\``, 77 | ]; 78 | const issueURI = base.with({ query: query.join("&") }); 79 | 80 | vscode.env.openExternal(issueURI); 81 | }, 2000); 82 | }); 83 | 84 | /** Helper that creates a child logger from the main logger. */ 85 | export const createChildLogger = (label: string) => { 86 | return logger.child({ 87 | level: "info", 88 | defaultMeta: { category: label }, 89 | format: winston.format.combine( 90 | winston.format.timestamp({ 91 | format: "YYYY-MM-DD HH:mm:ss", 92 | }), 93 | winston.format.errors({ stack: true }), 94 | winston.format.json() 95 | ), 96 | }); 97 | }; 98 | 99 | // Log HTTP requests made through axios 100 | const axiosLogger = createChildLogger("AXIOS"); 101 | 102 | axios.interceptors.request.use( 103 | (request) => { 104 | const method = request.method ?? "???"; 105 | axiosLogger.debug(`${method.toUpperCase()} requesting ${request.url}`); 106 | 107 | return request; 108 | }, 109 | (error: AxiosError) => { 110 | const url = error?.config?.url; 111 | const method = error.config?.method?.toUpperCase(); 112 | 113 | axiosLogger.error(`${url} ${method} ${error.code} ${error.message}`); 114 | return Promise.reject(error); 115 | } 116 | ); 117 | -------------------------------------------------------------------------------- /client/src/addon_manager/services/logging/vsCodeLogFileTransport.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import Transport from "winston-transport"; 3 | import winston from "winston"; 4 | import { MESSAGE } from "triple-beam"; 5 | import * as fs from "fs"; 6 | import { stringToByteArray } from "../string.service"; 7 | import dayjs from "dayjs"; 8 | 9 | export default class VSCodeLogFileTransport extends Transport { 10 | public static currentLogFile: vscode.Uri; 11 | 12 | public initialized = false; 13 | 14 | private logDir: vscode.Uri; 15 | 16 | private stream?: fs.WriteStream; 17 | 18 | constructor(logDir: vscode.Uri, opts?: Transport.TransportStreamOptions) { 19 | super(opts); 20 | this.logDir = logDir; 21 | } 22 | 23 | /** Initialize transport instance by creating the needed directories and files. */ 24 | public async init() { 25 | // Ensure log directory exists 26 | await vscode.workspace.fs.createDirectory(this.logDir); 27 | // Create subdirectory 28 | const addonLogsDir = vscode.Uri.joinPath(this.logDir, "addonManager"); 29 | await vscode.workspace.fs.createDirectory(addonLogsDir); 30 | // Create log file stream 31 | const logFileUri = vscode.Uri.joinPath( 32 | addonLogsDir, 33 | `${dayjs().format("HH")}.log` 34 | ); 35 | VSCodeLogFileTransport.currentLogFile = logFileUri; 36 | this.stream = fs.createWriteStream(logFileUri.fsPath, { 37 | flags: "a", 38 | }); 39 | this.initialized = true; 40 | return new vscode.Disposable(() => this.stream?.close); 41 | } 42 | 43 | /** Mark the start of the addon manager in the log */ 44 | public logStart() { 45 | return new Promise((resolve, reject) => { 46 | this.stream?.write( 47 | stringToByteArray("#### STARTUP ####\n"), 48 | (err) => { 49 | if (err) reject(err); 50 | resolve(true); 51 | } 52 | ); 53 | }); 54 | } 55 | 56 | public async log(info: winston.LogEntry, callback: winston.LogCallback) { 57 | if (!this.initialized) { 58 | return; 59 | } 60 | 61 | setImmediate(() => { 62 | this.emit("logged", info); 63 | }); 64 | 65 | this.stream?.write( 66 | stringToByteArray(info[MESSAGE as unknown as string] + "\n") 67 | ); 68 | 69 | callback(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /client/src/addon_manager/services/logging/vsCodeOutputTransport.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import Transport from "winston-transport"; 3 | import winston from "winston"; 4 | import { MESSAGE } from "triple-beam"; 5 | 6 | export default class VSCodeOutputTransport extends Transport { 7 | private readonly outputChannel: vscode.OutputChannel; 8 | 9 | constructor(opts?: Transport.TransportStreamOptions) { 10 | super(opts); 11 | this.outputChannel = vscode.window.createOutputChannel( 12 | "Lua Addon Manager", 13 | "log" 14 | ); 15 | } 16 | 17 | log(info: winston.LogEntry, callback: winston.LogCallback) { 18 | setImmediate(() => { 19 | this.emit("logged", info); 20 | }); 21 | 22 | this.outputChannel.appendLine(info[MESSAGE as unknown as string]); 23 | 24 | callback(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/src/addon_manager/services/settings.service.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as path from "path"; 3 | import { ConfigChange, getConfig, setConfig } from "../../languageserver"; 4 | import { createChildLogger } from "./logging.service"; 5 | import { LIBRARY_SETTING } from "../config"; 6 | 7 | const localLogger = createChildLogger("Settings"); 8 | 9 | /** An error with the user's configuration `.vscode/settings.json` or an 10 | * addon's `config.json`. */ 11 | class ConfigError extends Error { 12 | constructor(message: string) { 13 | super(message); 14 | localLogger.error(message); 15 | } 16 | } 17 | 18 | export const getLibraryPaths = async (): Promise< 19 | { folder: vscode.WorkspaceFolder; paths: string[] }[] 20 | > => { 21 | const result = []; 22 | 23 | if (!vscode.workspace.workspaceFolders) return []; 24 | 25 | for (const folder of vscode.workspace.workspaceFolders) { 26 | const libraries = await getConfig(LIBRARY_SETTING, folder.uri); 27 | result.push({ folder, paths: libraries }); 28 | } 29 | 30 | return result; 31 | }; 32 | 33 | export const applyAddonSettings = async ( 34 | folder: vscode.WorkspaceFolder, 35 | config: Record 36 | ) => { 37 | if (!folder) throw new ConfigError(`Workspace is not open!`); 38 | 39 | const changes: ConfigChange[] = []; 40 | for (const [newKey, newValue] of Object.entries(config)) { 41 | if (Array.isArray(newValue)) { 42 | newValue.forEach((val) => { 43 | changes.push({ 44 | action: "add", 45 | key: newKey, 46 | value: val, 47 | uri: folder.uri, 48 | }); 49 | }); 50 | } else if (typeof newValue === "object" && newValue !== null) { 51 | changes.push( 52 | ...Object.entries(newValue).map(([key, value]): ConfigChange => { 53 | return { 54 | action: "prop", 55 | key: newKey, 56 | prop: key, 57 | value, 58 | uri: folder.uri, 59 | }; 60 | }) 61 | ); 62 | } else { 63 | changes.push({ 64 | action: "set", 65 | key: newKey, 66 | value: newValue, 67 | uri: folder.uri, 68 | }); 69 | } 70 | } 71 | 72 | return await setConfig(changes); 73 | }; 74 | 75 | export const revokeAddonSettings = async ( 76 | folder: vscode.WorkspaceFolder, 77 | config: Record 78 | ) => { 79 | if (!folder) throw new ConfigError(`Workspace is not open!`); 80 | 81 | const changes: ConfigChange[] = []; 82 | for (const [newKey, newValue] of Object.entries(config)) { 83 | const currentValue = await getConfig(newKey, folder.uri); 84 | 85 | if (Array.isArray(newValue)) { 86 | // Only keep values that the addon settings does not contain 87 | const notAddon = currentValue.filter( 88 | (oldValue: any) => !newValue.includes(oldValue) 89 | ); 90 | changes.push({ 91 | action: "set", 92 | key: newKey, 93 | value: notAddon, 94 | uri: folder.uri, 95 | }); 96 | } else if (typeof newValue === "object" && newValue !== null) { 97 | for (const objectKey of Object.keys(newValue)) { 98 | delete currentValue[objectKey]; 99 | } 100 | // If object is now empty, delete it 101 | if (Object.keys(currentValue).length === 0) { 102 | changes.push({ 103 | action: "set", 104 | key: newKey, 105 | value: undefined, 106 | uri: folder.uri, 107 | }); 108 | } else { 109 | changes.push({ 110 | action: "set", 111 | key: newKey, 112 | value: currentValue, 113 | uri: folder.uri, 114 | }); 115 | } 116 | } 117 | } 118 | 119 | return await setConfig(changes); 120 | }; 121 | -------------------------------------------------------------------------------- /client/src/addon_manager/services/string.service.ts: -------------------------------------------------------------------------------- 1 | import { TextEncoder } from "util"; 2 | 3 | /** Pad a string to have spaces on either side, making it a set `length` 4 | * @param str The string to add padding to 5 | * @param length The new total length the string should be with padding 6 | */ 7 | export const padText = (str: string, length: number) => { 8 | const paddingLength = Math.max(0, length - str.length); 9 | const padding = " ".repeat(paddingLength / 2); 10 | 11 | const paddingLeft = " ".repeat(length - padding.length - str.length); 12 | 13 | return paddingLeft + str + padding; 14 | }; 15 | 16 | /** Convert a string to a byte array */ 17 | export const stringToByteArray = (str: string): Uint8Array => 18 | new TextEncoder().encode(str); 19 | 20 | /** Convert an object into a query string without the leading `?` */ 21 | export const objectToQueryString = ( 22 | obj: Record 23 | ): string => { 24 | return Object.keys(obj) 25 | .map( 26 | (key) => 27 | `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}` 28 | ) 29 | .join("&"); 30 | }; 31 | -------------------------------------------------------------------------------- /client/src/addon_manager/types/addon.d.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from "vscode"; 2 | 3 | export type AddonConfig = { 4 | name: string; 5 | description: string; 6 | settings: { [index: string]: Object }; 7 | }; 8 | 9 | export type AddonInfo = { 10 | name: string; 11 | description: string; 12 | size: number; 13 | hasPlugin: boolean; 14 | } 15 | 16 | export interface Addon { 17 | readonly name: string; 18 | readonly uri: Uri; 19 | 20 | displayName?: string; 21 | description?: string; 22 | size?: number; 23 | hasPlugin?: boolean; 24 | processing?: boolean; 25 | } 26 | -------------------------------------------------------------------------------- /client/src/addon_manager/types/webvue.ts: -------------------------------------------------------------------------------- 1 | export interface WebVueMessage { 2 | command: string; 3 | data: { [index: string]: unknown }; 4 | } 5 | 6 | export enum NotificationLevels { 7 | "error", 8 | "warn", 9 | "info", 10 | } 11 | 12 | export type Notification = { 13 | level: NotificationLevels; 14 | message: string; 15 | }; 16 | -------------------------------------------------------------------------------- /client/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as languageserver from './languageserver'; 3 | import * as psi from './psi/psiViewer'; 4 | import * as addonManager from './addon_manager/registration'; 5 | 6 | import luadoc from "../3rd/vscode-lua-doc/extension.js"; 7 | 8 | interface LuaDocContext extends vscode.ExtensionContext { 9 | ViewType: string; 10 | OpenCommand: string; 11 | } 12 | 13 | export function activate(context: vscode.ExtensionContext) { 14 | languageserver.activate(context); 15 | 16 | const luaDocContext: LuaDocContext = { 17 | asAbsolutePath: context.asAbsolutePath, 18 | environmentVariableCollection: context.environmentVariableCollection, 19 | extensionUri: context.extensionUri, 20 | globalState: context.globalState, 21 | storagePath: context.storagePath, 22 | subscriptions: context.subscriptions, 23 | workspaceState: context.workspaceState, 24 | extensionMode: context.extensionMode, 25 | globalStorageUri: context.globalStorageUri, 26 | logUri: context.logUri, 27 | logPath: context.logPath, 28 | globalStoragePath: context.globalStoragePath, 29 | extension: context.extension, 30 | secrets: context.secrets, 31 | storageUri: context.storageUri, 32 | extensionPath: context.extensionPath + '/client/3rd/vscode-lua-doc', 33 | ViewType: 'lua-doc', 34 | OpenCommand: 'extension.lua.doc', 35 | } 36 | 37 | luadoc.activate(luaDocContext); 38 | psi.activate(context); 39 | 40 | // Register and activate addon manager 41 | addonManager.activate(context); 42 | 43 | return { 44 | async reportAPIDoc(params: unknown) { 45 | await languageserver.reportAPIDoc(params); 46 | }, 47 | async setConfig(changes: languageserver.ConfigChange[]) { 48 | await languageserver.setConfig(changes); 49 | } 50 | }; 51 | } 52 | 53 | export function deactivate() { 54 | languageserver.deactivate(); 55 | } 56 | -------------------------------------------------------------------------------- /client/src/languageserver.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as os from 'os'; 3 | import * as fs from 'fs'; 4 | import * as vscode from 'vscode'; 5 | import * as LSP from 'vscode-languageserver-protocol'; 6 | import { 7 | workspace as Workspace, 8 | ExtensionContext, 9 | commands as Commands, 10 | TextDocument, 11 | Uri, 12 | window, 13 | Disposable, 14 | } from 'vscode'; 15 | import { 16 | LanguageClient, 17 | LanguageClientOptions, 18 | ServerOptions, 19 | DocumentSelector, 20 | LSPAny, 21 | ExecuteCommandRequest, 22 | TransportKind, 23 | } from 'vscode-languageclient/node'; 24 | 25 | export let defaultClient: LuaClient | null; 26 | 27 | function registerCustomCommands(context: ExtensionContext) { 28 | context.subscriptions.push(Commands.registerCommand('lua.config', (changes) => { 29 | const propMap: Record> = {}; 30 | 31 | for (const data of changes) { 32 | const config = Workspace.getConfiguration(undefined, Uri.parse(data.uri)); 33 | 34 | if (data.action === 'add') { 35 | const value = config.get(data.key); 36 | if (!Array.isArray(value)) throw new Error(`${data.key} is not an Array!`); 37 | value.push(data.value); 38 | config.update(data.key, value, data.global); 39 | continue; 40 | } 41 | if (data.action === 'set') { 42 | config.update(data.key, data.value, data.global); 43 | continue; 44 | } 45 | if (data.action === 'prop') { 46 | if (!propMap[data.key]) { 47 | let prop = config.get(data.key); 48 | if (typeof prop === 'object' && prop !== null) { 49 | propMap[data.key] = prop as Record; 50 | } 51 | } 52 | propMap[data.key][data.prop] = data.value; 53 | config.update(data.key, propMap[data.key], data.global); 54 | continue; 55 | } 56 | } 57 | })); 58 | 59 | context.subscriptions.push(Commands.registerCommand('lua.exportDocument', async () => { 60 | if (!defaultClient) { 61 | return; 62 | } 63 | const outputs = await vscode.window.showOpenDialog({ 64 | defaultUri: vscode.Uri.joinPath( 65 | context.extensionUri, 66 | 'server', 67 | 'log', 68 | ), 69 | openLabel: "Export to this folder", 70 | canSelectFiles: false, 71 | canSelectFolders: true, 72 | canSelectMany: false, 73 | }); 74 | const output = outputs?.[0]; 75 | if (!output) { 76 | return; 77 | } 78 | defaultClient.client?.sendRequest(ExecuteCommandRequest.type, { 79 | command: 'lua.exportDocument', 80 | arguments: [output.toString()], 81 | }); 82 | })); 83 | 84 | context.subscriptions.push(Commands.registerCommand('lua.reloadFFIMeta', async () => { 85 | defaultClient?.client?.sendRequest(ExecuteCommandRequest.type, { 86 | command: 'lua.reloadFFIMeta', 87 | }) 88 | })) 89 | 90 | context.subscriptions.push(Commands.registerCommand('lua.startServer', async () => { 91 | deactivate(); 92 | createClient(context); 93 | })); 94 | 95 | context.subscriptions.push(Commands.registerCommand('lua.stopServer', async () => { 96 | deactivate(); 97 | })); 98 | 99 | context.subscriptions.push(Commands.registerCommand('lua.showReferences', (uri: string, position: Record, locations: any[]) => { 100 | vscode.commands.executeCommand( 101 | 'editor.action.showReferences', 102 | vscode.Uri.parse(uri), 103 | new vscode.Position(position.line, position.character), 104 | locations.map((value) => { 105 | return new vscode.Location( 106 | vscode.Uri.parse(value.uri as any as string), 107 | new vscode.Range( 108 | value.range.start.line, 109 | value.range.start.character, 110 | value.range.end.line, 111 | value.range.end.character, 112 | ), 113 | ); 114 | }) 115 | ); 116 | })); 117 | } 118 | 119 | /** Creates a new {@link LuaClient} and starts it. */ 120 | export const createClient = (context: ExtensionContext) => { 121 | defaultClient = new LuaClient(context, [{ language: 'lua' }]) 122 | defaultClient.start(); 123 | } 124 | 125 | class LuaClient extends Disposable { 126 | public client: LanguageClient | undefined; 127 | private disposables = new Array(); 128 | constructor( 129 | private context: ExtensionContext, 130 | private documentSelector: DocumentSelector 131 | ) { 132 | super(() => { 133 | for (const disposable of this.disposables) { 134 | disposable.dispose(); 135 | } 136 | }); 137 | } 138 | 139 | async start() { 140 | // Options to control the language client 141 | const clientOptions: LanguageClientOptions = { 142 | // Register the server for plain text documents 143 | documentSelector: this.documentSelector, 144 | progressOnInitialization: true, 145 | markdown: { 146 | isTrusted: true, 147 | supportHtml: true, 148 | }, 149 | initializationOptions: { 150 | changeConfiguration: true, 151 | statusBar: true, 152 | viewDocument: true, 153 | trustByClient: true, 154 | useSemanticByRange: true, 155 | codeLensViewReferences: true, 156 | fixIndents: true, 157 | languageConfiguration: true, 158 | storagePath: this.context.globalStorageUri.fsPath, 159 | }, 160 | middleware: { 161 | provideHover: async () => undefined, 162 | } 163 | }; 164 | 165 | const config = Workspace.getConfiguration( 166 | undefined, 167 | vscode.workspace.workspaceFolders?.[0] 168 | ); 169 | const commandParam = config.get("Lua.misc.parameters"); 170 | const command = await this.getCommand(config); 171 | 172 | if (!Array.isArray(commandParam)) 173 | throw new Error("Lua.misc.parameters must be an Array!"); 174 | 175 | const port = this.getPort(commandParam); 176 | 177 | const serverOptions: ServerOptions = { 178 | command: command, 179 | transport: port 180 | ? { 181 | kind: TransportKind.socket, 182 | port: port, 183 | } 184 | : undefined, 185 | args: commandParam, 186 | }; 187 | 188 | this.client = new LanguageClient( 189 | "Lua", 190 | "Lua", 191 | serverOptions, 192 | clientOptions 193 | ); 194 | this.disposables.push(this.client); 195 | 196 | //client.registerProposedFeatures(); 197 | await this.client.start(); 198 | this.onCommand(); 199 | this.statusBar(); 200 | this.languageConfiguration(); 201 | this.provideHover(); 202 | } 203 | 204 | private async getCommand(config: vscode.WorkspaceConfiguration) { 205 | const executablePath = config.get("Lua.misc.executablePath"); 206 | 207 | if (typeof executablePath !== "string") 208 | throw new Error("Lua.misc.executablePath must be a string!"); 209 | 210 | if (executablePath && executablePath !== "") { 211 | return executablePath; 212 | } 213 | 214 | const platform: string = os.platform(); 215 | let command: string; 216 | let binDir: string | undefined; 217 | 218 | if ( 219 | ( 220 | await fs.promises.stat( 221 | this.context.asAbsolutePath("server/bin") 222 | ) 223 | ).isDirectory() 224 | ) { 225 | binDir = "bin"; 226 | } 227 | 228 | switch (platform) { 229 | case "win32": 230 | command = this.context.asAbsolutePath( 231 | path.join( 232 | "server", 233 | binDir ? binDir : "bin-Windows", 234 | "lua-language-server.exe" 235 | ) 236 | ); 237 | break; 238 | case "linux": 239 | command = this.context.asAbsolutePath( 240 | path.join( 241 | "server", 242 | binDir ? binDir : "bin-Linux", 243 | "lua-language-server" 244 | ) 245 | ); 246 | await fs.promises.chmod(command, "777"); 247 | break; 248 | case "darwin": 249 | command = this.context.asAbsolutePath( 250 | path.join( 251 | "server", 252 | binDir ? binDir : "bin-macOS", 253 | "lua-language-server" 254 | ) 255 | ); 256 | await fs.promises.chmod(command, "777"); 257 | break; 258 | default: 259 | throw new Error(`Unsupported operating system "${platform}"!`); 260 | } 261 | return command; 262 | } 263 | 264 | // Generated by Copilot 265 | private getPort(commandParam: string[]): number | undefined { 266 | // "--socket=xxxx" or "--socket xxxx" 267 | const portIndex = commandParam.findIndex((value) => { 268 | return value.startsWith("--socket"); 269 | }); 270 | if (portIndex === -1) { 271 | return undefined; 272 | } 273 | const port = 274 | commandParam[portIndex].split("=")[1] || 275 | commandParam[portIndex].split(" ")[1] || 276 | commandParam[portIndex + 1]; 277 | if (!port) { 278 | return undefined; 279 | } 280 | return Number(port); 281 | } 282 | 283 | async stop() { 284 | this.client?.stop(); 285 | this.dispose(); 286 | } 287 | 288 | private statusBar() { 289 | const client = this.client!; 290 | const bar = window.createStatusBarItem(vscode.StatusBarAlignment.Right); 291 | bar.text = "Lua"; 292 | bar.command = "Lua.statusBar"; 293 | this.disposables.push( 294 | Commands.registerCommand(bar.command, () => { 295 | client.sendNotification("$/status/click"); 296 | }) 297 | ); 298 | this.disposables.push( 299 | client.onNotification("$/status/show", () => { 300 | bar.show(); 301 | }) 302 | ); 303 | this.disposables.push( 304 | client.onNotification("$/status/hide", () => { 305 | bar.hide(); 306 | }) 307 | ); 308 | this.disposables.push( 309 | client.onNotification("$/status/report", (params) => { 310 | bar.text = params.text; 311 | bar.tooltip = params.tooltip; 312 | }) 313 | ); 314 | client.sendNotification("$/status/refresh"); 315 | this.disposables.push(bar); 316 | } 317 | 318 | private onCommand() { 319 | if (!this.client) { 320 | return; 321 | } 322 | this.disposables.push( 323 | this.client.onNotification("$/command", (params) => { 324 | Commands.executeCommand(params.command, params.data); 325 | }) 326 | ); 327 | } 328 | 329 | private languageConfiguration() { 330 | if (!this.client) { 331 | return; 332 | } 333 | 334 | function convertStringsToRegex(config: any): any { 335 | if (typeof config !== 'object' || config === null) { 336 | return config; 337 | } 338 | 339 | for (const key in config) { 340 | if (config.hasOwnProperty(key)) { 341 | const value = config[key]; 342 | 343 | if (typeof value === 'object' && value !== null) { 344 | convertStringsToRegex(value); 345 | } 346 | 347 | if (key === 'beforeText' || key === 'afterText') { 348 | if (typeof value === 'string') { 349 | config[key] = new RegExp(value); 350 | } 351 | } 352 | } 353 | } 354 | 355 | return config; 356 | } 357 | 358 | let configuration: Disposable | undefined; 359 | this.disposables.push( 360 | this.client.onNotification('$/languageConfiguration', (params) => { 361 | configuration?.dispose(); 362 | configuration = vscode.languages.setLanguageConfiguration(params.id, convertStringsToRegex(params.configuration)); 363 | this.disposables.push(configuration); 364 | }) 365 | ) 366 | } 367 | 368 | private provideHover() { 369 | const client = this.client; 370 | const levelMap = new WeakMap(); 371 | let provider = vscode.languages.registerHoverProvider('lua', { 372 | provideHover: async (document, position, token, context?: vscode.HoverContext) => { 373 | if (!client) { 374 | return null; 375 | } 376 | let level = 1; 377 | if (context?.previousHover) { 378 | level = levelMap.get(context.previousHover) ?? 0; 379 | if (context.verbosityDelta !== undefined) { 380 | level += context.verbosityDelta; 381 | } 382 | } 383 | let params = { 384 | level: level, 385 | ...client.code2ProtocolConverter.asTextDocumentPositionParams(document, position), 386 | } 387 | return client?.sendRequest( 388 | LSP.HoverRequest.type, 389 | params, 390 | token, 391 | ).then((result) => { 392 | if (token.isCancellationRequested) { 393 | return null; 394 | } 395 | if (result === null) { 396 | return null; 397 | } 398 | let verboseResult = result as LSP.Hover & { maxLevel?: number }; 399 | let maxLevel = verboseResult.maxLevel ?? 0; 400 | let hover = client.protocol2CodeConverter.asHover(result); 401 | let verboseHover = new vscode.VerboseHover( 402 | hover.contents, 403 | hover.range, 404 | level < maxLevel, 405 | level > 0, 406 | ); 407 | if (level > maxLevel) { 408 | level = maxLevel; 409 | } 410 | levelMap.set(verboseHover, level); 411 | return verboseHover; 412 | }, (error) => { 413 | return client.handleFailedRequest(LSP.HoverRequest.type, token, error, null); 414 | }); 415 | } 416 | }) 417 | this.disposables.push(provider) 418 | } 419 | } 420 | 421 | export function activate(context: ExtensionContext) { 422 | registerCustomCommands(context); 423 | function didOpenTextDocument(document: TextDocument) { 424 | // We are only interested in language mode text 425 | if (document.languageId !== 'lua') { 426 | return; 427 | } 428 | 429 | // Untitled files go to a default client. 430 | if (!defaultClient) { 431 | createClient(context); 432 | return; 433 | } 434 | } 435 | 436 | Workspace.onDidOpenTextDocument(didOpenTextDocument); 437 | Workspace.textDocuments.forEach(didOpenTextDocument); 438 | } 439 | 440 | export async function deactivate() { 441 | if (defaultClient) { 442 | defaultClient.stop(); 443 | defaultClient.dispose(); 444 | defaultClient = null; 445 | } 446 | return undefined; 447 | } 448 | vscode.SyntaxTokenType.String 449 | export async function reportAPIDoc(params: unknown) { 450 | if (!defaultClient) { 451 | return; 452 | } 453 | defaultClient.client?.sendNotification('$/api/report', params); 454 | } 455 | 456 | export type ConfigChange = { 457 | action: "set", 458 | key: string, 459 | value: LSPAny, 460 | uri: vscode.Uri, 461 | global?: boolean, 462 | } | { 463 | action: "add", 464 | key: string, 465 | value: LSPAny, 466 | uri: vscode.Uri, 467 | global?: boolean, 468 | } | { 469 | action: "prop", 470 | key: string, 471 | prop: string; 472 | value: LSPAny, 473 | uri: vscode.Uri, 474 | global?: boolean, 475 | } 476 | 477 | export async function setConfig(changes: ConfigChange[]): Promise { 478 | if (!defaultClient) { 479 | return false; 480 | } 481 | const params = []; 482 | for (const change of changes) { 483 | params.push({ 484 | action: change.action, 485 | prop: (change.action === "prop") ? change.prop : undefined as never, 486 | key: change.key, 487 | value: change.value, 488 | uri: change.uri.toString(), 489 | global: change.global, 490 | }); 491 | } 492 | await defaultClient.client?.sendRequest(ExecuteCommandRequest.type, { 493 | command: 'lua.setConfig', 494 | arguments: params, 495 | }); 496 | return true; 497 | } 498 | 499 | export async function getConfig(key: string, uri: vscode.Uri): Promise { 500 | if (!defaultClient) { 501 | return undefined; 502 | } 503 | return await defaultClient.client?.sendRequest(ExecuteCommandRequest.type, { 504 | command: 'lua.getConfig', 505 | arguments: [{ 506 | uri: uri.toString(), 507 | key: key, 508 | }] 509 | }); 510 | } 511 | -------------------------------------------------------------------------------- /client/src/psi/psiViewer.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as vscode from 'vscode'; 4 | import { defaultClient } from '../languageserver'; 5 | 6 | const LANGUAGE_ID = "lua"; 7 | /** 8 | * Manages webview panels 9 | */ 10 | class PsiViewer { 11 | /** 12 | * Track the currently panel. Only allow a single panel to exist at a time. 13 | */ 14 | public static currentPanel: PsiViewer | undefined; 15 | 16 | private static readonly viewType = 'LuaPsiView'; 17 | private static readonly title = "LuaPsiView"; 18 | private static readonly distDirectory = "client/web/dist"; 19 | 20 | private readonly panel: vscode.WebviewPanel; 21 | private readonly extensionPath: string; 22 | private readonly builtAppFolder: string; 23 | private disposables: vscode.Disposable[] = []; 24 | private timeoutToReqAnn?: NodeJS.Timeout; 25 | 26 | public static createOrShow(context: vscode.ExtensionContext) { 27 | // const column = vscode.window.activeTextEditor ? vscode.window.activeTextEditor.viewColumn : undefined; 28 | 29 | // If we already have a panel, show it. 30 | // Otherwise, create angular panel. 31 | if (PsiViewer.currentPanel) { 32 | PsiViewer.currentPanel.panel.reveal(vscode.ViewColumn.Two); 33 | } else { 34 | PsiViewer.currentPanel = new PsiViewer(context, vscode.ViewColumn.Two); 35 | PsiViewer.currentPanel.active(context); 36 | } 37 | return PsiViewer.currentPanel; 38 | } 39 | 40 | private constructor(private context: vscode.ExtensionContext, column: vscode.ViewColumn) { 41 | this.extensionPath = context.extensionPath; 42 | this.builtAppFolder = PsiViewer.distDirectory; 43 | 44 | // Create and show a new webview panel 45 | this.panel = vscode.window.createWebviewPanel(PsiViewer.viewType, PsiViewer.title, column, { 46 | // Enable javascript in the webview 47 | enableScripts: true, 48 | 49 | // And restrict the webview to only loading content from our extension's `media` directory. 50 | localResourceRoots: [vscode.Uri.file(path.join(this.extensionPath, this.builtAppFolder))] 51 | }); 52 | 53 | // Set the webview's initial html content 54 | this.panel.webview.html = this._getHtmlForWebview(); 55 | 56 | // Listen for when the panel is disposed 57 | // This happens when the user closes the panel or when the panel is closed programatically 58 | this.panel.onDidDispose(() => this.dispose(), null, this.disposables); 59 | } 60 | 61 | public dispose() { 62 | PsiViewer.currentPanel = undefined; 63 | 64 | // Clean up our resources 65 | this.panel.dispose(); 66 | 67 | while (this.disposables.length) { 68 | const x = this.disposables.pop(); 69 | if (x) { 70 | x.dispose(); 71 | } 72 | } 73 | } 74 | 75 | public post(message: unknown) { 76 | this.panel.webview.postMessage(message); 77 | } 78 | 79 | /** 80 | * Returns html of the start page (index.html) 81 | */ 82 | private _getHtmlForWebview() { 83 | // path to dist folder 84 | const appDistPath = path.join(this.extensionPath, PsiViewer.distDirectory); 85 | const appDistPathUri = vscode.Uri.file(appDistPath); 86 | 87 | // path as uri 88 | const baseUri = this.panel.webview.asWebviewUri(appDistPathUri); 89 | 90 | // get path to index.html file from dist folder 91 | const indexPath = path.join(appDistPath, 'index.html'); 92 | 93 | // read index file from file system 94 | let indexHtml = fs.readFileSync(indexPath, { encoding: 'utf8' }); 95 | 96 | // update the base URI tag 97 | indexHtml = indexHtml.replace('', ``); 98 | 99 | return indexHtml; 100 | } 101 | 102 | private active(context: vscode.ExtensionContext) { 103 | context.subscriptions.push(vscode.workspace.onDidChangeTextDocument(PsiViewer.onDidChangeTextDocument, null, context.subscriptions)); 104 | context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(PsiViewer.onDidChangeActiveTextEditor, null, context.subscriptions)); 105 | context.subscriptions.push(vscode.window.onDidChangeTextEditorSelection(PsiViewer.onDidChangeSelection, null, context.subscriptions)); 106 | } 107 | 108 | private requestPsi(editor: vscode.TextEditor) { 109 | if (this.timeoutToReqAnn) { 110 | clearTimeout(this.timeoutToReqAnn); 111 | } 112 | this.timeoutToReqAnn = setTimeout(() => { 113 | this.requestPsiImpl(editor); 114 | }, 150); 115 | } 116 | 117 | private requestPsiImpl(editor: vscode.TextEditor) { 118 | const client = defaultClient?.client; 119 | const params = { uri: editor.document.uri.toString() }; 120 | client?.sendRequest<{ data: unknown }>("$/psi/view", params).then(result => { 121 | if (result) { 122 | this.post({ 123 | type: "psi", 124 | value: [result.data] 125 | }); 126 | } 127 | }); 128 | } 129 | 130 | private requestPsiSelect(position: vscode.Position, uri: vscode.Uri) { 131 | const client = defaultClient?.client; 132 | const params = { uri: uri.toString(), position }; 133 | client?.sendRequest<{ data: unknown }>("$/psi/select", params).then(result => { 134 | if (result) { 135 | this.post({ 136 | type: "psi_select", 137 | value: result.data 138 | }); 139 | } 140 | }); 141 | } 142 | 143 | private static onDidChangeTextDocument(event: vscode.TextDocumentChangeEvent) { 144 | const activeEditor = vscode.window.activeTextEditor; 145 | const viewer = PsiViewer.currentPanel; 146 | if (activeEditor 147 | && activeEditor.document === event.document 148 | && activeEditor.document.languageId === LANGUAGE_ID) { 149 | viewer?.requestPsi(activeEditor); 150 | } 151 | } 152 | 153 | private static onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) { 154 | const viewer = PsiViewer.currentPanel; 155 | if (editor 156 | && editor.document.languageId === LANGUAGE_ID) { 157 | viewer?.requestPsi(editor); 158 | } 159 | } 160 | 161 | private static onDidChangeSelection(e: vscode.TextEditorSelectionChangeEvent) { 162 | if (e.kind === vscode.TextEditorSelectionChangeKind.Mouse 163 | || e.kind === vscode.TextEditorSelectionChangeKind.Keyboard) { 164 | const viewer = PsiViewer.currentPanel; 165 | if (viewer) { 166 | viewer.requestPsiSelect(e.selections[0].start, e.textEditor.document.uri); 167 | } 168 | } 169 | } 170 | } 171 | 172 | 173 | export function activate(context: vscode.ExtensionContext) { 174 | context.subscriptions.push( 175 | vscode.commands.registerCommand('lua.psi.view', () => { 176 | PsiViewer.createOrShow(context); 177 | }) 178 | ); 179 | } 180 | -------------------------------------------------------------------------------- /client/src/vscode.proposed/editorHoverVerbosityLevel.d.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | declare module 'vscode' { 7 | 8 | /** 9 | * A hover represents additional information for a symbol or word. Hovers are 10 | * rendered in a tooltip-like widget. 11 | */ 12 | export class VerboseHover extends Hover { 13 | 14 | /** 15 | * Can increase the verbosity of the hover 16 | */ 17 | canIncreaseVerbosity?: boolean; 18 | 19 | /** 20 | * Can decrease the verbosity of the hover 21 | */ 22 | canDecreaseVerbosity?: boolean; 23 | 24 | /** 25 | * Creates a new hover object. 26 | * 27 | * @param contents The contents of the hover. 28 | * @param range The range to which the hover applies. 29 | */ 30 | constructor(contents: MarkdownString | MarkedString | Array, range?: Range, canIncreaseVerbosity?: boolean, canDecreaseVerbosity?: boolean); 31 | } 32 | 33 | export interface HoverContext { 34 | 35 | /** 36 | * The delta by which to increase/decrease the hover verbosity level 37 | */ 38 | readonly verbosityDelta?: number; 39 | 40 | /** 41 | * The previous hover sent for the same position 42 | */ 43 | readonly previousHover?: Hover; 44 | } 45 | 46 | export enum HoverVerbosityAction { 47 | /** 48 | * Increase the hover verbosity 49 | */ 50 | Increase = 0, 51 | /** 52 | * Decrease the hover verbosity 53 | */ 54 | Decrease = 1 55 | } 56 | 57 | /** 58 | * The hover provider class 59 | */ 60 | export interface HoverProvider { 61 | 62 | /** 63 | * Provide a hover for the given position and document. Multiple hovers at the same 64 | * position will be merged by the editor. A hover can have a range which defaults 65 | * to the word range at the position when omitted. 66 | * 67 | * @param document The document in which the command was invoked. 68 | * @param position The position at which the command was invoked. 69 | * @param token A cancellation token. 70 | * @oaram context A hover context. 71 | * @returns A hover or a thenable that resolves to such. The lack of a result can be 72 | * signaled by returning `undefined` or `null`. 73 | */ 74 | provideHover(document: TextDocument, position: Position, token: CancellationToken, context?: HoverContext): ProviderResult; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "Node16", 4 | "target": "ES2022", 5 | "outDir": "out", 6 | "rootDir": ".", 7 | "lib": ["ES2022"], 8 | "sourceMap": true, 9 | "strict": true, 10 | "allowJs": true 11 | }, 12 | "include": ["src", "3rd"], 13 | "exclude": ["node_modules", ".vscode-test"] 14 | } 15 | -------------------------------------------------------------------------------- /client/web/dist/3rdpartylicenses.txt: -------------------------------------------------------------------------------- 1 | @angular/animations 2 | MIT 3 | 4 | @angular/cdk 5 | MIT 6 | The MIT License 7 | 8 | Copyright (c) 2022 Google LLC. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | 28 | 29 | @angular/common 30 | MIT 31 | 32 | @angular/core 33 | MIT 34 | 35 | @angular/material 36 | MIT 37 | The MIT License 38 | 39 | Copyright (c) 2022 Google LLC. 40 | 41 | Permission is hereby granted, free of charge, to any person obtaining a copy 42 | of this software and associated documentation files (the "Software"), to deal 43 | in the Software without restriction, including without limitation the rights 44 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 45 | copies of the Software, and to permit persons to whom the Software is 46 | furnished to do so, subject to the following conditions: 47 | 48 | The above copyright notice and this permission notice shall be included in 49 | all copies or substantial portions of the Software. 50 | 51 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 52 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 53 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 54 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 55 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 56 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 57 | THE SOFTWARE. 58 | 59 | 60 | @angular/platform-browser 61 | MIT 62 | 63 | @angular/router 64 | MIT 65 | 66 | rxjs 67 | Apache-2.0 68 | Apache License 69 | Version 2.0, January 2004 70 | http://www.apache.org/licenses/ 71 | 72 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 73 | 74 | 1. Definitions. 75 | 76 | "License" shall mean the terms and conditions for use, reproduction, 77 | and distribution as defined by Sections 1 through 9 of this document. 78 | 79 | "Licensor" shall mean the copyright owner or entity authorized by 80 | the copyright owner that is granting the License. 81 | 82 | "Legal Entity" shall mean the union of the acting entity and all 83 | other entities that control, are controlled by, or are under common 84 | control with that entity. For the purposes of this definition, 85 | "control" means (i) the power, direct or indirect, to cause the 86 | direction or management of such entity, whether by contract or 87 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 88 | outstanding shares, or (iii) beneficial ownership of such entity. 89 | 90 | "You" (or "Your") shall mean an individual or Legal Entity 91 | exercising permissions granted by this License. 92 | 93 | "Source" form shall mean the preferred form for making modifications, 94 | including but not limited to software source code, documentation 95 | source, and configuration files. 96 | 97 | "Object" form shall mean any form resulting from mechanical 98 | transformation or translation of a Source form, including but 99 | not limited to compiled object code, generated documentation, 100 | and conversions to other media types. 101 | 102 | "Work" shall mean the work of authorship, whether in Source or 103 | Object form, made available under the License, as indicated by a 104 | copyright notice that is included in or attached to the work 105 | (an example is provided in the Appendix below). 106 | 107 | "Derivative Works" shall mean any work, whether in Source or Object 108 | form, that is based on (or derived from) the Work and for which the 109 | editorial revisions, annotations, elaborations, or other modifications 110 | represent, as a whole, an original work of authorship. For the purposes 111 | of this License, Derivative Works shall not include works that remain 112 | separable from, or merely link (or bind by name) to the interfaces of, 113 | the Work and Derivative Works thereof. 114 | 115 | "Contribution" shall mean any work of authorship, including 116 | the original version of the Work and any modifications or additions 117 | to that Work or Derivative Works thereof, that is intentionally 118 | submitted to Licensor for inclusion in the Work by the copyright owner 119 | or by an individual or Legal Entity authorized to submit on behalf of 120 | the copyright owner. For the purposes of this definition, "submitted" 121 | means any form of electronic, verbal, or written communication sent 122 | to the Licensor or its representatives, including but not limited to 123 | communication on electronic mailing lists, source code control systems, 124 | and issue tracking systems that are managed by, or on behalf of, the 125 | Licensor for the purpose of discussing and improving the Work, but 126 | excluding communication that is conspicuously marked or otherwise 127 | designated in writing by the copyright owner as "Not a Contribution." 128 | 129 | "Contributor" shall mean Licensor and any individual or Legal Entity 130 | on behalf of whom a Contribution has been received by Licensor and 131 | subsequently incorporated within the Work. 132 | 133 | 2. Grant of Copyright License. Subject to the terms and conditions of 134 | this License, each Contributor hereby grants to You a perpetual, 135 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 136 | copyright license to reproduce, prepare Derivative Works of, 137 | publicly display, publicly perform, sublicense, and distribute the 138 | Work and such Derivative Works in Source or Object form. 139 | 140 | 3. Grant of Patent License. Subject to the terms and conditions of 141 | this License, each Contributor hereby grants to You a perpetual, 142 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 143 | (except as stated in this section) patent license to make, have made, 144 | use, offer to sell, sell, import, and otherwise transfer the Work, 145 | where such license applies only to those patent claims licensable 146 | by such Contributor that are necessarily infringed by their 147 | Contribution(s) alone or by combination of their Contribution(s) 148 | with the Work to which such Contribution(s) was submitted. If You 149 | institute patent litigation against any entity (including a 150 | cross-claim or counterclaim in a lawsuit) alleging that the Work 151 | or a Contribution incorporated within the Work constitutes direct 152 | or contributory patent infringement, then any patent licenses 153 | granted to You under this License for that Work shall terminate 154 | as of the date such litigation is filed. 155 | 156 | 4. Redistribution. You may reproduce and distribute copies of the 157 | Work or Derivative Works thereof in any medium, with or without 158 | modifications, and in Source or Object form, provided that You 159 | meet the following conditions: 160 | 161 | (a) You must give any other recipients of the Work or 162 | Derivative Works a copy of this License; and 163 | 164 | (b) You must cause any modified files to carry prominent notices 165 | stating that You changed the files; and 166 | 167 | (c) You must retain, in the Source form of any Derivative Works 168 | that You distribute, all copyright, patent, trademark, and 169 | attribution notices from the Source form of the Work, 170 | excluding those notices that do not pertain to any part of 171 | the Derivative Works; and 172 | 173 | (d) If the Work includes a "NOTICE" text file as part of its 174 | distribution, then any Derivative Works that You distribute must 175 | include a readable copy of the attribution notices contained 176 | within such NOTICE file, excluding those notices that do not 177 | pertain to any part of the Derivative Works, in at least one 178 | of the following places: within a NOTICE text file distributed 179 | as part of the Derivative Works; within the Source form or 180 | documentation, if provided along with the Derivative Works; or, 181 | within a display generated by the Derivative Works, if and 182 | wherever such third-party notices normally appear. The contents 183 | of the NOTICE file are for informational purposes only and 184 | do not modify the License. You may add Your own attribution 185 | notices within Derivative Works that You distribute, alongside 186 | or as an addendum to the NOTICE text from the Work, provided 187 | that such additional attribution notices cannot be construed 188 | as modifying the License. 189 | 190 | You may add Your own copyright statement to Your modifications and 191 | may provide additional or different license terms and conditions 192 | for use, reproduction, or distribution of Your modifications, or 193 | for any such Derivative Works as a whole, provided Your use, 194 | reproduction, and distribution of the Work otherwise complies with 195 | the conditions stated in this License. 196 | 197 | 5. Submission of Contributions. Unless You explicitly state otherwise, 198 | any Contribution intentionally submitted for inclusion in the Work 199 | by You to the Licensor shall be under the terms and conditions of 200 | this License, without any additional terms or conditions. 201 | Notwithstanding the above, nothing herein shall supersede or modify 202 | the terms of any separate license agreement you may have executed 203 | with Licensor regarding such Contributions. 204 | 205 | 6. Trademarks. This License does not grant permission to use the trade 206 | names, trademarks, service marks, or product names of the Licensor, 207 | except as required for reasonable and customary use in describing the 208 | origin of the Work and reproducing the content of the NOTICE file. 209 | 210 | 7. Disclaimer of Warranty. Unless required by applicable law or 211 | agreed to in writing, Licensor provides the Work (and each 212 | Contributor provides its Contributions) on an "AS IS" BASIS, 213 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 214 | implied, including, without limitation, any warranties or conditions 215 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 216 | PARTICULAR PURPOSE. You are solely responsible for determining the 217 | appropriateness of using or redistributing the Work and assume any 218 | risks associated with Your exercise of permissions under this License. 219 | 220 | 8. Limitation of Liability. In no event and under no legal theory, 221 | whether in tort (including negligence), contract, or otherwise, 222 | unless required by applicable law (such as deliberate and grossly 223 | negligent acts) or agreed to in writing, shall any Contributor be 224 | liable to You for damages, including any direct, indirect, special, 225 | incidental, or consequential damages of any character arising as a 226 | result of this License or out of the use or inability to use the 227 | Work (including but not limited to damages for loss of goodwill, 228 | work stoppage, computer failure or malfunction, or any and all 229 | other commercial damages or losses), even if such Contributor 230 | has been advised of the possibility of such damages. 231 | 232 | 9. Accepting Warranty or Additional Liability. While redistributing 233 | the Work or Derivative Works thereof, You may choose to offer, 234 | and charge a fee for, acceptance of support, warranty, indemnity, 235 | or other liability obligations and/or rights consistent with this 236 | License. However, in accepting such obligations, You may act only 237 | on Your own behalf and on Your sole responsibility, not on behalf 238 | of any other Contributor, and only if You agree to indemnify, 239 | defend, and hold each Contributor harmless for any liability 240 | incurred by, or claims asserted against, such Contributor by reason 241 | of your accepting any such warranty or additional liability. 242 | 243 | END OF TERMS AND CONDITIONS 244 | 245 | APPENDIX: How to apply the Apache License to your work. 246 | 247 | To apply the Apache License to your work, attach the following 248 | boilerplate notice, with the fields enclosed by brackets "[]" 249 | replaced with your own identifying information. (Don't include 250 | the brackets!) The text should be enclosed in the appropriate 251 | comment syntax for the file format. We also recommend that a 252 | file or class name and description of purpose be included on the 253 | same "printed page" as the copyright notice for easier 254 | identification within third-party archives. 255 | 256 | Copyright (c) 2015-2018 Google, Inc., Netflix, Inc., Microsoft Corp. and contributors 257 | 258 | Licensed under the Apache License, Version 2.0 (the "License"); 259 | you may not use this file except in compliance with the License. 260 | You may obtain a copy of the License at 261 | 262 | http://www.apache.org/licenses/LICENSE-2.0 263 | 264 | Unless required by applicable law or agreed to in writing, software 265 | distributed under the License is distributed on an "AS IS" BASIS, 266 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 267 | See the License for the specific language governing permissions and 268 | limitations under the License. 269 | 270 | 271 | 272 | tslib 273 | 0BSD 274 | Copyright (c) Microsoft Corporation. 275 | 276 | Permission to use, copy, modify, and/or distribute this software for any 277 | purpose with or without fee is hereby granted. 278 | 279 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 280 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 281 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 282 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 283 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 284 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 285 | PERFORMANCE OF THIS SOFTWARE. 286 | 287 | zone.js 288 | MIT 289 | The MIT License 290 | 291 | Copyright (c) 2010-2022 Google LLC. https://angular.io/license 292 | 293 | Permission is hereby granted, free of charge, to any person obtaining a copy 294 | of this software and associated documentation files (the "Software"), to deal 295 | in the Software without restriction, including without limitation the rights 296 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 297 | copies of the Software, and to permit persons to whom the Software is 298 | furnished to do so, subject to the following conditions: 299 | 300 | The above copyright notice and this permission notice shall be included in 301 | all copies or substantial portions of the Software. 302 | 303 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 304 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 305 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 306 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 307 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 308 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 309 | THE SOFTWARE. 310 | -------------------------------------------------------------------------------- /client/web/dist/assets/fonts/mat-icon-font.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuaLS/vscode-lua/5dc483420c6a8f568a7e714f0e5a1f9f9f671fba/client/web/dist/assets/fonts/mat-icon-font.woff2 -------------------------------------------------------------------------------- /client/web/dist/assets/scss/mat-icon.scss: -------------------------------------------------------------------------------- 1 | /* fallback */ 2 | @font-face { 3 | font-family: 'Material Icons'; 4 | font-style: normal; 5 | font-weight: 400; 6 | src: url('../fonts/mat-icon-font.woff2') format('woff2'); 7 | } 8 | 9 | .material-icons { 10 | font-family: 'Material Icons'; 11 | font-weight: normal; 12 | font-style: normal; 13 | font-size: 24px; 14 | line-height: 1; 15 | letter-spacing: normal; 16 | text-transform: none; 17 | display: inline-block; 18 | white-space: nowrap; 19 | word-wrap: normal; 20 | direction: ltr; 21 | -webkit-font-smoothing: antialiased; 22 | } 23 | -------------------------------------------------------------------------------- /client/web/dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuaLS/vscode-lua/5dc483420c6a8f568a7e714f0e5a1f9f9f671fba/client/web/dist/favicon.ico -------------------------------------------------------------------------------- /client/web/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | PsiViewer 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /client/web/dist/mat-icon-font.d36bf6bfd46ff3bb.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LuaLS/vscode-lua/5dc483420c6a8f568a7e714f0e5a1f9f9f671fba/client/web/dist/mat-icon-font.d36bf6bfd46ff3bb.woff2 -------------------------------------------------------------------------------- /client/web/dist/runtime.16fa3418f03cd751.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";var e,i={},_={};function n(e){var a=_[e];if(void 0!==a)return a.exports;var r=_[e]={exports:{}};return i[e](r,r.exports,n),r.exports}n.m=i,e=[],n.O=(a,r,u,l)=>{if(!r){var s=1/0;for(f=0;f=l)&&Object.keys(n.O).every(h=>n.O[h](r[t]))?r.splice(t--,1):(o=!1,l0&&e[f-1][2]>l;f--)e[f]=e[f-1];e[f]=[r,u,l]},n.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return n.d(a,{a}),a},n.d=(e,a)=>{for(var r in a)n.o(a,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:a[r]})},n.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),(()=>{var e={666:0};n.O.j=u=>0===e[u];var a=(u,l)=>{var t,c,[f,s,o]=l,v=0;if(f.some(d=>0!==e[d])){for(t in s)n.o(s,t)&&(n.m[t]=s[t]);if(o)var b=o(n)}for(u&&u(l);v