├── .github ├── actions │ └── setup-rust-env │ │ └── action.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── README.md ├── lua └── magento2_ls.lua ├── src ├── js.rs ├── lsp.rs ├── lsp │ ├── completion.rs │ ├── completion │ │ └── events.rs │ ├── definition.rs │ └── definition │ │ ├── component.rs │ │ ├── php.rs │ │ └── phtml.rs ├── m2.rs ├── main.rs ├── php.rs ├── queries.rs ├── state.rs ├── ts.rs └── xml.rs ├── stylua.toml ├── tests ├── app │ └── code │ │ └── Some │ │ └── Module │ │ ├── Test.php │ │ └── registration.php └── test.xml └── vscode ├── .gitignore ├── .vscodeignore ├── LICENSE ├── extension.js ├── jsconfig.json ├── logo.png ├── package-lock.json ├── package.json └── server └── .gitkeep /.github/actions/setup-rust-env/action.yml: -------------------------------------------------------------------------------- 1 | name: "Setup Rust Environment" 2 | 3 | description: "Setup the Rust CI environment for GitHub Action runners" 4 | 5 | runs: 6 | using: composite 7 | steps: 8 | - name: Setup Rust toolchain 9 | uses: dtolnay/rust-toolchain@master 10 | with: 11 | toolchain: stable 12 | targets: > 13 | x86_64-unknown-linux-gnu, 14 | x86_64-pc-windows-msvc, 15 | aarch64-pc-windows-msvc, 16 | x86_64-apple-darwin, 17 | aarch64-apple-darwin 18 | components: rustfmt, clippy 19 | 20 | - name: Setup rust cache 21 | uses: Swatinem/rust-cache@v2 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build & Release 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | build: 12 | name: Build 13 | runs-on: ${{ matrix.build.os }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | build: 18 | - { 19 | NAME: linux-x64, 20 | OS: ubuntu-22.04, 21 | TARGET: x86_64-unknown-linux-gnu, 22 | CODE_TARGET: linux-x64, 23 | } 24 | - { 25 | NAME: windows-x64, 26 | OS: windows-2022, 27 | TARGET: x86_64-pc-windows-msvc, 28 | CODE_TARGET: win32-x64 29 | } 30 | - { 31 | NAME: windows-arm64, 32 | OS: windows-2022, 33 | TARGET: aarch64-pc-windows-msvc, 34 | CODE_TARGET: win32-arm64, 35 | } 36 | - { 37 | NAME: darwin-x64, 38 | OS: macos-12, 39 | TARGET: x86_64-apple-darwin, 40 | CODE_TARGET: darwin-x64, 41 | } 42 | - { 43 | NAME: darwin-arm64, 44 | OS: macos-12, 45 | TARGET: aarch64-apple-darwin, 46 | CODE_TARGET: darwin-arm64, 47 | } 48 | steps: 49 | - name: Checkout code 50 | uses: actions/checkout@v3 51 | 52 | - name: Setup Rust env 53 | uses: "./.github/actions/setup-rust-env" 54 | 55 | - name: Build 56 | run: cargo build --release --locked --target ${{ matrix.build.TARGET }} 57 | 58 | - name: Rename magento2-ls binary 59 | shell: bash 60 | run: | 61 | binary_name="magento2-ls" 62 | 63 | extension="" 64 | # windows binaries have ".exe" extension 65 | if [[ "${{ matrix.build.OS }}" == *"windows"* ]]; then 66 | extension=".exe" 67 | fi 68 | 69 | mkdir -p dist 70 | cp "target/${{ matrix.build.TARGET }}/release/${binary_name}" "dist/${binary_name}-${{ matrix.build.NAME }}${extension}" 71 | 72 | - name: Check if release should be created 73 | shell: bash 74 | run: | 75 | if [[ $(git log -1 --pretty=%B) =~ ^release: ]]; then 76 | echo "SHOULD_RELEASE=yes" >> $GITHUB_ENV 77 | else 78 | echo "SHOULD_RELEASE=no" >> $GITHUB_ENV 79 | fi 80 | 81 | - name: Build vscode extension 82 | shell: bash 83 | run: | 84 | version=$(awk -F ' = ' '$1 ~ /version/ { gsub(/["]/, "", $2); printf("%s",$2) }' Cargo.toml) 85 | binary_name="magento2-ls" 86 | 87 | extension="" 88 | # windows binaries have ".exe" extension 89 | if [[ "${{ matrix.build.OS }}" == *"windows"* ]]; then 90 | extension=".exe" 91 | fi 92 | 93 | mkdir -p dist 94 | cp "target/${{ matrix.build.TARGET }}/release/${binary_name}" "vscode/server/${binary_name}${extension}" 95 | cp "README.md" "vscode/" 96 | cp "CHANGELOG.md" "vscode/" 97 | cd "vscode" 98 | npm install --include=dev 99 | if [[ "${{ env.SHOULD_RELEASE }}" == "yes" ]]; then 100 | npx vsce package --target ${{ matrix.build.CODE_TARGET }} -o ../dist/magento2-ls.${version}.${{ matrix.build.CODE_TARGET }}.vsix 101 | else 102 | npx vsce package --target ${{ matrix.build.CODE_TARGET }} -o ../dist/magento2-ls.${version}.${{ matrix.build.CODE_TARGET }}.vsix --pre-release 103 | fi 104 | 105 | - name: Upload dist 106 | uses: actions/upload-artifact@v3 107 | with: 108 | name: built-dist 109 | path: dist/* 110 | 111 | release: 112 | name: Release 113 | runs-on: ubuntu-22.04 114 | needs: build 115 | if: github.ref == 'refs/heads/master' 116 | steps: 117 | - name: Checkout code 118 | uses: actions/checkout@v3 119 | with: 120 | fetch-depth: 2 121 | 122 | - name: Download dist 123 | uses: actions/download-artifact@v3 124 | with: 125 | name: built-dist 126 | path: dist 127 | 128 | - name: Check if release should be created 129 | shell: bash 130 | run: | 131 | RELEASE_VERSION=$(awk -F ' = ' '$1 ~ /version/ { gsub(/["]/, "", $2); printf("%s",$2) }' Cargo.toml) 132 | if [[ $(git log -1 --pretty=%B) =~ ^release: ]]; then 133 | echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV 134 | git tag "$RELEASE_VERSION" 135 | git push -u origin "$RELEASE_VERSION" 136 | echo "SHOULD_RELEASE=yes" >> $GITHUB_ENV 137 | else 138 | git tag --force "latest@dev" 139 | git push --force -u origin "latest@dev" 140 | echo "SHOULD_RELEASE=no" >> $GITHUB_ENV 141 | fi 142 | 143 | - name: Publish release 144 | uses: softprops/action-gh-release@v1 145 | if: env.SHOULD_RELEASE == 'yes' 146 | with: 147 | files: dist/* 148 | tag_name: ${{ env.RELEASE_VERSION }} 149 | fail_on_unmatched_files: true 150 | generate_release_notes: true 151 | env: 152 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 153 | 154 | - name: Publish latest development 155 | uses: softprops/action-gh-release@v1 156 | if: env.SHOULD_RELEASE == 'no' 157 | with: 158 | files: dist/* 159 | tag_name: latest@dev 160 | fail_on_unmatched_files: true 161 | generate_release_notes: false 162 | prerelease: true 163 | env: 164 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 165 | 166 | - name: Publish Extension (Code Marketplace, release) 167 | if: env.SHOULD_RELEASE == 'yes' 168 | shell: bash 169 | run: | 170 | cd "vscode" 171 | npm install --include=dev 172 | npx vsce publish --pat ${{ secrets.MARKETPLACE_TOKEN }} --packagePath ../dist/magento2-ls.*.vsix 173 | 174 | - name: Publish Extension (OpenVSX, release) 175 | if: env.SHOULD_RELEASE == 'yes' 176 | shell: bash 177 | run: | 178 | cd "vscode" 179 | npm install --include=dev 180 | npx ovsx publish --pat ${{ secrets.OPENVSX_TOKEN }} --packagePath ../dist/magento2-ls.*.vsix 181 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ${{ matrix.build.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | build: 19 | - { 20 | NAME: linux-x64, 21 | OS: ubuntu-22.04, 22 | TARGET: x86_64-unknown-linux-gnu, 23 | } 24 | - { 25 | NAME: windows-x64, 26 | OS: windows-2022, 27 | TARGET: x86_64-pc-windows-msvc, 28 | } 29 | - { 30 | NAME: darwin-x64, 31 | OS: macos-12, 32 | TARGET: x86_64-apple-darwin, 33 | } 34 | 35 | steps: 36 | - name: Checkout code 37 | uses: actions/checkout@v3 38 | 39 | - name: Setup Rust env 40 | uses: "./.github/actions/setup-rust-env" 41 | 42 | - name: Build 43 | run: cargo build --verbose --target ${{ matrix.build.TARGET }} 44 | 45 | - name: Run tests 46 | run: cargo test --verbose --target ${{ matrix.build.TARGET }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | out/ 3 | 4 | 5 | # Added by cargo 6 | 7 | /target 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.0", 3 | "configurations": [ 4 | { 5 | "type": "extensionHost", 6 | "request": "launch", 7 | "name": "Debug LSP Extension", 8 | "runtimeExecutable": "${execPath}", 9 | "args": [ 10 | "--extensionDevelopmentPath=${workspaceRoot}/vscode", 11 | "--disable-extensions", 12 | "${workspaceRoot}/tests/test.xml" 13 | ], 14 | "preLaunchTask": "Build Server and Extension" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.3.0", 3 | "tasks": [ 4 | { 5 | "label": "Install Dependencies", 6 | "group": "build", 7 | "type": "npm", 8 | "script": "install", 9 | "path": "vscode/" 10 | }, 11 | { 12 | "label": "Build Server", 13 | "group": "build", 14 | "type": "shell", 15 | "command": "cargo build" 16 | }, 17 | { 18 | "label": "Build Server and Extension", 19 | "dependsOn": ["Build Server", "Install Dependencies"] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.0.7 - December 26, 2023] 4 | 5 | #### Features 6 | 7 | - Added templates from theme modules to template completions. 8 | - Added go to template (html) file from JS files. 9 | - Changed go to definition to work for any JS string. 10 | 11 | ## [0.0.6 - October 13, 2023] 12 | 13 | #### Features 14 | 15 | - Added mixins to the definition locations of JS components. 16 | - Added state updates when changes occur in `registration.php` and `requirejs-config.js`. 17 | - Increased indexing speed by searching only specific locations. 18 | - Added suggestions for library modules, such as `magento-framework`. 19 | 20 | ## [0.0.5 - October 4, 2023] 21 | 22 | #### Features 23 | 24 | - Added completion for PHP classes in XML files. 25 | - Added completion for PHP templates in XML files. 26 | - Added completion for JS components in XML and JS files. 27 | - Added completion for Magento events in XML files. 28 | 29 | ## [0.0.4 - September 25, 2023] 30 | 31 | #### Features 32 | 33 | - Added go to definition support for JS files. 34 | - Added support for workspaces. 35 | - Indexed files in separate threads to speed up startup. 36 | 37 | #### Features 38 | 39 | - Added support for VS Code. 40 | 41 | ## [0.0.3 - September 25, 2023] 42 | 43 | #### Features 44 | 45 | - Added support for VS Code. 46 | 47 | ## [0.0.2 - September 24, 2023] 48 | 49 | #### Features 50 | 51 | - Added download binary option for Neovim plugin. 52 | - Added support for Windows. 53 | 54 | ## [0.0.1 - September 23, 2023] 55 | 56 | #### Features 57 | 58 | - Go to PHP class definition from XML file. 59 | - Go to PHP constants definition from XML file. 60 | - Go to PHP method definition from XML file. 61 | - Go to template (phtml) file from XML file. 62 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.0.5" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.75" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 19 | 20 | [[package]] 21 | name = "autocfg" 22 | version = "1.1.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 25 | 26 | [[package]] 27 | name = "bincode" 28 | version = "1.3.3" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 31 | dependencies = [ 32 | "serde", 33 | ] 34 | 35 | [[package]] 36 | name = "bitflags" 37 | version = "1.3.2" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 40 | 41 | [[package]] 42 | name = "cc" 43 | version = "1.0.83" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 46 | dependencies = [ 47 | "jobserver", 48 | "libc", 49 | ] 50 | 51 | [[package]] 52 | name = "cfg-if" 53 | version = "1.0.0" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 56 | 57 | [[package]] 58 | name = "convert_case" 59 | version = "0.6.0" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" 62 | dependencies = [ 63 | "unicode-segmentation", 64 | ] 65 | 66 | [[package]] 67 | name = "crossbeam-channel" 68 | version = "0.5.8" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" 71 | dependencies = [ 72 | "cfg-if", 73 | "crossbeam-utils", 74 | ] 75 | 76 | [[package]] 77 | name = "crossbeam-utils" 78 | version = "0.8.16" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" 81 | dependencies = [ 82 | "cfg-if", 83 | ] 84 | 85 | [[package]] 86 | name = "form_urlencoded" 87 | version = "1.2.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" 90 | dependencies = [ 91 | "percent-encoding", 92 | ] 93 | 94 | [[package]] 95 | name = "glob" 96 | version = "0.3.1" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 99 | 100 | [[package]] 101 | name = "idna" 102 | version = "0.4.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" 105 | dependencies = [ 106 | "unicode-bidi", 107 | "unicode-normalization", 108 | ] 109 | 110 | [[package]] 111 | name = "itoa" 112 | version = "1.0.9" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 115 | 116 | [[package]] 117 | name = "jobserver" 118 | version = "0.1.26" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" 121 | dependencies = [ 122 | "libc", 123 | ] 124 | 125 | [[package]] 126 | name = "libc" 127 | version = "0.2.148" 128 | source = "registry+https://github.com/rust-lang/crates.io-index" 129 | checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" 130 | 131 | [[package]] 132 | name = "lock_api" 133 | version = "0.4.10" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" 136 | dependencies = [ 137 | "autocfg", 138 | "scopeguard", 139 | ] 140 | 141 | [[package]] 142 | name = "log" 143 | version = "0.4.20" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 146 | 147 | [[package]] 148 | name = "lsp-server" 149 | version = "0.7.4" 150 | source = "registry+https://github.com/rust-lang/crates.io-index" 151 | checksum = "b52dccdf3302eefab8c8a1273047f0a3c3dca4b527c8458d00c09484c8371928" 152 | dependencies = [ 153 | "crossbeam-channel", 154 | "log", 155 | "serde", 156 | "serde_json", 157 | ] 158 | 159 | [[package]] 160 | name = "lsp-types" 161 | version = "0.94.1" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" 164 | dependencies = [ 165 | "bitflags", 166 | "serde", 167 | "serde_json", 168 | "serde_repr", 169 | "url", 170 | ] 171 | 172 | [[package]] 173 | name = "magento2-ls" 174 | version = "0.0.7" 175 | dependencies = [ 176 | "anyhow", 177 | "bincode", 178 | "convert_case", 179 | "glob", 180 | "lsp-server", 181 | "lsp-types", 182 | "parking_lot", 183 | "serde", 184 | "serde_json", 185 | "tree-sitter", 186 | "tree-sitter-parsers", 187 | ] 188 | 189 | [[package]] 190 | name = "memchr" 191 | version = "2.6.3" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" 194 | 195 | [[package]] 196 | name = "parking_lot" 197 | version = "0.12.1" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 200 | dependencies = [ 201 | "lock_api", 202 | "parking_lot_core", 203 | ] 204 | 205 | [[package]] 206 | name = "parking_lot_core" 207 | version = "0.9.8" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" 210 | dependencies = [ 211 | "cfg-if", 212 | "libc", 213 | "redox_syscall", 214 | "smallvec", 215 | "windows-targets", 216 | ] 217 | 218 | [[package]] 219 | name = "percent-encoding" 220 | version = "2.3.0" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" 223 | 224 | [[package]] 225 | name = "proc-macro2" 226 | version = "1.0.67" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" 229 | dependencies = [ 230 | "unicode-ident", 231 | ] 232 | 233 | [[package]] 234 | name = "quote" 235 | version = "1.0.33" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 238 | dependencies = [ 239 | "proc-macro2", 240 | ] 241 | 242 | [[package]] 243 | name = "redox_syscall" 244 | version = "0.3.5" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 247 | dependencies = [ 248 | "bitflags", 249 | ] 250 | 251 | [[package]] 252 | name = "regex" 253 | version = "1.9.5" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" 256 | dependencies = [ 257 | "aho-corasick", 258 | "memchr", 259 | "regex-automata", 260 | "regex-syntax", 261 | ] 262 | 263 | [[package]] 264 | name = "regex-automata" 265 | version = "0.3.8" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" 268 | dependencies = [ 269 | "aho-corasick", 270 | "memchr", 271 | "regex-syntax", 272 | ] 273 | 274 | [[package]] 275 | name = "regex-syntax" 276 | version = "0.7.5" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" 279 | 280 | [[package]] 281 | name = "ryu" 282 | version = "1.0.15" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 285 | 286 | [[package]] 287 | name = "scopeguard" 288 | version = "1.2.0" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 291 | 292 | [[package]] 293 | name = "serde" 294 | version = "1.0.188" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" 297 | dependencies = [ 298 | "serde_derive", 299 | ] 300 | 301 | [[package]] 302 | name = "serde_derive" 303 | version = "1.0.188" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" 306 | dependencies = [ 307 | "proc-macro2", 308 | "quote", 309 | "syn", 310 | ] 311 | 312 | [[package]] 313 | name = "serde_json" 314 | version = "1.0.107" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" 317 | dependencies = [ 318 | "itoa", 319 | "ryu", 320 | "serde", 321 | ] 322 | 323 | [[package]] 324 | name = "serde_repr" 325 | version = "0.1.16" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" 328 | dependencies = [ 329 | "proc-macro2", 330 | "quote", 331 | "syn", 332 | ] 333 | 334 | [[package]] 335 | name = "smallvec" 336 | version = "1.11.1" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" 339 | 340 | [[package]] 341 | name = "syn" 342 | version = "2.0.33" 343 | source = "registry+https://github.com/rust-lang/crates.io-index" 344 | checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668" 345 | dependencies = [ 346 | "proc-macro2", 347 | "quote", 348 | "unicode-ident", 349 | ] 350 | 351 | [[package]] 352 | name = "tinyvec" 353 | version = "1.6.0" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 356 | dependencies = [ 357 | "tinyvec_macros", 358 | ] 359 | 360 | [[package]] 361 | name = "tinyvec_macros" 362 | version = "0.1.1" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 365 | 366 | [[package]] 367 | name = "tree-sitter" 368 | version = "0.20.10" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "e747b1f9b7b931ed39a548c1fae149101497de3c1fc8d9e18c62c1a66c683d3d" 371 | dependencies = [ 372 | "cc", 373 | "regex", 374 | ] 375 | 376 | [[package]] 377 | name = "tree-sitter-parsers" 378 | version = "0.0.5" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "0c15d2f5d9d5e4a7579210e5b57d1e125c7c89325ebe35729b03a28656a705c4" 381 | dependencies = [ 382 | "cc", 383 | "tree-sitter", 384 | ] 385 | 386 | [[package]] 387 | name = "unicode-bidi" 388 | version = "0.3.13" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 391 | 392 | [[package]] 393 | name = "unicode-ident" 394 | version = "1.0.12" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 397 | 398 | [[package]] 399 | name = "unicode-normalization" 400 | version = "0.1.22" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 403 | dependencies = [ 404 | "tinyvec", 405 | ] 406 | 407 | [[package]] 408 | name = "unicode-segmentation" 409 | version = "1.10.1" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 412 | 413 | [[package]] 414 | name = "url" 415 | version = "2.4.1" 416 | source = "registry+https://github.com/rust-lang/crates.io-index" 417 | checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" 418 | dependencies = [ 419 | "form_urlencoded", 420 | "idna", 421 | "percent-encoding", 422 | "serde", 423 | ] 424 | 425 | [[package]] 426 | name = "windows-targets" 427 | version = "0.48.5" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 430 | dependencies = [ 431 | "windows_aarch64_gnullvm", 432 | "windows_aarch64_msvc", 433 | "windows_i686_gnu", 434 | "windows_i686_msvc", 435 | "windows_x86_64_gnu", 436 | "windows_x86_64_gnullvm", 437 | "windows_x86_64_msvc", 438 | ] 439 | 440 | [[package]] 441 | name = "windows_aarch64_gnullvm" 442 | version = "0.48.5" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 445 | 446 | [[package]] 447 | name = "windows_aarch64_msvc" 448 | version = "0.48.5" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 451 | 452 | [[package]] 453 | name = "windows_i686_gnu" 454 | version = "0.48.5" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 457 | 458 | [[package]] 459 | name = "windows_i686_msvc" 460 | version = "0.48.5" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 463 | 464 | [[package]] 465 | name = "windows_x86_64_gnu" 466 | version = "0.48.5" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 469 | 470 | [[package]] 471 | name = "windows_x86_64_gnullvm" 472 | version = "0.48.5" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 475 | 476 | [[package]] 477 | name = "windows_x86_64_msvc" 478 | version = "0.48.5" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 481 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "magento2-ls" 3 | version = "0.0.7" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | anyhow = "1.0.75" 10 | bincode = "1.3.3" 11 | convert_case = "0.6.0" 12 | glob = "0.3.1" 13 | lsp-server = "0.7.4" 14 | lsp-types = "0.94.1" 15 | parking_lot = { version = "0.12.1", features = ["arc_lock"] } 16 | serde = { version = "1.0.188", features = ["derive"] } 17 | serde_json = "1.0.107" 18 | tree-sitter = "0.20.10" 19 | tree-sitter-parsers = "0.0.5" 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magento 2 Language Server 2 | 3 | ## Overview 4 | 5 | The Magento 2 Language Server is a tool that serves as a connection between Magento 2 XML, JS and PHP files, with the goal of enabling easier navigation between these files. 6 | 7 | Please note that the current version of the language server is considered to be of alpha quality. Although it is functional and can be used, it has limited functionality and may encounter issues. It has been tested on Linux and should also work on MacOS and Windows. 8 | 9 | ## Features 10 | 11 | ![go-to-definition](https://github.com/pbogut/magento2-ls/assets/1702152/20f556a8-5024-4a1b-befd-26ef1ded6000) 12 | 13 | - Go to the definition from XML files: 14 | - Go to the class (from ``, ``, ``, etc.) 15 | - Go to the constant (from ``) 16 | - Go to the method (from ``, ``) 17 | - Go to the template file (from ``, ``, etc.) 18 | - Go to the JavaScript component file (from ``) 19 | - Go to the definition from JS files: 20 | - Go to the JavaScript component file (from `define()` argument list) 21 | 22 | ![code-completion](https://github.com/pbogut/magento2-ls/assets/1702152/6341cf9e-2241-40c2-b374-e45d7026e1bc) 23 | 24 | - Completion of various Magento entities: 25 | - Template suggestions inside `template=""` attributes. 26 | - Template suggestions inside tags with `xsi:type="string"` and `name=template` attributes. 27 | - Event names inside `` attribute (static list of built-in events). 28 | - PHP Class suggestions in ``, ``, `class`, and `instance` attributes. 29 | - PHP Class suggestions in tags with `xsi:type="object"` attribute. 30 | - PHP Class suggestions in ``, ``, and `` tags. 31 | - JS Component suggestions in tags with `xsi:type="string"` and `name="component"` attributes. 32 | - JS Component suggestions in the argument list of the `define()` function in JavaScript files. 33 | 34 | ## Installation 35 | 36 | ### Neovim (with Packer) 37 | 38 | Please add the following lines to your init.lua file if you are using Packer as your plugin manager. 39 | 40 | ```lua 41 | use({ 'pbogut/magento2-ls', 42 | -- Build using cargo build --release 43 | run = "require'magento2_ls'.build()" , 44 | -- Alternatively, you can download the compiled binary from the GitHub release. 45 | -- run = "require'magento2_ls'.get_server()" , 46 | config = "require'magento2_ls'.setup()" 47 | }) 48 | ``` 49 | 50 | The command `require('magento2_ls').setup()` will register the language server with Neovim's built-in Language Server Protocol (LSP) using the function `vim.lsp.start()`. If you need to rebuild the language server for any reason, you can do it by using: 51 | 52 | ```lua 53 | require('magento2_ls').build() 54 | ``` 55 | 56 | Alternatively, you can download the compiled binary for your system by using: 57 | 58 | ```lua 59 | require('magento2_ls').get_server() 60 | ``` 61 | 62 | ### Visual Studio Code 63 | 64 | You can download the `vsix` file from the [GitHub Releases](https://github.com/pbogut/magento2-ls/releases) page. 65 | 66 | ### Non goals 67 | 68 | Be PHP Language Server (or XML LS) in any capacity. 69 | [Intelephense](https://intelephense.com/) works nice with Magento 2 if you need 70 | LS for your project. 71 | 72 | ## Requirements 73 | 74 | In order to complete Magento classes, the Magento root folder must be opened in the workspace. 75 | 76 | The language server detects Magento modules by searching for `registration.php` files in the following locations: 77 | 78 | - The root folder (for modules added to the workspace) 79 | - `app/code/*/*/` - for local modules 80 | - `vendor/*/*/` - for vendor modules 81 | - `app/design/*/*/*/` - for themes. 82 | 83 | 84 | ## Contributing 85 | 86 | If you would like to contribute, please feel free to submit pull requests or open issues on the [GitHub repository](https://github.com/pbogut/magento2-ls). 87 | 88 | ## License 89 | 90 | The Magento 2 Language Server is released under the MIT License. 91 | The software is provided "as is", without warranty of any kind. 92 | -------------------------------------------------------------------------------- /lua/magento2_ls.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local function script_path(append) 4 | append = append or '' 5 | local str = debug.getinfo(1, 'S').source:sub(2) 6 | str = str:match('(.*[/\\])') or './' 7 | return str .. append 8 | end 9 | 10 | local clientId = nil 11 | 12 | local destination = script_path('../target/release') 13 | 14 | local function start_lsp(opts) 15 | if clientId ~= nil then 16 | vim.lsp.buf_attach_client(0, clientId) 17 | else 18 | clientId = vim.lsp.start(opts) 19 | end 20 | end 21 | 22 | ---@return string|nil 23 | local function get_machine() 24 | local machine = vim.loop.os_uname().machine 25 | if machine == 'x86_64' then 26 | return 'x64' 27 | elseif machine == 'aarch64' or machine == 'arm64' then 28 | return 'arm64' 29 | end 30 | end 31 | 32 | ---@return string|nil 33 | local function get_system() 34 | local os = vim.loop.os_uname().sysname 35 | if os == 'Linux' then 36 | return 'linux' 37 | elseif os == 'Darwin' then 38 | return 'darwin' 39 | elseif os == 'Windows' then 40 | return 'windows' 41 | end 42 | end 43 | 44 | ---@return string|nil 45 | local function get_package() 46 | local system = get_system() 47 | local machine = get_machine() 48 | if system and machine then 49 | return system .. '-' .. machine 50 | end 51 | end 52 | 53 | ---@return string 54 | local function get_version() 55 | local file = io.open(script_path('../Cargo.toml'), 'r') 56 | if file ~= nil then 57 | for line in file:lines() do 58 | if line:match('^version = "(.*)"$') then 59 | local version = line:match('^version = "(.*)"$') 60 | return version 61 | end 62 | end 63 | end 64 | 65 | return '0.0.0' 66 | end 67 | 68 | ---@return string|nil 69 | local function get_bin_name() 70 | local package = get_package() 71 | if not package then 72 | return nil 73 | end 74 | if get_system() == 'windows' then 75 | return 'magento2-ls-' .. package .. '.exe' 76 | else 77 | return 'magento2-ls-' .. package 78 | end 79 | end 80 | 81 | ---@param bin_name string 82 | ---@return string 83 | local function get_bin_url(bin_name) 84 | local base_url = 'https://github.com/pbogut/magento2-ls/releases/download/' .. get_version() 85 | return base_url .. '/' .. bin_name 86 | end 87 | 88 | ---@param bin_url string 89 | local function download_server(bin_url) 90 | local bin = destination .. '/magento2-ls' 91 | if get_system() == 'windows' then 92 | bin = bin .. '.exe' 93 | end 94 | vim.fn.mkdir(destination, 'p') 95 | local cmd = 'curl -L -o ' .. bin .. ' ' .. bin_url 96 | vim.fn.jobstart(cmd, { 97 | on_exit = function(_, code) 98 | if code == 0 then 99 | vim.notify('Server download successful', vim.log.levels.INFO, { title = 'magento2-ls' }) 100 | if get_system() ~= 'windows' then 101 | vim.fn.system('chmod +x ' .. bin) 102 | end 103 | else 104 | vim.notify('Server download failed', vim.log.levels.ERROR, { title = 'magento2-ls' }) 105 | end 106 | end, 107 | }) 108 | end 109 | 110 | M.setup = function(opts) 111 | opts = opts or {} 112 | opts = vim.tbl_deep_extend('keep', opts, { 113 | filetypes = { 'xml', 'javascript' }, 114 | name = 'magento2-ls', 115 | cmd = { script_path('../target/release/magento2-ls') .. (get_system() == 'windows' and '.exe' or '') }, 116 | root_dir = vim.fn.getcwd(), 117 | }) 118 | 119 | for _, ft in ipairs(opts.filetypes) do 120 | if ft == vim.o.filetype then 121 | start_lsp(opts) 122 | end 123 | end 124 | 125 | local augroup = vim.api.nvim_create_augroup('magento2_ls', { clear = true }) 126 | local pattern = table.concat(opts.filetypes, ',') 127 | 128 | vim.api.nvim_create_autocmd('FileType', { 129 | group = augroup, 130 | pattern = pattern, 131 | callback = function() 132 | start_lsp(opts) 133 | end, 134 | }) 135 | end 136 | 137 | M.get_server = function() 138 | local bin_name = get_bin_name() 139 | local bin_url = '' 140 | if bin_name then 141 | bin_url = get_bin_url(bin_name) 142 | download_server(bin_url) 143 | else 144 | vim.ui.select({ 145 | 'magento2-ls-darwin-arm64', 146 | 'magento2-ls-darwin-x64', 147 | 'magento2-ls-linux-x64', 148 | 'magento2-ls-windows-arm64.exe', 149 | 'magento2-ls-windows-x64.exe', 150 | }, { 151 | prompt = 'Select package for your system', 152 | }, function(selected) 153 | if selected then 154 | bin_url = get_bin_url(selected) 155 | download_server(bin_url) 156 | end 157 | end) 158 | end 159 | end 160 | 161 | M.build = function() 162 | local cmd = 'cd ' .. vim.fn.shellescape(script_path('..')) .. ' && cargo build --release' 163 | vim.fn.jobstart(cmd, { 164 | on_exit = function(_, code) 165 | if code == 0 then 166 | vim.notify('Build successful', vim.log.levels.INFO, { title = 'magento2-ls' }) 167 | else 168 | vim.notify('Build failed', vim.log.levels.ERROR, { title = 'magento2-ls' }) 169 | end 170 | end, 171 | }) 172 | end 173 | 174 | return M 175 | -------------------------------------------------------------------------------- /src/js.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use glob::glob; 4 | use lsp_types::{Position, Range}; 5 | use tree_sitter::{Node, QueryCursor}; 6 | 7 | use crate::{ 8 | m2::{M2Area, M2Item, M2Path}, 9 | queries, 10 | state::{ArcState, State}, 11 | ts::{self, node_at_position}, 12 | }; 13 | 14 | enum JSTypes { 15 | Map, 16 | Paths, 17 | Mixins, 18 | } 19 | 20 | #[allow(clippy::module_name_repetitions)] 21 | #[derive(Debug, Clone, PartialEq, Eq)] 22 | pub enum JsCompletionType { 23 | Definition, 24 | } 25 | 26 | #[allow(clippy::module_name_repetitions)] 27 | #[derive(Debug, Clone, PartialEq, Eq)] 28 | pub struct JsCompletion { 29 | pub text: String, 30 | pub range: Range, 31 | pub kind: JsCompletionType, 32 | } 33 | 34 | pub fn update_index(state: &ArcState, path: &PathBuf) { 35 | // if current workspace is magento module 36 | process_glob(state, &path.append(&["view", "*", "requirejs-config.js"])); 37 | // if current workspace is magento installation 38 | process_glob( 39 | state, 40 | &path.append(&["vendor", "*", "*", "view", "*", "requirejs-config.js"]), 41 | ); 42 | process_glob( 43 | state, 44 | &path.append(&["vendor", "*", "*", "Magento_Theme", "requirejs-config.js"]), 45 | ); 46 | process_glob( 47 | state, 48 | &path.append(&["app", "code", "*", "*", "view", "*", "requirejs-config.js"]), 49 | ); 50 | process_glob( 51 | state, 52 | &path.append(&["app", "design", "**", "requirejs-config.js"]), 53 | ); 54 | } 55 | 56 | pub fn maybe_index_file(state: &mut State, content: &str, file_path: &PathBuf) { 57 | if file_path.to_path_str().ends_with("requirejs-config.js") { 58 | update_index_from_config(state, content, file_path); 59 | } 60 | } 61 | 62 | fn index_file(state: &ArcState, file_path: &PathBuf) { 63 | let content = 64 | std::fs::read_to_string(file_path).expect("Should have been able to read the file"); 65 | 66 | update_index_from_config(&mut state.lock(), &content, file_path); 67 | } 68 | 69 | fn process_glob(state: &ArcState, glob_path: &PathBuf) { 70 | let modules = glob(glob_path.to_path_str()) 71 | .expect("Failed to read glob pattern") 72 | .filter_map(Result::ok); 73 | 74 | for file_path in modules { 75 | index_file(state, &file_path); 76 | } 77 | } 78 | 79 | pub fn get_completion_item(content: &str, pos: Position) -> Option { 80 | let tree = tree_sitter_parsers::parse(content, "javascript"); 81 | let query = queries::js_completion_definition_item(); 82 | let mut cursor = QueryCursor::new(); 83 | let matches = cursor.matches(query, tree.root_node(), content.as_bytes()); 84 | 85 | for m in matches { 86 | let node = m.captures[1].node; 87 | if node_at_position(node, pos) { 88 | let mut text = ts::get_node_text_before_pos(node, content, pos); 89 | if text.is_empty() { 90 | return None; 91 | } 92 | text = text[1..].to_string(); 93 | let range = Range { 94 | start: Position { 95 | line: node.start_position().row as u32, 96 | character: 1 + node.start_position().column as u32, 97 | }, 98 | end: pos, 99 | }; 100 | 101 | return Some(JsCompletion { 102 | text, 103 | range, 104 | kind: JsCompletionType::Definition, 105 | }); 106 | } 107 | } 108 | 109 | None 110 | } 111 | 112 | pub fn get_item_from_position(state: &State, path: &PathBuf, pos: Position) -> Option { 113 | let content = state.get_file(path)?; 114 | get_item_from_pos(state, content, path, pos) 115 | } 116 | 117 | pub fn text_to_component(state: &State, text: &str, path: &Path) -> Option { 118 | let mut text = text; 119 | if text.starts_with("text!") { 120 | text = &text[5..]; 121 | } 122 | let text = &resolve_paths(state, text, &path.to_path_buf().get_area())?; 123 | let text = resolve_maps(state, text, &path.to_path_buf().get_area())?; 124 | return resolved_text_to_component(state, text, path); 125 | } 126 | 127 | fn get_item_from_pos(state: &State, content: &str, path: &Path, pos: Position) -> Option { 128 | let tree = tree_sitter_parsers::parse(content, "javascript"); 129 | let query = queries::js_item_from_pos(); 130 | let mut cursor = QueryCursor::new(); 131 | let matches = cursor.matches(query, tree.root_node(), content.as_bytes()); 132 | 133 | for m in matches { 134 | if node_at_position(m.captures[0].node, pos) { 135 | let text = get_node_text(m.captures[0].node, content); 136 | return text_to_component(state, text, path); 137 | } 138 | } 139 | 140 | None 141 | } 142 | 143 | fn resolve_paths(state: &State, text: &str, area: &M2Area) -> Option { 144 | let mut result = String::from(text); 145 | let paths = state.get_component_paths_for_area(area); 146 | for path in paths { 147 | let path_slash = path.clone() + "/"; 148 | if text == path || text.starts_with(&path_slash) { 149 | let new_path = state.get_component_path(&path, area)?; 150 | result = result.replacen(&path, new_path, 1); 151 | }; 152 | } 153 | Some(result) 154 | } 155 | 156 | fn resolve_maps<'a>(state: &'a State, text: &'a str, area: &M2Area) -> Option<&'a str> { 157 | state.get_component_map(text, area).map_or_else( 158 | || { 159 | area.lower_area() 160 | .map_or_else(|| Some(text), |a| resolve_maps(state, text, &a)) 161 | }, 162 | |t| resolve_maps(state, t, area), 163 | ) 164 | } 165 | 166 | fn resolved_text_to_component(state: &State, text: &str, path: &Path) -> Option { 167 | let begining = text.split('/').next().unwrap_or(""); 168 | 169 | if text.ends_with(".html") { 170 | let mut parts = text.splitn(2, '/'); 171 | let mod_name = parts.next()?.to_string(); 172 | let mod_path = state.get_module_path(&mod_name)?; 173 | Some(M2Item::ModHtml(mod_name, parts.next()?.into(), mod_path)) 174 | } else if begining.chars().next().unwrap_or('a') == '.' { 175 | let mut path = path.to_path_buf(); 176 | path.pop(); 177 | Some(M2Item::RelComponent(text.into(), path)) 178 | } else if text.split('/').count() > 1 179 | && begining.matches('_').count() == 1 180 | && begining.chars().next().unwrap_or('a').is_uppercase() 181 | { 182 | let mut parts = text.splitn(2, '/'); 183 | let mod_name = parts.next()?.to_string(); 184 | let mod_path = state.get_module_path(&mod_name)?; 185 | Some(M2Item::ModComponent( 186 | mod_name, 187 | parts.next()?.into(), 188 | mod_path, 189 | )) 190 | } else { 191 | Some(M2Item::Component(text.into())) 192 | } 193 | } 194 | 195 | fn update_index_from_config(state: &mut State, content: &str, file_path: &PathBuf) { 196 | state.set_source_file(file_path); 197 | let area = &file_path.get_area(); 198 | let tree = tree_sitter_parsers::parse(content, "javascript"); 199 | let query = queries::js_require_config(); 200 | 201 | let mut cursor = QueryCursor::new(); 202 | let matches = cursor.matches(query, tree.root_node(), content.as_bytes()); 203 | 204 | for m in matches { 205 | let key = get_node_text(m.captures[2].node, content); 206 | let val = get_node_text(m.captures[3].node, content); 207 | match get_kind(m.captures[1].node, content) { 208 | Some(JSTypes::Map) => state.add_component_map(key, val, area), 209 | Some(JSTypes::Paths) => state.add_component_path(key, val, area), 210 | Some(JSTypes::Mixins) => state.add_component_mixin(key, val, area), 211 | None => continue, 212 | }; 213 | } 214 | } 215 | 216 | fn get_kind(node: Node, content: &str) -> Option { 217 | match get_node_text(node, content) { 218 | "map" => Some(JSTypes::Map), 219 | "paths" => Some(JSTypes::Paths), 220 | "mixins" => Some(JSTypes::Mixins), 221 | _ => None, 222 | } 223 | } 224 | 225 | fn get_node_text<'a>(node: Node, content: &'a str) -> &'a str { 226 | let result = node 227 | .utf8_text(content.as_bytes()) 228 | .unwrap_or("") 229 | .trim_matches('\\'); 230 | 231 | if node.kind() == "string" { 232 | get_node_text(node.child(0).unwrap_or(node), content) 233 | .chars() 234 | .next() 235 | .map_or(result, |trim| result.trim_matches(trim)) 236 | } else { 237 | result 238 | } 239 | } 240 | 241 | #[cfg(test)] 242 | mod test { 243 | use std::path::PathBuf; 244 | 245 | use super::*; 246 | 247 | #[test] 248 | fn test_update_index_from_config() { 249 | let state = State::new(); 250 | let content = r#" 251 | var config = { 252 | map: { 253 | '*': { 254 | 'some/js/component': 'Some_Model/js/component', 255 | otherComp: 'Some_Other/js/comp' 256 | } 257 | }, 258 | "paths": { 259 | 'other/core/extension': 'Other_Module/js/core_ext', 260 | prototype: 'Something_Else/js/prototype.min' 261 | }, 262 | config: { 263 | mixins: { 264 | "Mage_Module/js/smth" : { 265 | "My_Module/js/mixin/smth" : true 266 | }, 267 | Adobe_Module: { 268 | "My_Module/js/mixin/adobe": true 269 | }, 270 | } 271 | } 272 | }; 273 | "#; 274 | 275 | let arc_state = state.into_arc(); 276 | update_index_from_config(&mut arc_state.lock(), content, &PathBuf::from("")); 277 | 278 | let mut result = State::new(); 279 | result.add_component_path( 280 | "other/core/extension", 281 | "Other_Module/js/core_ext", 282 | &M2Area::Base, 283 | ); 284 | result.add_component_path( 285 | "prototype", 286 | "Something_Else/js/prototype.min", 287 | &M2Area::Base, 288 | ); 289 | result.add_component_map( 290 | "some/js/component", 291 | "Some_Model/js/component", 292 | &M2Area::Base, 293 | ); 294 | result.add_component_map("otherComp", "Some_Other/js/comp", &M2Area::Base); 295 | result.add_component_mixin( 296 | "Mage_Module/js/smth", 297 | "My_Module/js/mixin/smth", 298 | &M2Area::Base, 299 | ); 300 | result.add_component_mixin("Adobe_Module", "My_Module/js/mixin/adobe", &M2Area::Base); 301 | result.set_source_file(&PathBuf::from("")); 302 | 303 | let computed = arc_state.lock(); 304 | assert_eq!(computed.get_modules(), result.get_modules()); 305 | for module in [ 306 | "prototype", 307 | "otherComp", 308 | "other/core/extension", 309 | "some/js/component", 310 | ] { 311 | assert_eq!( 312 | computed.get_component_map(module, &M2Area::Base), 313 | result.get_component_map(module, &M2Area::Base) 314 | ); 315 | } 316 | for mixin in ["Mage_Module/js/smth", "Adobe_Module"] { 317 | assert_eq!( 318 | computed.get_component_mixins_for_area(mixin, &M2Area::Base), 319 | result.get_component_mixins_for_area(mixin, &M2Area::Base) 320 | ); 321 | } 322 | } 323 | 324 | #[test] 325 | fn get_item_from_pos_mod_component() { 326 | let item = get_test_item( 327 | r#" 328 | define([ 329 | 'Some_Module/some/vie|w', 330 | ], function (someView) {}) 331 | "#, 332 | "/a/b/c", 333 | ); 334 | assert_eq!( 335 | item, 336 | Some(M2Item::ModComponent( 337 | "Some_Module".into(), 338 | "some/view".into(), 339 | PathBuf::from("/a/b/c/Some_Module") 340 | )) 341 | ); 342 | } 343 | 344 | #[test] 345 | fn get_item_from_pos_component() { 346 | let item = get_test_item( 347 | r#" 348 | define([ 349 | 'jqu|ery', 350 | ], function ($) {}) 351 | "#, 352 | "/a/b/c", 353 | ); 354 | assert_eq!(item, Some(M2Item::Component("jquery".into()))); 355 | } 356 | 357 | #[test] 358 | fn get_item_from_pos_component_with_slashes() { 359 | let item = get_test_item( 360 | r#" 361 | define([ 362 | 'jqu|ery-ui-modules/widget', 363 | ], function (widget) {}) 364 | "#, 365 | "/a/b/c", 366 | ); 367 | assert_eq!( 368 | item, 369 | Some(M2Item::Component("jquery-ui-modules/widget".into())) 370 | ); 371 | } 372 | 373 | fn get_test_item(xml: &str, path: &str) -> Option { 374 | let win_path = format!("c:{}", path.replace('/', "\\")); 375 | let mut character = 0; 376 | let mut line = 0; 377 | for l in xml.lines() { 378 | if l.contains('|') { 379 | character = l.find('|').expect("Test has to have a | character") as u32; 380 | break; 381 | } 382 | line += 1; 383 | } 384 | let pos = Position { line, character }; 385 | let uri = PathBuf::from(if cfg!(windows) { &win_path } else { path }); 386 | let mut state = State::new(); 387 | state.add_module_path("Some_Module", PathBuf::from("/a/b/c/Some_Module")); 388 | get_item_from_pos(&state, &xml.replace('|', ""), &uri, pos) 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /src/lsp.rs: -------------------------------------------------------------------------------- 1 | mod completion; 2 | mod definition; 3 | 4 | use lsp_types::{ 5 | CompletionParams, CompletionResponse, GotoDefinitionParams, GotoDefinitionResponse, 6 | }; 7 | 8 | use crate::state::State; 9 | 10 | use self::{completion::get_completion_from_params, definition::get_location_from_params}; 11 | 12 | pub fn completion_handler(state: &State, params: &CompletionParams) -> CompletionResponse { 13 | CompletionResponse::Array( 14 | get_completion_from_params(state, params).map_or(vec![], |loc_list| loc_list), 15 | ) 16 | } 17 | 18 | pub fn definition_handler(state: &State, params: &GotoDefinitionParams) -> GotoDefinitionResponse { 19 | GotoDefinitionResponse::Array( 20 | get_location_from_params(state, params).map_or(vec![], |loc_list| loc_list), 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/lsp/completion.rs: -------------------------------------------------------------------------------- 1 | mod events; 2 | 3 | use std::path::PathBuf; 4 | 5 | use glob::glob; 6 | use lsp_types::{ 7 | CompletionItem, CompletionItemKind, CompletionParams, CompletionTextEdit, Position, Range, 8 | TextEdit, 9 | }; 10 | 11 | use crate::{ 12 | js::{self, JsCompletionType}, 13 | m2::{self, M2Area, M2Path, M2Uri}, 14 | state::State, 15 | xml, 16 | }; 17 | 18 | pub fn get_completion_from_params( 19 | state: &State, 20 | params: &CompletionParams, 21 | ) -> Option> { 22 | let path = params 23 | .text_document_position 24 | .text_document 25 | .uri 26 | .to_path_buf(); 27 | let pos = params.text_document_position.position; 28 | 29 | match path.get_ext().as_str() { 30 | "xml" => xml_completion_handler(state, &path, pos), 31 | "js" => js_completion_handler(state, &path, pos), 32 | _ => None, 33 | } 34 | } 35 | 36 | fn js_completion_handler( 37 | state: &State, 38 | path: &PathBuf, 39 | pos: Position, 40 | ) -> Option> { 41 | let at_position = js::get_completion_item(state.get_file(path)?, pos)?; 42 | 43 | match at_position.kind { 44 | JsCompletionType::Definition => completion_for_component( 45 | state, 46 | &at_position.text, 47 | at_position.range, 48 | &path.get_area(), 49 | ), 50 | } 51 | } 52 | 53 | fn xml_completion_handler( 54 | state: &State, 55 | path: &PathBuf, 56 | pos: Position, 57 | ) -> Option> { 58 | let at_position = xml::get_current_position_path(state.get_file(path)?, pos)?; 59 | match at_position { 60 | x if x.match_path("[@template]") => { 61 | completion_for_template(state, &x.text, x.range, &path.get_area()) 62 | } 63 | x if x.attribute_eq("xsi:type", "string") && x.attribute_eq("name", "template") => { 64 | completion_for_template(state, &x.text, x.range, &path.get_area()) 65 | } 66 | x if x.attribute_eq("xsi:type", "string") && x.attribute_eq("name", "component") => { 67 | completion_for_component(state, &x.text, x.range, &path.get_area()) 68 | } 69 | x if x.match_path("/config/event[@name]") && path.ends_with("events.xml") => { 70 | Some(events::get_completion_items(x.range)) 71 | } 72 | x if x.match_path("/config/preference[@for]") && path.ends_with("di.xml") => { 73 | completion_for_classes(state, &x.text, x.range) 74 | } 75 | x if x.match_path("/config/preference[@type]") && path.ends_with("di.xml") => { 76 | completion_for_classes(state, &x.text, x.range) 77 | } 78 | x if x.match_path("/virtualType[@type]") && path.ends_with("di.xml") => { 79 | completion_for_classes(state, &x.text, x.range) 80 | } 81 | x if x.match_path("[@class]") || x.match_path("[@instance]") => { 82 | completion_for_classes(state, &x.text, x.range) 83 | } 84 | x if x.attribute_in("xsi:type", &["object", "const", "init_parameter"]) => { 85 | completion_for_classes(state, &x.text, x.range) 86 | } 87 | x if x.match_path("/type[@name]") => completion_for_classes(state, &x.text, x.range), 88 | // Should be /source_model[$text], but html parser dont like undersores 89 | x if x.match_path("/source[$text]") && x.attribute_eq("_model", "") => { 90 | completion_for_classes(state, &x.text, x.range) 91 | } 92 | // Should be /backend_model[$text], but html parser dont like undersores 93 | x if x.match_path("/backend[$text]") && x.attribute_eq("_model", "") => { 94 | completion_for_classes(state, &x.text, x.range) 95 | } 96 | // Should be /frontend_model[$text], but html parser dont like undersores 97 | x if x.match_path("/frontend[$text]") && x.attribute_eq("_model", "") => { 98 | completion_for_classes(state, &x.text, x.range) 99 | } 100 | _ => None, 101 | } 102 | } 103 | 104 | fn completion_for_classes(state: &State, text: &str, range: Range) -> Option> { 105 | let text = text.trim_start_matches('\\'); 106 | if text.is_empty() || (m2::is_part_of_class_name(text) && text.matches('\\').count() == 0) { 107 | Some(completion_for_classes_prefix(state, range)) 108 | } else if text.matches('\\').count() >= 1 { 109 | let mut result = completion_for_classes_prefix(state, range); 110 | result.extend(completion_for_classes_full(state, text, range)); 111 | Some(result) 112 | } else { 113 | None 114 | } 115 | } 116 | 117 | fn completion_for_classes_prefix(state: &State, range: Range) -> Vec { 118 | let module_prefixes = state.get_module_class_prefixes(); 119 | string_vec_and_range_to_completion_list(module_prefixes, range) 120 | } 121 | 122 | fn completion_for_classes_full(state: &State, text: &str, range: Range) -> Vec { 123 | let mut classes = vec![]; 124 | let mut index = 0; 125 | let splits: Vec = text 126 | .chars() 127 | .filter_map(|c| { 128 | index += 1; 129 | if c == '\\' { 130 | Some(index) 131 | } else { 132 | None 133 | } 134 | }) 135 | .collect(); 136 | 137 | for spllit in splits { 138 | let prefix = &text[..spllit - 1]; 139 | if let Some(module_path) = state.get_module_path(prefix) { 140 | let candidates = glob(module_path.append(&["**", "*.php"]).to_path_str()) 141 | .expect("Failed to read glob pattern"); 142 | for p in candidates { 143 | let path = p.map_or_else(|_| std::path::PathBuf::new(), |p| p); 144 | let rel_path = path.relative_to(&module_path).str_components().join("\\"); 145 | let class_suffix = rel_path.trim_end_matches(".php"); 146 | let class = format!("{}\\{}", prefix, class_suffix); 147 | 148 | if class.ends_with("\\registration") { 149 | continue; 150 | } 151 | 152 | if !class.starts_with(&text[..index - 1]) { 153 | continue; 154 | } 155 | 156 | classes.push(class); 157 | } 158 | } 159 | } 160 | 161 | string_vec_and_range_to_completion_list(classes, range) 162 | } 163 | 164 | fn completion_for_template( 165 | state: &State, 166 | text: &str, 167 | range: Range, 168 | area: &M2Area, 169 | ) -> Option> { 170 | if text.is_empty() || m2::is_part_of_module_name(text) { 171 | let modules = state.get_modules(); 172 | Some(string_vec_and_range_to_completion_list(modules, range)) 173 | } else if text.contains("::") { 174 | let module_name = text.split("::").next()?; 175 | let path = state.get_module_path(module_name)?; 176 | let mut theme_paths = state.list_themes_paths(&area); 177 | theme_paths.push(&path); 178 | 179 | let mut files = vec![]; 180 | for area_string in area.path_candidates() { 181 | let view_path = path.append(&["view", area_string, "templates"]); 182 | let glob_path = view_path.append(&["**", "*.phtml"]); 183 | files.extend(glob::glob(glob_path.to_path_str()).ok()?.map(|file| { 184 | let path = file 185 | .unwrap_or_default() 186 | .relative_to(&view_path) 187 | .str_components() 188 | .join("/"); 189 | String::from(module_name) + "::" + &path 190 | })); 191 | } 192 | for theme_path in theme_paths { 193 | let view_path = theme_path.append(&[module_name, "templates"]); 194 | let glob_path = view_path.append(&["**", "*.phtml"]); 195 | files.extend(glob::glob(glob_path.to_path_str()).ok()?.map(|file| { 196 | let path = file 197 | .unwrap_or_default() 198 | .relative_to(&view_path) 199 | .str_components() 200 | .join("/"); 201 | String::from(module_name) + "::" + &path 202 | })); 203 | } 204 | Some(string_vec_and_range_to_completion_list(files, range)) 205 | } else { 206 | None 207 | } 208 | } 209 | 210 | fn completion_for_component( 211 | state: &State, 212 | text: &str, 213 | range: Range, 214 | area: &M2Area, 215 | ) -> Option> { 216 | if text.contains('/') { 217 | let module_name = text.split('/').next()?; 218 | let mut files = vec![]; 219 | if let Some(path) = state.get_module_path(module_name) { 220 | for area in area.path_candidates() { 221 | let view_path = path.append(&["view", area, "web"]); 222 | let glob_path = view_path.append(&["**", "*.js"]); 223 | files.extend(glob::glob(glob_path.to_path_str()).ok()?.map(|file| { 224 | let path = file 225 | .unwrap_or_default() 226 | .relative_to(&view_path) 227 | .str_components() 228 | .join("/"); 229 | let path = path.trim_end_matches(".js"); 230 | String::from(module_name) + "/" + path 231 | })); 232 | } 233 | } 234 | let workspaces = state.workspace_paths(); 235 | for path in workspaces { 236 | let view_path = path.append(&["lib", "web"]); 237 | let glob_path = view_path.append(&["**", "*.js"]); 238 | files.extend(glob::glob(glob_path.to_path_str()).ok()?.map(|file| { 239 | let path = file 240 | .unwrap_or_default() 241 | .relative_to(&view_path) 242 | .str_components() 243 | .join("/"); 244 | path.trim_end_matches(".js").to_string() 245 | })); 246 | } 247 | 248 | files.extend(state.get_component_maps_for_area(area)); 249 | if let Some(lower_area) = area.lower_area() { 250 | files.extend(state.get_component_maps_for_area(&lower_area)); 251 | } 252 | Some(string_vec_and_range_to_completion_list(files, range)) 253 | } else { 254 | let mut modules = vec![]; 255 | modules.extend(state.get_modules()); 256 | modules.extend(state.get_component_maps_for_area(area)); 257 | if let Some(lower_area) = area.lower_area() { 258 | modules.extend(state.get_component_maps_for_area(&lower_area)); 259 | } 260 | let workspaces = state.workspace_paths(); 261 | for path in workspaces { 262 | let view_path = path.append(&["lib", "web"]); 263 | let glob_path = view_path.append(&["**", "*.js"]); 264 | modules.extend(glob::glob(glob_path.to_path_str()).ok()?.map(|file| { 265 | let path = file 266 | .unwrap_or_default() 267 | .relative_to(&view_path) 268 | .str_components() 269 | .join("/"); 270 | path.trim_end_matches(".js").to_string() 271 | })); 272 | } 273 | Some(string_vec_and_range_to_completion_list(modules, range)) 274 | } 275 | } 276 | 277 | fn string_vec_and_range_to_completion_list( 278 | mut strings: Vec, 279 | range: Range, 280 | ) -> Vec { 281 | strings.sort_unstable(); 282 | strings.dedup(); 283 | strings 284 | .iter() 285 | .map(|label| CompletionItem { 286 | label: label.clone(), 287 | text_edit: Some(CompletionTextEdit::Edit(TextEdit { 288 | range, 289 | new_text: label.clone(), 290 | })), 291 | label_details: None, 292 | kind: Some(CompletionItemKind::FILE), 293 | detail: None, 294 | ..CompletionItem::default() 295 | }) 296 | .collect() 297 | } 298 | -------------------------------------------------------------------------------- /src/lsp/completion/events.rs: -------------------------------------------------------------------------------- 1 | use lsp_types::{CompletionItem, CompletionItemKind, CompletionTextEdit, Range, TextEdit}; 2 | pub const EVENT_LIST: [&str; 344] = [ 3 | "abstract_search_result_load_after", 4 | "abstract_search_result_load_before", 5 | "admin_sales_order_address_update", 6 | "admin_system_config_changed_section_admin", 7 | "admin_system_config_changed_section_catalog", 8 | "admin_system_config_changed_section_cataloginventory", 9 | "admin_system_config_changed_section_currency", 10 | "admin_system_config_changed_section_currency_before_reinit", 11 | "admin_system_config_changed_section_{section}", 12 | "adminhtml_block_eav_attribute_edit_form_init", 13 | "adminhtml_block_html_before", 14 | "adminhtml_block_salesrule_actions_prepareform", 15 | "adminhtml_cache_flush_all", 16 | "adminhtml_cache_flush_system", 17 | "adminhtml_cache_refresh_type", 18 | "adminhtml_catalog_category_tree_can_add_root_category", 19 | "adminhtml_catalog_category_tree_can_add_sub_category", 20 | "adminhtml_catalog_category_tree_is_moveable", 21 | "adminhtml_catalog_product_attribute_edit_frontend_prepare_form", 22 | "adminhtml_catalog_product_attribute_set_main_html_before", 23 | "adminhtml_catalog_product_attribute_set_toolbar_main_html_before", 24 | "adminhtml_catalog_product_edit_element_types", 25 | "adminhtml_catalog_product_edit_prepare_form", 26 | "adminhtml_catalog_product_edit_tab_attributes_create_html_before", 27 | "adminhtml_catalog_product_form_prepare_excluded_field_list", 28 | "adminhtml_catalog_product_grid_prepare_massaction", 29 | "adminhtml_cmspage_on_delete", 30 | "adminhtml_controller_catalogrule_prepare_save", 31 | "adminhtml_customer_orders_add_action_renderer", 32 | "adminhtml_customer_prepare_save", 33 | "adminhtml_customer_save_after", 34 | "adminhtml_product_attribute_types", 35 | "adminhtml_sales_order_create_process_data", 36 | "adminhtml_sales_order_create_process_data_before", 37 | "adminhtml_sales_order_create_process_item_after", 38 | "adminhtml_sales_order_create_process_item_before", 39 | "adminhtml_sales_order_creditmemo_register_before", 40 | "adminhtml_store_edit_form_prepare_form", 41 | "adminhtml_system_config_advanced_disableoutput_render_before", 42 | "adminhtml_widget_grid_filter_collection", 43 | "backend_auth_user_login_failed", 44 | "backend_auth_user_login_success", 45 | "backend_block_widget_grid_prepare_grid_before", 46 | "catalog_block_product_list_collection", 47 | "catalog_block_product_status_display", 48 | "catalog_category_change_products", 49 | "catalog_category_delete_after_done", 50 | "catalog_category_flat_loadnodes_before", 51 | "catalog_category_move_after", 52 | "catalog_category_prepare_save", 53 | "catalog_category_save_after", 54 | "catalog_category_save_before", 55 | "catalog_category_tree_init_inactive_category_ids", 56 | "catalog_controller_category_delete", 57 | "catalog_controller_category_init_after", 58 | "catalog_controller_product_init_after", 59 | "catalog_controller_product_init_before", 60 | "catalog_controller_product_view", 61 | "catalog_prepare_price_select", 62 | "catalog_product_attribute_update_before", 63 | "catalog_product_collection_apply_limitations_after", 64 | "catalog_product_collection_before_add_count_to_categories", 65 | "catalog_product_collection_load_after", 66 | "catalog_product_compare_add_product", 67 | "catalog_product_compare_item_collection_clear", 68 | "catalog_product_compare_remove_product", 69 | "catalog_product_delete_after_done", 70 | "catalog_product_delete_before", 71 | "catalog_product_edit_action", 72 | "catalog_product_gallery_prepare_layout", 73 | "catalog_product_gallery_upload_image_after", 74 | "catalog_product_get_final_price", 75 | "catalog_product_import_bunch_delete_after", 76 | "catalog_product_import_bunch_delete_commit_after", 77 | "catalog_product_import_bunch_delete_commit_before", 78 | "catalog_product_import_bunch_save_after", 79 | "catalog_product_import_finish_before", 80 | "catalog_product_is_salable_after", 81 | "catalog_product_is_salable_before", 82 | "catalog_product_load_after", 83 | "catalog_product_new_action", 84 | "catalog_product_option_price_configuration_after", 85 | "catalog_product_prepare_index_select", 86 | "catalog_product_save_after", 87 | "catalog_product_save_before", 88 | "catalog_product_to_website_change", 89 | "catalog_product_type_prepare_%s_options", 90 | "catalog_product_upsell", 91 | "catalog_product_validate_variations_before", 92 | "catalog_product_view_config", 93 | "catalogrule_after_apply", 94 | "catalogrule_before_apply", 95 | "catalogrule_dirty_notice", 96 | "catalogsearch_reset_search_result", 97 | "category_move", 98 | "category_prepare_ajax_response", 99 | "catelogsearch_searchable_attributes_load_after", 100 | "checkout_allow_guest", 101 | "checkout_cart_add_product_complete", 102 | "checkout_cart_product_add_after", 103 | "checkout_cart_product_update_after", 104 | "checkout_cart_save_after", 105 | "checkout_cart_save_before", 106 | "checkout_cart_update_item_complete", 107 | "checkout_cart_update_items_after", 108 | "checkout_cart_update_items_before", 109 | "checkout_controller_multishipping_shipping_post", 110 | "checkout_controller_onepage_saveOrder", 111 | "checkout_multishipping_refund_all", 112 | "checkout_onepage_controller_success_action", 113 | "checkout_quote_destroy", 114 | "checkout_quote_init", 115 | "checkout_submit_all_after", 116 | "checkout_submit_before", 117 | "checkout_type_multishipping_create_orders_single", 118 | "checkout_type_multishipping_set_shipping_items", 119 | "checkout_type_onepage_save_order_after", 120 | "clean_cache_after_reindex", 121 | "clean_cache_by_tags", 122 | "clean_catalog_images_cache_after", 123 | "clean_media_cache_after", 124 | "clean_static_files_cache_after", 125 | "cms_controller_router_match_before", 126 | "cms_page_delete_after", 127 | "cms_page_prepare_save", 128 | "cms_page_render", 129 | "cms_page_save_after", 130 | "cms_wysiwyg_images_static_urls_allowed", 131 | "config_data_dev_grid_async_indexing_disabled", 132 | "config_data_sales_email_general_async_sending_disabled", 133 | "controller_action_catalog_product_save_entity_after", 134 | "controller_action_layout_render_before", 135 | "controller_action_nocookies", 136 | "controller_action_noroute", 137 | "controller_action_postdispatch", 138 | "controller_action_predispatch", 139 | "controller_front_send_response_before", 140 | "core_collection_abstract_load_after", 141 | "core_collection_abstract_load_before", 142 | "core_layout_block_create_after", 143 | "core_layout_render_element", 144 | "currency_display_options_forming", 145 | "custom_quote_process", 146 | "customer_account_edited", 147 | "customer_address_format", 148 | "customer_address_save_after", 149 | "customer_address_save_before", 150 | "customer_customer_authenticated", 151 | "customer_data_object_login", 152 | "customer_login", 153 | "customer_logout", 154 | "customer_register_success", 155 | "customer_save_after_data_object", 156 | "customer_session_init", 157 | "depersonalize_clear_session", 158 | "eav_collection_abstract_load_before", 159 | "email_creditmemo_comment_set_template_vars_befor", 160 | "email_creditmemo_set_template_vars_before", 161 | "email_invoice_comment_set_template_vars_before", 162 | "email_invoice_set_template_vars_before", 163 | "email_order_comment_set_template_vars_before", 164 | "email_order_set_template_vars_before", 165 | "email_shipment_comment_set_template_vars_before", 166 | "email_shipment_set_template_vars_before", 167 | "entity_manager_delete_before", 168 | "entity_manager_load_after", 169 | "entity_manager_load_before", 170 | "entity_manager_save_after", 171 | "entity_manager_save_before", 172 | "gift_options_prepare_items", 173 | "items_additional_data", 174 | "layout_generate_blocks_after", 175 | "layout_generate_blocks_before", 176 | "layout_load_before", 177 | "layout_render_before", 178 | "layout_render_before_{frontname}_{foldername}_{controllerfile}", 179 | "load_customer_quote_before", 180 | "magento_catalog_api_data_categoryinterface_delete_after", 181 | "magento_catalog_api_data_categoryinterface_delete_before", 182 | "magento_catalog_api_data_categoryinterface_load_after", 183 | "magento_catalog_api_data_categoryinterface_save_after", 184 | "magento_catalog_api_data_categoryinterface_save_before", 185 | "magento_catalog_api_data_categorytreeinterface_delete_after", 186 | "magento_catalog_api_data_categorytreeinterface_delete_before", 187 | "magento_catalog_api_data_categorytreeinterface_load_after", 188 | "magento_catalog_api_data_categorytreeinterface_save_after", 189 | "magento_catalog_api_data_categorytreeinterface_save_before", 190 | "magento_catalog_api_data_productinterface_delete_after", 191 | "magento_catalog_api_data_productinterface_delete_before", 192 | "magento_catalog_api_data_productinterface_load_after", 193 | "magento_catalog_api_data_productinterface_save_after", 194 | "magento_catalog_api_data_productinterface_save_before", 195 | "magento_cms_api_data_blockinterface_delete_after", 196 | "magento_cms_api_data_blockinterface_delete_before", 197 | "magento_cms_api_data_blockinterface_load_after", 198 | "magento_cms_api_data_blockinterface_save_after", 199 | "magento_cms_api_data_blockinterface_save_before", 200 | "magento_cms_api_data_pageinterface_delete_after", 201 | "magento_cms_api_data_pageinterface_delete_before", 202 | "magento_cms_api_data_pageinterface_load_after", 203 | "magento_cms_api_data_pageinterface_save_after", 204 | "magento_cms_api_data_pageinterface_save_before", 205 | "model_delete_after", 206 | "model_delete_before", 207 | "model_delete_commit_after", 208 | "model_load_after", 209 | "model_load_before", 210 | "model_save_after", 211 | "model_save_before", 212 | "model_save_commit_after", 213 | "multishipping_checkout_controller_success_action", 214 | "newsletter_subscriber_save_commit_after", 215 | "newsletter_subscriber_save_commit_before", 216 | "on_view_report", 217 | "order_cancel_after", 218 | "payment_cart_collect_items_and_amounts", 219 | "payment_form_block_to_html_before", 220 | "payment_method_assign_data", 221 | "payment_method_assign_data_vault", 222 | "payment_method_assign_data_{PAYMENT_CODE}", 223 | "payment_method_is_active", 224 | "paypal_express_place_order_success", 225 | "permissions_role_html_before", 226 | "persistent_session_expired", 227 | "prepare_catalog_product_collection_prices", 228 | "prepare_catalog_product_index_select", 229 | "product_attribute_form_build", 230 | "product_attribute_form_build_front_tab", 231 | "product_attribute_form_build_main_tab", 232 | "product_attribute_grid_build", 233 | "product_option_renderer_init", 234 | "rating_rating_collection_load_before", 235 | "restore_quote", 236 | "review_controller_product_init", 237 | "review_controller_product_init_before", 238 | "review_review_collection_load_before", 239 | "rss_catalog_category_xml_callback", 240 | "rss_catalog_new_xml_callback", 241 | "rss_catalog_notify_stock_collection_select", 242 | "rss_catalog_review_collection_select", 243 | "rss_catalog_special_xml_callback", 244 | "rss_order_new_collection_select", 245 | "rss_wishlist_xml_callback", 246 | "sales_convert_order_item_to_quote_item", 247 | "sales_convert_order_to_quote", 248 | "sales_convert_quote_to_order", 249 | "sales_model_service_quote_submit_before", 250 | "sales_model_service_quote_submit_failure", 251 | "sales_model_service_quote_submit_success", 252 | "sales_order_creditmemo_cancel", 253 | "sales_order_creditmemo_delete_after", 254 | "sales_order_creditmemo_load_after", 255 | "sales_order_creditmemo_load_before", 256 | "sales_order_creditmemo_process_relation", 257 | "sales_order_creditmemo_refund", 258 | "sales_order_creditmemo_save_after", 259 | "sales_order_delete_after", 260 | "sales_order_delete_before", 261 | "sales_order_grid_collection_load_before", 262 | "sales_order_invoice_cancel", 263 | "sales_order_invoice_delete_after", 264 | "sales_order_invoice_load_after", 265 | "sales_order_invoice_load_before", 266 | "sales_order_invoice_pay", 267 | "sales_order_invoice_process_relation", 268 | "sales_order_invoice_register", 269 | "sales_order_invoice_save_after", 270 | "sales_order_item_cancel", 271 | "sales_order_item_save_commit_after", 272 | "sales_order_load_after", 273 | "sales_order_payment_cancel", 274 | "sales_order_payment_cancel_creditmemo", 275 | "sales_order_payment_cancel_invoice", 276 | "sales_order_payment_capture", 277 | "sales_order_payment_pay", 278 | "sales_order_payment_place_end", 279 | "sales_order_payment_place_start", 280 | "sales_order_payment_refund", 281 | "sales_order_payment_save_after", 282 | "sales_order_payment_save_before", 283 | "sales_order_payment_void", 284 | "sales_order_place_after", 285 | "sales_order_place_before", 286 | "sales_order_process_relation", 287 | "sales_order_save_after", 288 | "sales_order_save_before", 289 | "sales_order_save_commit_after", 290 | "sales_order_shipment_delete_after", 291 | "sales_order_shipment_load_after", 292 | "sales_order_shipment_load_before", 293 | "sales_order_shipment_process_relation", 294 | "sales_order_shipment_save_after", 295 | "sales_order_state_change_before", 296 | "sales_order_status_unassign", 297 | "sales_prepare_amount_expression", 298 | "sales_quote_add_item", 299 | "sales_quote_address_collect_totals_after", 300 | "sales_quote_address_collection_load_after", 301 | "sales_quote_address_discount_item", 302 | "sales_quote_collect_totals_after", 303 | "sales_quote_collect_totals_before", 304 | "sales_quote_item_collection_products_after_load", 305 | "sales_quote_item_qty_set_after", 306 | "sales_quote_item_save_after", 307 | "sales_quote_item_save_before", 308 | "sales_quote_item_set_product", 309 | "sales_quote_merge_after", 310 | "sales_quote_merge_before", 311 | "sales_quote_payment_import_data_before", 312 | "sales_quote_product_add_after", 313 | "sales_quote_remove_item", 314 | "sales_quote_save_after", 315 | "sales_sale_collection_query_before", 316 | "salesrule_rule_condition_combine", 317 | "salesrule_rule_get_coupon_types", 318 | "sendfriend_product", 319 | "session_abstract_add_messages", 320 | "session_abstract_clear_messages", 321 | "shortcut_buttons_container", 322 | "store_add", 323 | "store_address_format", 324 | "store_delete", 325 | "store_edit", 326 | "store_group_save", 327 | "store_save_after", 328 | "tax_rate_data_fetch", 329 | "tax_settings_change_after", 330 | "theme_delete_before", 331 | "theme_save_after", 332 | "view_block_abstract_to_html_after", 333 | "view_block_abstract_to_html_before", 334 | "view_message_block_render_grouped_html_after", 335 | "visitor_activity_save", 336 | "visitor_init", 337 | "wishlist_add_item", 338 | "wishlist_add_product", 339 | "wishlist_items_renewed", 340 | "wishlist_product_add_after", 341 | "wishlist_share", 342 | "{eventPrefix}_add_is_active_filter", 343 | "{eventPrefix}_move_after", 344 | "{eventPrefix}_move_before", 345 | "{eventPrefix}_validate_after", 346 | "{eventPrefix}_validate_before", 347 | ]; 348 | 349 | pub fn get_completion_items(range: Range) -> Vec { 350 | EVENT_LIST 351 | .iter() 352 | .map(|event| CompletionItem { 353 | label: (*event).to_string(), 354 | text_edit: Some(CompletionTextEdit::Edit(TextEdit { 355 | range, 356 | new_text: (*event).to_string(), 357 | })), 358 | label_details: None, 359 | kind: Some(CompletionItemKind::EVENT), 360 | detail: None, 361 | ..CompletionItem::default() 362 | }) 363 | .collect() 364 | } 365 | -------------------------------------------------------------------------------- /src/lsp/definition.rs: -------------------------------------------------------------------------------- 1 | mod component; 2 | mod php; 3 | mod phtml; 4 | 5 | use std::path::Path; 6 | 7 | use lsp_types::{GotoDefinitionParams, Location, Range, Url}; 8 | 9 | use crate::{ 10 | m2::{M2Item, M2Uri}, 11 | state::State, 12 | }; 13 | 14 | pub fn get_location_from_params( 15 | state: &State, 16 | params: &GotoDefinitionParams, 17 | ) -> Option> { 18 | let path = params 19 | .text_document_position_params 20 | .text_document 21 | .uri 22 | .to_path_buf(); 23 | let pos = params.text_document_position_params.position; 24 | let item = state.get_item_from_position(&path, pos)?; 25 | Some(match item { 26 | M2Item::ModComponent(mod_name, file_path, mod_path) => { 27 | component::mod_location(state, mod_name, &file_path, mod_path, &path) 28 | } 29 | M2Item::RelComponent(comp, path) => component::find_rel(comp, &path)?, 30 | M2Item::ModHtml(_, file_path, mod_path) => { 31 | component::mod_html_location(&file_path, mod_path, &path) 32 | } 33 | M2Item::Component(comp) => component::find_plain(state, &comp), 34 | M2Item::AdminPhtml(mod_name, template) => phtml::find_admin(state, &mod_name, &template), 35 | M2Item::FrontPhtml(mod_name, template) => phtml::find_front(state, &mod_name, &template), 36 | M2Item::BasePhtml(mod_name, template) => phtml::find_base(state, &mod_name, &template), 37 | M2Item::Class(class) => vec![php::find_class(state, &class)?], 38 | M2Item::Method(class, method) => vec![php::find_method(state, &class, &method)?], 39 | M2Item::Const(class, constant) => vec![php::find_const(state, &class, &constant)?], 40 | }) 41 | } 42 | 43 | fn path_to_location(path: &Path) -> Option { 44 | if path.is_file() { 45 | Some(Location { 46 | uri: Url::from_file_path(path).expect("Should be valid Url"), 47 | range: Range::default(), 48 | }) 49 | } else { 50 | None 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/lsp/definition/component.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use lsp_types::Location; 4 | 5 | use crate::{ 6 | m2::{M2Item, M2Path}, 7 | state::State, 8 | }; 9 | 10 | use super::path_to_location; 11 | 12 | pub fn find_plain(state: &State, comp: &str) -> Vec { 13 | let mut result = vec![]; 14 | let workspace_paths = state.workspace_paths(); 15 | for path in workspace_paths { 16 | let path = path.append(&["lib", "web", comp]).append_ext("js"); 17 | if let Some(location) = path_to_location(&path) { 18 | result.push(location); 19 | } 20 | } 21 | result 22 | } 23 | 24 | pub fn find_rel(comp: String, path: &Path) -> Option> { 25 | let mut path = path.join(comp); 26 | path.set_extension("js"); 27 | path_to_location(&path).map(|location| vec![location]) 28 | } 29 | 30 | pub fn mod_location( 31 | state: &State, 32 | mod_name: String, 33 | file_path: &str, 34 | mod_path: PathBuf, 35 | path: &PathBuf, 36 | ) -> Vec { 37 | let mut result = vec![]; 38 | let mut components = vec![M2Item::ModComponent( 39 | mod_name.clone(), 40 | file_path.to_string(), 41 | mod_path, 42 | )]; 43 | 44 | let area = path.get_area(); 45 | components.extend(state.get_component_mixins_for_area(mod_name + "/" + file_path, &area)); 46 | 47 | for component in components { 48 | if let M2Item::ModComponent(_, file_path, mod_path) = component { 49 | for area_path in area.path_candidates() { 50 | let comp_path = mod_path 51 | .append(&["view", area_path, "web", &file_path]) 52 | .append_ext("js"); 53 | if let Some(location) = path_to_location(&comp_path) { 54 | result.push(location); 55 | } 56 | } 57 | } 58 | } 59 | 60 | result 61 | } 62 | 63 | pub fn mod_html_location(file_path: &str, mod_path: PathBuf, path: &PathBuf) -> Vec { 64 | let mut result = vec![]; 65 | let area = path.get_area(); 66 | for area_path in area.path_candidates() { 67 | let comp_path = mod_path.append(&["view", area_path, "web", &file_path]); 68 | if let Some(location) = path_to_location(&comp_path) { 69 | result.push(location); 70 | } 71 | } 72 | 73 | result 74 | } 75 | -------------------------------------------------------------------------------- /src/lsp/definition/php.rs: -------------------------------------------------------------------------------- 1 | use lsp_types::Location; 2 | 3 | use crate::{ 4 | php::{parse_php_file, PHPClass}, 5 | state::State, 6 | }; 7 | 8 | pub fn find_class(state: &State, class: &str) -> Option { 9 | let phpclass = get_php_class_from_class_name(state, class)?; 10 | Some(Location { 11 | uri: phpclass.uri.clone(), 12 | range: phpclass.range, 13 | }) 14 | } 15 | 16 | pub fn find_method(state: &State, class: &str, method: &str) -> Option { 17 | let phpclass = get_php_class_from_class_name(state, class)?; 18 | Some(Location { 19 | uri: phpclass.uri.clone(), 20 | range: phpclass 21 | .methods 22 | .get(method) 23 | .map_or(phpclass.range, |method| method.range), 24 | }) 25 | } 26 | 27 | pub fn find_const(state: &State, class: &str, constant: &str) -> Option { 28 | let phpclass = get_php_class_from_class_name(state, class)?; 29 | Some(Location { 30 | uri: phpclass.uri.clone(), 31 | range: phpclass 32 | .constants 33 | .get(constant) 34 | .map_or(phpclass.range, |method| method.range), 35 | }) 36 | } 37 | 38 | fn get_php_class_from_class_name(state: &State, class: &str) -> Option { 39 | let module_path = state.split_class_to_path_and_suffix(class); 40 | match module_path { 41 | None => None, 42 | Some((mut file_path, suffix)) => { 43 | for part in suffix { 44 | file_path.push(part); 45 | } 46 | file_path.set_extension("php"); 47 | 48 | match file_path.try_exists() { 49 | Ok(true) => parse_php_file(&file_path), 50 | _ => None, 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lsp/definition/phtml.rs: -------------------------------------------------------------------------------- 1 | use lsp_types::Location; 2 | 3 | use crate::{ 4 | m2::{M2Area, M2Path}, 5 | state::State, 6 | }; 7 | 8 | use super::path_to_location; 9 | 10 | pub fn find_admin(state: &State, mod_name: &str, template: &str) -> Vec { 11 | let mut result = vec![]; 12 | add_phtml_in_mod_location(state, &mut result, mod_name, template, &M2Area::Adminhtml); 13 | add_phtml_in_admin_theme_location(state, &mut result, mod_name, template); 14 | result 15 | } 16 | 17 | pub fn find_front(state: &State, mod_name: &str, template: &str) -> Vec { 18 | let mut result = vec![]; 19 | add_phtml_in_mod_location(state, &mut result, mod_name, template, &M2Area::Frontend); 20 | add_phtml_in_front_theme_location(state, &mut result, mod_name, template); 21 | result 22 | } 23 | 24 | pub fn find_base(state: &State, mod_name: &str, template: &str) -> Vec { 25 | let mut result = vec![]; 26 | add_phtml_in_mod_location(state, &mut result, mod_name, template, &M2Area::Base); 27 | add_phtml_in_front_theme_location(state, &mut result, mod_name, template); 28 | add_phtml_in_admin_theme_location(state, &mut result, mod_name, template); 29 | result 30 | } 31 | 32 | fn add_phtml_in_mod_location( 33 | state: &State, 34 | result: &mut Vec, 35 | mod_name: &str, 36 | template: &str, 37 | area: &M2Area, 38 | ) { 39 | let mod_path = state.get_module_path(mod_name); 40 | if let Some(path) = mod_path { 41 | for area in area.path_candidates() { 42 | let templ_path = path.append(&["view", area, "templates", template]); 43 | if let Some(location) = path_to_location(&templ_path) { 44 | result.push(location); 45 | } 46 | } 47 | } 48 | } 49 | 50 | fn add_phtml_in_admin_theme_location( 51 | state: &State, 52 | result: &mut Vec, 53 | mod_name: &str, 54 | template: &str, 55 | ) { 56 | #[allow(clippy::significant_drop_in_scrutinee)] 57 | for theme_path in state.list_admin_themes_paths() { 58 | let path = theme_path.append(&[mod_name, "templates", template]); 59 | if let Some(location) = path_to_location(&path) { 60 | result.push(location); 61 | } 62 | } 63 | } 64 | 65 | fn add_phtml_in_front_theme_location( 66 | state: &State, 67 | result: &mut Vec, 68 | mod_name: &str, 69 | template: &str, 70 | ) { 71 | #[allow(clippy::significant_drop_in_scrutinee)] 72 | for theme_path in state.list_front_themes_paths() { 73 | let path = theme_path.append(&[mod_name, "templates", template]); 74 | if let Some(location) = path_to_location(&path) { 75 | result.push(location); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/m2.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use lsp_types::Url; 4 | 5 | #[allow(clippy::module_name_repetitions)] 6 | #[derive(Debug, Clone, PartialEq, Eq)] 7 | pub enum M2Item { 8 | ModHtml(String, String, PathBuf), 9 | Component(String), 10 | ModComponent(String, String, PathBuf), 11 | RelComponent(String, PathBuf), 12 | Class(String), 13 | Method(String, String), 14 | Const(String, String), 15 | FrontPhtml(String, String), 16 | AdminPhtml(String, String), 17 | BasePhtml(String, String), 18 | } 19 | 20 | #[allow(clippy::module_name_repetitions)] 21 | #[derive(Debug, Clone, PartialEq, Eq)] 22 | pub enum M2Area { 23 | Frontend, 24 | Adminhtml, 25 | Base, 26 | } 27 | 28 | impl M2Area { 29 | pub fn path_candidates(&self) -> Vec<&str> { 30 | match self { 31 | Self::Frontend => vec!["frontend", "base"], 32 | Self::Adminhtml => vec!["adminhtml", "base"], 33 | Self::Base => vec!["frontend", "adminhtml", "base"], 34 | } 35 | } 36 | 37 | pub const fn lower_area(&self) -> Option { 38 | match self { 39 | Self::Frontend | Self::Adminhtml => Some(Self::Base), 40 | Self::Base => None, 41 | } 42 | } 43 | } 44 | 45 | #[allow(clippy::module_name_repetitions)] 46 | pub trait M2Uri { 47 | fn to_path_buf(&self) -> PathBuf; 48 | } 49 | 50 | #[allow(clippy::module_name_repetitions)] 51 | pub trait M2Path { 52 | fn has_components(&self, parts: &[&str]) -> bool; 53 | fn str_components(&self) -> Vec<&str>; 54 | fn relative_to>(&self, base: P) -> PathBuf; 55 | fn append(&self, parts: &[&str]) -> Self; 56 | fn append_ext(&self, ext: &str) -> Self; 57 | fn get_ext(&self) -> String; 58 | fn is_frontend(&self) -> bool; 59 | fn is_test(&self) -> bool; 60 | fn get_area(&self) -> M2Area; 61 | fn to_path_str(&self) -> &str; 62 | } 63 | 64 | impl M2Path for PathBuf { 65 | fn append(&self, parts: &[&str]) -> Self { 66 | let mut path = self.clone(); 67 | for part in parts { 68 | path = path.join(part); 69 | } 70 | path 71 | } 72 | 73 | fn append_ext(&self, ext: &str) -> Self { 74 | let mut path = self.clone(); 75 | 76 | match path.extension() { 77 | None => { 78 | path.set_extension(ext); 79 | } 80 | Some(extention) => { 81 | let current_ext = extention.to_str().unwrap_or_default().to_string(); 82 | path.set_extension(current_ext + "." + ext); 83 | } 84 | } 85 | 86 | path 87 | } 88 | 89 | fn relative_to>(&self, base: P) -> PathBuf { 90 | self.strip_prefix(base).unwrap_or(self).to_path_buf() 91 | } 92 | 93 | fn to_path_str(&self) -> &str { 94 | self.to_str() 95 | .expect("PathBuf should convert to path String") 96 | } 97 | 98 | fn get_area(&self) -> M2Area { 99 | if self.has_components(&["view", "base"]) || self.has_components(&["design", "base"]) { 100 | M2Area::Base 101 | } else if self.has_components(&["view", "frontend"]) 102 | || self.has_components(&["design", "frontend"]) 103 | { 104 | M2Area::Frontend 105 | } else if self.has_components(&["view", "adminhtml"]) 106 | || self.has_components(&["design", "adminhtml"]) 107 | { 108 | M2Area::Adminhtml 109 | } else { 110 | M2Area::Base 111 | } 112 | } 113 | 114 | fn str_components(&self) -> Vec<&str> { 115 | self.components() 116 | .map(|c| c.as_os_str().to_str().unwrap_or_default()) 117 | .collect() 118 | } 119 | 120 | fn has_components(&self, parts: &[&str]) -> bool { 121 | let mut start = false; 122 | let mut part_id = 0; 123 | for component in self.components() { 124 | let component = component 125 | .as_os_str() 126 | .to_str() 127 | .expect("Component should convert to &str"); 128 | if start && parts[part_id] != component { 129 | return false; 130 | } 131 | if parts[part_id] == component { 132 | start = true; 133 | part_id += 1; 134 | } 135 | if start && parts.len() == part_id { 136 | return true; 137 | } 138 | } 139 | false 140 | } 141 | 142 | fn get_ext(&self) -> String { 143 | self.extension() 144 | .unwrap_or_default() 145 | .to_str() 146 | .unwrap_or_default() 147 | .to_lowercase() 148 | } 149 | 150 | fn is_frontend(&self) -> bool { 151 | self.has_components(&["view", "frontend"]) 152 | || self.has_components(&["app", "design", "frontend"]) 153 | } 154 | 155 | fn is_test(&self) -> bool { 156 | self.has_components(&["dev", "tests"]) 157 | } 158 | } 159 | 160 | impl M2Uri for Url { 161 | fn to_path_buf(&self) -> PathBuf { 162 | self.to_file_path().expect("Url should convert to PathBuf") 163 | } 164 | } 165 | 166 | pub fn is_part_of_module_name(text: &str) -> bool { 167 | for char in text.chars() { 168 | if !char.is_alphanumeric() && char != '_' { 169 | return false; 170 | } 171 | } 172 | true 173 | } 174 | 175 | pub fn is_part_of_class_name(text: &str) -> bool { 176 | for char in text.chars() { 177 | if !char.is_alphanumeric() && char != '\\' { 178 | return false; 179 | } 180 | } 181 | true 182 | } 183 | 184 | pub(crate) fn try_any_item_from_str(text: &str, area: &M2Area) -> Option { 185 | if does_ext_eq(text, "phtml") { 186 | try_phtml_item_from_str(text, area) 187 | } else if text.contains("::") { 188 | try_const_item_from_str(text) 189 | } else if text.chars().next()?.is_uppercase() { 190 | Some(get_class_item_from_str(text)) 191 | } else { 192 | None 193 | } 194 | } 195 | 196 | pub(crate) fn try_const_item_from_str(text: &str) -> Option { 197 | if text.split("::").count() == 2 { 198 | let mut parts = text.split("::"); 199 | Some(M2Item::Const(parts.next()?.into(), parts.next()?.into())) 200 | } else { 201 | None 202 | } 203 | } 204 | 205 | pub(crate) fn get_class_item_from_str(text: &str) -> M2Item { 206 | M2Item::Class(text.into()) 207 | } 208 | 209 | pub(crate) fn try_phtml_item_from_str(text: &str, area: &M2Area) -> Option { 210 | if text.split("::").count() == 2 { 211 | let mut parts = text.split("::"); 212 | match area { 213 | M2Area::Frontend => Some(M2Item::FrontPhtml( 214 | parts.next()?.into(), 215 | parts.next()?.into(), 216 | )), 217 | M2Area::Adminhtml => Some(M2Item::AdminPhtml( 218 | parts.next()?.into(), 219 | parts.next()?.into(), 220 | )), 221 | M2Area::Base => Some(M2Item::BasePhtml( 222 | parts.next()?.into(), 223 | parts.next()?.into(), 224 | )), 225 | } 226 | } else { 227 | None 228 | } 229 | } 230 | 231 | fn does_ext_eq(path: &str, ext: &str) -> bool { 232 | Path::new(path) 233 | .extension() 234 | .map_or(false, |e| e.eq_ignore_ascii_case(ext)) 235 | } 236 | 237 | #[cfg(test)] 238 | mod test { 239 | use crate::m2::M2Path; 240 | 241 | #[test] 242 | fn test_has_components_when_components_in_the_middle() { 243 | let path = std::path::PathBuf::from("app/code/Magento/Checkout/Block/Cart.php"); 244 | assert!(path.has_components(&["Magento", "Checkout"])); 245 | } 246 | 247 | #[test] 248 | fn test_has_components_when_components_at_start() { 249 | let path = std::path::PathBuf::from("app/code/Magento/Checkout/Block/Cart.php"); 250 | assert!(path.has_components(&["app", "code"])); 251 | } 252 | 253 | #[test] 254 | fn test_has_components_when_components_at_end() { 255 | let path = std::path::PathBuf::from("app/code/Magento/Checkout/Block/Cart.php"); 256 | assert!(path.has_components(&["Block", "Cart.php"])); 257 | } 258 | 259 | #[test] 260 | fn test_has_components_when_components_are_not_in_order() { 261 | let path = std::path::PathBuf::from("app/code/Magento/Checkout/Block/Cart.php"); 262 | assert!(!path.has_components(&["Checkout", "Cart.php"])); 263 | } 264 | 265 | #[test] 266 | fn test_if_extention_can_be_add_with_append() { 267 | let path = std::path::PathBuf::from("app/code/Magento/Checkout/Block/Cart"); 268 | assert_eq!( 269 | path.append_ext("php").to_str().unwrap(), 270 | "app/code/Magento/Checkout/Block/Cart.php" 271 | ); 272 | } 273 | 274 | #[test] 275 | fn test_is_part_of_class_name_when_module_name() { 276 | assert!(!super::is_part_of_class_name("Some_Module")); 277 | } 278 | 279 | #[test] 280 | fn test_is_part_of_class_name_when_module_class() { 281 | assert!(super::is_part_of_class_name("Some\\Module")); 282 | } 283 | 284 | #[test] 285 | fn test_is_part_of_class_name_when_only_one_letter() { 286 | assert!(super::is_part_of_class_name("N")); 287 | } 288 | 289 | #[test] 290 | fn test_is_part_of_module_name_when_module_name() { 291 | assert!(super::is_part_of_module_name("Some_Module")); 292 | } 293 | 294 | #[test] 295 | fn test_is_part_of_module_name_when_module_class() { 296 | assert!(!super::is_part_of_module_name("Some\\Module")); 297 | } 298 | 299 | #[test] 300 | fn test_is_part_of_module_name_when_only_one_letter() { 301 | assert!(super::is_part_of_module_name("N")); 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod js; 2 | mod lsp; 3 | mod m2; 4 | mod php; 5 | mod queries; 6 | mod state; 7 | mod ts; 8 | mod xml; 9 | 10 | use std::error::Error; 11 | 12 | use anyhow::{Context, Result}; 13 | use lsp_server::{Connection, ExtractError, Message, Request, RequestId, Response}; 14 | use lsp_types::{ 15 | request::{Completion, GotoDefinition}, 16 | CompletionOptions, DidChangeTextDocumentParams, DidCloseTextDocumentParams, 17 | DidOpenTextDocumentParams, InitializeParams, OneOf, ServerCapabilities, 18 | TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, 19 | WorkDoneProgressOptions, 20 | }; 21 | 22 | use crate::{ 23 | m2::{M2Path, M2Uri}, 24 | state::State, 25 | }; 26 | 27 | fn main() -> Result<(), Box> { 28 | // Note that we must have our logging only write out to stderr. 29 | eprintln!("Starting magento2-ls LSP server"); 30 | 31 | // Create the transport. Includes the stdio (stdin and stdout) versions but this could 32 | // also be implemented to use sockets or HTTP. 33 | let (connection, io_threads) = Connection::stdio(); 34 | 35 | // Run the server and wait for the two threads to end (typically by trigger LSP Exit event). 36 | let server_capabilities = serde_json::to_value(ServerCapabilities { 37 | definition_provider: Some(OneOf::Left(true)), 38 | completion_provider: Some(CompletionOptions { 39 | resolve_provider: Some(false), 40 | trigger_characters: Some(vec![ 41 | String::from(">"), 42 | String::from("\""), 43 | String::from("'"), 44 | String::from(":"), 45 | String::from("\\"), 46 | String::from("/"), 47 | ]), 48 | work_done_progress_options: WorkDoneProgressOptions { 49 | work_done_progress: None, 50 | }, 51 | all_commit_characters: None, 52 | completion_item: None, 53 | }), 54 | text_document_sync: Some(TextDocumentSyncCapability::Options( 55 | TextDocumentSyncOptions { 56 | open_close: Some(true), 57 | change: Some(TextDocumentSyncKind::FULL), //TODO change to INCREMENTAL 58 | will_save: None, 59 | will_save_wait_until: None, 60 | // save: Some(SaveOptions::default().into()), 61 | save: None, 62 | }, 63 | )), 64 | ..Default::default() 65 | }) 66 | .context("Deserializing server capabilities")?; 67 | let initialization_params = connection.initialize(server_capabilities)?; 68 | 69 | main_loop(&connection, initialization_params)?; 70 | io_threads.join()?; 71 | 72 | // Shut down gracefully. 73 | eprintln!("shutting down server"); 74 | Ok(()) 75 | } 76 | 77 | fn main_loop( 78 | connection: &Connection, 79 | init_params: serde_json::Value, 80 | ) -> Result<(), Box> { 81 | let params: InitializeParams = 82 | serde_json::from_value(init_params).context("Deserializing initialize params")?; 83 | 84 | let state = State::new().into_arc(); 85 | let mut threads = vec![]; 86 | 87 | if let Some(uri) = params.root_uri { 88 | let path = uri.to_file_path().expect("Invalid root path"); 89 | threads.extend(State::update_index(&state, &path)); 90 | }; 91 | 92 | if let Some(folders) = params.workspace_folders { 93 | for folder in folders { 94 | let path = folder.uri.to_file_path().expect("Invalid workspace path"); 95 | threads.extend(State::update_index(&state, &path)); 96 | } 97 | } 98 | 99 | eprintln!("Starting main loop"); 100 | for msg in &connection.receiver { 101 | match msg { 102 | Message::Request(req) => { 103 | #[cfg(debug_assertions)] 104 | eprintln!("request: {:?}", req.method); 105 | if connection.handle_shutdown(&req)? { 106 | return Ok(()); 107 | } 108 | match req.method.as_str() { 109 | "textDocument/completion" => { 110 | let (id, params) = cast::(req)?; 111 | let result = lsp::completion_handler(&state.lock(), ¶ms); 112 | connection.sender.send(get_response_message(id, result))?; 113 | } 114 | "textDocument/definition" => { 115 | let (id, params) = cast::(req)?; 116 | let result = lsp::definition_handler(&state.lock(), ¶ms); 117 | connection.sender.send(get_response_message(id, result))?; 118 | } 119 | _ => { 120 | eprintln!("unhandled request: {:?}", req.method); 121 | } 122 | } 123 | } 124 | Message::Response(_resp) => { 125 | #[cfg(debug_assertions)] 126 | eprintln!("response: {_resp:?}"); 127 | } 128 | Message::Notification(not) => match not.method.as_str() { 129 | "textDocument/didOpen" => { 130 | let params: DidOpenTextDocumentParams = serde_json::from_value(not.params) 131 | .context("Deserializing notification params")?; 132 | let path = params.text_document.uri.to_path_buf(); 133 | state.lock().set_file(&path, params.text_document.text); 134 | #[cfg(debug_assertions)] 135 | eprintln!("textDocument/didOpen: {path:?}"); 136 | } 137 | "textDocument/didChange" => { 138 | let params: DidChangeTextDocumentParams = serde_json::from_value(not.params) 139 | .context("Deserializing notification params")?; 140 | let path = params.text_document.uri.to_path_buf(); 141 | match path.get_ext().as_str() { 142 | "js" | "xml" => state 143 | .lock() 144 | .set_file(&path, ¶ms.content_changes[0].text), 145 | "php" if path.ends_with("registration.php") => state 146 | .lock() 147 | .set_file(&path, ¶ms.content_changes[0].text), 148 | _ => (), 149 | } 150 | #[cfg(debug_assertions)] 151 | eprintln!("textDocument/didChange: {path:?}"); 152 | } 153 | "textDocument/didClose" => { 154 | let params: DidCloseTextDocumentParams = serde_json::from_value(not.params) 155 | .context("Deserializing notification params")?; 156 | let path = params.text_document.uri.to_path_buf(); 157 | state.lock().del_file(&path); 158 | #[cfg(debug_assertions)] 159 | eprintln!("textDocument/didClose: {path:?}"); 160 | } 161 | _ => { 162 | eprintln!("unhandled notification: {:?}", not.method); 163 | } 164 | }, 165 | } 166 | } 167 | 168 | for thread in threads { 169 | thread.join().ok(); 170 | } 171 | 172 | Ok(()) 173 | } 174 | 175 | fn get_response_message(id: RequestId, result: T) -> Message 176 | where 177 | T: serde::Serialize, 178 | { 179 | let result = serde_json::to_value(&result).expect("Error serializing response"); 180 | Message::Response(Response { 181 | id, 182 | result: Some(result), 183 | error: None, 184 | }) 185 | } 186 | 187 | fn cast(req: Request) -> Result<(RequestId, R::Params), ExtractError> 188 | where 189 | R: lsp_types::request::Request, 190 | R::Params: serde::de::DeserializeOwned, 191 | { 192 | req.extract(R::METHOD) 193 | } 194 | -------------------------------------------------------------------------------- /src/php.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use convert_case::{Case, Casing}; 7 | use glob::glob; 8 | use lsp_types::{Position, Range, Url}; 9 | use tree_sitter::{Node, QueryCursor}; 10 | 11 | use crate::{ 12 | m2::M2Path, 13 | queries, 14 | state::{ArcState, State}, 15 | ts::{self, get_range_from_node}, 16 | }; 17 | 18 | #[derive(Debug, Clone)] 19 | pub struct PHPClass { 20 | pub fqn: String, 21 | pub uri: Url, 22 | pub range: Range, 23 | pub methods: HashMap, 24 | pub constants: HashMap, 25 | } 26 | 27 | #[derive(Debug, Clone)] 28 | pub struct PHPMethod { 29 | pub name: String, 30 | pub range: Range, 31 | } 32 | 33 | #[derive(Debug, Clone)] 34 | pub struct PHPConst { 35 | pub name: String, 36 | pub range: Range, 37 | } 38 | 39 | #[derive(Debug, Clone)] 40 | enum M2Module { 41 | Module(String), 42 | Library(String), 43 | FrontTheme(String), 44 | AdminTheme(String), 45 | } 46 | 47 | fn register_param_to_module(param: &str) -> Option { 48 | if param.matches('/').count() == 2 { 49 | if param.starts_with("frontend") { 50 | Some(M2Module::FrontTheme(param.into())) 51 | } else { 52 | Some(M2Module::AdminTheme(param.into())) 53 | } 54 | } else if param.matches('/').count() == 1 { 55 | let mut parts = param.splitn(2, '/'); 56 | let p1 = parts.next()?.to_case(Case::Pascal); 57 | let p2 = parts.next()?; 58 | 59 | if p2.matches('-').count() > 0 { 60 | let mut parts = p2.splitn(2, '-'); 61 | let p2 = parts.next()?.to_case(Case::Pascal); 62 | let p3 = parts.next()?.to_case(Case::Pascal); 63 | Some(M2Module::Library(format!("{}\\{}\\{}", p1, p2, p3))) 64 | } else { 65 | Some(M2Module::Library(format!( 66 | "{}\\{}", 67 | p1, 68 | p2.to_case(Case::Pascal) 69 | ))) 70 | } 71 | } else if param.matches('_').count() == 1 { 72 | let mut parts = param.split('_'); 73 | Some(M2Module::Module(format!( 74 | "{}\\{}", 75 | parts.next()?, 76 | parts.next()? 77 | ))) 78 | } else { 79 | None 80 | } 81 | } 82 | 83 | pub fn update_index(state: &ArcState, path: &PathBuf) { 84 | // if current workspace is magento module 85 | process_glob(state, &path.append(&["registration.php"])); 86 | // if current workspace is magento installation 87 | process_glob( 88 | state, 89 | &path.append(&["vendor", "*", "*", "registration.php"]), 90 | ); // vendor modules / themes 91 | process_glob( 92 | state, 93 | &path.append(&["app", "code", "*", "*", "registration.php"]), 94 | ); // local modules 95 | process_glob( 96 | state, 97 | &path.append(&["app", "design", "*", "*", "*", "registration.php"]), 98 | ); // local themes 99 | process_glob( 100 | state, 101 | &path.append(&[ 102 | "vendor", 103 | "magento", 104 | "magento2-base", 105 | "setup", 106 | "src", 107 | "Magento", 108 | "Setup", 109 | "registration.php", 110 | ]), 111 | ); // magento2-base setup module 112 | } 113 | 114 | pub fn maybe_index_file(state: &mut State, content: &str, file_path: &PathBuf) { 115 | if file_path.to_path_str().ends_with("registration.php") { 116 | update_index_from_registration(state, content, file_path); 117 | } 118 | } 119 | 120 | fn update_index_from_registration(state: &mut State, content: &str, file_path: &Path) { 121 | state.set_source_file(file_path); 122 | let query = queries::php_registration(); 123 | let tree = tree_sitter_parsers::parse(content, "php"); 124 | let mut cursor = QueryCursor::new(); 125 | let matches = cursor.matches(query, tree.root_node(), content.as_bytes()); 126 | for m in matches { 127 | let mod_name = ts::get_node_str(m.captures[1].node, content) 128 | .trim_matches('"') 129 | .trim_matches('\''); 130 | 131 | let mut parent = file_path.to_path_buf(); 132 | parent.pop(); 133 | 134 | state.add_module_path(mod_name, parent.clone()); 135 | 136 | match register_param_to_module(mod_name) { 137 | Some(M2Module::Module(m)) => { 138 | state.add_module(mod_name).add_module_path(m, parent); 139 | } 140 | Some(M2Module::Library(l)) => { 141 | state 142 | .add_module(&l.replace('\\', "_")) 143 | .add_module_path(l, parent); 144 | } 145 | Some(M2Module::FrontTheme(t)) => { 146 | state.add_front_theme_path(t, parent); 147 | } 148 | Some(M2Module::AdminTheme(t)) => { 149 | state.add_admin_theme_path(t, parent); 150 | } 151 | _ => (), 152 | } 153 | } 154 | } 155 | 156 | fn process_glob(state: &ArcState, glob_path: &PathBuf) { 157 | let modules = glob(glob_path.to_path_str()) 158 | .expect("Failed to read glob pattern") 159 | .filter_map(Result::ok); 160 | 161 | for file_path in modules { 162 | if file_path.is_test() { 163 | return; 164 | } 165 | 166 | let content = 167 | std::fs::read_to_string(&file_path).expect("Should have been able to read the file"); 168 | 169 | update_index_from_registration(&mut state.lock(), &content, &file_path); 170 | } 171 | } 172 | 173 | pub fn parse_php_file(file_path: &PathBuf) -> Option { 174 | let content = 175 | std::fs::read_to_string(file_path).expect("Should have been able to read the file"); 176 | let tree = tree_sitter_parsers::parse(&content, "php"); 177 | let query = queries::php_class(); 178 | 179 | let mut cursor = QueryCursor::new(); 180 | let matches = cursor.matches(query, tree.root_node(), content.as_bytes()); 181 | 182 | let mut ns: Option = None; 183 | let mut cls: Option = None; 184 | let mut methods: HashMap = HashMap::new(); 185 | let mut constants: HashMap = HashMap::new(); 186 | 187 | for m in matches { 188 | if m.pattern_index == 0 { 189 | ns = Some(m.captures[0].node); 190 | } 191 | if m.pattern_index == 1 || m.pattern_index == 2 { 192 | cls = Some(m.captures[0].node); 193 | } 194 | if m.pattern_index == 3 { 195 | let method_node = m.captures[1].node; 196 | let method_name = ts::get_node_str(method_node, &content); 197 | if !method_name.is_empty() { 198 | methods.insert( 199 | method_name.into(), 200 | PHPMethod { 201 | name: method_name.into(), 202 | range: get_range_from_node(method_node), 203 | }, 204 | ); 205 | } 206 | } 207 | if m.pattern_index == 4 { 208 | let const_node = m.captures[0].node; 209 | let const_name = const_node.utf8_text(content.as_bytes()).unwrap_or(""); 210 | if !const_name.is_empty() { 211 | constants.insert( 212 | const_name.into(), 213 | PHPConst { 214 | name: const_name.into(), 215 | range: get_range_from_node(const_node), 216 | }, 217 | ); 218 | } 219 | } 220 | } 221 | 222 | if ns.is_none() || cls.is_none() { 223 | return None; 224 | } 225 | 226 | let ns_node = ns.expect("ns is some"); 227 | let cls_node = cls.expect("cls is some"); 228 | let ns_text = ns_node.utf8_text(content.as_bytes()).unwrap_or(""); 229 | let cls_text = cls_node.utf8_text(content.as_bytes()).unwrap_or(""); 230 | 231 | let fqn = ns_text.to_string() + "\\" + cls_text; 232 | if fqn == "\\" { 233 | return None; 234 | } 235 | 236 | let uri = Url::from_file_path(file_path.clone()).expect("Path can not be converted to Url"); 237 | let range = Range { 238 | start: Position { 239 | line: cls_node.start_position().row as u32, 240 | character: cls_node.start_position().column as u32, 241 | }, 242 | end: Position { 243 | line: cls_node.end_position().row as u32, 244 | character: cls_node.end_position().column as u32, 245 | }, 246 | }; 247 | 248 | Some(PHPClass { 249 | fqn, 250 | uri, 251 | range, 252 | methods, 253 | constants, 254 | }) 255 | } 256 | -------------------------------------------------------------------------------- /src/queries.rs: -------------------------------------------------------------------------------- 1 | use std::sync::OnceLock; 2 | 3 | use tree_sitter::{Language, Query}; 4 | 5 | pub static JS_REQUIRE_CONFIG: OnceLock = OnceLock::new(); 6 | pub static JS_ITEM_FROM_POS: OnceLock = OnceLock::new(); 7 | pub static JS_COMPLETION_ITEM_DEFINITION: OnceLock = OnceLock::new(); 8 | 9 | pub static PHP_REGISTRATION: OnceLock = OnceLock::new(); 10 | pub static PHP_CLASS: OnceLock = OnceLock::new(); 11 | 12 | pub static XML_TAG_AT_POS: OnceLock = OnceLock::new(); 13 | pub static XML_CURRENT_POSITION_PATH: OnceLock = OnceLock::new(); 14 | 15 | pub fn js_completion_definition_item() -> &'static Query { 16 | query( 17 | &JS_COMPLETION_ITEM_DEFINITION, 18 | r#" 19 | ( 20 | (identifier) @def (#eq? @def define) 21 | (arguments (array [(string) (ERROR) (binary_expression)] @str)) 22 | ) 23 | "#, 24 | "javascript", 25 | ) 26 | } 27 | 28 | pub fn js_require_config() -> &'static Query { 29 | let map_query = r#" 30 | ( 31 | (identifier) @config 32 | (object (pair [(property_identifier) (string)] @mapkey 33 | (object (pair (object (pair 34 | [(property_identifier) (string)] @key + (string) @val 35 | )))) 36 | )) 37 | 38 | (#eq? @config config) 39 | (#match? @mapkey "[\"']?map[\"']?") 40 | ) 41 | "#; 42 | 43 | let mixins_query = r#" 44 | ( 45 | (identifier) @config 46 | (object (pair [(property_identifier) (string)] ; @configkey 47 | (object (pair [(property_identifier) (string)] @mixins 48 | (object (pair [(property_identifier) (string)] @key 49 | (object (pair [(property_identifier) (string)] @val (true))) 50 | )) 51 | )) 52 | )) 53 | 54 | (#match? @config config) 55 | ; (#match? @configkey "[\"']?config[\"']?") 56 | (#match? @mixins "[\"']?mixins[\"']?") 57 | ) 58 | "#; 59 | 60 | let path_query = r#" 61 | ( 62 | (identifier) @config 63 | (object (pair [(property_identifier) (string)] @pathskey 64 | (((object (pair 65 | [(property_identifier) (string)] @key + (string) @val 66 | )))) 67 | )) 68 | 69 | (#eq? @config config) 70 | (#match? @pathskey "[\"']?paths[\"']?") 71 | ) 72 | "#; 73 | 74 | let query_string = format!("{} {} {}", map_query, path_query, mixins_query); 75 | query(&JS_REQUIRE_CONFIG, &query_string, "javascript") 76 | } 77 | 78 | pub fn php_registration() -> &'static Query { 79 | query( 80 | &PHP_REGISTRATION, 81 | r#" 82 | (scoped_call_expression 83 | (name) @reg (#eq? @reg register) 84 | (arguments 85 | (string) @module_name 86 | ) 87 | ) 88 | "#, 89 | "php", 90 | ) 91 | } 92 | 93 | pub fn php_class() -> &'static Query { 94 | query( 95 | &PHP_CLASS, 96 | r#" 97 | (namespace_definition (namespace_name) @namespace) ; pattern: 0 98 | (class_declaration (name) @class) ; pattern: 1 99 | (interface_declaration (name) @class) ; pattern: 2 100 | ((method_declaration (visibility_modifier) 101 | @_vis (name) @name) (#eq? @_vis "public")) ; pattern: 3 102 | (const_element (name) @const) ; pattern: 4 103 | "#, 104 | "php", 105 | ) 106 | } 107 | 108 | pub fn xml_tag_at_pos() -> &'static Query { 109 | query( 110 | &XML_TAG_AT_POS, 111 | r#" 112 | (element 113 | (start_tag 114 | (tag_name) @tag_name 115 | (attribute 116 | (attribute_name) @attr_name 117 | (quoted_attribute_value (attribute_value) @attr_val)? 118 | )? 119 | ) @tag 120 | (text)? @text 121 | ) 122 | (element 123 | (self_closing_tag 124 | (tag_name) @tag_name 125 | (attribute 126 | (attribute_name) @attr_name 127 | (quoted_attribute_value (attribute_value) @attr_val)? 128 | ) 129 | ) @tag 130 | ) 131 | "#, 132 | "html", 133 | ) 134 | } 135 | 136 | pub fn xml_current_position_path() -> &'static Query { 137 | query( 138 | &XML_CURRENT_POSITION_PATH, 139 | r#" 140 | (tag_name) @tag_name 141 | (attribute_value) @attr_val 142 | (text) @text 143 | ((quoted_attribute_value) @q_attr_val (#eq? @q_attr_val "\"\"")) 144 | ((quoted_attribute_value) @q_attr_val (#eq? @q_attr_val "\"")) 145 | ">" @tag_end 146 | "#, 147 | "html", 148 | ) 149 | } 150 | 151 | pub fn js_item_from_pos() -> &'static Query { 152 | query( 153 | &JS_ITEM_FROM_POS, 154 | r#" 155 | (string) @str 156 | "#, 157 | "javascript", 158 | ) 159 | } 160 | 161 | fn query(static_query: &'static OnceLock, query: &str, lang: &str) -> &'static Query { 162 | static_query.get_or_init(|| { 163 | Query::new(get_language(lang), query) 164 | .map_err(|e| eprintln!("Error creating query: {:?}", e)) 165 | .expect("Error creating query") 166 | }) 167 | } 168 | 169 | fn get_language(lang: &str) -> Language { 170 | tree_sitter_parsers::parse("", lang).language() 171 | } 172 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | path::{Path, PathBuf}, 4 | sync::Arc, 5 | thread::{spawn, JoinHandle}, 6 | time::SystemTime, 7 | }; 8 | 9 | use lsp_types::Position; 10 | use parking_lot::Mutex; 11 | 12 | use crate::{ 13 | js, 14 | m2::{M2Area, M2Item, M2Path}, 15 | php, xml, 16 | }; 17 | 18 | trait HashMapId { 19 | fn id(&self) -> usize; 20 | } 21 | 22 | impl HashMapId for M2Area { 23 | fn id(&self) -> usize { 24 | match self { 25 | Self::Frontend => 0, 26 | Self::Adminhtml => 1, 27 | Self::Base => 2, 28 | } 29 | } 30 | } 31 | 32 | #[derive(Debug, Clone, PartialEq, Eq)] 33 | enum Trackee { 34 | Module(String), 35 | ModulePath(String), 36 | JsMap(M2Area, String), 37 | JsMixin(M2Area, String), 38 | JsPaths(M2Area, String), 39 | Themes(M2Area, String), 40 | } 41 | 42 | #[derive(Debug, Clone, PartialEq, Eq)] 43 | struct TrackingList(HashMap>); 44 | 45 | impl TrackingList { 46 | pub fn new() -> Self { 47 | Self(HashMap::new()) 48 | } 49 | 50 | pub fn track(&mut self, source_path: &Path, trackee: Trackee) { 51 | self.0 52 | .entry(source_path.into()) 53 | .or_insert_with(Vec::new) 54 | .push(trackee); 55 | } 56 | 57 | pub fn maybe_track(&mut self, source_path: Option<&PathBuf>, trackee: Trackee) { 58 | if let Some(source_path) = source_path { 59 | self.track(source_path, trackee); 60 | } 61 | } 62 | 63 | pub fn untrack(&mut self, source_path: &Path) -> Option> { 64 | self.0.remove(source_path) 65 | } 66 | } 67 | 68 | #[derive(Debug, Clone, PartialEq, Eq)] 69 | pub struct State { 70 | source_file: Option, 71 | track_entities: TrackingList, 72 | buffers: HashMap, 73 | modules: Vec, 74 | module_paths: HashMap, 75 | front_themes: HashMap, 76 | admin_themes: HashMap, 77 | js_maps: [HashMap; 3], 78 | js_mixins: [HashMap>; 3], 79 | js_paths: [HashMap; 3], 80 | workspaces: Vec, 81 | } 82 | 83 | #[allow(clippy::module_name_repetitions)] 84 | pub type ArcState = Arc>; 85 | 86 | impl State { 87 | pub fn new() -> Self { 88 | Self { 89 | source_file: None, 90 | track_entities: TrackingList::new(), 91 | buffers: HashMap::new(), 92 | modules: vec![], 93 | module_paths: HashMap::new(), 94 | front_themes: HashMap::new(), 95 | admin_themes: HashMap::new(), 96 | js_maps: [HashMap::new(), HashMap::new(), HashMap::new()], 97 | js_mixins: [HashMap::new(), HashMap::new(), HashMap::new()], 98 | js_paths: [HashMap::new(), HashMap::new(), HashMap::new()], 99 | workspaces: vec![], 100 | } 101 | } 102 | 103 | pub fn set_source_file(&mut self, path: &Path) { 104 | self.source_file = Some(path.to_owned()); 105 | } 106 | 107 | pub fn clear_from_source(&mut self, path: &Path) { 108 | if let Some(list) = self.track_entities.untrack(path) { 109 | for trackee in list { 110 | match trackee { 111 | Trackee::JsMap(area, name) => { 112 | self.js_maps[area.id()].remove(&name); 113 | } 114 | Trackee::JsMixin(area, name) => { 115 | self.js_mixins[area.id()].remove(&name); 116 | } 117 | Trackee::JsPaths(area, name) => { 118 | self.js_paths[area.id()].remove(&name); 119 | } 120 | Trackee::Module(module) => { 121 | self.modules.retain(|m| m != &module); 122 | } 123 | Trackee::ModulePath(module) => { 124 | self.module_paths.remove(&module); 125 | } 126 | Trackee::Themes(area, module) => match area { 127 | M2Area::Frontend => { 128 | self.front_themes.remove(&module); 129 | } 130 | M2Area::Adminhtml => { 131 | self.admin_themes.remove(&module); 132 | } 133 | M2Area::Base => { 134 | self.front_themes.remove(&module); 135 | self.admin_themes.remove(&module); 136 | } 137 | }, 138 | } 139 | } 140 | } 141 | } 142 | 143 | pub fn set_file(&mut self, path: &Path, content: S) 144 | where 145 | S: Into, 146 | { 147 | let content = content.into(); 148 | self.clear_from_source(path); 149 | js::maybe_index_file(self, &content, &path.to_owned()); 150 | php::maybe_index_file(self, &content, &path.to_owned()); 151 | 152 | self.buffers.insert(path.to_owned(), content); 153 | } 154 | 155 | pub fn get_file(&self, path: &PathBuf) -> Option<&String> { 156 | self.buffers.get(path) 157 | } 158 | 159 | pub fn del_file(&mut self, path: &PathBuf) { 160 | self.buffers.remove(path); 161 | } 162 | 163 | pub fn get_modules(&self) -> Vec { 164 | let mut modules = self.modules.clone(); 165 | modules.sort_unstable(); 166 | modules.dedup(); 167 | modules 168 | } 169 | 170 | pub fn get_module_class_prefixes(&self) -> Vec { 171 | self.get_modules() 172 | .iter() 173 | .map(|m| m.replace('_', "\\")) 174 | .collect() 175 | } 176 | 177 | pub fn get_module_path(&self, module: &str) -> Option { 178 | self.module_paths.get(module).cloned() 179 | } 180 | 181 | pub fn add_module(&mut self, module: &str) -> &mut Self { 182 | self.track_entities 183 | .maybe_track(self.source_file.as_ref(), Trackee::Module(module.into())); 184 | 185 | self.modules.push(module.into()); 186 | self 187 | } 188 | 189 | pub fn add_module_path(&mut self, module: S, path: PathBuf) -> &mut Self 190 | where 191 | S: Into, 192 | { 193 | let module = module.into(); 194 | self.track_entities.maybe_track( 195 | self.source_file.as_ref(), 196 | Trackee::ModulePath(module.clone()), 197 | ); 198 | 199 | self.module_paths.insert(module, path); 200 | self 201 | } 202 | 203 | pub fn add_admin_theme_path(&mut self, name: S, path: PathBuf) 204 | where 205 | S: Into, 206 | { 207 | let name = name.into(); 208 | self.track_entities.maybe_track( 209 | self.source_file.as_ref(), 210 | Trackee::Themes(M2Area::Adminhtml, name.clone()), 211 | ); 212 | 213 | self.admin_themes.insert(name, path); 214 | } 215 | 216 | pub fn add_front_theme_path(&mut self, name: S, path: PathBuf) 217 | where 218 | S: Into, 219 | { 220 | let name = name.into(); 221 | self.track_entities.maybe_track( 222 | self.source_file.as_ref(), 223 | Trackee::Themes(M2Area::Frontend, name.clone()), 224 | ); 225 | 226 | self.front_themes.insert(name, path); 227 | } 228 | 229 | pub fn get_component_map(&self, name: &str, area: &M2Area) -> Option<&String> { 230 | self.js_maps[area.id()].get(name) 231 | } 232 | 233 | pub fn get_component_maps_for_area(&self, area: &M2Area) -> Vec { 234 | self.js_maps[area.id()] 235 | .keys() 236 | .map(ToString::to_string) 237 | .collect() 238 | } 239 | 240 | pub fn add_component_map(&mut self, name: S, val: S, area: &M2Area) 241 | where 242 | S: Into, 243 | { 244 | let name = name.into(); 245 | self.track_entities.maybe_track( 246 | self.source_file.as_ref(), 247 | Trackee::JsMap(area.clone(), name.clone()), 248 | ); 249 | 250 | self.js_maps[area.id()].insert(name, val.into()); 251 | } 252 | 253 | pub fn add_component_mixin(&mut self, name: S, val: S, area: &M2Area) 254 | where 255 | S: Into, 256 | { 257 | let name = name.into(); 258 | let val = val.into(); 259 | 260 | self.track_entities.maybe_track( 261 | self.source_file.as_ref(), 262 | Trackee::JsMixin(area.clone(), name.clone()), 263 | ); 264 | 265 | self.js_mixins[area.id()] 266 | .entry(name) 267 | .or_insert_with(Vec::new) 268 | .push(val); 269 | } 270 | 271 | pub fn get_component_mixins_for_area(&self, name: S, area: &M2Area) -> Vec 272 | where 273 | S: Into, 274 | { 275 | let empty_path = Path::new(""); 276 | self.js_mixins[area.id()] 277 | .get(&name.into()) 278 | .unwrap_or(&vec![]) 279 | .iter() 280 | .filter_map(|mod_string| js::text_to_component(self, mod_string, empty_path)) 281 | .collect() 282 | } 283 | 284 | pub fn add_component_path(&mut self, name: S, val: S, area: &M2Area) 285 | where 286 | S: Into, 287 | { 288 | let name = name.into(); 289 | self.track_entities.maybe_track( 290 | self.source_file.as_ref(), 291 | Trackee::JsPaths(area.clone(), name.clone()), 292 | ); 293 | 294 | self.js_paths[area.id()].insert(name, val.into()); 295 | } 296 | 297 | pub fn get_component_path(&self, name: &str, area: &M2Area) -> Option<&String> { 298 | self.js_paths[area.id()].get(name) 299 | } 300 | 301 | pub fn get_component_paths_for_area(&self, area: &M2Area) -> Vec { 302 | self.js_paths[area.id()] 303 | .keys() 304 | .map(ToString::to_string) 305 | .collect() 306 | } 307 | 308 | pub fn list_front_themes_paths(&self) -> Vec<&PathBuf> { 309 | self.front_themes.values().collect::>() 310 | } 311 | 312 | pub fn list_admin_themes_paths(&self) -> Vec<&PathBuf> { 313 | self.admin_themes.values().collect::>() 314 | } 315 | 316 | pub fn list_themes_paths(&self, area: &M2Area) -> Vec<&PathBuf> { 317 | match area { 318 | M2Area::Base => self 319 | .admin_themes 320 | .values() 321 | .chain(self.front_themes.values()) 322 | .collect::>(), 323 | M2Area::Adminhtml => self.admin_themes.values().collect::>(), 324 | M2Area::Frontend => self.front_themes.values().collect::>(), 325 | } 326 | } 327 | 328 | pub fn workspace_paths(&self) -> Vec { 329 | self.workspaces.clone() 330 | } 331 | 332 | pub fn add_workspace_path(&mut self, path: &Path) { 333 | self.workspaces.push(path.to_path_buf()); 334 | } 335 | 336 | pub fn has_workspace_path(&mut self, path: &Path) -> bool { 337 | self.workspaces.contains(&path.to_path_buf()) 338 | } 339 | 340 | pub fn get_item_from_position(&self, path: &PathBuf, pos: Position) -> Option { 341 | match path.get_ext().as_str() { 342 | "js" => js::get_item_from_position(self, path, pos), 343 | "xml" => xml::get_item_from_position(self, path, pos), 344 | _ => None, 345 | } 346 | } 347 | 348 | pub fn into_arc(self) -> ArcState { 349 | Arc::new(Mutex::new(self)) 350 | } 351 | 352 | pub fn update_index(arc_state: &ArcState, path: &Path) -> Vec> { 353 | let mut state = arc_state.lock(); 354 | if state.has_workspace_path(path) { 355 | vec![] 356 | } else { 357 | state.add_workspace_path(path); 358 | vec![ 359 | spawn_index(arc_state, path, php::update_index, "PHP Indexing"), 360 | spawn_index(arc_state, path, js::update_index, "JS Indexing"), 361 | ] 362 | } 363 | } 364 | 365 | pub fn split_class_to_path_and_suffix(&self, class: &str) -> Option<(PathBuf, Vec)> { 366 | let mut parts = class.split('\\').collect::>(); 367 | let mut suffix = vec![]; 368 | 369 | while let Some(part) = parts.pop() { 370 | suffix.push(part.to_string()); 371 | let prefix = parts.join("\\"); 372 | let module_path = self.get_module_path(&prefix); 373 | match module_path { 374 | Some(mod_path) => { 375 | suffix.reverse(); 376 | return Some((mod_path, suffix)); 377 | } 378 | None => continue, 379 | } 380 | } 381 | None 382 | } 383 | } 384 | 385 | fn spawn_index( 386 | state: &ArcState, 387 | path: &Path, 388 | callback: fn(&ArcState, &PathBuf), 389 | msg: &str, 390 | ) -> JoinHandle<()> { 391 | let state = Arc::clone(state); 392 | let path = path.to_path_buf(); 393 | let msg = msg.to_owned(); 394 | 395 | spawn(move || { 396 | eprintln!("Start {}", msg); 397 | let index_start = SystemTime::now(); 398 | callback(&state, &path); 399 | index_start.elapsed().map_or_else( 400 | |_| eprintln!("{} done", msg), 401 | |d| eprintln!("{} done in {:?}", msg, d), 402 | ); 403 | }) 404 | } 405 | -------------------------------------------------------------------------------- /src/ts.rs: -------------------------------------------------------------------------------- 1 | use lsp_types::{Position, Range}; 2 | use tree_sitter::Node; 3 | 4 | pub fn get_range_from_node(node: Node) -> Range { 5 | Range { 6 | start: Position { 7 | line: node.start_position().row as u32, 8 | character: node.start_position().column as u32, 9 | }, 10 | end: Position { 11 | line: node.end_position().row as u32, 12 | character: node.end_position().column as u32, 13 | }, 14 | } 15 | } 16 | 17 | pub fn get_node_text_before_pos(node: Node, content: &str, pos: Position) -> String { 18 | let text = node.utf8_text(content.as_bytes()).unwrap_or(""); 19 | 20 | let node_start_pos = node.start_position(); 21 | let node_end_pos = node.end_position(); 22 | 23 | let text = if node_end_pos.row == node_start_pos.row { 24 | text.to_string() 25 | } else { 26 | let take_lines = pos.line as usize - node_start_pos.row; 27 | text.split('\n') 28 | .take(take_lines + 1) 29 | .collect::>() 30 | .join("\n") 31 | }; 32 | 33 | if pos.line as usize == node_start_pos.row { 34 | let end = pos.character as usize - node_start_pos.column; 35 | text.chars().take(end).collect::() 36 | } else { 37 | let mut trimed = false; 38 | let mut lines = text 39 | .split('\n') 40 | .rev() 41 | .map(|line| { 42 | if trimed { 43 | line.to_owned() 44 | } else { 45 | trimed = true; 46 | line.chars() 47 | .take(pos.character as usize) 48 | .collect::() 49 | } 50 | }) 51 | .collect::>(); 52 | 53 | lines.reverse(); 54 | lines.join("\n") 55 | } 56 | } 57 | 58 | pub fn get_node_str<'a>(node: Node, content: &'a str) -> &'a str { 59 | node.utf8_text(content.as_bytes()) 60 | .unwrap_or("") 61 | .trim_matches('\\') 62 | } 63 | 64 | pub fn node_at_position(node: Node, pos: Position) -> bool { 65 | let start = node.start_position(); 66 | let end = node.end_position(); 67 | if pos.line < start.row as u32 || pos.line > end.row as u32 { 68 | return false; 69 | } 70 | if pos.line == start.row as u32 && pos.character < start.column as u32 { 71 | return false; 72 | } 73 | if pos.line == end.row as u32 && pos.character > end.column as u32 { 74 | return false; 75 | } 76 | true 77 | } 78 | 79 | pub fn node_last_child(node: Node) -> Option { 80 | let children_count = node.child_count(); 81 | node.child(children_count - 1) 82 | } 83 | -------------------------------------------------------------------------------- /src/xml.rs: -------------------------------------------------------------------------------- 1 | use lsp_types::{Position, Range}; 2 | use std::{collections::HashMap, path::PathBuf}; 3 | use tree_sitter::{Node, QueryCursor}; 4 | 5 | use crate::{ 6 | js, 7 | m2::{self, M2Item, M2Path}, 8 | queries, 9 | state::State, 10 | ts::{get_node_str, get_node_text_before_pos, node_at_position, node_last_child}, 11 | }; 12 | 13 | #[allow(clippy::module_name_repetitions)] 14 | #[derive(Debug, Clone, PartialEq, Eq)] 15 | enum XmlPart { 16 | Text, 17 | Attribute(String), 18 | None, 19 | } 20 | 21 | #[allow(clippy::module_name_repetitions)] 22 | #[derive(Debug, Clone, PartialEq, Eq)] 23 | pub struct XmlCompletion { 24 | pub path: String, 25 | pub text: String, 26 | pub range: Range, 27 | pub tag: Option, 28 | } 29 | 30 | impl XmlCompletion { 31 | pub fn match_path(&self, text: &str) -> bool { 32 | self.path.ends_with(text) 33 | } 34 | 35 | pub fn attribute_eq(&self, attr: &str, val: &str) -> bool { 36 | self.tag.as_ref().map_or(false, |t| { 37 | t.attributes.get(attr).map_or(false, |v| v == val) 38 | }) 39 | } 40 | 41 | pub fn attribute_in(&self, attr: &str, vals: &[&str]) -> bool { 42 | self.tag.as_ref().map_or(false, |t| { 43 | t.attributes 44 | .get(attr) 45 | .map_or(false, |v| vals.contains(&v.as_ref())) 46 | }) 47 | } 48 | } 49 | 50 | #[allow(clippy::module_name_repetitions)] 51 | #[derive(Debug, Clone, PartialEq, Eq)] 52 | pub struct XmlTag { 53 | name: String, 54 | attributes: HashMap, 55 | text: String, 56 | hover_on: XmlPart, 57 | } 58 | 59 | impl XmlTag { 60 | fn new() -> Self { 61 | Self { 62 | name: String::new(), 63 | attributes: HashMap::new(), 64 | text: String::new(), 65 | hover_on: XmlPart::None, 66 | } 67 | } 68 | } 69 | 70 | pub fn get_current_position_path(content: &str, pos: Position) -> Option { 71 | let tree = tree_sitter_parsers::parse(content, "html"); 72 | let query = queries::xml_current_position_path(); 73 | let mut cursor = QueryCursor::new(); 74 | let captures = cursor.captures(query, tree.root_node(), content.as_bytes()); 75 | for (m, i) in captures { 76 | let node = m.captures[i].node; 77 | if node_at_position(node, pos) { 78 | let mut text = get_node_text_before_pos(node, content, pos); 79 | if node.kind() == ">" && text.is_empty() { 80 | // this is end of tag node but if text is empty 81 | // the tag is not really closed yet, just should be 82 | continue; 83 | } 84 | let mut start_col = node.start_position().column as u32; 85 | if node.kind() == "quoted_attribute_value" { 86 | if text == "\"" { 87 | start_col += 1; 88 | text = String::new(); 89 | } else { 90 | continue; 91 | } 92 | } 93 | if node.kind() == ">" && text == ">" { 94 | start_col += 1; 95 | text = String::new(); 96 | } 97 | let path = node_to_path(node, content)?; 98 | let tag = node_to_tag(node, content); 99 | let range = Range { 100 | start: Position { 101 | line: node.start_position().row as u32, 102 | character: start_col, 103 | }, 104 | end: pos, 105 | }; 106 | return Some(XmlCompletion { 107 | path, 108 | text, 109 | range, 110 | tag, 111 | }); 112 | } 113 | } 114 | None 115 | } 116 | 117 | // fn node_dive_in<'a>(node: Option>, list: &mut Vec>) { 118 | // if node.is_none() { 119 | // return; 120 | // } 121 | // if let Some(n) = node { 122 | // list.push(n.clone()); 123 | // node_dive_in(n.child(0), list); 124 | // node_dive_in(n.next_sibling(), list); 125 | // } 126 | // } 127 | 128 | // fn node_walk_forward(node: Node) -> Vec { 129 | // let mut list = vec![]; 130 | // node_dive_in(Some(node), &mut list); 131 | // list 132 | // } 133 | 134 | fn node_walk_back(node: Node) -> Option { 135 | node.prev_sibling().map_or_else(|| node.parent(), Some) 136 | } 137 | 138 | fn node_to_tag(node: Node, content: &str) -> Option { 139 | let mut current_node = node; 140 | while let Some(node) = node_walk_back(current_node) { 141 | current_node = node; 142 | if node.kind() == "self_closing_tag" || node.kind() == "start_tag" { 143 | let text = get_node_str(node, content); 144 | if text.chars().last()? != '>' { 145 | return None; 146 | } 147 | return get_xml_tag_at_pos( 148 | text, 149 | Position { 150 | line: 0, 151 | character: 0, 152 | }, 153 | ); 154 | } 155 | } 156 | None 157 | } 158 | 159 | fn node_to_path(node: Node, content: &str) -> Option { 160 | let mut path = vec![]; 161 | let mut current_node = node; 162 | let mut has_attr = false; 163 | let mut node_ids = vec![]; 164 | let mut on_text_node = false; 165 | let mut pop_last = false; 166 | let text = get_node_str(node, content); 167 | if node.kind() == ">" && text == ">" { 168 | on_text_node = true; 169 | } 170 | 171 | if node.kind() == "text" && node.prev_sibling().is_some() { 172 | if let Some(last) = node_last_child(node.prev_sibling()?) { 173 | if last.kind() == ">" && get_node_str(last, content) == ">" { 174 | on_text_node = true; 175 | } 176 | } 177 | } 178 | 179 | while let Some(node) = node_walk_back(current_node) { 180 | current_node = node; 181 | if node_ids.contains(&node.id()) { 182 | continue; 183 | } 184 | node_ids.push(node.id()); 185 | if node.kind() == "attribute_name" && !has_attr { 186 | let attr_name = get_node_str(node, content); 187 | has_attr = true; 188 | path.push((node.kind(), attr_name)); 189 | } else if node.kind() == "self_closing_tag" || node.kind() == "start_tag" { 190 | if node.child(0).is_some() { 191 | if node_ids.contains(&node.child(0)?.id()) { 192 | continue; 193 | } 194 | path.push((node.kind(), get_node_str(node.child(1)?, content))); 195 | } 196 | } else if node.kind() == "tag_name" && node.parent()?.kind() != "end_tag" { 197 | path.push((node.kind(), get_node_str(node, content))); 198 | } else if node.kind() == "tag_name" && node.parent()?.kind() == "end_tag" { 199 | pop_last = true; 200 | on_text_node = false; 201 | } 202 | } 203 | path.reverse(); 204 | if pop_last { 205 | path.pop(); 206 | } 207 | if on_text_node { 208 | path.push(("text", "[$text]")); 209 | } 210 | let mut result = String::new(); 211 | for (kind, name) in path { 212 | match kind { 213 | "text" => result.push_str(name), 214 | "attribute_name" => { 215 | result.push_str("[@"); 216 | result.push_str(name); 217 | result.push(']'); 218 | } 219 | "self_closing_tag" | "start_tag" | "tag_name" => { 220 | result.push_str(&format!("/{}", name)); 221 | } 222 | 223 | _ => (), 224 | } 225 | } 226 | Some(result) 227 | } 228 | 229 | pub fn get_item_from_position(state: &State, path: &PathBuf, pos: Position) -> Option { 230 | let content = state.get_file(path)?; 231 | get_item_from_pos(state, content, path, pos) 232 | } 233 | 234 | fn get_item_from_pos( 235 | state: &State, 236 | content: &str, 237 | path: &PathBuf, 238 | pos: Position, 239 | ) -> Option { 240 | let tag = get_xml_tag_at_pos(content, pos)?; 241 | 242 | match tag.hover_on { 243 | XmlPart::Attribute(ref attr_name) => match attr_name.as_str() { 244 | "method" | "instance" | "class" => try_method_item_from_tag(&tag).or_else(|| { 245 | m2::try_any_item_from_str(tag.attributes.get(attr_name)?, &path.get_area()) 246 | }), 247 | "template" => { 248 | m2::try_phtml_item_from_str(tag.attributes.get(attr_name)?, &path.get_area()) 249 | } 250 | _ => m2::try_any_item_from_str(tag.attributes.get(attr_name)?, &path.get_area()), 251 | }, 252 | XmlPart::Text => { 253 | let text = tag.text.trim_matches('\\'); 254 | let empty = String::new(); 255 | let xsi_type = tag.attributes.get("xsi:type").unwrap_or(&empty); 256 | 257 | match xsi_type.as_str() { 258 | "object" => Some(m2::get_class_item_from_str(text)), 259 | "init_parameter" => m2::try_const_item_from_str(text), 260 | "string" => { 261 | if tag.attributes.get("name").is_some_and(|s| s == "component") { 262 | js::text_to_component(state, text, path) 263 | } else { 264 | m2::try_any_item_from_str(text, &path.get_area()) 265 | } 266 | } 267 | _ => m2::try_any_item_from_str(text, &path.get_area()), 268 | } 269 | } 270 | XmlPart::None => None, 271 | } 272 | } 273 | 274 | fn get_xml_tag_at_pos(content: &str, pos: Position) -> Option { 275 | let tree = tree_sitter_parsers::parse(content, "html"); 276 | let query = queries::xml_tag_at_pos(); 277 | 278 | let mut cursor = QueryCursor::new(); 279 | let captures = cursor.captures(query, tree.root_node(), content.as_bytes()); 280 | 281 | let mut last_attribute_name = ""; 282 | let mut last_tag_id: Option = None; 283 | let mut tag = XmlTag::new(); 284 | 285 | for (m, i) in captures { 286 | let first = m.captures[0].node; // always (self)opening tag 287 | let last = m.captures[m.captures.len() - 1].node; 288 | if !node_at_position(first, pos) && !node_at_position(last, pos) { 289 | continue; 290 | } 291 | let id = m.captures[0].node.id(); // id of tag name 292 | if last_tag_id.is_none() || last_tag_id != Some(id) { 293 | last_tag_id = Some(id); 294 | tag = XmlTag::new(); 295 | } 296 | let node = m.captures[i].node; 297 | let hovered = node_at_position(node, pos); 298 | match node.kind() { 299 | "tag_name" => { 300 | tag.name = get_node_str(node, content).into(); 301 | } 302 | "attribute_name" => { 303 | last_attribute_name = get_node_str(node, content); 304 | tag.attributes 305 | .insert(last_attribute_name.into(), String::new()); 306 | } 307 | "attribute_value" => { 308 | tag.attributes.insert( 309 | last_attribute_name.into(), 310 | get_node_str(node, content).into(), 311 | ); 312 | if hovered { 313 | tag.hover_on = XmlPart::Attribute(last_attribute_name.into()); 314 | } 315 | } 316 | "text" => { 317 | tag.text = get_node_str(node, content).into(); 318 | if hovered { 319 | tag.hover_on = XmlPart::Text; 320 | } 321 | } 322 | _ => (), 323 | } 324 | } 325 | 326 | if tag.name.is_empty() { 327 | return None; 328 | } 329 | 330 | Some(tag) 331 | } 332 | 333 | fn try_method_item_from_tag(tag: &XmlTag) -> Option { 334 | if tag.attributes.get("instance").is_some() && tag.attributes.get("method").is_some() { 335 | Some(M2Item::Method( 336 | tag.attributes.get("instance")?.into(), 337 | tag.attributes.get("method")?.into(), 338 | )) 339 | } else if tag.attributes.get("class").is_some() && tag.attributes.get("method").is_some() { 340 | Some(M2Item::Method( 341 | tag.attributes.get("class")?.into(), 342 | tag.attributes.get("method")?.into(), 343 | )) 344 | } else { 345 | None 346 | } 347 | } 348 | 349 | #[cfg(test)] 350 | mod test { 351 | use super::*; 352 | use std::path::PathBuf; 353 | 354 | fn get_position_from_test_xml(xml: &str) -> Position { 355 | let mut character = 0; 356 | let mut line = 0; 357 | for l in xml.lines() { 358 | if l.contains('|') { 359 | character = l.find('|').expect("Test has to have a | character") as u32; 360 | break; 361 | } 362 | line += 1; 363 | } 364 | Position { line, character } 365 | } 366 | 367 | fn get_test_position_path(xml: &str) -> Option { 368 | let pos = get_position_from_test_xml(xml); 369 | get_current_position_path(&xml.replace('|', ""), pos) 370 | } 371 | 372 | fn get_test_item_from_pos(xml: &str, path: &str) -> Option { 373 | let win_path = format!("c:{}", path.replace('/', "\\")); 374 | let pos = get_position_from_test_xml(xml); 375 | let uri = PathBuf::from(if cfg!(windows) { &win_path } else { path }); 376 | let state = State::new(); 377 | get_item_from_pos(&state, &xml.replace('|', ""), &uri, pos) 378 | } 379 | 380 | fn get_test_xml_tag_at_pos(xml: &str) -> Option { 381 | let pos = get_position_from_test_xml(xml); 382 | get_xml_tag_at_pos(&xml.replace('|', ""), pos) 383 | } 384 | 385 | #[test] 386 | fn test_get_item_from_pos_class_in_tag_text() { 387 | let item = get_test_item_from_pos(r#"|A\B\C"#, "/a/b/c"); 388 | 389 | assert_eq!(item, Some(M2Item::Class("A\\B\\C".into()))); 390 | } 391 | 392 | #[test] 393 | fn test_get_item_from_pos_template_in_tag_attribute() { 394 | let item = get_test_item_from_pos( 395 | r#""#, 396 | "/a/design/adminhtml/c", 397 | ); 398 | assert_eq!( 399 | item, 400 | Some(M2Item::AdminPhtml( 401 | "Some_Module".into(), 402 | "path/to/file.phtml".into() 403 | )) 404 | ); 405 | } 406 | 407 | #[test] 408 | fn test_get_item_from_pos_frontend_template_in_tag_attribute() { 409 | let item = get_test_item_from_pos( 410 | r#""#, 411 | "/a/view/frontend/c", 412 | ); 413 | assert_eq!( 414 | item, 415 | Some(M2Item::FrontPhtml( 416 | "Some_Module".into(), 417 | "path/to/file.phtml".into() 418 | )) 419 | ); 420 | } 421 | 422 | #[test] 423 | fn test_get_item_from_pos_method_in_job_tag_attribute() { 424 | let item = get_test_item_from_pos( 425 | r#""#, 426 | "/a/a/c", 427 | ); 428 | assert_eq!( 429 | item, 430 | Some(M2Item::Method("A\\B\\C".into(), "metHod".into())) 431 | ); 432 | } 433 | 434 | #[test] 435 | fn test_get_item_from_pos_method_in_service_tag_attribute() { 436 | let item = get_test_item_from_pos( 437 | r#""#, 438 | "/a/a/c", 439 | ); 440 | assert_eq!( 441 | item, 442 | Some(M2Item::Method("A\\B\\C".into(), "metHod".into())) 443 | ); 444 | } 445 | 446 | #[test] 447 | fn test_get_item_from_pos_class_in_service_tag_attribute() { 448 | let item = get_test_item_from_pos( 449 | r#"xx"#, 450 | "/a/a/c", 451 | ); 452 | assert_eq!( 453 | item, 454 | Some(M2Item::Method("A\\B\\C".into(), "metHod".into())) 455 | ); 456 | } 457 | 458 | #[test] 459 | fn test_get_item_from_pos_attribute_in_tag_with_method() { 460 | let item = get_test_item_from_pos( 461 | r#"xx"#, 462 | "/a/a/c", 463 | ); 464 | assert_eq!(item, Some(M2Item::Class("A\\B\\C".into()))); 465 | } 466 | 467 | #[test] 468 | fn test_get_item_from_pos_class_in_text_in_tag() { 469 | let item = get_test_item_from_pos(r#"|A\B\C"#, "/a/a/c"); 470 | assert_eq!(item, Some(M2Item::Class("A\\B\\C".into()))); 471 | } 472 | 473 | #[test] 474 | fn test_get_item_from_pos_const_in_text_in_tag() { 475 | let item = get_test_item_from_pos( 476 | r#"\|A\B\C::CONST_ANT"#, 477 | "/a/a/c", 478 | ); 479 | assert_eq!( 480 | item, 481 | Some(M2Item::Const("A\\B\\C".into(), "CONST_ANT".into())) 482 | ); 483 | } 484 | 485 | #[test] 486 | fn test_get_item_from_pos_template_in_text_in_tag() { 487 | let item = get_test_item_from_pos( 488 | r#"Some_Module::fi|le.phtml"#, 489 | "/a/view/adminhtml/c", 490 | ); 491 | assert_eq!( 492 | item, 493 | Some(M2Item::AdminPhtml( 494 | "Some_Module".into(), 495 | "file.phtml".into() 496 | )) 497 | ); 498 | } 499 | 500 | #[test] 501 | fn test_get_item_from_pos_method_attribute_in_tag() { 502 | let item = get_test_item_from_pos( 503 | r#"xx"#, 504 | "/a/a/c", 505 | ); 506 | assert_eq!(item, None) 507 | } 508 | 509 | #[test] 510 | fn test_should_get_most_inner_tag_from_nested() { 511 | let item = get_test_item_from_pos( 512 | r#" 513 | 514 | 515 | 516 | Some\Cl|ass\Name 517 | multiselect 518 | select 519 | \\A\\B\\C 520 | 521 | 522 | 523 | "#, 524 | "/a/a/c", 525 | ); 526 | assert_eq!(item, Some(M2Item::Class("Some\\Class\\Name".into()))) 527 | } 528 | 529 | #[test] 530 | fn test_should_get_class_from_class_attribute_of_block_tag() { 531 | let item = get_test_item_from_pos( 532 | r#" 533 | 534 | "#, 535 | "/a/a/c", 536 | ); 537 | assert_eq!(item, Some(M2Item::Class("A\\B\\C".into()))) 538 | } 539 | 540 | #[test] 541 | fn test_get_current_position_path_when_starting_inside_attribute() { 542 | let item = get_test_position_path( 543 | r#" 544 | 545 | 546 | 547 | 560 | 561 | 562 | 565 | 566 | 567 | "#, 568 | ); 569 | 570 | let item = item.unwrap(); 571 | assert_eq!(item.path, "/config/type/block[@template]"); 572 | assert_eq!(item.text, "Modu"); 573 | } 574 | 575 | #[test] 576 | fn test_get_current_position_path_when_in_empty_attribute_value() { 577 | let item = get_test_position_path( 578 | r#" 579 | 580 | 581 | 584 | 585 | 586 | "#, 587 | ); 588 | 589 | let item = item.unwrap(); 590 | assert_eq!(item.path, "/config/type/block[@class]"); 591 | assert_eq!(item.text, ""); 592 | } 593 | 594 | #[test] 595 | fn test_get_current_position_path_when_after_empty_attribute_value() { 596 | let item = get_test_position_path( 597 | r#" 598 | 599 | 600 | 603 | 604 | 605 | "#, 606 | ); 607 | 608 | let item = item.unwrap(); 609 | assert_eq!(item.path, "/config/type/block"); 610 | assert_eq!(item.text, ""); 611 | assert!(item.tag.is_none()); 612 | } 613 | 614 | #[test] 615 | fn test_get_current_position_path_when_before_empty_attribute_value() { 616 | let item = get_test_position_path( 617 | r#" 618 | 619 | 620 | 623 | 624 | 625 | "#, 626 | ); 627 | 628 | assert!(item.is_none()); // nothig to complete here 629 | } 630 | 631 | #[test] 632 | fn test_get_current_position_path_when_starting_inside_tag() { 633 | let item = get_test_position_path( 634 | r#" 635 | 636 | 637 | |Nana 638 | 640 | 641 | 642 | "#, 643 | ); 644 | let item = item.unwrap(); 645 | assert_eq!(item.path, "/config/type/block[$text]"); 646 | assert_eq!(item.text, ""); 647 | assert!(item.tag.is_none()); 648 | } 649 | 650 | #[test] 651 | fn test_get_current_position_path_when_inside_tag() { 652 | let item = get_test_position_path( 653 | r#" 654 | 655 | 656 | Nan|a 657 | 659 | 660 | 661 | "#, 662 | ); 663 | 664 | let item = item.unwrap(); 665 | assert_eq!(item.path, "/config/type/block[$text]"); 666 | assert_eq!(item.text, "Nan"); 667 | assert!(item.tag.is_none()); 668 | } 669 | 670 | #[test] 671 | fn test_get_current_position_path_outside_attribute_and_text() { 672 | let item = get_test_position_path( 673 | r#" 674 | 675 | 677 | 678 | "#, 679 | ); 680 | 681 | let item = item.unwrap(); 682 | assert_eq!(item.path, "/config/item"); 683 | assert_eq!(item.text, ""); 684 | assert!(item.tag.is_none()); 685 | } 686 | 687 | #[test] 688 | fn test_get_current_position_path_between_start_and_end_tag() { 689 | let item = get_test_position_path( 690 | r#" 691 | 692 | 693 | 694 | 695 | 696 | | 697 | 698 | 699 | 700 | 701 | 702 | "#, 703 | ); 704 | 705 | let item = dbg!(item).unwrap(); 706 | assert!(item.attribute_eq("xsi:type", "string")); 707 | assert!(item.attribute_eq("name", "component")); 708 | } 709 | 710 | #[test] 711 | fn test_get_xml_tag_at_position_0_when_content_is_opening_tag() { 712 | let item = get_test_xml_tag_at_pos(r#"|"#); 713 | 714 | let item = item.unwrap(); 715 | assert_eq!(item.name, "item"); 716 | assert!(item.attributes.get("name").is_some()); 717 | assert!(item.attributes.get("attribute").is_some()); 718 | } 719 | 720 | #[test] 721 | fn test_unfinished_xml_at_text_not_empty() { 722 | let item = get_test_position_path( 723 | r#" 724 | 725 | 726 | Nan|a 727 | "#, 728 | ); 729 | 730 | let item = item.unwrap(); 731 | assert_eq!(item.path, "/config/type/block[$text]"); 732 | assert_eq!(item.text, "Nan"); 733 | assert!(item.tag.is_none()); 734 | } 735 | 736 | #[test] 737 | fn test_unfinished_xml_at_text_empty() { 738 | let item = get_test_position_path( 739 | r#" 740 | 741 | 742 | | 743 | "#, 744 | ); 745 | 746 | let item = item.unwrap(); 747 | assert_eq!(item.path, "/config/type/block[$text]"); 748 | assert_eq!(item.text, ""); 749 | assert!(item.tag.is_none()); 750 | } 751 | 752 | #[test] 753 | fn test_unfinished_xml_tag_not_closed() { 754 | let item = get_test_position_path( 755 | r#" 756 | 757 | 758 | 770 | 771 | 772 | Nan|a 773 | 774 | 775 | "#, 776 | ); 777 | 778 | let item = item.unwrap(); 779 | assert_eq!(item.path, "/config/type/block[$text]"); 780 | assert_eq!(item.text, "Nan"); 781 | assert!(item.tag.is_none()); 782 | } 783 | 784 | #[test] 785 | fn test_unfinished_current_tag_at_text_empty() { 786 | let item = get_test_position_path( 787 | r#" 788 | 789 | 790 | | 791 | 792 | 793 | "#, 794 | ); 795 | 796 | let item = item.unwrap(); 797 | assert_eq!(item.path, "/config/type/block[$text]"); 798 | assert_eq!(item.text, ""); 799 | assert!(item.tag.is_none()); 800 | } 801 | 802 | #[test] 803 | fn test_unfinished_current_tag_tag_not_closed() { 804 | let item = get_test_position_path( 805 | r#" 806 | 807 | 808 | 810 | 811 | "#, 812 | ); 813 | 814 | let item = item.unwrap(); 815 | assert!(!item.match_path("[$text]")); 816 | } 817 | 818 | #[test] 819 | fn test_valid_xml_at_text_not_empty() { 820 | let item = get_test_position_path( 821 | r#" 822 | 823 | 824 | Nan|a 825 | 826 | 827 | "#, 828 | ); 829 | 830 | let item = item.unwrap(); 831 | assert_eq!(item.path, "/config/type/block[$text]"); 832 | assert_eq!(item.text, "Nan"); 833 | assert!(item.tag.is_none()); 834 | } 835 | 836 | #[test] 837 | fn test_valid_xml_at_text_empty() { 838 | let item = get_test_position_path( 839 | r#" 840 | 841 | 842 | | 843 | 844 | 845 | "#, 846 | ); 847 | 848 | let item = item.unwrap(); 849 | assert_eq!(item.path, "/config/type/block[$text]"); 850 | assert_eq!(item.text, ""); 851 | assert!(item.tag.is_none()); 852 | } 853 | 854 | #[test] 855 | fn test_valid_xml_tag_not_closed() { 856 | let item = get_test_position_path( 857 | r#" 858 | 859 | 860 | 861 | 862 | 863 | "#, 864 | ); 865 | 866 | let item = item.unwrap(); 867 | assert!(!item.match_path("[$text]")); 868 | } 869 | 870 | #[test] 871 | fn test_valid_xml_type_after_tag() { 872 | let item = get_test_position_path( 873 | r#" 874 | 875 | 876 | A\B\C| 877 | 878 | 879 | "#, 880 | ); 881 | 882 | let item = dbg!(item).unwrap(); 883 | assert_eq!(item.path, "/config/type"); 884 | assert!(item.tag.is_none()); 885 | } 886 | 887 | #[test] 888 | fn test_valid_xml_tag_with_underscore() { 889 | let item = get_test_position_path( 890 | r#" 891 | 892 | 893 | asdf| 894 | 895 | 896 | "#, 897 | ); 898 | 899 | let item = dbg!(item).unwrap(); 900 | assert!(item.match_path("/source[$text]")); 901 | assert!(item.attribute_eq("_model", "")); 902 | } 903 | } 904 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | indent_type = "Spaces" 2 | quote_style = "AutoPreferSingle" 3 | column_width = 120 4 | indent_width = 2 5 | -------------------------------------------------------------------------------- /tests/app/code/Some/Module/Test.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | Some\Module\Test::TEST 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /vscode/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *.vsix 3 | -------------------------------------------------------------------------------- /vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | *.vsix 2 | -------------------------------------------------------------------------------- /vscode/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 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 | -------------------------------------------------------------------------------- /vscode/extension.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const { workspace } = require("vscode"); 3 | const { LanguageClient } = require("vscode-languageclient/node"); 4 | 5 | module.exports = { 6 | /** @param {import("vscode").ExtensionContext} context*/ 7 | activate(context) { 8 | const extension = process.platform === "win32" ? ".exe" : ""; 9 | 10 | /** @type {import("vscode-languageclient/node").ServerOptions} */ 11 | const serverOptions = { 12 | run: { 13 | command: context.asAbsolutePath("server/magento2-ls") + extension, 14 | }, 15 | debug: { 16 | command: 17 | context.asAbsolutePath("../target/debug/magento2-ls") + extension, 18 | }, 19 | }; 20 | 21 | /** @type {import("vscode-languageclient/node").LanguageClientOptions} */ 22 | const clientOptions = { 23 | documentSelector: [ 24 | { scheme: "file", language: "xml" }, 25 | { scheme: "file", language: "javascript" }, 26 | ], 27 | }; 28 | 29 | const client = new LanguageClient( 30 | "magento2-ls", 31 | "Magento 2 Language Server", 32 | serverOptions, 33 | clientOptions, 34 | ); 35 | 36 | workspace.onDidChangeWorkspaceFolders((_event) => { 37 | // TODO implement `workspace/didChangeWorkspaceFolders` in the server. 38 | // For now just restart server when workspace folders change 39 | client.restart(); 40 | }); 41 | 42 | client.start(); 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /vscode/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true 4 | }, 5 | "exclude": ["node_modules"] 6 | } 7 | -------------------------------------------------------------------------------- /vscode/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbogut/magento2-ls/7d5c7697fd029331254b528f48c82a296ea685b0/vscode/logo.png -------------------------------------------------------------------------------- /vscode/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magento2-ls", 3 | "version": "0.0.6", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "magento2-ls", 9 | "version": "0.0.6", 10 | "dependencies": { 11 | "vscode-languageclient": "^8.1.0" 12 | }, 13 | "devDependencies": { 14 | "@vscode/vsce": "^2.21.0" 15 | }, 16 | "engines": { 17 | "vscode": "^1.74.0" 18 | } 19 | }, 20 | "node_modules/@vscode/vsce": { 21 | "version": "2.21.0", 22 | "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.21.0.tgz", 23 | "integrity": "sha512-KuxYqScqUY/duJbkj9eE2tN2X/WJoGAy54hHtxT3ZBkM6IzrOg7H7CXGUPBxNlmqku2w/cAjOUSrgIHlzz0mbA==", 24 | "dev": true, 25 | "dependencies": { 26 | "azure-devops-node-api": "^11.0.1", 27 | "chalk": "^2.4.2", 28 | "cheerio": "^1.0.0-rc.9", 29 | "commander": "^6.2.1", 30 | "glob": "^7.0.6", 31 | "hosted-git-info": "^4.0.2", 32 | "jsonc-parser": "^3.2.0", 33 | "leven": "^3.1.0", 34 | "markdown-it": "^12.3.2", 35 | "mime": "^1.3.4", 36 | "minimatch": "^3.0.3", 37 | "parse-semver": "^1.1.1", 38 | "read": "^1.0.7", 39 | "semver": "^7.5.2", 40 | "tmp": "^0.2.1", 41 | "typed-rest-client": "^1.8.4", 42 | "url-join": "^4.0.1", 43 | "xml2js": "^0.5.0", 44 | "yauzl": "^2.3.1", 45 | "yazl": "^2.2.2" 46 | }, 47 | "bin": { 48 | "vsce": "vsce" 49 | }, 50 | "engines": { 51 | "node": ">= 14" 52 | }, 53 | "optionalDependencies": { 54 | "keytar": "^7.7.0" 55 | } 56 | }, 57 | "node_modules/ansi-styles": { 58 | "version": "3.2.1", 59 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 60 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 61 | "dev": true, 62 | "dependencies": { 63 | "color-convert": "^1.9.0" 64 | }, 65 | "engines": { 66 | "node": ">=4" 67 | } 68 | }, 69 | "node_modules/argparse": { 70 | "version": "2.0.1", 71 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 72 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 73 | "dev": true 74 | }, 75 | "node_modules/azure-devops-node-api": { 76 | "version": "11.2.0", 77 | "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-11.2.0.tgz", 78 | "integrity": "sha512-XdiGPhrpaT5J8wdERRKs5g8E0Zy1pvOYTli7z9E8nmOn3YGp4FhtjhrOyFmX/8veWCwdI69mCHKJw6l+4J/bHA==", 79 | "dev": true, 80 | "dependencies": { 81 | "tunnel": "0.0.6", 82 | "typed-rest-client": "^1.8.4" 83 | } 84 | }, 85 | "node_modules/balanced-match": { 86 | "version": "1.0.2", 87 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 88 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 89 | }, 90 | "node_modules/base64-js": { 91 | "version": "1.5.1", 92 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 93 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 94 | "dev": true, 95 | "funding": [ 96 | { 97 | "type": "github", 98 | "url": "https://github.com/sponsors/feross" 99 | }, 100 | { 101 | "type": "patreon", 102 | "url": "https://www.patreon.com/feross" 103 | }, 104 | { 105 | "type": "consulting", 106 | "url": "https://feross.org/support" 107 | } 108 | ], 109 | "optional": true 110 | }, 111 | "node_modules/bl": { 112 | "version": "4.1.0", 113 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 114 | "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 115 | "dev": true, 116 | "optional": true, 117 | "dependencies": { 118 | "buffer": "^5.5.0", 119 | "inherits": "^2.0.4", 120 | "readable-stream": "^3.4.0" 121 | } 122 | }, 123 | "node_modules/boolbase": { 124 | "version": "1.0.0", 125 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 126 | "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", 127 | "dev": true 128 | }, 129 | "node_modules/brace-expansion": { 130 | "version": "1.1.11", 131 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 132 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 133 | "dev": true, 134 | "dependencies": { 135 | "balanced-match": "^1.0.0", 136 | "concat-map": "0.0.1" 137 | } 138 | }, 139 | "node_modules/buffer": { 140 | "version": "5.7.1", 141 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 142 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 143 | "dev": true, 144 | "funding": [ 145 | { 146 | "type": "github", 147 | "url": "https://github.com/sponsors/feross" 148 | }, 149 | { 150 | "type": "patreon", 151 | "url": "https://www.patreon.com/feross" 152 | }, 153 | { 154 | "type": "consulting", 155 | "url": "https://feross.org/support" 156 | } 157 | ], 158 | "optional": true, 159 | "dependencies": { 160 | "base64-js": "^1.3.1", 161 | "ieee754": "^1.1.13" 162 | } 163 | }, 164 | "node_modules/buffer-crc32": { 165 | "version": "0.2.13", 166 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", 167 | "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", 168 | "dev": true, 169 | "engines": { 170 | "node": "*" 171 | } 172 | }, 173 | "node_modules/call-bind": { 174 | "version": "1.0.2", 175 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 176 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 177 | "dev": true, 178 | "dependencies": { 179 | "function-bind": "^1.1.1", 180 | "get-intrinsic": "^1.0.2" 181 | }, 182 | "funding": { 183 | "url": "https://github.com/sponsors/ljharb" 184 | } 185 | }, 186 | "node_modules/chalk": { 187 | "version": "2.4.2", 188 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 189 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 190 | "dev": true, 191 | "dependencies": { 192 | "ansi-styles": "^3.2.1", 193 | "escape-string-regexp": "^1.0.5", 194 | "supports-color": "^5.3.0" 195 | }, 196 | "engines": { 197 | "node": ">=4" 198 | } 199 | }, 200 | "node_modules/cheerio": { 201 | "version": "1.0.0-rc.12", 202 | "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", 203 | "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", 204 | "dev": true, 205 | "dependencies": { 206 | "cheerio-select": "^2.1.0", 207 | "dom-serializer": "^2.0.0", 208 | "domhandler": "^5.0.3", 209 | "domutils": "^3.0.1", 210 | "htmlparser2": "^8.0.1", 211 | "parse5": "^7.0.0", 212 | "parse5-htmlparser2-tree-adapter": "^7.0.0" 213 | }, 214 | "engines": { 215 | "node": ">= 6" 216 | }, 217 | "funding": { 218 | "url": "https://github.com/cheeriojs/cheerio?sponsor=1" 219 | } 220 | }, 221 | "node_modules/cheerio-select": { 222 | "version": "2.1.0", 223 | "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", 224 | "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", 225 | "dev": true, 226 | "dependencies": { 227 | "boolbase": "^1.0.0", 228 | "css-select": "^5.1.0", 229 | "css-what": "^6.1.0", 230 | "domelementtype": "^2.3.0", 231 | "domhandler": "^5.0.3", 232 | "domutils": "^3.0.1" 233 | }, 234 | "funding": { 235 | "url": "https://github.com/sponsors/fb55" 236 | } 237 | }, 238 | "node_modules/chownr": { 239 | "version": "1.1.4", 240 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 241 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", 242 | "dev": true, 243 | "optional": true 244 | }, 245 | "node_modules/color-convert": { 246 | "version": "1.9.3", 247 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 248 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 249 | "dev": true, 250 | "dependencies": { 251 | "color-name": "1.1.3" 252 | } 253 | }, 254 | "node_modules/color-name": { 255 | "version": "1.1.3", 256 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 257 | "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", 258 | "dev": true 259 | }, 260 | "node_modules/commander": { 261 | "version": "6.2.1", 262 | "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", 263 | "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", 264 | "dev": true, 265 | "engines": { 266 | "node": ">= 6" 267 | } 268 | }, 269 | "node_modules/concat-map": { 270 | "version": "0.0.1", 271 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 272 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 273 | "dev": true 274 | }, 275 | "node_modules/css-select": { 276 | "version": "5.1.0", 277 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", 278 | "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", 279 | "dev": true, 280 | "dependencies": { 281 | "boolbase": "^1.0.0", 282 | "css-what": "^6.1.0", 283 | "domhandler": "^5.0.2", 284 | "domutils": "^3.0.1", 285 | "nth-check": "^2.0.1" 286 | }, 287 | "funding": { 288 | "url": "https://github.com/sponsors/fb55" 289 | } 290 | }, 291 | "node_modules/css-what": { 292 | "version": "6.1.0", 293 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", 294 | "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", 295 | "dev": true, 296 | "engines": { 297 | "node": ">= 6" 298 | }, 299 | "funding": { 300 | "url": "https://github.com/sponsors/fb55" 301 | } 302 | }, 303 | "node_modules/decompress-response": { 304 | "version": "6.0.0", 305 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 306 | "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 307 | "dev": true, 308 | "optional": true, 309 | "dependencies": { 310 | "mimic-response": "^3.1.0" 311 | }, 312 | "engines": { 313 | "node": ">=10" 314 | }, 315 | "funding": { 316 | "url": "https://github.com/sponsors/sindresorhus" 317 | } 318 | }, 319 | "node_modules/deep-extend": { 320 | "version": "0.6.0", 321 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 322 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", 323 | "dev": true, 324 | "optional": true, 325 | "engines": { 326 | "node": ">=4.0.0" 327 | } 328 | }, 329 | "node_modules/detect-libc": { 330 | "version": "2.0.2", 331 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", 332 | "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", 333 | "dev": true, 334 | "optional": true, 335 | "engines": { 336 | "node": ">=8" 337 | } 338 | }, 339 | "node_modules/dom-serializer": { 340 | "version": "2.0.0", 341 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 342 | "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 343 | "dev": true, 344 | "dependencies": { 345 | "domelementtype": "^2.3.0", 346 | "domhandler": "^5.0.2", 347 | "entities": "^4.2.0" 348 | }, 349 | "funding": { 350 | "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" 351 | } 352 | }, 353 | "node_modules/domelementtype": { 354 | "version": "2.3.0", 355 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 356 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", 357 | "dev": true, 358 | "funding": [ 359 | { 360 | "type": "github", 361 | "url": "https://github.com/sponsors/fb55" 362 | } 363 | ] 364 | }, 365 | "node_modules/domhandler": { 366 | "version": "5.0.3", 367 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 368 | "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 369 | "dev": true, 370 | "dependencies": { 371 | "domelementtype": "^2.3.0" 372 | }, 373 | "engines": { 374 | "node": ">= 4" 375 | }, 376 | "funding": { 377 | "url": "https://github.com/fb55/domhandler?sponsor=1" 378 | } 379 | }, 380 | "node_modules/domutils": { 381 | "version": "3.1.0", 382 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", 383 | "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", 384 | "dev": true, 385 | "dependencies": { 386 | "dom-serializer": "^2.0.0", 387 | "domelementtype": "^2.3.0", 388 | "domhandler": "^5.0.3" 389 | }, 390 | "funding": { 391 | "url": "https://github.com/fb55/domutils?sponsor=1" 392 | } 393 | }, 394 | "node_modules/end-of-stream": { 395 | "version": "1.4.4", 396 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 397 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 398 | "dev": true, 399 | "optional": true, 400 | "dependencies": { 401 | "once": "^1.4.0" 402 | } 403 | }, 404 | "node_modules/entities": { 405 | "version": "4.5.0", 406 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 407 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 408 | "dev": true, 409 | "engines": { 410 | "node": ">=0.12" 411 | }, 412 | "funding": { 413 | "url": "https://github.com/fb55/entities?sponsor=1" 414 | } 415 | }, 416 | "node_modules/escape-string-regexp": { 417 | "version": "1.0.5", 418 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 419 | "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", 420 | "dev": true, 421 | "engines": { 422 | "node": ">=0.8.0" 423 | } 424 | }, 425 | "node_modules/expand-template": { 426 | "version": "2.0.3", 427 | "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", 428 | "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", 429 | "dev": true, 430 | "optional": true, 431 | "engines": { 432 | "node": ">=6" 433 | } 434 | }, 435 | "node_modules/fd-slicer": { 436 | "version": "1.1.0", 437 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", 438 | "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", 439 | "dev": true, 440 | "dependencies": { 441 | "pend": "~1.2.0" 442 | } 443 | }, 444 | "node_modules/fs-constants": { 445 | "version": "1.0.0", 446 | "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 447 | "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", 448 | "dev": true, 449 | "optional": true 450 | }, 451 | "node_modules/fs.realpath": { 452 | "version": "1.0.0", 453 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 454 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 455 | "dev": true 456 | }, 457 | "node_modules/function-bind": { 458 | "version": "1.1.1", 459 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 460 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 461 | "dev": true 462 | }, 463 | "node_modules/get-intrinsic": { 464 | "version": "1.2.1", 465 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", 466 | "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", 467 | "dev": true, 468 | "dependencies": { 469 | "function-bind": "^1.1.1", 470 | "has": "^1.0.3", 471 | "has-proto": "^1.0.1", 472 | "has-symbols": "^1.0.3" 473 | }, 474 | "funding": { 475 | "url": "https://github.com/sponsors/ljharb" 476 | } 477 | }, 478 | "node_modules/github-from-package": { 479 | "version": "0.0.0", 480 | "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", 481 | "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", 482 | "dev": true, 483 | "optional": true 484 | }, 485 | "node_modules/glob": { 486 | "version": "7.2.3", 487 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", 488 | "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", 489 | "dev": true, 490 | "dependencies": { 491 | "fs.realpath": "^1.0.0", 492 | "inflight": "^1.0.4", 493 | "inherits": "2", 494 | "minimatch": "^3.1.1", 495 | "once": "^1.3.0", 496 | "path-is-absolute": "^1.0.0" 497 | }, 498 | "engines": { 499 | "node": "*" 500 | }, 501 | "funding": { 502 | "url": "https://github.com/sponsors/isaacs" 503 | } 504 | }, 505 | "node_modules/has": { 506 | "version": "1.0.3", 507 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 508 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 509 | "dev": true, 510 | "dependencies": { 511 | "function-bind": "^1.1.1" 512 | }, 513 | "engines": { 514 | "node": ">= 0.4.0" 515 | } 516 | }, 517 | "node_modules/has-flag": { 518 | "version": "3.0.0", 519 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 520 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", 521 | "dev": true, 522 | "engines": { 523 | "node": ">=4" 524 | } 525 | }, 526 | "node_modules/has-proto": { 527 | "version": "1.0.1", 528 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", 529 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", 530 | "dev": true, 531 | "engines": { 532 | "node": ">= 0.4" 533 | }, 534 | "funding": { 535 | "url": "https://github.com/sponsors/ljharb" 536 | } 537 | }, 538 | "node_modules/has-symbols": { 539 | "version": "1.0.3", 540 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 541 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 542 | "dev": true, 543 | "engines": { 544 | "node": ">= 0.4" 545 | }, 546 | "funding": { 547 | "url": "https://github.com/sponsors/ljharb" 548 | } 549 | }, 550 | "node_modules/hosted-git-info": { 551 | "version": "4.1.0", 552 | "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", 553 | "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", 554 | "dev": true, 555 | "dependencies": { 556 | "lru-cache": "^6.0.0" 557 | }, 558 | "engines": { 559 | "node": ">=10" 560 | } 561 | }, 562 | "node_modules/htmlparser2": { 563 | "version": "8.0.2", 564 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", 565 | "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", 566 | "dev": true, 567 | "funding": [ 568 | "https://github.com/fb55/htmlparser2?sponsor=1", 569 | { 570 | "type": "github", 571 | "url": "https://github.com/sponsors/fb55" 572 | } 573 | ], 574 | "dependencies": { 575 | "domelementtype": "^2.3.0", 576 | "domhandler": "^5.0.3", 577 | "domutils": "^3.0.1", 578 | "entities": "^4.4.0" 579 | } 580 | }, 581 | "node_modules/ieee754": { 582 | "version": "1.2.1", 583 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 584 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 585 | "dev": true, 586 | "funding": [ 587 | { 588 | "type": "github", 589 | "url": "https://github.com/sponsors/feross" 590 | }, 591 | { 592 | "type": "patreon", 593 | "url": "https://www.patreon.com/feross" 594 | }, 595 | { 596 | "type": "consulting", 597 | "url": "https://feross.org/support" 598 | } 599 | ], 600 | "optional": true 601 | }, 602 | "node_modules/inflight": { 603 | "version": "1.0.6", 604 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 605 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 606 | "dev": true, 607 | "dependencies": { 608 | "once": "^1.3.0", 609 | "wrappy": "1" 610 | } 611 | }, 612 | "node_modules/inherits": { 613 | "version": "2.0.4", 614 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 615 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 616 | "dev": true 617 | }, 618 | "node_modules/ini": { 619 | "version": "1.3.8", 620 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 621 | "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", 622 | "dev": true, 623 | "optional": true 624 | }, 625 | "node_modules/jsonc-parser": { 626 | "version": "3.2.0", 627 | "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", 628 | "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", 629 | "dev": true 630 | }, 631 | "node_modules/keytar": { 632 | "version": "7.9.0", 633 | "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", 634 | "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", 635 | "dev": true, 636 | "hasInstallScript": true, 637 | "optional": true, 638 | "dependencies": { 639 | "node-addon-api": "^4.3.0", 640 | "prebuild-install": "^7.0.1" 641 | } 642 | }, 643 | "node_modules/leven": { 644 | "version": "3.1.0", 645 | "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", 646 | "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", 647 | "dev": true, 648 | "engines": { 649 | "node": ">=6" 650 | } 651 | }, 652 | "node_modules/linkify-it": { 653 | "version": "3.0.3", 654 | "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", 655 | "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", 656 | "dev": true, 657 | "dependencies": { 658 | "uc.micro": "^1.0.1" 659 | } 660 | }, 661 | "node_modules/lru-cache": { 662 | "version": "6.0.0", 663 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 664 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 665 | "dependencies": { 666 | "yallist": "^4.0.0" 667 | }, 668 | "engines": { 669 | "node": ">=10" 670 | } 671 | }, 672 | "node_modules/markdown-it": { 673 | "version": "12.3.2", 674 | "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", 675 | "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", 676 | "dev": true, 677 | "dependencies": { 678 | "argparse": "^2.0.1", 679 | "entities": "~2.1.0", 680 | "linkify-it": "^3.0.1", 681 | "mdurl": "^1.0.1", 682 | "uc.micro": "^1.0.5" 683 | }, 684 | "bin": { 685 | "markdown-it": "bin/markdown-it.js" 686 | } 687 | }, 688 | "node_modules/markdown-it/node_modules/entities": { 689 | "version": "2.1.0", 690 | "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", 691 | "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", 692 | "dev": true, 693 | "funding": { 694 | "url": "https://github.com/fb55/entities?sponsor=1" 695 | } 696 | }, 697 | "node_modules/mdurl": { 698 | "version": "1.0.1", 699 | "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", 700 | "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", 701 | "dev": true 702 | }, 703 | "node_modules/mime": { 704 | "version": "1.6.0", 705 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 706 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 707 | "dev": true, 708 | "bin": { 709 | "mime": "cli.js" 710 | }, 711 | "engines": { 712 | "node": ">=4" 713 | } 714 | }, 715 | "node_modules/mimic-response": { 716 | "version": "3.1.0", 717 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 718 | "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", 719 | "dev": true, 720 | "optional": true, 721 | "engines": { 722 | "node": ">=10" 723 | }, 724 | "funding": { 725 | "url": "https://github.com/sponsors/sindresorhus" 726 | } 727 | }, 728 | "node_modules/minimatch": { 729 | "version": "3.1.2", 730 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 731 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 732 | "dev": true, 733 | "dependencies": { 734 | "brace-expansion": "^1.1.7" 735 | }, 736 | "engines": { 737 | "node": "*" 738 | } 739 | }, 740 | "node_modules/minimist": { 741 | "version": "1.2.8", 742 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 743 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 744 | "dev": true, 745 | "optional": true, 746 | "funding": { 747 | "url": "https://github.com/sponsors/ljharb" 748 | } 749 | }, 750 | "node_modules/mkdirp-classic": { 751 | "version": "0.5.3", 752 | "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 753 | "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", 754 | "dev": true, 755 | "optional": true 756 | }, 757 | "node_modules/mute-stream": { 758 | "version": "0.0.8", 759 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", 760 | "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", 761 | "dev": true 762 | }, 763 | "node_modules/napi-build-utils": { 764 | "version": "1.0.2", 765 | "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", 766 | "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", 767 | "dev": true, 768 | "optional": true 769 | }, 770 | "node_modules/node-abi": { 771 | "version": "3.47.0", 772 | "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.47.0.tgz", 773 | "integrity": "sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==", 774 | "dev": true, 775 | "optional": true, 776 | "dependencies": { 777 | "semver": "^7.3.5" 778 | }, 779 | "engines": { 780 | "node": ">=10" 781 | } 782 | }, 783 | "node_modules/node-addon-api": { 784 | "version": "4.3.0", 785 | "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", 786 | "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", 787 | "dev": true, 788 | "optional": true 789 | }, 790 | "node_modules/nth-check": { 791 | "version": "2.1.1", 792 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", 793 | "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", 794 | "dev": true, 795 | "dependencies": { 796 | "boolbase": "^1.0.0" 797 | }, 798 | "funding": { 799 | "url": "https://github.com/fb55/nth-check?sponsor=1" 800 | } 801 | }, 802 | "node_modules/object-inspect": { 803 | "version": "1.12.3", 804 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", 805 | "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", 806 | "dev": true, 807 | "funding": { 808 | "url": "https://github.com/sponsors/ljharb" 809 | } 810 | }, 811 | "node_modules/once": { 812 | "version": "1.4.0", 813 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 814 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 815 | "dev": true, 816 | "dependencies": { 817 | "wrappy": "1" 818 | } 819 | }, 820 | "node_modules/parse-semver": { 821 | "version": "1.1.1", 822 | "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", 823 | "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", 824 | "dev": true, 825 | "dependencies": { 826 | "semver": "^5.1.0" 827 | } 828 | }, 829 | "node_modules/parse-semver/node_modules/semver": { 830 | "version": "5.7.2", 831 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", 832 | "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", 833 | "dev": true, 834 | "bin": { 835 | "semver": "bin/semver" 836 | } 837 | }, 838 | "node_modules/parse5": { 839 | "version": "7.1.2", 840 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", 841 | "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", 842 | "dev": true, 843 | "dependencies": { 844 | "entities": "^4.4.0" 845 | }, 846 | "funding": { 847 | "url": "https://github.com/inikulin/parse5?sponsor=1" 848 | } 849 | }, 850 | "node_modules/parse5-htmlparser2-tree-adapter": { 851 | "version": "7.0.0", 852 | "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", 853 | "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", 854 | "dev": true, 855 | "dependencies": { 856 | "domhandler": "^5.0.2", 857 | "parse5": "^7.0.0" 858 | }, 859 | "funding": { 860 | "url": "https://github.com/inikulin/parse5?sponsor=1" 861 | } 862 | }, 863 | "node_modules/path-is-absolute": { 864 | "version": "1.0.1", 865 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 866 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 867 | "dev": true, 868 | "engines": { 869 | "node": ">=0.10.0" 870 | } 871 | }, 872 | "node_modules/pend": { 873 | "version": "1.2.0", 874 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", 875 | "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", 876 | "dev": true 877 | }, 878 | "node_modules/prebuild-install": { 879 | "version": "7.1.1", 880 | "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", 881 | "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", 882 | "dev": true, 883 | "optional": true, 884 | "dependencies": { 885 | "detect-libc": "^2.0.0", 886 | "expand-template": "^2.0.3", 887 | "github-from-package": "0.0.0", 888 | "minimist": "^1.2.3", 889 | "mkdirp-classic": "^0.5.3", 890 | "napi-build-utils": "^1.0.1", 891 | "node-abi": "^3.3.0", 892 | "pump": "^3.0.0", 893 | "rc": "^1.2.7", 894 | "simple-get": "^4.0.0", 895 | "tar-fs": "^2.0.0", 896 | "tunnel-agent": "^0.6.0" 897 | }, 898 | "bin": { 899 | "prebuild-install": "bin.js" 900 | }, 901 | "engines": { 902 | "node": ">=10" 903 | } 904 | }, 905 | "node_modules/pump": { 906 | "version": "3.0.0", 907 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 908 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 909 | "dev": true, 910 | "optional": true, 911 | "dependencies": { 912 | "end-of-stream": "^1.1.0", 913 | "once": "^1.3.1" 914 | } 915 | }, 916 | "node_modules/qs": { 917 | "version": "6.11.2", 918 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", 919 | "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", 920 | "dev": true, 921 | "dependencies": { 922 | "side-channel": "^1.0.4" 923 | }, 924 | "engines": { 925 | "node": ">=0.6" 926 | }, 927 | "funding": { 928 | "url": "https://github.com/sponsors/ljharb" 929 | } 930 | }, 931 | "node_modules/rc": { 932 | "version": "1.2.8", 933 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 934 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 935 | "dev": true, 936 | "optional": true, 937 | "dependencies": { 938 | "deep-extend": "^0.6.0", 939 | "ini": "~1.3.0", 940 | "minimist": "^1.2.0", 941 | "strip-json-comments": "~2.0.1" 942 | }, 943 | "bin": { 944 | "rc": "cli.js" 945 | } 946 | }, 947 | "node_modules/read": { 948 | "version": "1.0.7", 949 | "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", 950 | "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", 951 | "dev": true, 952 | "dependencies": { 953 | "mute-stream": "~0.0.4" 954 | }, 955 | "engines": { 956 | "node": ">=0.8" 957 | } 958 | }, 959 | "node_modules/readable-stream": { 960 | "version": "3.6.2", 961 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", 962 | "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", 963 | "dev": true, 964 | "optional": true, 965 | "dependencies": { 966 | "inherits": "^2.0.3", 967 | "string_decoder": "^1.1.1", 968 | "util-deprecate": "^1.0.1" 969 | }, 970 | "engines": { 971 | "node": ">= 6" 972 | } 973 | }, 974 | "node_modules/rimraf": { 975 | "version": "3.0.2", 976 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 977 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 978 | "dev": true, 979 | "dependencies": { 980 | "glob": "^7.1.3" 981 | }, 982 | "bin": { 983 | "rimraf": "bin.js" 984 | }, 985 | "funding": { 986 | "url": "https://github.com/sponsors/isaacs" 987 | } 988 | }, 989 | "node_modules/safe-buffer": { 990 | "version": "5.2.1", 991 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 992 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 993 | "dev": true, 994 | "funding": [ 995 | { 996 | "type": "github", 997 | "url": "https://github.com/sponsors/feross" 998 | }, 999 | { 1000 | "type": "patreon", 1001 | "url": "https://www.patreon.com/feross" 1002 | }, 1003 | { 1004 | "type": "consulting", 1005 | "url": "https://feross.org/support" 1006 | } 1007 | ], 1008 | "optional": true 1009 | }, 1010 | "node_modules/sax": { 1011 | "version": "1.2.4", 1012 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 1013 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", 1014 | "dev": true 1015 | }, 1016 | "node_modules/semver": { 1017 | "version": "7.5.4", 1018 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", 1019 | "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", 1020 | "dependencies": { 1021 | "lru-cache": "^6.0.0" 1022 | }, 1023 | "bin": { 1024 | "semver": "bin/semver.js" 1025 | }, 1026 | "engines": { 1027 | "node": ">=10" 1028 | } 1029 | }, 1030 | "node_modules/side-channel": { 1031 | "version": "1.0.4", 1032 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 1033 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 1034 | "dev": true, 1035 | "dependencies": { 1036 | "call-bind": "^1.0.0", 1037 | "get-intrinsic": "^1.0.2", 1038 | "object-inspect": "^1.9.0" 1039 | }, 1040 | "funding": { 1041 | "url": "https://github.com/sponsors/ljharb" 1042 | } 1043 | }, 1044 | "node_modules/simple-concat": { 1045 | "version": "1.0.1", 1046 | "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 1047 | "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", 1048 | "dev": true, 1049 | "funding": [ 1050 | { 1051 | "type": "github", 1052 | "url": "https://github.com/sponsors/feross" 1053 | }, 1054 | { 1055 | "type": "patreon", 1056 | "url": "https://www.patreon.com/feross" 1057 | }, 1058 | { 1059 | "type": "consulting", 1060 | "url": "https://feross.org/support" 1061 | } 1062 | ], 1063 | "optional": true 1064 | }, 1065 | "node_modules/simple-get": { 1066 | "version": "4.0.1", 1067 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", 1068 | "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", 1069 | "dev": true, 1070 | "funding": [ 1071 | { 1072 | "type": "github", 1073 | "url": "https://github.com/sponsors/feross" 1074 | }, 1075 | { 1076 | "type": "patreon", 1077 | "url": "https://www.patreon.com/feross" 1078 | }, 1079 | { 1080 | "type": "consulting", 1081 | "url": "https://feross.org/support" 1082 | } 1083 | ], 1084 | "optional": true, 1085 | "dependencies": { 1086 | "decompress-response": "^6.0.0", 1087 | "once": "^1.3.1", 1088 | "simple-concat": "^1.0.0" 1089 | } 1090 | }, 1091 | "node_modules/string_decoder": { 1092 | "version": "1.3.0", 1093 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 1094 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 1095 | "dev": true, 1096 | "optional": true, 1097 | "dependencies": { 1098 | "safe-buffer": "~5.2.0" 1099 | } 1100 | }, 1101 | "node_modules/strip-json-comments": { 1102 | "version": "2.0.1", 1103 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 1104 | "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", 1105 | "dev": true, 1106 | "optional": true, 1107 | "engines": { 1108 | "node": ">=0.10.0" 1109 | } 1110 | }, 1111 | "node_modules/supports-color": { 1112 | "version": "5.5.0", 1113 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 1114 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 1115 | "dev": true, 1116 | "dependencies": { 1117 | "has-flag": "^3.0.0" 1118 | }, 1119 | "engines": { 1120 | "node": ">=4" 1121 | } 1122 | }, 1123 | "node_modules/tar-fs": { 1124 | "version": "2.1.1", 1125 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", 1126 | "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", 1127 | "dev": true, 1128 | "optional": true, 1129 | "dependencies": { 1130 | "chownr": "^1.1.1", 1131 | "mkdirp-classic": "^0.5.2", 1132 | "pump": "^3.0.0", 1133 | "tar-stream": "^2.1.4" 1134 | } 1135 | }, 1136 | "node_modules/tar-stream": { 1137 | "version": "2.2.0", 1138 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 1139 | "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 1140 | "dev": true, 1141 | "optional": true, 1142 | "dependencies": { 1143 | "bl": "^4.0.3", 1144 | "end-of-stream": "^1.4.1", 1145 | "fs-constants": "^1.0.0", 1146 | "inherits": "^2.0.3", 1147 | "readable-stream": "^3.1.1" 1148 | }, 1149 | "engines": { 1150 | "node": ">=6" 1151 | } 1152 | }, 1153 | "node_modules/tmp": { 1154 | "version": "0.2.1", 1155 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", 1156 | "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", 1157 | "dev": true, 1158 | "dependencies": { 1159 | "rimraf": "^3.0.0" 1160 | }, 1161 | "engines": { 1162 | "node": ">=8.17.0" 1163 | } 1164 | }, 1165 | "node_modules/tunnel": { 1166 | "version": "0.0.6", 1167 | "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", 1168 | "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", 1169 | "dev": true, 1170 | "engines": { 1171 | "node": ">=0.6.11 <=0.7.0 || >=0.7.3" 1172 | } 1173 | }, 1174 | "node_modules/tunnel-agent": { 1175 | "version": "0.6.0", 1176 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1177 | "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", 1178 | "dev": true, 1179 | "optional": true, 1180 | "dependencies": { 1181 | "safe-buffer": "^5.0.1" 1182 | }, 1183 | "engines": { 1184 | "node": "*" 1185 | } 1186 | }, 1187 | "node_modules/typed-rest-client": { 1188 | "version": "1.8.11", 1189 | "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", 1190 | "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", 1191 | "dev": true, 1192 | "dependencies": { 1193 | "qs": "^6.9.1", 1194 | "tunnel": "0.0.6", 1195 | "underscore": "^1.12.1" 1196 | } 1197 | }, 1198 | "node_modules/uc.micro": { 1199 | "version": "1.0.6", 1200 | "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", 1201 | "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", 1202 | "dev": true 1203 | }, 1204 | "node_modules/underscore": { 1205 | "version": "1.13.6", 1206 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", 1207 | "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", 1208 | "dev": true 1209 | }, 1210 | "node_modules/url-join": { 1211 | "version": "4.0.1", 1212 | "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", 1213 | "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", 1214 | "dev": true 1215 | }, 1216 | "node_modules/util-deprecate": { 1217 | "version": "1.0.2", 1218 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1219 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 1220 | "dev": true, 1221 | "optional": true 1222 | }, 1223 | "node_modules/vscode-jsonrpc": { 1224 | "version": "8.1.0", 1225 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz", 1226 | "integrity": "sha512-6TDy/abTQk+zDGYazgbIPc+4JoXdwC8NHU9Pbn4UJP1fehUyZmM4RHp5IthX7A6L5KS30PRui+j+tbbMMMafdw==", 1227 | "engines": { 1228 | "node": ">=14.0.0" 1229 | } 1230 | }, 1231 | "node_modules/vscode-languageclient": { 1232 | "version": "8.1.0", 1233 | "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", 1234 | "integrity": "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing==", 1235 | "dependencies": { 1236 | "minimatch": "^5.1.0", 1237 | "semver": "^7.3.7", 1238 | "vscode-languageserver-protocol": "3.17.3" 1239 | }, 1240 | "engines": { 1241 | "vscode": "^1.67.0" 1242 | } 1243 | }, 1244 | "node_modules/vscode-languageclient/node_modules/brace-expansion": { 1245 | "version": "2.0.1", 1246 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 1247 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 1248 | "dependencies": { 1249 | "balanced-match": "^1.0.0" 1250 | } 1251 | }, 1252 | "node_modules/vscode-languageclient/node_modules/minimatch": { 1253 | "version": "5.1.6", 1254 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", 1255 | "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", 1256 | "dependencies": { 1257 | "brace-expansion": "^2.0.1" 1258 | }, 1259 | "engines": { 1260 | "node": ">=10" 1261 | } 1262 | }, 1263 | "node_modules/vscode-languageserver-protocol": { 1264 | "version": "3.17.3", 1265 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.3.tgz", 1266 | "integrity": "sha512-924/h0AqsMtA5yK22GgMtCYiMdCOtWTSGgUOkgEDX+wk2b0x4sAfLiO4NxBxqbiVtz7K7/1/RgVrVI0NClZwqA==", 1267 | "dependencies": { 1268 | "vscode-jsonrpc": "8.1.0", 1269 | "vscode-languageserver-types": "3.17.3" 1270 | } 1271 | }, 1272 | "node_modules/vscode-languageserver-types": { 1273 | "version": "3.17.3", 1274 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", 1275 | "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" 1276 | }, 1277 | "node_modules/wrappy": { 1278 | "version": "1.0.2", 1279 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1280 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1281 | "dev": true 1282 | }, 1283 | "node_modules/xml2js": { 1284 | "version": "0.5.0", 1285 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", 1286 | "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", 1287 | "dev": true, 1288 | "dependencies": { 1289 | "sax": ">=0.6.0", 1290 | "xmlbuilder": "~11.0.0" 1291 | }, 1292 | "engines": { 1293 | "node": ">=4.0.0" 1294 | } 1295 | }, 1296 | "node_modules/xmlbuilder": { 1297 | "version": "11.0.1", 1298 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", 1299 | "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", 1300 | "dev": true, 1301 | "engines": { 1302 | "node": ">=4.0" 1303 | } 1304 | }, 1305 | "node_modules/yallist": { 1306 | "version": "4.0.0", 1307 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 1308 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 1309 | }, 1310 | "node_modules/yauzl": { 1311 | "version": "2.10.0", 1312 | "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", 1313 | "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", 1314 | "dev": true, 1315 | "dependencies": { 1316 | "buffer-crc32": "~0.2.3", 1317 | "fd-slicer": "~1.1.0" 1318 | } 1319 | }, 1320 | "node_modules/yazl": { 1321 | "version": "2.5.1", 1322 | "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", 1323 | "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", 1324 | "dev": true, 1325 | "dependencies": { 1326 | "buffer-crc32": "~0.2.3" 1327 | } 1328 | } 1329 | } 1330 | } 1331 | -------------------------------------------------------------------------------- /vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "magento2-ls", 3 | "displayName": "Magento2 Language Server", 4 | "description": "Language Server for Magento 2 Projects", 5 | "version": "0.0.7", 6 | "publisher": "pbogut", 7 | "author": { 8 | "name": "Paweł Bogut", 9 | "email": "pbogut@pbogut.me" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/pbogut/magento2-ls" 14 | }, 15 | "engines": { 16 | "vscode": "^1.74.0" 17 | }, 18 | "icon": "logo.png", 19 | "activationEvents": [ 20 | "onLanguage:xml", 21 | "onLanguage:javascript" 22 | ], 23 | "main": "./extension", 24 | "dependencies": { 25 | "vscode-languageclient": "^8.1.0" 26 | }, 27 | "devDependencies": { 28 | "@vscode/vsce": "^2.21.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /vscode/server/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbogut/magento2-ls/7d5c7697fd029331254b528f48c82a296ea685b0/vscode/server/.gitkeep --------------------------------------------------------------------------------