├── .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 | 
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 | 
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
--------------------------------------------------------------------------------