├── .config └── nextest.toml ├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── README.md ├── extension.toml ├── languages ├── erb │ ├── config.toml │ ├── highlights.scm │ └── injections.scm ├── rbs │ ├── config.toml │ ├── highlights.scm │ ├── indents.scm │ └── injections.scm └── ruby │ ├── brackets.scm │ ├── config.toml │ ├── embedding.scm │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── outline.scm │ ├── overrides.scm │ ├── runnables.scm │ ├── tasks.json │ └── textobjects.scm ├── renovate.json └── src ├── bundler.rs ├── command_executor.rs ├── gemset.rs ├── language_servers ├── language_server.rs ├── mod.rs ├── rubocop.rs ├── ruby_lsp.rs ├── solargraph.rs ├── sorbet.rs └── steep.rs └── ruby.rs /.config/nextest.toml: -------------------------------------------------------------------------------- 1 | [profile.ci] 2 | # Don't fail fast in CI to run the full test suite. 3 | fail-fast = false 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Unix-style newlines with a newline ending every file 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.rs] 11 | indent_size = 4 12 | indent_style = space 13 | 14 | [*.toml] 15 | indent_size = 2 16 | indent_style = space 17 | 18 | [*.scm] 19 | indent_style = space 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: 7 | - main 8 | 9 | name: CI 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | env: 16 | RUSTFLAGS: -D warnings 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | # By default actions/checkout checks out a merge commit. Check out the PR head instead. 21 | # https://github.com/actions/checkout#checkout-pull-request-head-commit-instead-of-merge-commit 22 | ref: ${{ github.event.pull_request.head.sha }} 23 | - uses: actions-rust-lang/setup-rust-toolchain@v1 24 | with: 25 | toolchain: stable 26 | target: wasm32-wasip2 27 | override: true 28 | components: rustfmt, clippy 29 | - name: Lint (clippy) 30 | uses: actions-rs/cargo@v1 31 | with: 32 | command: clippy 33 | args: --all-features --all-targets 34 | - name: Rustfmt Check 35 | uses: actions-rust-lang/rustfmt@v1 36 | 37 | build: 38 | name: Build and test 39 | runs-on: ubuntu-latest 40 | env: 41 | RUSTFLAGS: -D warnings 42 | steps: 43 | - uses: actions/checkout@v4 44 | with: 45 | ref: ${{ github.event.pull_request.head.sha }} 46 | - uses: actions-rust-lang/setup-rust-toolchain@v1 47 | with: 48 | toolchain: 1.87 49 | target: wasm32-wasip2 50 | override: true 51 | - name: Build extension 52 | uses: actions-rs/cargo@v1 53 | with: 54 | command: build 55 | args: --target wasm32-wasip2 --all-features 56 | - name: Install latest nextest release 57 | uses: taiki-e/install-action@nextest 58 | - name: Test with latest nextest release 59 | uses: actions-rs/cargo@v1 60 | with: 61 | command: nextest 62 | args: run --all-features --profile ci 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.wasm 2 | /target 3 | grammars/ 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "adler2" 7 | version = "2.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 10 | 11 | [[package]] 12 | name = "aho-corasick" 13 | version = "1.1.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 16 | dependencies = [ 17 | "memchr", 18 | ] 19 | 20 | [[package]] 21 | name = "anyhow" 22 | version = "1.0.89" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" 25 | 26 | [[package]] 27 | name = "auditable-serde" 28 | version = "0.8.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "5c7bf8143dfc3c0258df908843e169b5cc5fcf76c7718bd66135ef4a9cd558c5" 31 | dependencies = [ 32 | "semver", 33 | "serde", 34 | "serde_json", 35 | "topological-sort", 36 | ] 37 | 38 | [[package]] 39 | name = "autocfg" 40 | version = "1.4.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 43 | 44 | [[package]] 45 | name = "bitflags" 46 | version = "2.6.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 49 | 50 | [[package]] 51 | name = "cfg-if" 52 | version = "1.0.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 55 | 56 | [[package]] 57 | name = "crc32fast" 58 | version = "1.4.2" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 61 | dependencies = [ 62 | "cfg-if", 63 | ] 64 | 65 | [[package]] 66 | name = "displaydoc" 67 | version = "0.2.5" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 70 | dependencies = [ 71 | "proc-macro2", 72 | "quote", 73 | "syn", 74 | ] 75 | 76 | [[package]] 77 | name = "equivalent" 78 | version = "1.0.1" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 81 | 82 | [[package]] 83 | name = "flate2" 84 | version = "1.1.1" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" 87 | dependencies = [ 88 | "crc32fast", 89 | "miniz_oxide", 90 | ] 91 | 92 | [[package]] 93 | name = "foldhash" 94 | version = "0.1.5" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" 97 | 98 | [[package]] 99 | name = "form_urlencoded" 100 | version = "1.2.1" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 103 | dependencies = [ 104 | "percent-encoding", 105 | ] 106 | 107 | [[package]] 108 | name = "futures" 109 | version = "0.3.31" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 112 | dependencies = [ 113 | "futures-channel", 114 | "futures-core", 115 | "futures-executor", 116 | "futures-io", 117 | "futures-sink", 118 | "futures-task", 119 | "futures-util", 120 | ] 121 | 122 | [[package]] 123 | name = "futures-channel" 124 | version = "0.3.31" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 127 | dependencies = [ 128 | "futures-core", 129 | "futures-sink", 130 | ] 131 | 132 | [[package]] 133 | name = "futures-core" 134 | version = "0.3.31" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 137 | 138 | [[package]] 139 | name = "futures-executor" 140 | version = "0.3.31" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 143 | dependencies = [ 144 | "futures-core", 145 | "futures-task", 146 | "futures-util", 147 | ] 148 | 149 | [[package]] 150 | name = "futures-io" 151 | version = "0.3.31" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 154 | 155 | [[package]] 156 | name = "futures-macro" 157 | version = "0.3.31" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 160 | dependencies = [ 161 | "proc-macro2", 162 | "quote", 163 | "syn", 164 | ] 165 | 166 | [[package]] 167 | name = "futures-sink" 168 | version = "0.3.31" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 171 | 172 | [[package]] 173 | name = "futures-task" 174 | version = "0.3.31" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 177 | 178 | [[package]] 179 | name = "futures-util" 180 | version = "0.3.31" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 183 | dependencies = [ 184 | "futures-channel", 185 | "futures-core", 186 | "futures-io", 187 | "futures-macro", 188 | "futures-sink", 189 | "futures-task", 190 | "memchr", 191 | "pin-project-lite", 192 | "pin-utils", 193 | "slab", 194 | ] 195 | 196 | [[package]] 197 | name = "hashbrown" 198 | version = "0.15.3" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 201 | dependencies = [ 202 | "foldhash", 203 | ] 204 | 205 | [[package]] 206 | name = "heck" 207 | version = "0.5.0" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 210 | 211 | [[package]] 212 | name = "icu_collections" 213 | version = "1.5.0" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 216 | dependencies = [ 217 | "displaydoc", 218 | "yoke", 219 | "zerofrom", 220 | "zerovec", 221 | ] 222 | 223 | [[package]] 224 | name = "icu_locid" 225 | version = "1.5.0" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 228 | dependencies = [ 229 | "displaydoc", 230 | "litemap", 231 | "tinystr", 232 | "writeable", 233 | "zerovec", 234 | ] 235 | 236 | [[package]] 237 | name = "icu_locid_transform" 238 | version = "1.5.0" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 241 | dependencies = [ 242 | "displaydoc", 243 | "icu_locid", 244 | "icu_locid_transform_data", 245 | "icu_provider", 246 | "tinystr", 247 | "zerovec", 248 | ] 249 | 250 | [[package]] 251 | name = "icu_locid_transform_data" 252 | version = "1.5.1" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" 255 | 256 | [[package]] 257 | name = "icu_normalizer" 258 | version = "1.5.0" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 261 | dependencies = [ 262 | "displaydoc", 263 | "icu_collections", 264 | "icu_normalizer_data", 265 | "icu_properties", 266 | "icu_provider", 267 | "smallvec", 268 | "utf16_iter", 269 | "utf8_iter", 270 | "write16", 271 | "zerovec", 272 | ] 273 | 274 | [[package]] 275 | name = "icu_normalizer_data" 276 | version = "1.5.1" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" 279 | 280 | [[package]] 281 | name = "icu_properties" 282 | version = "1.5.1" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 285 | dependencies = [ 286 | "displaydoc", 287 | "icu_collections", 288 | "icu_locid_transform", 289 | "icu_properties_data", 290 | "icu_provider", 291 | "tinystr", 292 | "zerovec", 293 | ] 294 | 295 | [[package]] 296 | name = "icu_properties_data" 297 | version = "1.5.1" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" 300 | 301 | [[package]] 302 | name = "icu_provider" 303 | version = "1.5.0" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 306 | dependencies = [ 307 | "displaydoc", 308 | "icu_locid", 309 | "icu_provider_macros", 310 | "stable_deref_trait", 311 | "tinystr", 312 | "writeable", 313 | "yoke", 314 | "zerofrom", 315 | "zerovec", 316 | ] 317 | 318 | [[package]] 319 | name = "icu_provider_macros" 320 | version = "1.5.0" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 323 | dependencies = [ 324 | "proc-macro2", 325 | "quote", 326 | "syn", 327 | ] 328 | 329 | [[package]] 330 | name = "id-arena" 331 | version = "2.2.1" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" 334 | 335 | [[package]] 336 | name = "idna" 337 | version = "1.0.3" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 340 | dependencies = [ 341 | "idna_adapter", 342 | "smallvec", 343 | "utf8_iter", 344 | ] 345 | 346 | [[package]] 347 | name = "idna_adapter" 348 | version = "1.2.0" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 351 | dependencies = [ 352 | "icu_normalizer", 353 | "icu_properties", 354 | ] 355 | 356 | [[package]] 357 | name = "indexmap" 358 | version = "2.9.0" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 361 | dependencies = [ 362 | "equivalent", 363 | "hashbrown", 364 | "serde", 365 | ] 366 | 367 | [[package]] 368 | name = "itoa" 369 | version = "1.0.11" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 372 | 373 | [[package]] 374 | name = "leb128fmt" 375 | version = "0.1.0" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" 378 | 379 | [[package]] 380 | name = "litemap" 381 | version = "0.7.5" 382 | source = "registry+https://github.com/rust-lang/crates.io-index" 383 | checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" 384 | 385 | [[package]] 386 | name = "log" 387 | version = "0.4.22" 388 | source = "registry+https://github.com/rust-lang/crates.io-index" 389 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 390 | 391 | [[package]] 392 | name = "memchr" 393 | version = "2.7.4" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 396 | 397 | [[package]] 398 | name = "miniz_oxide" 399 | version = "0.8.8" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" 402 | dependencies = [ 403 | "adler2", 404 | ] 405 | 406 | [[package]] 407 | name = "once_cell" 408 | version = "1.21.3" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 411 | 412 | [[package]] 413 | name = "percent-encoding" 414 | version = "2.3.1" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 417 | 418 | [[package]] 419 | name = "pin-project-lite" 420 | version = "0.2.16" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 423 | 424 | [[package]] 425 | name = "pin-utils" 426 | version = "0.1.0" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 429 | 430 | [[package]] 431 | name = "prettyplease" 432 | version = "0.2.32" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" 435 | dependencies = [ 436 | "proc-macro2", 437 | "syn", 438 | ] 439 | 440 | [[package]] 441 | name = "proc-macro2" 442 | version = "1.0.95" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 445 | dependencies = [ 446 | "unicode-ident", 447 | ] 448 | 449 | [[package]] 450 | name = "quote" 451 | version = "1.0.37" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 454 | dependencies = [ 455 | "proc-macro2", 456 | ] 457 | 458 | [[package]] 459 | name = "regex" 460 | version = "1.11.1" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 463 | dependencies = [ 464 | "aho-corasick", 465 | "memchr", 466 | "regex-automata", 467 | "regex-syntax", 468 | ] 469 | 470 | [[package]] 471 | name = "regex-automata" 472 | version = "0.4.9" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 475 | dependencies = [ 476 | "aho-corasick", 477 | "memchr", 478 | "regex-syntax", 479 | ] 480 | 481 | [[package]] 482 | name = "regex-syntax" 483 | version = "0.8.5" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 486 | 487 | [[package]] 488 | name = "ryu" 489 | version = "1.0.18" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 492 | 493 | [[package]] 494 | name = "semver" 495 | version = "1.0.23" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 498 | dependencies = [ 499 | "serde", 500 | ] 501 | 502 | [[package]] 503 | name = "serde" 504 | version = "1.0.210" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 507 | dependencies = [ 508 | "serde_derive", 509 | ] 510 | 511 | [[package]] 512 | name = "serde_derive" 513 | version = "1.0.210" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 516 | dependencies = [ 517 | "proc-macro2", 518 | "quote", 519 | "syn", 520 | ] 521 | 522 | [[package]] 523 | name = "serde_json" 524 | version = "1.0.128" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" 527 | dependencies = [ 528 | "itoa", 529 | "memchr", 530 | "ryu", 531 | "serde", 532 | ] 533 | 534 | [[package]] 535 | name = "slab" 536 | version = "0.4.9" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 539 | dependencies = [ 540 | "autocfg", 541 | ] 542 | 543 | [[package]] 544 | name = "smallvec" 545 | version = "1.13.2" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 548 | 549 | [[package]] 550 | name = "spdx" 551 | version = "0.10.6" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "47317bbaf63785b53861e1ae2d11b80d6b624211d42cb20efcd210ee6f8a14bc" 554 | dependencies = [ 555 | "smallvec", 556 | ] 557 | 558 | [[package]] 559 | name = "stable_deref_trait" 560 | version = "1.2.0" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 563 | 564 | [[package]] 565 | name = "syn" 566 | version = "2.0.101" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 569 | dependencies = [ 570 | "proc-macro2", 571 | "quote", 572 | "unicode-ident", 573 | ] 574 | 575 | [[package]] 576 | name = "synstructure" 577 | version = "0.13.2" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 580 | dependencies = [ 581 | "proc-macro2", 582 | "quote", 583 | "syn", 584 | ] 585 | 586 | [[package]] 587 | name = "tinystr" 588 | version = "0.7.6" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 591 | dependencies = [ 592 | "displaydoc", 593 | "zerovec", 594 | ] 595 | 596 | [[package]] 597 | name = "topological-sort" 598 | version = "0.2.2" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" 601 | 602 | [[package]] 603 | name = "unicode-ident" 604 | version = "1.0.13" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 607 | 608 | [[package]] 609 | name = "unicode-xid" 610 | version = "0.2.6" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" 613 | 614 | [[package]] 615 | name = "url" 616 | version = "2.5.4" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 619 | dependencies = [ 620 | "form_urlencoded", 621 | "idna", 622 | "percent-encoding", 623 | ] 624 | 625 | [[package]] 626 | name = "utf16_iter" 627 | version = "1.0.5" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 630 | 631 | [[package]] 632 | name = "utf8_iter" 633 | version = "1.0.4" 634 | source = "registry+https://github.com/rust-lang/crates.io-index" 635 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 636 | 637 | [[package]] 638 | name = "wasm-encoder" 639 | version = "0.227.1" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" 642 | dependencies = [ 643 | "leb128fmt", 644 | "wasmparser", 645 | ] 646 | 647 | [[package]] 648 | name = "wasm-metadata" 649 | version = "0.227.1" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "ce1ef0faabbbba6674e97a56bee857ccddf942785a336c8b47b42373c922a91d" 652 | dependencies = [ 653 | "anyhow", 654 | "auditable-serde", 655 | "flate2", 656 | "indexmap", 657 | "serde", 658 | "serde_derive", 659 | "serde_json", 660 | "spdx", 661 | "url", 662 | "wasm-encoder", 663 | "wasmparser", 664 | ] 665 | 666 | [[package]] 667 | name = "wasmparser" 668 | version = "0.227.1" 669 | source = "registry+https://github.com/rust-lang/crates.io-index" 670 | checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" 671 | dependencies = [ 672 | "bitflags", 673 | "hashbrown", 674 | "indexmap", 675 | "semver", 676 | ] 677 | 678 | [[package]] 679 | name = "wit-bindgen" 680 | version = "0.41.0" 681 | source = "registry+https://github.com/rust-lang/crates.io-index" 682 | checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" 683 | dependencies = [ 684 | "wit-bindgen-rt", 685 | "wit-bindgen-rust-macro", 686 | ] 687 | 688 | [[package]] 689 | name = "wit-bindgen-core" 690 | version = "0.41.0" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" 693 | dependencies = [ 694 | "anyhow", 695 | "heck", 696 | "wit-parser", 697 | ] 698 | 699 | [[package]] 700 | name = "wit-bindgen-rt" 701 | version = "0.41.0" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" 704 | dependencies = [ 705 | "bitflags", 706 | "futures", 707 | "once_cell", 708 | ] 709 | 710 | [[package]] 711 | name = "wit-bindgen-rust" 712 | version = "0.41.0" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "9d0809dc5ba19e2e98661bf32fc0addc5a3ca5bf3a6a7083aa6ba484085ff3ce" 715 | dependencies = [ 716 | "anyhow", 717 | "heck", 718 | "indexmap", 719 | "prettyplease", 720 | "syn", 721 | "wasm-metadata", 722 | "wit-bindgen-core", 723 | "wit-component", 724 | ] 725 | 726 | [[package]] 727 | name = "wit-bindgen-rust-macro" 728 | version = "0.41.0" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "ad19eec017904e04c60719592a803ee5da76cb51c81e3f6fbf9457f59db49799" 731 | dependencies = [ 732 | "anyhow", 733 | "prettyplease", 734 | "proc-macro2", 735 | "quote", 736 | "syn", 737 | "wit-bindgen-core", 738 | "wit-bindgen-rust", 739 | ] 740 | 741 | [[package]] 742 | name = "wit-component" 743 | version = "0.227.1" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" 746 | dependencies = [ 747 | "anyhow", 748 | "bitflags", 749 | "indexmap", 750 | "log", 751 | "serde", 752 | "serde_derive", 753 | "serde_json", 754 | "wasm-encoder", 755 | "wasm-metadata", 756 | "wasmparser", 757 | "wit-parser", 758 | ] 759 | 760 | [[package]] 761 | name = "wit-parser" 762 | version = "0.227.1" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "ddf445ed5157046e4baf56f9138c124a0824d4d1657e7204d71886ad8ce2fc11" 765 | dependencies = [ 766 | "anyhow", 767 | "id-arena", 768 | "indexmap", 769 | "log", 770 | "semver", 771 | "serde", 772 | "serde_derive", 773 | "serde_json", 774 | "unicode-xid", 775 | "wasmparser", 776 | ] 777 | 778 | [[package]] 779 | name = "write16" 780 | version = "1.0.0" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 783 | 784 | [[package]] 785 | name = "writeable" 786 | version = "0.5.5" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 789 | 790 | [[package]] 791 | name = "yoke" 792 | version = "0.7.5" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 795 | dependencies = [ 796 | "serde", 797 | "stable_deref_trait", 798 | "yoke-derive", 799 | "zerofrom", 800 | ] 801 | 802 | [[package]] 803 | name = "yoke-derive" 804 | version = "0.7.5" 805 | source = "registry+https://github.com/rust-lang/crates.io-index" 806 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 807 | dependencies = [ 808 | "proc-macro2", 809 | "quote", 810 | "syn", 811 | "synstructure", 812 | ] 813 | 814 | [[package]] 815 | name = "zed_extension_api" 816 | version = "0.5.0" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "40ef88a8e5aeff67b0996b1795d56338f04c02de95f1f147577944aa37b801d6" 819 | dependencies = [ 820 | "serde", 821 | "serde_json", 822 | "wit-bindgen", 823 | ] 824 | 825 | [[package]] 826 | name = "zed_ruby" 827 | version = "0.9.0" 828 | dependencies = [ 829 | "regex", 830 | "zed_extension_api", 831 | ] 832 | 833 | [[package]] 834 | name = "zerofrom" 835 | version = "0.1.6" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 838 | dependencies = [ 839 | "zerofrom-derive", 840 | ] 841 | 842 | [[package]] 843 | name = "zerofrom-derive" 844 | version = "0.1.6" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 847 | dependencies = [ 848 | "proc-macro2", 849 | "quote", 850 | "syn", 851 | "synstructure", 852 | ] 853 | 854 | [[package]] 855 | name = "zerovec" 856 | version = "0.10.4" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 859 | dependencies = [ 860 | "yoke", 861 | "zerofrom", 862 | "zerovec-derive", 863 | ] 864 | 865 | [[package]] 866 | name = "zerovec-derive" 867 | version = "0.10.3" 868 | source = "registry+https://github.com/rust-lang/crates.io-index" 869 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 870 | dependencies = [ 871 | "proc-macro2", 872 | "quote", 873 | "syn", 874 | ] 875 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "zed_ruby" 3 | version = "0.9.0" 4 | edition = "2021" 5 | publish = false 6 | license = "Apache-2.0" 7 | 8 | [lib] 9 | path = "src/ruby.rs" 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | regex = "1.11.1" 14 | zed_extension_api = "0.5.0" 15 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Copyright 2022 - 2024 Zed Industries, Inc. 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | 18 | 19 | 20 | 21 | Apache License 22 | Version 2.0, January 2004 23 | http://www.apache.org/licenses/ 24 | 25 | 26 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 27 | 28 | 29 | 1. Definitions. 30 | 31 | 32 | "License" shall mean the terms and conditions for use, reproduction, 33 | and distribution as defined by Sections 1 through 9 of this document. 34 | 35 | 36 | "Licensor" shall mean the copyright owner or entity authorized by 37 | the copyright owner that is granting the License. 38 | 39 | 40 | "Legal Entity" shall mean the union of the acting entity and all 41 | other entities that control, are controlled by, or are under common 42 | control with that entity. For the purposes of this definition, 43 | "control" means (i) the power, direct or indirect, to cause the 44 | direction or management of such entity, whether by contract or 45 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 46 | outstanding shares, or (iii) beneficial ownership of such entity. 47 | 48 | 49 | "You" (or "Your") shall mean an individual or Legal Entity 50 | exercising permissions granted by this License. 51 | 52 | 53 | "Source" form shall mean the preferred form for making modifications, 54 | including but not limited to software source code, documentation 55 | source, and configuration files. 56 | 57 | 58 | "Object" form shall mean any form resulting from mechanical 59 | transformation or translation of a Source form, including but 60 | not limited to compiled object code, generated documentation, 61 | and conversions to other media types. 62 | 63 | 64 | "Work" shall mean the work of authorship, whether in Source or 65 | Object form, made available under the License, as indicated by a 66 | copyright notice that is included in or attached to the work 67 | (an example is provided in the Appendix below). 68 | 69 | 70 | "Derivative Works" shall mean any work, whether in Source or Object 71 | form, that is based on (or derived from) the Work and for which the 72 | editorial revisions, annotations, elaborations, or other modifications 73 | represent, as a whole, an original work of authorship. For the purposes 74 | of this License, Derivative Works shall not include works that remain 75 | separable from, or merely link (or bind by name) to the interfaces of, 76 | the Work and Derivative Works thereof. 77 | 78 | 79 | "Contribution" shall mean any work of authorship, including 80 | the original version of the Work and any modifications or additions 81 | to that Work or Derivative Works thereof, that is intentionally 82 | submitted to Licensor for inclusion in the Work by the copyright owner 83 | or by an individual or Legal Entity authorized to submit on behalf of 84 | the copyright owner. For the purposes of this definition, "submitted" 85 | means any form of electronic, verbal, or written communication sent 86 | to the Licensor or its representatives, including but not limited to 87 | communication on electronic mailing lists, source code control systems, 88 | and issue tracking systems that are managed by, or on behalf of, the 89 | Licensor for the purpose of discussing and improving the Work, but 90 | excluding communication that is conspicuously marked or otherwise 91 | designated in writing by the copyright owner as "Not a Contribution." 92 | 93 | 94 | "Contributor" shall mean Licensor and any individual or Legal Entity 95 | on behalf of whom a Contribution has been received by Licensor and 96 | subsequently incorporated within the Work. 97 | 98 | 99 | 2. Grant of Copyright License. Subject to the terms and conditions of 100 | this License, each Contributor hereby grants to You a perpetual, 101 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 102 | copyright license to reproduce, prepare Derivative Works of, 103 | publicly display, publicly perform, sublicense, and distribute the 104 | Work and such Derivative Works in Source or Object form. 105 | 106 | 107 | 3. Grant of Patent License. Subject to the terms and conditions of 108 | this License, each Contributor hereby grants to You a perpetual, 109 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 110 | (except as stated in this section) patent license to make, have made, 111 | use, offer to sell, sell, import, and otherwise transfer the Work, 112 | where such license applies only to those patent claims licensable 113 | by such Contributor that are necessarily infringed by their 114 | Contribution(s) alone or by combination of their Contribution(s) 115 | with the Work to which such Contribution(s) was submitted. If You 116 | institute patent litigation against any entity (including a 117 | cross-claim or counterclaim in a lawsuit) alleging that the Work 118 | or a Contribution incorporated within the Work constitutes direct 119 | or contributory patent infringement, then any patent licenses 120 | granted to You under this License for that Work shall terminate 121 | as of the date such litigation is filed. 122 | 123 | 124 | 4. Redistribution. You may reproduce and distribute copies of the 125 | Work or Derivative Works thereof in any medium, with or without 126 | modifications, and in Source or Object form, provided that You 127 | meet the following conditions: 128 | 129 | 130 | (a) You must give any other recipients of the Work or 131 | Derivative Works a copy of this License; and 132 | 133 | 134 | (b) You must cause any modified files to carry prominent notices 135 | stating that You changed the files; and 136 | 137 | 138 | (c) You must retain, in the Source form of any Derivative Works 139 | that You distribute, all copyright, patent, trademark, and 140 | attribution notices from the Source form of the Work, 141 | excluding those notices that do not pertain to any part of 142 | the Derivative Works; and 143 | 144 | 145 | (d) If the Work includes a "NOTICE" text file as part of its 146 | distribution, then any Derivative Works that You distribute must 147 | include a readable copy of the attribution notices contained 148 | within such NOTICE file, excluding those notices that do not 149 | pertain to any part of the Derivative Works, in at least one 150 | of the following places: within a NOTICE text file distributed 151 | as part of the Derivative Works; within the Source form or 152 | documentation, if provided along with the Derivative Works; or, 153 | within a display generated by the Derivative Works, if and 154 | wherever such third-party notices normally appear. The contents 155 | of the NOTICE file are for informational purposes only and 156 | do not modify the License. You may add Your own attribution 157 | notices within Derivative Works that You distribute, alongside 158 | or as an addendum to the NOTICE text from the Work, provided 159 | that such additional attribution notices cannot be construed 160 | as modifying the License. 161 | 162 | 163 | You may add Your own copyright statement to Your modifications and 164 | may provide additional or different license terms and conditions 165 | for use, reproduction, or distribution of Your modifications, or 166 | for any such Derivative Works as a whole, provided Your use, 167 | reproduction, and distribution of the Work otherwise complies with 168 | the conditions stated in this License. 169 | 170 | 171 | 5. Submission of Contributions. Unless You explicitly state otherwise, 172 | any Contribution intentionally submitted for inclusion in the Work 173 | by You to the Licensor shall be under the terms and conditions of 174 | this License, without any additional terms or conditions. 175 | Notwithstanding the above, nothing herein shall supersede or modify 176 | the terms of any separate license agreement you may have executed 177 | with Licensor regarding such Contributions. 178 | 179 | 180 | 6. Trademarks. This License does not grant permission to use the trade 181 | names, trademarks, service marks, or product names of the Licensor, 182 | except as required for reasonable and customary use in describing the 183 | origin of the Work and reproducing the content of the NOTICE file. 184 | 185 | 186 | 7. Disclaimer of Warranty. Unless required by applicable law or 187 | agreed to in writing, Licensor provides the Work (and each 188 | Contributor provides its Contributions) on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 190 | implied, including, without limitation, any warranties or conditions 191 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 192 | PARTICULAR PURPOSE. You are solely responsible for determining the 193 | appropriateness of using or redistributing the Work and assume any 194 | risks associated with Your exercise of permissions under this License. 195 | 196 | 197 | 8. Limitation of Liability. In no event and under no legal theory, 198 | whether in tort (including negligence), contract, or otherwise, 199 | unless required by applicable law (such as deliberate and grossly 200 | negligent acts) or agreed to in writing, shall any Contributor be 201 | liable to You for damages, including any direct, indirect, special, 202 | incidental, or consequential damages of any character arising as a 203 | result of this License or out of the use or inability to use the 204 | Work (including but not limited to damages for loss of goodwill, 205 | work stoppage, computer failure or malfunction, or any and all 206 | other commercial damages or losses), even if such Contributor 207 | has been advised of the possibility of such damages. 208 | 209 | 210 | 9. Accepting Warranty or Additional Liability. While redistributing 211 | the Work or Derivative Works thereof, You may choose to offer, 212 | and charge a fee for, acceptance of support, warranty, indemnity, 213 | or other liability obligations and/or rights consistent with this 214 | License. However, in accepting such obligations, You may act only 215 | on Your own behalf and on Your sole responsibility, not on behalf 216 | of any other Contributor, and only if You agree to indemnify, 217 | defend, and hold each Contributor harmless for any liability 218 | incurred by, or claims asserted against, such Contributor by reason 219 | of your accepting any such warranty or additional liability. 220 | 221 | 222 | END OF TERMS AND CONDITIONS 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ruby extension for Zed 2 | 3 | [Documentation](https://zed.dev/docs/languages/ruby) 4 | -------------------------------------------------------------------------------- /extension.toml: -------------------------------------------------------------------------------- 1 | id = "ruby" 2 | name = "Ruby" 3 | description = "Ruby support." 4 | version = "0.9.0" 5 | schema_version = 1 6 | authors = ["Vitaly Slobodin "] 7 | repository = "https://github.com/zed-extensions/ruby" 8 | 9 | [language_servers.solargraph] 10 | name = "Solargraph" 11 | languages = ["Ruby"] 12 | 13 | [language_servers.ruby-lsp] 14 | name = "Ruby LSP" 15 | languages = ["Ruby", "ERB"] 16 | 17 | [language_servers.rubocop] 18 | name = "Rubocop" 19 | languages = ["Ruby"] 20 | 21 | [language_servers.steep] 22 | name = "Steep" 23 | languages = ["Ruby"] 24 | 25 | [language_servers.sorbet] 26 | name = "Sorbet" 27 | languages = ["Ruby"] 28 | 29 | [grammars.ruby] 30 | repository = "https://github.com/tree-sitter/tree-sitter-ruby" 31 | commit = "71bd32fb7607035768799732addba884a37a6210" 32 | 33 | [grammars.embedded_template] 34 | repository = "https://github.com/tree-sitter/tree-sitter-embedded-template" 35 | commit = "332262529bc51abf5746317b2255ccc2fff778f8" 36 | 37 | [grammars.rbs] 38 | repository = "https://github.com/joker1007/tree-sitter-rbs" 39 | commit = "de893b166476205b09e79cd3689f95831269579a" 40 | 41 | [[capabilities]] 42 | kind = "process:exec" 43 | command = "gem" 44 | args = ["install", "--norc", "--no-user-install", "--no-format-executable", "--no-document", "*"] 45 | 46 | [[capabilities]] 47 | kind = "process:exec" 48 | command = "gem" 49 | args = ["list", "--norc", "--exact", "*"] 50 | 51 | [[capabilities]] 52 | kind = "process:exec" 53 | command = "bundle" 54 | args = ["info", "--version", "*"] 55 | 56 | [[capabilities]] 57 | kind = "process:exec" 58 | command = "gem" 59 | args = ["outdated", "--norc"] 60 | 61 | [[capabilities]] 62 | kind = "process:exec" 63 | command = "gem" 64 | args = ["update", "--norc", "*"] 65 | -------------------------------------------------------------------------------- /languages/erb/config.toml: -------------------------------------------------------------------------------- 1 | name = "ERB" 2 | grammar = "embedded_template" 3 | path_suffixes = ["erb"] 4 | autoclose_before = ">})" 5 | brackets = [{ start = "<", end = ">", close = true, newline = true }] 6 | block_comment = ["<%#", "%>"] 7 | word_characters = ["?", "!"] 8 | -------------------------------------------------------------------------------- /languages/erb/highlights.scm: -------------------------------------------------------------------------------- 1 | (comment_directive) @comment 2 | 3 | [ 4 | "<%#" 5 | "<%" 6 | "<%=" 7 | "<%_" 8 | "<%-" 9 | "%>" 10 | "-%>" 11 | "_%>" 12 | ] @keyword 13 | -------------------------------------------------------------------------------- /languages/erb/injections.scm: -------------------------------------------------------------------------------- 1 | ((code) @content 2 | (#set! "language" "ruby") 3 | (#set! "combined")) 4 | 5 | ((content) @content 6 | (#set! "language" "html") 7 | (#set! "combined")) 8 | -------------------------------------------------------------------------------- /languages/rbs/config.toml: -------------------------------------------------------------------------------- 1 | name = "RBS" 2 | grammar = "rbs" 3 | path_suffixes = ["rbs"] 4 | autoclose_before = "]})" 5 | brackets = [ 6 | { start = "(", end = ")", close = true, newline = false }, 7 | { start = "{", end = "}", close = true, newline = false }, 8 | { start = "[", end = "]", close = true, newline = false }, 9 | ] 10 | line_comments = ["#"] 11 | word_characters = ["?", "!"] 12 | -------------------------------------------------------------------------------- /languages/rbs/highlights.scm: -------------------------------------------------------------------------------- 1 | ; Taken from https://github.com/nvim-treesitter/nvim-treesitter/blob/master/queries/rbs/highlights.scm 2 | ; Use directive 3 | (use_clause 4 | [ 5 | (type_name) 6 | (simple_type_name) 7 | ] @type) 8 | 9 | ; Builtin constants and Keywords 10 | [ 11 | "true" 12 | "false" 13 | ] @boolean 14 | 15 | "nil" @constant.builtin 16 | 17 | [ 18 | "use" 19 | "as" 20 | "module" 21 | "def" 22 | "attr_reader" 23 | "attr_writer" 24 | "attr_accessor" 25 | "end" 26 | "alias" 27 | ] @keyword 28 | 29 | [ 30 | "interface" 31 | "type" 32 | "class" 33 | ] @keyword.type 34 | 35 | (class_decl 36 | "end" @keyword.type) 37 | 38 | (interface_decl 39 | "end" @keyword.type) 40 | 41 | "def" @keyword.function 42 | 43 | ; Members of declaration 44 | [ 45 | "include" 46 | "extend" 47 | "prepend" 48 | ] @function.method 49 | 50 | (visibility) @keyword.modifier 51 | 52 | (comment) @comment 53 | 54 | (method_member 55 | (method_name 56 | [ 57 | (identifier) 58 | (identifier_suffix) 59 | (constant) 60 | (constant_suffix) 61 | (operator) 62 | (setter) 63 | (constant_setter) 64 | ] @function.method)) 65 | 66 | (attribute_member 67 | (method_name 68 | [ 69 | (identifier) 70 | (identifier_suffix) 71 | (constant) 72 | (constant_suffix) 73 | (operator) 74 | (setter) 75 | (constant_setter) 76 | ] @function.method)) 77 | 78 | [ 79 | (ivar_name) 80 | (cvar_name) 81 | ] @variable.member 82 | 83 | (alias_member 84 | (method_name) @function) 85 | 86 | (class_name 87 | (constant) @type) 88 | 89 | (module_name 90 | (constant) @type) 91 | 92 | (interface_name 93 | (interface) @type) 94 | 95 | (alias_name 96 | (identifier) @type) 97 | 98 | (type_variable) @constant 99 | 100 | (namespace 101 | (constant) @module) 102 | 103 | (builtin_type) @type.builtin 104 | 105 | (const_name 106 | (constant) @constant) 107 | 108 | (global_name) @variable 109 | 110 | ; Standard Arguments 111 | (parameter 112 | (var_name) @variable.parameter) 113 | 114 | ; Keyword Arguments 115 | (keyword) @variable.parameter 116 | 117 | ; Self 118 | (self) @variable.builtin 119 | 120 | ; Literal 121 | (type 122 | (symbol_literal) @string.special.symbol) 123 | 124 | (type 125 | (string_literal 126 | (escape_sequence) @string.escape)) 127 | 128 | (type 129 | (string_literal) @string) 130 | 131 | (type 132 | (integer_literal) @number) 133 | 134 | (type 135 | (record_type 136 | key: (record_key) @string.special.symbol)) 137 | 138 | ; Operators 139 | [ 140 | "=" 141 | "->" 142 | "<" 143 | "**" 144 | "*" 145 | "&" 146 | "|" 147 | "^" 148 | ] @operator 149 | 150 | ; Punctuation 151 | [ 152 | "(" 153 | ")" 154 | "[" 155 | "]" 156 | "{" 157 | "}" 158 | ] @punctuation.bracket 159 | 160 | [ 161 | "," 162 | "." 163 | ] @punctuation.delimiter 164 | -------------------------------------------------------------------------------- /languages/rbs/indents.scm: -------------------------------------------------------------------------------- 1 | [ 2 | (class_decl) 3 | (module_decl) 4 | (interface_decl) 5 | (parameters) 6 | (tuple_type) 7 | (record_type) 8 | ] @indent.begin 9 | 10 | (_ "[" "]" @end) @indent 11 | (_ "{" "}" @end) @indent 12 | (_ "(" ")" @end) @indent 13 | 14 | (comment) @indent.ignore 15 | -------------------------------------------------------------------------------- /languages/rbs/injections.scm: -------------------------------------------------------------------------------- 1 | ((comment) @injection.content 2 | (#set! injection.language "comment")) 3 | -------------------------------------------------------------------------------- /languages/ruby/brackets.scm: -------------------------------------------------------------------------------- 1 | ("[" @open "]" @close) 2 | ("{" @open "}" @close) 3 | ("\"" @open "\"" @close) 4 | ("do" @open "end" @close) 5 | 6 | (block_parameters "|" @open "|" @close) 7 | (interpolation "#{" @open "}" @close) 8 | 9 | (if "if" @open "end" @close) 10 | (unless "unless" @open "end" @close) 11 | (begin "begin" @open "end" @close) 12 | (module "module" @open "end" @close) 13 | (_ . "def" @open "end" @close) 14 | (_ . "class" @open "end" @close) 15 | -------------------------------------------------------------------------------- /languages/ruby/config.toml: -------------------------------------------------------------------------------- 1 | name = "Ruby" 2 | grammar = "ruby" 3 | path_suffixes = [ 4 | "rb", 5 | "Gemfile", 6 | "Guardfile", 7 | "rake", 8 | "Rakefile", 9 | "ru", 10 | "thor", 11 | "cap", 12 | "capfile", 13 | "Capfile", 14 | "jbuilder", 15 | "rabl", 16 | "rxml", 17 | "builder", 18 | "gemspec", 19 | "thor", 20 | "irbrc", 21 | "pryrc", 22 | "simplecov", 23 | "Steepfile", 24 | "Podfile", 25 | "Brewfile", 26 | "Vagrantfile", 27 | "Puppetfile", 28 | "Fastfile", 29 | "Appfile", 30 | "Matchfile", 31 | "Cheffile", 32 | "Hobofile", 33 | "Appraisals", 34 | "Rantfile", 35 | "Berksfile", 36 | "Berksfile.lock", 37 | "Thorfile", 38 | "Dangerfile", 39 | "Deliverfile", 40 | "Scanfile", 41 | "Snapfile", 42 | "Gymfile", 43 | ] 44 | first_line_pattern = '^#!.*\bruby\b' 45 | line_comments = ["# "] 46 | autoclose_before = ";:.,=}])>" 47 | brackets = [ 48 | { start = "{", end = "}", close = true, newline = true }, 49 | { start = "[", end = "]", close = true, newline = true }, 50 | { start = "(", end = ")", close = true, newline = true }, 51 | { start = "\"", end = "\"", close = true, newline = false, not_in = [ 52 | "comment", 53 | "string", 54 | ] }, 55 | { start = "'", end = "'", close = true, newline = false, not_in = [ 56 | "comment", 57 | "string", 58 | ] }, 59 | ] 60 | collapsed_placeholder = "# ..." 61 | tab_size = 2 62 | scope_opt_in_language_servers = ["tailwindcss-language-server"] 63 | word_characters = ["?", "!"] 64 | 65 | [overrides.string] 66 | completion_query_characters = ["-", "."] 67 | opt_into_language_servers = ["tailwindcss-language-server"] 68 | -------------------------------------------------------------------------------- /languages/ruby/embedding.scm: -------------------------------------------------------------------------------- 1 | ( 2 | (comment)* @context 3 | . 4 | [ 5 | (module 6 | "module" @name 7 | name: (_) @name) 8 | (method 9 | "def" @name 10 | name: (_) @name 11 | body: (body_statement) @collapse) 12 | (class 13 | "class" @name 14 | name: (_) @name) 15 | (singleton_method 16 | "def" @name 17 | object: (_) @name 18 | "." @name 19 | name: (_) @name 20 | body: (body_statement) @collapse) 21 | ] @item 22 | ) 23 | -------------------------------------------------------------------------------- /languages/ruby/highlights.scm: -------------------------------------------------------------------------------- 1 | ; Variables 2 | [ 3 | (identifier) 4 | (global_variable) 5 | ] @variable 6 | 7 | ; Keywords 8 | 9 | [ 10 | "alias" 11 | "and" 12 | "begin" 13 | "break" 14 | "case" 15 | "class" 16 | "def" 17 | "do" 18 | "else" 19 | "elsif" 20 | "end" 21 | "ensure" 22 | "for" 23 | "if" 24 | "in" 25 | "module" 26 | "next" 27 | "or" 28 | "rescue" 29 | "retry" 30 | "return" 31 | "then" 32 | "unless" 33 | "until" 34 | "when" 35 | "while" 36 | "yield" 37 | ] @keyword 38 | 39 | ((identifier) @keyword 40 | (#match? @keyword "^(private|protected|public)$")) 41 | 42 | ; Function calls 43 | 44 | (program 45 | (call 46 | (identifier) @keyword.import) 47 | (#any-of? @keyword.import "require" "require_relative" "load")) 48 | 49 | "defined?" @function.method.builtin 50 | 51 | (call 52 | method: [(identifier) (constant)] @function.method) 53 | 54 | ; Function definitions 55 | 56 | (alias (identifier) @function.method) 57 | (setter (identifier) @function.method) 58 | (method name: [(identifier) (constant)] @function.method) 59 | (singleton_method name: [(identifier) (constant)] @function.method) 60 | (method_parameters [ 61 | (identifier) @variable.parameter 62 | (optional_parameter name: (identifier) @variable.parameter) 63 | (keyword_parameter [name: (identifier) (":")] @variable.parameter) 64 | ]) 65 | 66 | (block_parameters (identifier) @variable.parameter) 67 | 68 | ; Identifiers 69 | 70 | ((identifier) @constant.builtin 71 | (#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$")) 72 | 73 | (file) @constant.builtin 74 | (line) @constant.builtin 75 | (encoding) @constant.builtin 76 | 77 | (hash_splat_nil 78 | "**" @operator 79 | ) @constant.builtin 80 | 81 | (constant) @type 82 | 83 | ((constant) @constant 84 | (#match? @constant "^[A-Z\\d_]+$")) 85 | 86 | (superclass 87 | (constant) @type.super) 88 | 89 | (superclass 90 | (scope_resolution 91 | (constant) @type.super)) 92 | 93 | (superclass 94 | (scope_resolution 95 | (scope_resolution 96 | (constant) @type.super))) 97 | 98 | (self) @variable.special 99 | (super) @variable.special 100 | 101 | [ 102 | (class_variable) 103 | (instance_variable) 104 | ] @variable.special 105 | 106 | ((call 107 | !receiver 108 | method: (identifier) @function.builtin) 109 | (#any-of? @function.builtin "include" "extend" "prepend" "refine" "using")) 110 | 111 | ((identifier) @keyword.exception 112 | (#any-of? @keyword.exception "raise" "fail" "catch" "throw")) 113 | 114 | ; Literals 115 | 116 | [ 117 | (string) 118 | (bare_string) 119 | (subshell) 120 | (heredoc_body) 121 | (heredoc_beginning) 122 | ] @string 123 | 124 | [ 125 | (simple_symbol) 126 | (delimited_symbol) 127 | (hash_key_symbol) 128 | (bare_symbol) 129 | ] @string.special.symbol 130 | 131 | (regex) @string.regex 132 | (escape_sequence) @string.escape 133 | 134 | [ 135 | (integer) 136 | (float) 137 | ] @number 138 | 139 | [ 140 | (true) 141 | (false) 142 | ] @boolean 143 | 144 | [ 145 | (nil) 146 | ] @constant.builtin 147 | 148 | (comment) @comment 149 | 150 | ; Operators 151 | 152 | [ 153 | "!" 154 | "~" 155 | "+" 156 | "-" 157 | "**" 158 | "*" 159 | "/" 160 | "%" 161 | "<<" 162 | ">>" 163 | "&" 164 | "|" 165 | "^" 166 | ">" 167 | "<" 168 | "<=" 169 | ">=" 170 | "==" 171 | "!=" 172 | "=~" 173 | "!~" 174 | "<=>" 175 | "||" 176 | "&&" 177 | ".." 178 | "..." 179 | "=" 180 | "**=" 181 | "*=" 182 | "/=" 183 | "%=" 184 | "+=" 185 | "-=" 186 | "<<=" 187 | ">>=" 188 | "&&=" 189 | "&=" 190 | "||=" 191 | "|=" 192 | "^=" 193 | "=>" 194 | "->" 195 | (operator) 196 | ] @operator 197 | 198 | [ 199 | "," 200 | ";" 201 | "." 202 | "::" 203 | ] @punctuation.delimiter 204 | 205 | [ 206 | "(" 207 | ")" 208 | "[" 209 | "]" 210 | "{" 211 | "}" 212 | "%w(" 213 | "%i(" 214 | ] @punctuation.bracket 215 | 216 | (interpolation 217 | "#{" @punctuation.special 218 | "}" @punctuation.special) @embedded 219 | -------------------------------------------------------------------------------- /languages/ruby/indents.scm: -------------------------------------------------------------------------------- 1 | (method "end" @end) @indent 2 | (class "end" @end) @indent 3 | (module "end" @end) @indent 4 | (begin "end" @end) @indent 5 | (singleton_method "end" @end) @indent 6 | (do_block "end" @end) @indent 7 | 8 | [ 9 | (then) 10 | (call) 11 | ] @indent 12 | 13 | [ 14 | (ensure) 15 | (rescue) 16 | ] @outdent 17 | 18 | (_ "[" "]" @end) @indent 19 | (_ "{" "}" @end) @indent 20 | (_ "(" ")" @end) @indent 21 | -------------------------------------------------------------------------------- /languages/ruby/injections.scm: -------------------------------------------------------------------------------- 1 | (heredoc_body 2 | (heredoc_content) @content 3 | (heredoc_end) @language 4 | (#downcase! @language)) 5 | 6 | ((regex 7 | (string_content) @content) 8 | (#set! "language" "regex")) 9 | -------------------------------------------------------------------------------- /languages/ruby/outline.scm: -------------------------------------------------------------------------------- 1 | ; Class definitions, e.g. `class Foo` 2 | (class 3 | "class" @context 4 | name: (_) @name) @item 5 | 6 | ; Singleton class definitions `class << self` 7 | (singleton_class 8 | "class" @context 9 | "<<" @context 10 | value: (self) @context 11 | ) @item 12 | 13 | ; Method definition with a modifier, e.g. `private def foo` 14 | (body_statement 15 | (call 16 | method: (identifier) @context 17 | arguments: (argument_list 18 | (method 19 | "def" @context 20 | name: (_) @name) 21 | )) @item 22 | ) 23 | 24 | ; Method definition without modieifer, e.g. `def foo` 25 | (body_statement 26 | (method 27 | "def" @context 28 | name: (_) @name) @item 29 | ) 30 | 31 | ; Root method definition with modifier, e.g. `private def foo` 32 | (program 33 | (call 34 | method: (identifier) @context 35 | arguments: (argument_list 36 | (method 37 | "def" @context 38 | name: (_) @name) 39 | )) @item 40 | ) 41 | 42 | ; Root method definition without modifier, e.g. `def foo` 43 | (program 44 | (method 45 | "def" @context 46 | name: (_) @name) @item 47 | ) 48 | 49 | ; Root singleton method definition, e.g. `def self.foo` 50 | (program 51 | (singleton_method 52 | "def" @context 53 | object: (_) @context 54 | "." @context 55 | name: (_) @name) @item 56 | ) 57 | 58 | ; Singleton method definition without modifier, e.g. `def self.foo` 59 | (body_statement 60 | (singleton_method 61 | "def" @context 62 | object: (_) @context 63 | "." @context 64 | name: (_) @name) @item 65 | ) 66 | 67 | ; Singleton method definition with modifier, e.g. `private_class_method def self.foo` 68 | (body_statement 69 | (call 70 | method: (identifier) @context 71 | arguments: (argument_list 72 | (singleton_method 73 | "def" @context 74 | object: (_) @context 75 | "." @context 76 | name: (_) @name) @item 77 | )) @item 78 | ) 79 | 80 | ; Module definition, e.g. `module Foo` 81 | (module 82 | "module" @context 83 | name: (_) @name) @item 84 | 85 | ; Constant assignment 86 | (assignment left: (constant) @name) @item 87 | 88 | ; Class macros such as `alias_method`, `include`, `belongs_to`, `has_many`, `attr_reader` 89 | (class 90 | (body_statement 91 | (call 92 | method: (identifier) @name 93 | arguments: (argument_list . [ 94 | (string) @name 95 | (simple_symbol) @name 96 | (scope_resolution) @name 97 | (constant) @name 98 | "," @context 99 | ]* [ 100 | (string) @name 101 | (simple_symbol) @name 102 | (scope_resolution) @name 103 | (constant) @name 104 | ] 105 | ) 106 | ) @item 107 | ) 108 | ) 109 | 110 | ; Module macros such as `alias_method`, `include` 111 | (module 112 | (body_statement 113 | (call 114 | method: (identifier) @name 115 | arguments: (argument_list . [ 116 | (string) @name 117 | (simple_symbol) @name 118 | (scope_resolution) @name 119 | (constant) @name 120 | "," @context 121 | ]* [ 122 | (string) @name 123 | (simple_symbol) @name 124 | (scope_resolution) @name 125 | (constant) @name 126 | ] 127 | ) 128 | ) @item 129 | ) 130 | ) 131 | 132 | ; Class macros without arguments, such as `private` 133 | (class 134 | (body_statement 135 | (identifier) @name @item 136 | ) 137 | ) 138 | 139 | (class 140 | (body_statement 141 | (call 142 | method: (identifier) @name 143 | !arguments 144 | ) @item 145 | ) 146 | ) 147 | 148 | ; Module macros without arguments, such as `private` 149 | (module 150 | (body_statement 151 | (identifier) @name @item 152 | ) 153 | ) 154 | 155 | (module 156 | (body_statement 157 | (call 158 | method: (identifier) @name 159 | !arguments 160 | ) @item 161 | ) 162 | ) 163 | 164 | ; Root test methods 165 | (program 166 | (call 167 | method: (identifier) @run @name (#any-of? @run "describe" "context" "test" "it" "shared_examples") 168 | arguments: (argument_list . [ 169 | (string) @name 170 | (simple_symbol) @name 171 | (scope_resolution) @name 172 | (constant) @name 173 | "," @context 174 | ]* [ 175 | (string) @name 176 | (simple_symbol) @name 177 | (scope_resolution) @name 178 | (constant) @name 179 | ] 180 | ) 181 | ) @item 182 | ) 183 | 184 | ; Nested test methods 185 | (call 186 | method: (identifier) @ctx (#any-of? @ctx "describe" "context" "shared_examples") 187 | arguments: (argument_list . [ 188 | (string) 189 | (simple_symbol) 190 | (scope_resolution) 191 | (constant) 192 | ]+ 193 | ) 194 | block: (_ 195 | (_ 196 | (call 197 | method: (identifier) @run @name (#any-of? @run "describe" "context" "test" "it" "shared_examples") 198 | arguments: (argument_list . [ 199 | (string) @name 200 | (simple_symbol) @name 201 | (scope_resolution) @name 202 | (constant) @name 203 | "," @context 204 | ]* [ 205 | (string) @name 206 | (simple_symbol) @name 207 | (scope_resolution) @name 208 | (constant) @name 209 | ] 210 | ) 211 | ) @item 212 | ) 213 | ) 214 | ) 215 | 216 | ; RSpec one-liners 217 | (call 218 | method: (identifier) @ctx (#any-of? @ctx "describe" "context" "shared_examples") 219 | arguments: (argument_list . [ 220 | (string) 221 | (simple_symbol) 222 | (scope_resolution) 223 | (constant) 224 | ]+ 225 | ) 226 | block: (_ 227 | (_ 228 | (call 229 | method: (identifier) @run @name (#any-of? @run "it") 230 | block: (block 231 | body: (block_body 232 | (call 233 | receiver: (identifier) @expectation (#any-of? @expectation "is_expected") 234 | method: (identifier) @negation (#any-of? @negation "to" "not_to" "to_not") 235 | ) 236 | ) 237 | ) @name 238 | ) @item 239 | ) 240 | ) 241 | ) 242 | -------------------------------------------------------------------------------- /languages/ruby/overrides.scm: -------------------------------------------------------------------------------- 1 | (comment) @comment.inclusive 2 | 3 | (string) @string 4 | -------------------------------------------------------------------------------- /languages/ruby/runnables.scm: -------------------------------------------------------------------------------- 1 | ; Adapted from the following sources: 2 | ; Minitest: https://github.com/zidhuss/neotest-minitest/blob/main/lua/neotest-minitest/init.lua 3 | ; RSpec: https://github.com/olimorris/neotest-rspec/blob/main/lua/neotest-rspec/init.lua 4 | 5 | ; Tests that inherit from a specific class 6 | ( 7 | (class 8 | name: [ 9 | (constant) @run @name @RUBY_TEST_NAME 10 | (scope_resolution scope: (constant) name: (constant) @run) 11 | ] 12 | (superclass (scope_resolution) @superclass (#match? @superclass "(::IntegrationTest|::TestCase|::SystemTestCase|Minitest::Test|TLDR)$")) 13 | ) @_ruby-test 14 | (#set! tag ruby-test) 15 | ) 16 | 17 | ( 18 | (call 19 | method: (identifier) @run (#eq? @run "test") 20 | arguments: (argument_list (string (string_content) @name @RUBY_TEST_NAME)) 21 | ) @_ruby-test 22 | (#set! tag ruby-test) 23 | ) 24 | 25 | ; Methods that begin with test_ 26 | ( 27 | (method 28 | name: (identifier) @run (#match? @run "^test_") 29 | ) @_ruby-test 30 | (#set! tag ruby-test) 31 | ) 32 | 33 | ; System tests that inherit from ApplicationSystemTestCase 34 | ( 35 | (class 36 | name: (constant) @run @name @RUBY_TEST_NAME (superclass) @superclass (#match? @superclass "(ApplicationSystemTestCase)$") 37 | ) @_ruby-test 38 | (#set! tag ruby-test) 39 | ) 40 | 41 | ; Examples 42 | ( 43 | (call 44 | method: (identifier) @run (#any-of? @run "describe" "context" "it" "its" "specify") 45 | arguments: (argument_list . (_) @name @RUBY_TEST_NAME) 46 | ) @_ruby-test 47 | (#set! tag ruby-test) 48 | ) 49 | 50 | ; Examples (one-liner syntax) 51 | ( 52 | (call 53 | method: (identifier) @run (#any-of? @run "it" "its" "specify") 54 | block: (_) @name @RUBY_TEST_NAME 55 | !arguments 56 | ) @_ruby-test 57 | (#set! tag ruby-test) 58 | ) 59 | -------------------------------------------------------------------------------- /languages/ruby/tasks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "label": "bundle exec rake", 4 | "command": "bundle", 5 | "args": ["exec", "rake"] 6 | }, 7 | { 8 | "label": "bundle install", 9 | "command": "bundle", 10 | "args": ["install"] 11 | }, 12 | { 13 | "label": "Evaluate selected Ruby code", 14 | "command": "ruby", 15 | "args": ["-e", "$ZED_SELECTED_TEXT"] 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /languages/ruby/textobjects.scm: -------------------------------------------------------------------------------- 1 | ; Adapted from https://github.com/helix-editor/helix/blob/master/runtime/queries/ruby/textobjects.scm 2 | 3 | ; Class and Modules 4 | (class 5 | body: (_)? @class.inside) @class.around 6 | 7 | (singleton_class 8 | value: (_) 9 | (_)+ @class.inside) @class.around 10 | 11 | (module 12 | body: (_)? @class.inside) @class.around 13 | 14 | ; Functions and Blocks 15 | (singleton_method 16 | body: (_)? @function.inside) @function.around 17 | 18 | (method 19 | body: (_)? @function.inside) @function.around 20 | 21 | ; Comments 22 | (comment) @comment.inside 23 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | ":semanticCommitsDisabled", 6 | ":separateMultipleMajorReleases", 7 | "helpers:pinGitHubActionDigests" 8 | ], 9 | "dependencyDashboard": true, 10 | "timezone": "America/New_York", 11 | "schedule": ["after 3pm on Wednesday"], 12 | "major": { 13 | "dependencyDashboardApproval": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/bundler.rs: -------------------------------------------------------------------------------- 1 | use crate::command_executor::CommandExecutor; 2 | use std::path::Path; 3 | 4 | /// A simple wrapper around the `bundle` command. 5 | pub struct Bundler { 6 | pub working_dir: String, 7 | envs: Vec<(String, String)>, 8 | command_executor: Box, 9 | } 10 | 11 | impl Bundler { 12 | /// Creates a new `Bundler` instance. 13 | /// 14 | /// # Arguments 15 | /// * `working_dir` - The working directory where `bundle` commands should be executed. 16 | /// * `command_executor` - An executor for `bundle` commands. 17 | pub fn new( 18 | working_dir: String, 19 | envs: Vec<(String, String)>, 20 | command_executor: Box, 21 | ) -> Self { 22 | Bundler { 23 | working_dir, 24 | envs, 25 | command_executor, 26 | } 27 | } 28 | 29 | /// Retrieves the installed version of a gem using `bundle info --version `. 30 | /// 31 | /// # Arguments 32 | /// * `name` - The name of the gem. 33 | /// 34 | /// # Returns 35 | /// A `Result` containing the version string if successful, or an error message. 36 | pub fn installed_gem_version(&self, name: &str) -> Result { 37 | let args = vec!["--version".into(), name.into()]; 38 | 39 | self.execute_bundle_command("info".into(), args) 40 | } 41 | 42 | fn execute_bundle_command(&self, cmd: String, args: Vec) -> Result { 43 | let bundle_gemfile_path = Path::new(&self.working_dir).join("Gemfile"); 44 | let bundle_gemfile = bundle_gemfile_path 45 | .to_str() 46 | .ok_or_else(|| "Invalid path to Gemfile".to_string())?; 47 | 48 | let full_args: Vec = std::iter::once(cmd).chain(args).collect(); 49 | let command_envs: Vec<(String, String)> = self 50 | .envs 51 | .iter() 52 | .cloned() 53 | .chain(std::iter::once(( 54 | "BUNDLE_GEMFILE".to_string(), 55 | bundle_gemfile.to_string(), 56 | ))) 57 | .collect(); 58 | 59 | self.command_executor 60 | .execute("bundle", full_args, command_envs) 61 | .and_then(|output| match output.status { 62 | Some(0) => Ok(String::from_utf8_lossy(&output.stdout).to_string()), 63 | Some(status) => { 64 | let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 65 | Err(format!( 66 | "'bundle' command failed (status: {})\nError: {}", 67 | status, stderr 68 | )) 69 | } 70 | None => { 71 | let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 72 | Err(format!("Failed to execute 'bundle' command: {}", stderr)) 73 | } 74 | }) 75 | } 76 | } 77 | 78 | #[cfg(test)] 79 | mod tests { 80 | use super::*; 81 | use crate::command_executor::CommandExecutor; 82 | use std::cell::RefCell; 83 | use zed_extension_api::process::Output; 84 | 85 | struct MockExecutorConfig { 86 | output_to_return: Option>, 87 | expected_command_name: Option, 88 | expected_args: Option>, 89 | expected_envs: Option>, 90 | } 91 | 92 | struct MockCommandExecutor { 93 | config: RefCell, 94 | } 95 | 96 | impl MockCommandExecutor { 97 | fn new() -> Self { 98 | MockCommandExecutor { 99 | config: RefCell::new(MockExecutorConfig { 100 | output_to_return: None, 101 | expected_command_name: None, 102 | expected_args: None, 103 | expected_envs: None, 104 | }), 105 | } 106 | } 107 | 108 | fn expect( 109 | &self, 110 | command_name: &str, 111 | full_args: &[&str], 112 | final_envs: &[(&str, &str)], 113 | output: Result, 114 | ) { 115 | let mut config = self.config.borrow_mut(); 116 | config.expected_command_name = Some(command_name.to_string()); 117 | config.expected_args = Some(full_args.iter().map(|s| s.to_string()).collect()); 118 | config.expected_envs = Some( 119 | final_envs 120 | .iter() 121 | .map(|&(k, v)| (k.to_string(), v.to_string())) 122 | .collect(), 123 | ); 124 | config.output_to_return = Some(output); 125 | } 126 | } 127 | 128 | impl CommandExecutor for MockCommandExecutor { 129 | fn execute( 130 | &self, 131 | command_name: &str, 132 | args: Vec, 133 | envs: Vec<(String, String)>, 134 | ) -> Result { 135 | let mut config = self.config.borrow_mut(); 136 | 137 | if let Some(expected_name) = &config.expected_command_name { 138 | assert_eq!(command_name, expected_name, "Mock: Command name mismatch"); 139 | } 140 | if let Some(expected_args) = &config.expected_args { 141 | assert_eq!(&args, expected_args, "Mock: Args mismatch"); 142 | } 143 | if let Some(expected_envs) = &config.expected_envs { 144 | assert_eq!(&envs, expected_envs, "Mock: Env mismatch"); 145 | } 146 | 147 | config.output_to_return.take().expect( 148 | "MockCommandExecutor: output_to_return was not set or already consumed for the test", 149 | ) 150 | } 151 | } 152 | 153 | fn create_mock_executor_for_success( 154 | version: &str, 155 | dir: &str, 156 | gem: &str, 157 | ) -> MockCommandExecutor { 158 | let mock = MockCommandExecutor::new(); 159 | let gemfile_path = format!("{}/Gemfile", dir); 160 | mock.expect( 161 | "bundle", 162 | &["info", "--version", gem], 163 | &[("BUNDLE_GEMFILE", &gemfile_path)], 164 | Ok(Output { 165 | status: Some(0), 166 | stdout: version.as_bytes().to_vec(), 167 | stderr: Vec::new(), 168 | }), 169 | ); 170 | mock 171 | } 172 | 173 | #[test] 174 | fn test_installed_gem_version_success() { 175 | let mock_executor = create_mock_executor_for_success("8.0.0", "test_dir", "rails"); 176 | let bundler = Bundler::new("test_dir".into(), vec![], Box::new(mock_executor)); 177 | let version = bundler 178 | .installed_gem_version("rails") 179 | .expect("Expected successful version"); 180 | assert_eq!(version, "8.0.0", "Installed gem version should match"); 181 | } 182 | 183 | #[test] 184 | fn test_installed_gem_version_command_error() { 185 | let mock_executor = MockCommandExecutor::new(); 186 | let gem_name = "unknown_gem"; 187 | let error_output = "Could not find gem 'unknown_gem'."; 188 | let gemfile_path = "test_dir/Gemfile"; 189 | 190 | mock_executor.expect( 191 | "bundle", 192 | &["info", "--version", gem_name], 193 | &[("BUNDLE_GEMFILE", gemfile_path)], 194 | Ok(Output { 195 | status: Some(1), 196 | stdout: Vec::new(), 197 | stderr: error_output.as_bytes().to_vec(), 198 | }), 199 | ); 200 | 201 | let bundler = Bundler::new("test_dir".into(), vec![], Box::new(mock_executor)); 202 | let result = bundler.installed_gem_version(gem_name); 203 | 204 | assert!( 205 | result.is_err(), 206 | "Expected error for failed gem version check" 207 | ); 208 | let err_msg = result.unwrap_err(); 209 | assert!( 210 | err_msg.contains("'bundle' command failed (status: 1)"), 211 | "Error message should contain status" 212 | ); 213 | assert!( 214 | err_msg.contains(error_output), 215 | "Error message should contain stderr output" 216 | ); 217 | } 218 | 219 | #[test] 220 | fn test_installed_gem_version_execution_failure_from_executor() { 221 | let mock_executor = MockCommandExecutor::new(); 222 | let gem_name = "critical_gem"; 223 | let specific_error_msg = "Mocked execution failure"; 224 | let gemfile_path = "test_dir/Gemfile"; 225 | 226 | mock_executor.expect( 227 | "bundle", 228 | &["info", "--version", gem_name], 229 | &[("BUNDLE_GEMFILE", gemfile_path)], 230 | Err(specific_error_msg.to_string()), 231 | ); 232 | 233 | let bundler = Bundler::new("test_dir".into(), vec![], Box::new(mock_executor)); 234 | let result = bundler.installed_gem_version(gem_name); 235 | 236 | assert!(result.is_err(), "Expected error from executor failure"); 237 | assert_eq!( 238 | result.unwrap_err(), 239 | specific_error_msg, 240 | "Error message should match executor error" 241 | ); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/command_executor.rs: -------------------------------------------------------------------------------- 1 | use zed_extension_api::{self as zed}; 2 | 3 | pub trait CommandExecutor { 4 | /// Executes a command with the given arguments and environment variables. 5 | /// 6 | /// # Arguments 7 | /// 8 | /// * `cmd` - The name or path of the command to execute (e.g., "gem", "bundle"). 9 | /// * `args` - A vector of string arguments to pass to the command. 10 | /// * `envs` - A vector of key-value pairs representing environment variables 11 | /// to set for the command's execution context. 12 | /// 13 | /// # Returns 14 | /// 15 | /// A `Result` containing the `Output` of the command if successful. The `Output` 16 | /// typically includes stdout, stderr, and the exit status. Returns an error 17 | /// if the command execution fails at a lower level (e.g., command not found, 18 | /// or if the `zed_extension_api::Command` itself returns an error). 19 | fn execute( 20 | &self, 21 | cmd: &str, 22 | args: Vec, 23 | envs: Vec<(String, String)>, 24 | ) -> zed::Result; 25 | } 26 | 27 | /// An implementation of `CommandExecutor` that executes commands 28 | /// using the `zed_extension_api::Command`. 29 | pub struct RealCommandExecutor; 30 | 31 | impl CommandExecutor for RealCommandExecutor { 32 | fn execute( 33 | &self, 34 | cmd: &str, 35 | args: Vec, 36 | envs: Vec<(String, String)>, 37 | ) -> zed::Result { 38 | zed::Command::new(cmd).args(args).envs(envs).output() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/gemset.rs: -------------------------------------------------------------------------------- 1 | use crate::command_executor::CommandExecutor; 2 | use regex::Regex; 3 | 4 | /// A simple wrapper around the `gem` command. 5 | pub struct Gemset { 6 | pub gem_home: String, 7 | command_executor: Box, 8 | } 9 | 10 | impl Gemset { 11 | pub fn new(gem_home: String, command_executor: Box) -> Self { 12 | Self { 13 | gem_home, 14 | command_executor, 15 | } 16 | } 17 | 18 | /// Returns the full path to a gem binary executable. 19 | pub fn gem_bin_path(&self, bin_name: impl Into) -> Result { 20 | let bin_name = bin_name.into(); 21 | let path = std::path::Path::new(&self.gem_home) 22 | .join("bin") 23 | .join(&bin_name); 24 | 25 | path.to_str() 26 | .map(ToString::to_string) 27 | .ok_or_else(|| format!("Failed to convert path for '{}'", bin_name)) 28 | } 29 | 30 | pub fn gem_path_env(&self) -> Vec<(String, String)> { 31 | vec![( 32 | "GEM_PATH".to_string(), 33 | format!("{}:$GEM_PATH", self.gem_home), 34 | )] 35 | } 36 | 37 | pub fn install_gem(&self, name: &str) -> Result<(), String> { 38 | let args = vec![ 39 | "--no-user-install".to_string(), 40 | "--no-format-executable".to_string(), 41 | "--no-document".to_string(), 42 | name.into(), 43 | ]; 44 | 45 | self.execute_gem_command("install".into(), args) 46 | .map_err(|e| format!("Failed to install gem '{}': {}", name, e))?; 47 | 48 | Ok(()) 49 | } 50 | 51 | pub fn update_gem(&self, name: &str) -> Result<(), String> { 52 | self.execute_gem_command("update".into(), vec![name.into()]) 53 | .map_err(|e| format!("Failed to update gem '{}': {}", name, e))?; 54 | Ok(()) 55 | } 56 | 57 | pub fn installed_gem_version(&self, name: &str) -> Result, String> { 58 | let re = Regex::new(r"^(\S+) \((.+)\)$") 59 | .map_err(|e| format!("Failed to compile regex: {}", e))?; 60 | 61 | let args = vec!["--exact".to_string(), name.into()]; 62 | let output_str = self.execute_gem_command("list".into(), args)?; 63 | 64 | for line in output_str.lines() { 65 | let captures = match re.captures(line) { 66 | Some(c) => c, 67 | None => continue, 68 | }; 69 | 70 | let gem_package = captures.get(1).map(|m| m.as_str()); 71 | let version = captures.get(2).map(|m| m.as_str()); 72 | 73 | if gem_package == Some(name) { 74 | return Ok(version.map(|v| v.to_owned())); 75 | } 76 | } 77 | Ok(None) 78 | } 79 | 80 | pub fn is_outdated_gem(&self, name: &str) -> Result { 81 | self.execute_gem_command("outdated".into(), vec![]) 82 | .map(|output| { 83 | output 84 | .lines() 85 | .any(|line| line.split_whitespace().next().is_some_and(|n| n == name)) 86 | }) 87 | } 88 | 89 | fn execute_gem_command(&self, cmd: String, args: Vec) -> Result { 90 | let full_args: Vec = std::iter::once(cmd) 91 | .chain(std::iter::once("--norc".to_string())) 92 | .chain(args) 93 | .collect(); 94 | let command_envs = vec![("GEM_HOME".to_string(), self.gem_home.clone())]; 95 | 96 | self.command_executor 97 | .execute("gem", full_args, command_envs) 98 | .and_then(|output| match output.status { 99 | Some(0) => Ok(String::from_utf8_lossy(&output.stdout).to_string()), 100 | Some(status) => { 101 | let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 102 | Err(format!( 103 | "Gem command failed (status: {})\nError: {}", 104 | status, stderr 105 | )) 106 | } 107 | None => { 108 | let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 109 | Err(format!("Failed to execute gem command: {}", stderr)) 110 | } 111 | }) 112 | } 113 | } 114 | 115 | #[cfg(test)] 116 | mod tests { 117 | use super::*; 118 | use crate::command_executor::CommandExecutor; 119 | use std::cell::RefCell; 120 | use zed_extension_api::process::Output; 121 | 122 | struct MockExecutorConfig { 123 | expected_command_name: Option, 124 | expected_args: Option>, 125 | expected_envs: Option>, 126 | output_to_return: Option>, 127 | } 128 | 129 | struct MockGemCommandExecutor { 130 | config: RefCell, 131 | } 132 | 133 | impl MockGemCommandExecutor { 134 | fn new() -> Self { 135 | MockGemCommandExecutor { 136 | config: RefCell::new(MockExecutorConfig { 137 | expected_command_name: None, 138 | expected_args: None, 139 | expected_envs: None, 140 | output_to_return: None, 141 | }), 142 | } 143 | } 144 | 145 | fn expect( 146 | &self, 147 | command_name: &str, 148 | full_args: &[&str], 149 | final_envs: &[(&str, &str)], 150 | output: Result, 151 | ) { 152 | let mut config = self.config.borrow_mut(); 153 | config.expected_command_name = Some(command_name.to_string()); 154 | config.expected_args = Some(full_args.iter().map(|s| s.to_string()).collect()); 155 | config.expected_envs = Some( 156 | final_envs 157 | .iter() 158 | .map(|&(k, v)| (k.to_string(), v.to_string())) 159 | .collect(), 160 | ); 161 | config.output_to_return = Some(output); 162 | } 163 | } 164 | 165 | impl CommandExecutor for MockGemCommandExecutor { 166 | fn execute( 167 | &self, 168 | command_name: &str, 169 | args: Vec, 170 | envs: Vec<(String, String)>, 171 | ) -> Result { 172 | let mut config = self.config.borrow_mut(); 173 | 174 | if let Some(expected_name) = &config.expected_command_name { 175 | assert_eq!(command_name, expected_name, "Mock: Command name mismatch"); 176 | } 177 | if let Some(expected_args) = &config.expected_args { 178 | assert_eq!(&args, expected_args, "Mock: Args mismatch"); 179 | } 180 | if let Some(expected_envs) = &config.expected_envs { 181 | assert_eq!(&envs, expected_envs, "Mock: Env mismatch"); 182 | } 183 | 184 | config 185 | .output_to_return 186 | .take() 187 | .expect("MockGemCommandExecutor: output_to_return was not set or already consumed") 188 | } 189 | } 190 | 191 | const TEST_GEM_HOME: &str = "/test/gem_home"; 192 | 193 | fn create_gemset(mock_executor: MockGemCommandExecutor) -> Gemset { 194 | Gemset::new(TEST_GEM_HOME.to_string(), Box::new(mock_executor)) 195 | } 196 | 197 | #[test] 198 | fn test_gem_bin_path() { 199 | let gemset = Gemset::new( 200 | TEST_GEM_HOME.to_string(), 201 | Box::new(MockGemCommandExecutor::new()), 202 | ); 203 | let path = gemset.gem_bin_path("ruby-lsp").unwrap(); 204 | assert_eq!(path, "/test/gem_home/bin/ruby-lsp"); 205 | } 206 | 207 | #[test] 208 | fn test_gem_path_env() { 209 | let gemset = Gemset::new( 210 | TEST_GEM_HOME.to_string(), 211 | Box::new(MockGemCommandExecutor::new()), 212 | ); 213 | let env = gemset.gem_path_env(); 214 | assert_eq!(env.len(), 1); 215 | assert_eq!(env[0].0, "GEM_PATH"); 216 | assert_eq!(env[0].1, "/test/gem_home:$GEM_PATH"); 217 | } 218 | 219 | #[test] 220 | fn test_install_gem_success() { 221 | let mock_executor = MockGemCommandExecutor::new(); 222 | let gem_name = "ruby-lsp"; 223 | mock_executor.expect( 224 | "gem", 225 | &[ 226 | "install", 227 | "--norc", 228 | "--no-user-install", 229 | "--no-format-executable", 230 | "--no-document", 231 | gem_name, 232 | ], 233 | &[("GEM_HOME", TEST_GEM_HOME)], 234 | Ok(Output { 235 | status: Some(0), 236 | stdout: "Successfully installed ruby-lsp-1.0.0".as_bytes().to_vec(), 237 | stderr: Vec::new(), 238 | }), 239 | ); 240 | let gemset = create_gemset(mock_executor); 241 | assert!(gemset.install_gem(gem_name).is_ok()); 242 | } 243 | 244 | #[test] 245 | fn test_install_gem_failure() { 246 | let mock_executor = MockGemCommandExecutor::new(); 247 | let gem_name = "ruby-lsp"; 248 | mock_executor.expect( 249 | "gem", 250 | &[ 251 | "install", 252 | "--norc", 253 | "--no-user-install", 254 | "--no-format-executable", 255 | "--no-document", 256 | gem_name, 257 | ], 258 | &[("GEM_HOME", TEST_GEM_HOME)], 259 | Ok(Output { 260 | status: Some(1), 261 | stdout: Vec::new(), 262 | stderr: "Installation error".as_bytes().to_vec(), 263 | }), 264 | ); 265 | let gemset = create_gemset(mock_executor); 266 | let result = gemset.install_gem(gem_name); 267 | assert!(result.is_err()); 268 | assert!(result 269 | .unwrap_err() 270 | .contains("Failed to install gem 'ruby-lsp'")); 271 | } 272 | 273 | #[test] 274 | fn test_update_gem_success() { 275 | let mock_executor = MockGemCommandExecutor::new(); 276 | let gem_name = "ruby-lsp"; 277 | mock_executor.expect( 278 | "gem", 279 | &["update", "--norc", gem_name], 280 | &[("GEM_HOME", TEST_GEM_HOME)], 281 | Ok(Output { 282 | status: Some(0), 283 | stdout: "Gems updated: ruby-lsp".as_bytes().to_vec(), 284 | stderr: Vec::new(), 285 | }), 286 | ); 287 | let gemset = create_gemset(mock_executor); 288 | assert!(gemset.update_gem(gem_name).is_ok()); 289 | } 290 | 291 | #[test] 292 | fn test_update_gem_failure() { 293 | let mock_executor = MockGemCommandExecutor::new(); 294 | let gem_name = "ruby-lsp"; 295 | mock_executor.expect( 296 | "gem", 297 | &["update", "--norc", gem_name], 298 | &[("GEM_HOME", TEST_GEM_HOME)], 299 | Ok(Output { 300 | status: Some(1), 301 | stdout: Vec::new(), 302 | stderr: "Update error".as_bytes().to_vec(), 303 | }), 304 | ); 305 | let gemset = create_gemset(mock_executor); 306 | let result = gemset.update_gem(gem_name); 307 | assert!(result.is_err()); 308 | assert!(result 309 | .unwrap_err() 310 | .contains("Failed to update gem 'ruby-lsp'")); 311 | } 312 | 313 | #[test] 314 | fn test_installed_gem_version_found() { 315 | let mock_executor = MockGemCommandExecutor::new(); 316 | let gem_name = "ruby-lsp"; 317 | let expected_version = "1.2.3"; 318 | let gem_list_output = format!( 319 | "{}\n{} ({})\n{}", 320 | "ignore this", gem_name, expected_version, "other_gem (3.2.1)" 321 | ); 322 | 323 | mock_executor.expect( 324 | "gem", 325 | &["list", "--norc", "--exact", gem_name], 326 | &[("GEM_HOME", TEST_GEM_HOME)], 327 | Ok(Output { 328 | status: Some(0), 329 | stdout: gem_list_output.as_bytes().to_vec(), 330 | stderr: Vec::new(), 331 | }), 332 | ); 333 | let gemset = create_gemset(mock_executor); 334 | let version = gemset.installed_gem_version(gem_name).unwrap(); 335 | assert_eq!(version, Some(expected_version.to_string())); 336 | } 337 | 338 | #[test] 339 | fn test_installed_gem_version_found_with_default() { 340 | let mock_executor = MockGemCommandExecutor::new(); 341 | let gem_name = "prism"; 342 | let version_in_output = "default: 1.2.0"; 343 | let gem_list_output = format!( 344 | "{}\n{} ({})\n{}", 345 | "*** LOCAL GEMS ***", gem_name, version_in_output, "abbrev (0.1.2)" 346 | ); 347 | 348 | mock_executor.expect( 349 | "gem", 350 | &["list", "--norc", "--exact", gem_name], 351 | &[("GEM_HOME", TEST_GEM_HOME)], 352 | Ok(Output { 353 | status: Some(0), 354 | stdout: gem_list_output.as_bytes().to_vec(), 355 | stderr: Vec::new(), 356 | }), 357 | ); 358 | let gemset = create_gemset(mock_executor); 359 | let version = gemset.installed_gem_version(gem_name).unwrap(); 360 | assert_eq!(version, Some(version_in_output.to_string())); 361 | } 362 | 363 | #[test] 364 | fn test_installed_gem_version_not_found() { 365 | let mock_executor = MockGemCommandExecutor::new(); 366 | let gem_name = "non_existent_gem"; 367 | let gem_list_output = "other_gem (1.0.0)\nanother_gem (2.0.0)"; 368 | 369 | mock_executor.expect( 370 | "gem", 371 | &["list", "--norc", "--exact", gem_name], 372 | &[("GEM_HOME", TEST_GEM_HOME)], 373 | Ok(Output { 374 | status: Some(0), 375 | stdout: gem_list_output.as_bytes().to_vec(), 376 | stderr: Vec::new(), 377 | }), 378 | ); 379 | let gemset = create_gemset(mock_executor); 380 | let version = gemset.installed_gem_version(gem_name).unwrap(); 381 | assert_eq!(version, None); 382 | } 383 | 384 | #[test] 385 | fn test_installed_gem_version_command_failure() { 386 | let mock_executor = MockGemCommandExecutor::new(); 387 | let gem_name = "ruby-lsp"; 388 | mock_executor.expect( 389 | "gem", 390 | &["list", "--norc", "--exact", gem_name], 391 | &[("GEM_HOME", TEST_GEM_HOME)], 392 | Ok(Output { 393 | status: Some(127), 394 | stdout: Vec::new(), 395 | stderr: "gem list error".as_bytes().to_vec(), 396 | }), 397 | ); 398 | let gemset = create_gemset(mock_executor); 399 | let result = gemset.installed_gem_version(gem_name); 400 | assert!(result.is_err()); 401 | assert!(result 402 | .unwrap_err() 403 | .contains("Gem command failed (status: 127)")); 404 | } 405 | 406 | #[test] 407 | fn test_is_outdated_gem_true() { 408 | let mock_executor = MockGemCommandExecutor::new(); 409 | let gem_name = "ruby-lsp"; 410 | let outdated_output = format!( 411 | "{} (3.3.2 < 3.3.4)\n{} (2.9.1 < 2.11.3)\n{} (0.5.6 < 0.5.8)", 412 | "csv", gem_name, "net-imap" 413 | ); 414 | 415 | mock_executor.expect( 416 | "gem", 417 | &["outdated", "--norc"], 418 | &[("GEM_HOME", TEST_GEM_HOME)], 419 | Ok(Output { 420 | status: Some(0), 421 | stdout: outdated_output.as_bytes().to_vec(), 422 | stderr: Vec::new(), 423 | }), 424 | ); 425 | let gemset = create_gemset(mock_executor); 426 | let is_outdated = gemset.is_outdated_gem(gem_name).unwrap(); 427 | assert!(is_outdated); 428 | } 429 | 430 | #[test] 431 | fn test_is_outdated_gem_false() { 432 | let mock_executor = MockGemCommandExecutor::new(); 433 | let gem_name = "ruby-lsp"; 434 | let outdated_output = "csv (3.3.2 < 3.3.4)"; 435 | 436 | mock_executor.expect( 437 | "gem", 438 | &["outdated", "--norc"], 439 | &[("GEM_HOME", TEST_GEM_HOME)], 440 | Ok(Output { 441 | status: Some(0), 442 | stdout: outdated_output.as_bytes().to_vec(), 443 | stderr: Vec::new(), 444 | }), 445 | ); 446 | let gemset = create_gemset(mock_executor); 447 | let is_outdated = gemset.is_outdated_gem(gem_name).unwrap(); 448 | assert!(!is_outdated); 449 | } 450 | 451 | #[test] 452 | fn test_is_outdated_gem_command_failure() { 453 | let mock_executor = MockGemCommandExecutor::new(); 454 | let gem_name = "ruby-lsp"; 455 | mock_executor.expect( 456 | "gem", 457 | &["outdated", "--norc"], 458 | &[("GEM_HOME", TEST_GEM_HOME)], 459 | Ok(Output { 460 | status: Some(1), 461 | stdout: Vec::new(), 462 | stderr: "outdated command error".as_bytes().to_vec(), 463 | }), 464 | ); 465 | let gemset = create_gemset(mock_executor); 466 | let result = gemset.is_outdated_gem(gem_name); 467 | assert!(result.is_err()); 468 | assert!(result 469 | .unwrap_err() 470 | .contains("Gem command failed (status: 1)")); 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /src/language_servers/language_server.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | use std::collections::HashMap; 3 | 4 | use crate::{bundler::Bundler, command_executor::RealCommandExecutor, gemset::Gemset}; 5 | use zed_extension_api::{self as zed}; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct LanguageServerBinary { 9 | pub path: String, 10 | pub args: Option>, 11 | pub env: Option>, 12 | } 13 | 14 | #[derive(Clone, Debug, Default)] 15 | pub struct LspBinarySettings { 16 | #[allow(dead_code)] 17 | pub path: Option, 18 | pub arguments: Option>, 19 | } 20 | 21 | pub trait WorktreeLike { 22 | #[allow(dead_code)] 23 | fn root_path(&self) -> String; 24 | #[allow(dead_code)] 25 | fn shell_env(&self) -> Vec<(String, String)>; 26 | fn read_text_file(&self, path: &str) -> Result; 27 | fn lsp_binary_settings(&self, server_id: &str) -> Result, String>; 28 | } 29 | 30 | impl WorktreeLike for zed::Worktree { 31 | fn root_path(&self) -> String { 32 | zed::Worktree::root_path(self) 33 | } 34 | 35 | fn shell_env(&self) -> Vec<(String, String)> { 36 | zed::Worktree::shell_env(self) 37 | } 38 | 39 | fn read_text_file(&self, path: &str) -> Result { 40 | zed::Worktree::read_text_file(self, path) 41 | } 42 | 43 | fn lsp_binary_settings(&self, server_id: &str) -> Result, String> { 44 | match zed::settings::LspSettings::for_worktree(server_id, self) { 45 | Ok(lsp_settings) => Ok(lsp_settings.binary.map(|b| LspBinarySettings { 46 | path: b.path, 47 | arguments: b.arguments, 48 | })), 49 | Err(e) => Err(e), 50 | } 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | pub struct FakeWorktree { 56 | root_path: String, 57 | shell_env: Vec<(String, String)>, 58 | files: HashMap>, 59 | lsp_binary_settings_map: HashMap, String>>, 60 | } 61 | 62 | #[cfg(test)] 63 | impl FakeWorktree { 64 | pub fn new(root_path: String) -> Self { 65 | FakeWorktree { 66 | root_path, 67 | shell_env: Vec::new(), 68 | files: HashMap::new(), 69 | lsp_binary_settings_map: HashMap::new(), 70 | } 71 | } 72 | 73 | pub fn add_file(&mut self, path: String, content: Result) { 74 | self.files.insert(path, content); 75 | } 76 | 77 | pub fn add_lsp_binary_setting( 78 | &mut self, 79 | server_id: String, 80 | settings: Result, String>, 81 | ) { 82 | self.lsp_binary_settings_map.insert(server_id, settings); 83 | } 84 | } 85 | 86 | #[cfg(test)] 87 | impl WorktreeLike for FakeWorktree { 88 | fn root_path(&self) -> String { 89 | self.root_path.clone() 90 | } 91 | 92 | fn shell_env(&self) -> Vec<(String, String)> { 93 | self.shell_env.clone() 94 | } 95 | 96 | fn read_text_file(&self, path: &str) -> Result { 97 | self.files 98 | .get(path) 99 | .cloned() 100 | .unwrap_or_else(|| Err(format!("File not found in mock: {}", path))) 101 | } 102 | 103 | fn lsp_binary_settings(&self, server_id: &str) -> Result, String> { 104 | self.lsp_binary_settings_map 105 | .get(server_id) 106 | .cloned() 107 | .unwrap_or(Ok(None)) 108 | } 109 | } 110 | 111 | pub trait LanguageServer { 112 | const SERVER_ID: &str; 113 | const EXECUTABLE_NAME: &str; 114 | const GEM_NAME: &str; 115 | 116 | fn get_executable_args(&self, _worktree: &T) -> Vec { 117 | Vec::new() 118 | } 119 | 120 | fn language_server_command( 121 | &mut self, 122 | language_server_id: &zed::LanguageServerId, 123 | worktree: &zed::Worktree, 124 | ) -> zed::Result { 125 | let binary = self.language_server_binary(language_server_id, worktree)?; 126 | 127 | zed::set_language_server_installation_status( 128 | language_server_id, 129 | &zed::LanguageServerInstallationStatus::None, 130 | ); 131 | 132 | Ok(zed::Command { 133 | command: binary.path, 134 | args: binary.args.unwrap_or(self.get_executable_args(worktree)), 135 | env: binary.env.unwrap_or_default(), 136 | }) 137 | } 138 | 139 | fn language_server_binary( 140 | &self, 141 | language_server_id: &zed::LanguageServerId, 142 | worktree: &zed::Worktree, 143 | ) -> zed::Result { 144 | let lsp_settings = 145 | zed::settings::LspSettings::for_worktree(language_server_id.as_ref(), worktree)?; 146 | 147 | if let Some(binary_settings) = &lsp_settings.binary { 148 | if let Some(path) = &binary_settings.path { 149 | return Ok(LanguageServerBinary { 150 | path: path.clone(), 151 | args: binary_settings.arguments.clone(), 152 | env: Some(worktree.shell_env()), 153 | }); 154 | } 155 | } 156 | 157 | let use_bundler = lsp_settings 158 | .settings 159 | .as_ref() 160 | .and_then(|settings| settings["use_bundler"].as_bool()) 161 | .unwrap_or(true); 162 | 163 | if !use_bundler { 164 | return self.try_find_on_path_or_extension_gemset(language_server_id, worktree); 165 | } 166 | 167 | let bundler = Bundler::new( 168 | worktree.root_path(), 169 | worktree.shell_env(), 170 | Box::new(RealCommandExecutor), 171 | ); 172 | match bundler.installed_gem_version(Self::GEM_NAME) { 173 | Ok(_version) => { 174 | let bundle_path = worktree 175 | .which("bundle") 176 | .ok_or_else(|| "Unable to find 'bundle' command".to_string())?; 177 | 178 | Ok(LanguageServerBinary { 179 | path: bundle_path, 180 | args: Some( 181 | vec!["exec".into(), Self::EXECUTABLE_NAME.into()] 182 | .into_iter() 183 | .chain(self.get_executable_args(worktree)) 184 | .collect(), 185 | ), 186 | env: Some(worktree.shell_env()), 187 | }) 188 | } 189 | Err(_e) => self.try_find_on_path_or_extension_gemset(language_server_id, worktree), 190 | } 191 | } 192 | 193 | fn try_find_on_path_or_extension_gemset( 194 | &self, 195 | language_server_id: &zed::LanguageServerId, 196 | worktree: &zed::Worktree, 197 | ) -> zed::Result { 198 | if let Some(path) = worktree.which(Self::EXECUTABLE_NAME) { 199 | Ok(LanguageServerBinary { 200 | path, 201 | args: Some(self.get_executable_args(worktree)), 202 | env: Some(worktree.shell_env()), 203 | }) 204 | } else { 205 | self.extension_gemset_language_server_binary(language_server_id, worktree) 206 | } 207 | } 208 | 209 | fn extension_gemset_language_server_binary( 210 | &self, 211 | language_server_id: &zed::LanguageServerId, 212 | worktree: &zed::Worktree, 213 | ) -> zed::Result { 214 | let gem_home = std::env::current_dir() 215 | .map_err(|e| format!("Failed to get extension directory: {}", e))? 216 | .to_string_lossy() 217 | .to_string(); 218 | 219 | let gemset = Gemset::new(gem_home.clone(), Box::new(RealCommandExecutor)); 220 | 221 | zed::set_language_server_installation_status( 222 | language_server_id, 223 | &zed::LanguageServerInstallationStatus::CheckingForUpdate, 224 | ); 225 | 226 | match gemset.installed_gem_version(Self::GEM_NAME) { 227 | Ok(Some(_version)) => { 228 | if gemset 229 | .is_outdated_gem(Self::GEM_NAME) 230 | .map_err(|e| e.to_string())? 231 | { 232 | zed::set_language_server_installation_status( 233 | language_server_id, 234 | &zed::LanguageServerInstallationStatus::Downloading, 235 | ); 236 | 237 | gemset 238 | .update_gem(Self::GEM_NAME) 239 | .map_err(|e| e.to_string())?; 240 | } 241 | 242 | let executable_path = gemset 243 | .gem_bin_path(Self::EXECUTABLE_NAME) 244 | .map_err(|e| e.to_string())?; 245 | 246 | Ok(LanguageServerBinary { 247 | path: executable_path, 248 | args: Some(self.get_executable_args(worktree)), 249 | env: Some(gemset.gem_path_env()), 250 | }) 251 | } 252 | Ok(None) => { 253 | zed::set_language_server_installation_status( 254 | language_server_id, 255 | &zed::LanguageServerInstallationStatus::Downloading, 256 | ); 257 | 258 | gemset 259 | .install_gem(Self::GEM_NAME) 260 | .map_err(|e| e.to_string())?; 261 | 262 | let executable_path = gemset 263 | .gem_bin_path(Self::EXECUTABLE_NAME) 264 | .map_err(|e| e.to_string())?; 265 | 266 | Ok(LanguageServerBinary { 267 | path: executable_path, 268 | args: Some(self.get_executable_args(worktree)), 269 | env: Some(gemset.gem_path_env()), 270 | }) 271 | } 272 | Err(e) => Err(e), 273 | } 274 | } 275 | } 276 | 277 | #[cfg(test)] 278 | mod tests { 279 | use super::{FakeWorktree, LanguageServer, WorktreeLike}; 280 | 281 | struct TestServer {} 282 | 283 | impl TestServer { 284 | fn new() -> Self { 285 | Self {} 286 | } 287 | } 288 | 289 | impl LanguageServer for TestServer { 290 | const SERVER_ID: &'static str = "test-server"; 291 | const EXECUTABLE_NAME: &'static str = "test-exe"; 292 | const GEM_NAME: &'static str = "test"; 293 | 294 | fn get_executable_args(&self, _worktree: &T) -> Vec { 295 | vec!["--test-arg".into()] 296 | } 297 | } 298 | 299 | #[test] 300 | fn test_default_executable_args() { 301 | let test_server = TestServer::new(); 302 | let mock_worktree = FakeWorktree::new("/path/to/project".to_string()); 303 | 304 | assert_eq!( 305 | test_server.get_executable_args(&mock_worktree), 306 | vec!["--test-arg"], 307 | "Default executable args should match expected vector" 308 | ); 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/language_servers/mod.rs: -------------------------------------------------------------------------------- 1 | mod language_server; 2 | mod rubocop; 3 | mod ruby_lsp; 4 | mod solargraph; 5 | mod sorbet; 6 | mod steep; 7 | 8 | pub use language_server::LanguageServer; 9 | pub use rubocop::*; 10 | pub use ruby_lsp::*; 11 | pub use solargraph::*; 12 | pub use sorbet::*; 13 | pub use steep::*; 14 | -------------------------------------------------------------------------------- /src/language_servers/rubocop.rs: -------------------------------------------------------------------------------- 1 | use super::{language_server::WorktreeLike, LanguageServer}; 2 | 3 | pub struct Rubocop {} 4 | 5 | impl LanguageServer for Rubocop { 6 | const SERVER_ID: &str = "rubocop"; 7 | const EXECUTABLE_NAME: &str = "rubocop"; 8 | const GEM_NAME: &str = "rubocop"; 9 | 10 | fn get_executable_args(&self, _worktree: &T) -> Vec { 11 | vec!["--lsp".to_string()] 12 | } 13 | } 14 | 15 | impl Rubocop { 16 | pub fn new() -> Self { 17 | Self {} 18 | } 19 | } 20 | 21 | #[cfg(test)] 22 | mod tests { 23 | use crate::language_servers::{language_server::FakeWorktree, LanguageServer, Rubocop}; 24 | 25 | #[test] 26 | fn test_server_id() { 27 | assert_eq!(Rubocop::SERVER_ID, "rubocop"); 28 | } 29 | 30 | #[test] 31 | fn test_executable_name() { 32 | assert_eq!(Rubocop::EXECUTABLE_NAME, "rubocop"); 33 | } 34 | 35 | #[test] 36 | fn test_executable_args() { 37 | let rubocop = Rubocop::new(); 38 | let mock_worktree = FakeWorktree::new("/path/to/project".to_string()); 39 | 40 | assert_eq!(rubocop.get_executable_args(&mock_worktree), vec!["--lsp"]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/language_servers/ruby_lsp.rs: -------------------------------------------------------------------------------- 1 | use zed_extension_api::{self as zed}; 2 | 3 | use super::LanguageServer; 4 | 5 | pub struct RubyLsp {} 6 | 7 | impl LanguageServer for RubyLsp { 8 | const SERVER_ID: &str = "ruby-lsp"; 9 | const EXECUTABLE_NAME: &str = "ruby-lsp"; 10 | const GEM_NAME: &str = "ruby-lsp"; 11 | } 12 | 13 | impl RubyLsp { 14 | pub fn new() -> Self { 15 | Self {} 16 | } 17 | 18 | pub fn label_for_completion(&self, completion: zed::lsp::Completion) -> Option { 19 | let highlight_name = match completion.kind? { 20 | zed::lsp::CompletionKind::Class | zed::lsp::CompletionKind::Module => "type", 21 | zed::lsp::CompletionKind::Constant => "constant", 22 | zed::lsp::CompletionKind::Method => "function.method", 23 | zed::lsp::CompletionKind::Reference => "function.method", 24 | zed::lsp::CompletionKind::Keyword => "keyword", 25 | _ => return None, 26 | }; 27 | 28 | let len = completion.label.len(); 29 | let mut spans = vec![zed::CodeLabelSpan::literal( 30 | completion.label, 31 | Some(highlight_name.to_string()), 32 | )]; 33 | 34 | if let Some(detail) = completion 35 | .label_details 36 | .and_then(|label_details| label_details.detail) 37 | { 38 | spans.push(zed::CodeLabelSpan::literal(" ", None)); 39 | spans.push(zed::CodeLabelSpan::literal(detail, None)); 40 | } 41 | 42 | Some(zed::CodeLabel { 43 | code: Default::default(), 44 | spans, 45 | filter_range: (0..len).into(), 46 | }) 47 | } 48 | 49 | pub fn label_for_symbol(&self, symbol: zed::lsp::Symbol) -> Option { 50 | let name = &symbol.name; 51 | 52 | match symbol.kind { 53 | zed::lsp::SymbolKind::Method => { 54 | let code = format!("def {name}; end"); 55 | let filter_range = 0..name.len(); 56 | let display_range = 4..4 + name.len(); 57 | 58 | Some(zed::CodeLabel { 59 | code, 60 | spans: vec![zed::CodeLabelSpan::code_range(display_range)], 61 | filter_range: filter_range.into(), 62 | }) 63 | } 64 | zed::lsp::SymbolKind::Class | zed::lsp::SymbolKind::Module => { 65 | let code = format!("class {name}; end"); 66 | let filter_range = 0..name.len(); 67 | let display_range = 6..6 + name.len(); 68 | 69 | Some(zed::CodeLabel { 70 | code, 71 | spans: vec![zed::CodeLabelSpan::code_range(display_range)], 72 | filter_range: filter_range.into(), 73 | }) 74 | } 75 | zed::lsp::SymbolKind::Constant => { 76 | let code = name.to_uppercase(); 77 | let filter_range = 0..name.len(); 78 | let display_range = 0..name.len(); 79 | 80 | Some(zed::CodeLabel { 81 | code, 82 | spans: vec![zed::CodeLabelSpan::code_range(display_range)], 83 | filter_range: filter_range.into(), 84 | }) 85 | } 86 | _ => None, 87 | } 88 | } 89 | } 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | use crate::language_servers::{language_server::FakeWorktree, LanguageServer, RubyLsp}; 94 | 95 | #[test] 96 | fn test_server_id() { 97 | assert_eq!(RubyLsp::SERVER_ID, "ruby-lsp"); 98 | } 99 | 100 | #[test] 101 | fn test_executable_name() { 102 | assert_eq!(RubyLsp::EXECUTABLE_NAME, "ruby-lsp"); 103 | } 104 | 105 | #[test] 106 | fn test_executable_args() { 107 | let ruby_lsp = RubyLsp::new(); 108 | let mock_worktree = FakeWorktree::new("/path/to/project".to_string()); 109 | 110 | assert_eq!( 111 | ruby_lsp.get_executable_args(&mock_worktree), 112 | vec![] as Vec 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/language_servers/solargraph.rs: -------------------------------------------------------------------------------- 1 | use zed_extension_api::{self as zed}; 2 | 3 | use super::{language_server::WorktreeLike, LanguageServer}; 4 | 5 | pub struct Solargraph {} 6 | 7 | impl LanguageServer for Solargraph { 8 | const SERVER_ID: &str = "solargraph"; 9 | const EXECUTABLE_NAME: &str = "solargraph"; 10 | const GEM_NAME: &str = "solargraph"; 11 | 12 | fn get_executable_args(&self, _worktree: &T) -> Vec { 13 | vec!["stdio".to_string()] 14 | } 15 | } 16 | 17 | impl Solargraph { 18 | pub fn new() -> Self { 19 | Self {} 20 | } 21 | 22 | pub fn label_for_completion(&self, completion: zed::lsp::Completion) -> Option { 23 | let highlight_name = match completion.kind? { 24 | zed::lsp::CompletionKind::Class | zed::lsp::CompletionKind::Module => "type", 25 | zed::lsp::CompletionKind::Constant => "constant", 26 | zed::lsp::CompletionKind::Method => "function.method", 27 | zed::lsp::CompletionKind::Keyword => { 28 | if completion.label.starts_with(':') { 29 | "string.special.symbol" 30 | } else { 31 | "keyword" 32 | } 33 | } 34 | zed::lsp::CompletionKind::Variable => { 35 | if completion.label.starts_with('@') { 36 | "property" 37 | } else { 38 | return None; 39 | } 40 | } 41 | _ => return None, 42 | }; 43 | 44 | let len = completion.label.len(); 45 | let name_span = 46 | zed::CodeLabelSpan::literal(completion.label, Some(highlight_name.to_string())); 47 | 48 | Some(zed::CodeLabel { 49 | code: Default::default(), 50 | spans: if let Some(detail) = completion.detail { 51 | vec![ 52 | name_span, 53 | zed::CodeLabelSpan::literal(" ", None), 54 | zed::CodeLabelSpan::literal(detail, None), 55 | ] 56 | } else { 57 | vec![name_span] 58 | }, 59 | filter_range: (0..len).into(), 60 | }) 61 | } 62 | 63 | pub fn label_for_symbol(&self, symbol: zed::lsp::Symbol) -> Option { 64 | let name = &symbol.name; 65 | 66 | match symbol.kind { 67 | zed::lsp::SymbolKind::Method => { 68 | let mut parts = name.split('#'); 69 | let container_name = parts.next()?; 70 | let method_name = parts.next()?; 71 | 72 | if parts.next().is_some() { 73 | return None; 74 | } 75 | 76 | let filter_range = 0..name.len(); 77 | 78 | let spans = vec![ 79 | zed::CodeLabelSpan::literal(container_name, Some("type".to_string())), 80 | zed::CodeLabelSpan::literal("#", None), 81 | zed::CodeLabelSpan::literal(method_name, Some("function.method".to_string())), 82 | ]; 83 | 84 | Some(zed::CodeLabel { 85 | code: name.to_string(), 86 | spans, 87 | filter_range: filter_range.into(), 88 | }) 89 | } 90 | zed::lsp::SymbolKind::Class | zed::lsp::SymbolKind::Module => { 91 | let class = "class "; 92 | let code = format!("{class}{name}"); 93 | let filter_range = 0..name.len(); 94 | let display_range = class.len()..class.len() + name.len(); 95 | 96 | Some(zed::CodeLabel { 97 | code, 98 | spans: vec![zed::CodeLabelSpan::code_range(display_range)], 99 | filter_range: filter_range.into(), 100 | }) 101 | } 102 | zed::lsp::SymbolKind::Constant => { 103 | let code = name.to_uppercase().to_string(); 104 | let filter_range = 0..name.len(); 105 | let display_range = 0..name.len(); 106 | 107 | Some(zed::CodeLabel { 108 | code, 109 | spans: vec![zed::CodeLabelSpan::code_range(display_range)], 110 | filter_range: filter_range.into(), 111 | }) 112 | } 113 | _ => None, 114 | } 115 | } 116 | } 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | use crate::language_servers::{language_server::FakeWorktree, LanguageServer, Solargraph}; 121 | 122 | #[test] 123 | fn test_server_id() { 124 | assert_eq!(Solargraph::SERVER_ID, "solargraph"); 125 | } 126 | 127 | #[test] 128 | fn test_executable_name() { 129 | assert_eq!(Solargraph::EXECUTABLE_NAME, "solargraph"); 130 | } 131 | 132 | #[test] 133 | fn test_executable_args() { 134 | let solargraph = Solargraph::new(); 135 | let mock_worktree = FakeWorktree::new("/path/to/project".to_string()); 136 | 137 | assert_eq!( 138 | solargraph.get_executable_args(&mock_worktree), 139 | vec!["stdio"] 140 | ); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/language_servers/sorbet.rs: -------------------------------------------------------------------------------- 1 | use super::{language_server::WorktreeLike, LanguageServer}; 2 | 3 | pub struct Sorbet {} 4 | 5 | impl LanguageServer for Sorbet { 6 | const SERVER_ID: &str = "sorbet"; 7 | const EXECUTABLE_NAME: &str = "srb"; 8 | const GEM_NAME: &str = "sorbet"; 9 | 10 | fn get_executable_args(&self, worktree: &T) -> Vec { 11 | let binary_settings = worktree 12 | .lsp_binary_settings(Self::SERVER_ID) 13 | .unwrap_or_default(); 14 | 15 | let default_args = vec![ 16 | "tc".to_string(), 17 | "--lsp".to_string(), 18 | "--enable-experimental-lsp-document-highlight".to_string(), 19 | ]; 20 | 21 | // test if sorbet/config is present 22 | match worktree.read_text_file("sorbet/config") { 23 | Ok(_) => { 24 | // Config file exists, prefer custom arguments if available. 25 | binary_settings 26 | .and_then(|bs| bs.arguments) 27 | .unwrap_or(default_args) 28 | } 29 | Err(_) => { 30 | // gross, but avoid sorbet errors in a non-sorbet 31 | // environment by using an empty config 32 | vec![ 33 | "tc".to_string(), 34 | "--lsp".to_string(), 35 | "--dir".to_string(), 36 | "./".to_string(), 37 | ] 38 | } 39 | } 40 | } 41 | } 42 | 43 | impl Sorbet { 44 | pub fn new() -> Self { 45 | Self {} 46 | } 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | use crate::language_servers::{ 52 | language_server::{FakeWorktree, LspBinarySettings}, 53 | LanguageServer, Sorbet, 54 | }; 55 | 56 | #[test] 57 | fn test_server_id() { 58 | assert_eq!(Sorbet::SERVER_ID, "sorbet"); 59 | } 60 | 61 | #[test] 62 | fn test_executable_name() { 63 | assert_eq!(Sorbet::EXECUTABLE_NAME, "srb"); 64 | } 65 | 66 | #[test] 67 | fn test_executable_args_no_config_file() { 68 | let sorbet = Sorbet::new(); 69 | let mut fake_worktree = FakeWorktree::new("/path/to/project".to_string()); 70 | 71 | fake_worktree.add_file( 72 | "sorbet/config".to_string(), 73 | Err("File not found".to_string()), 74 | ); 75 | fake_worktree.add_lsp_binary_setting(Sorbet::SERVER_ID.to_string(), Ok(None)); 76 | 77 | let expected_args_no_config = vec![ 78 | "tc".to_string(), 79 | "--lsp".to_string(), 80 | "--dir".to_string(), 81 | "./".to_string(), 82 | ]; 83 | assert_eq!( 84 | sorbet.get_executable_args(&fake_worktree), 85 | expected_args_no_config, 86 | "Should use fallback arguments when sorbet/config is not found" 87 | ); 88 | } 89 | 90 | #[test] 91 | fn test_executable_args_with_config_and_custom_settings() { 92 | let sorbet = Sorbet::new(); 93 | let mut fake_worktree = FakeWorktree::new("/path/to/project".to_string()); 94 | 95 | fake_worktree.add_file("sorbet/config".to_string(), Ok("--dir\n.".to_string())); 96 | 97 | let custom_args = vec!["--custom-arg1".to_string(), "value1".to_string()]; 98 | fake_worktree.add_lsp_binary_setting( 99 | Sorbet::SERVER_ID.to_string(), 100 | Ok(Some(LspBinarySettings { 101 | path: None, 102 | arguments: Some(custom_args.clone()), 103 | })), 104 | ); 105 | 106 | assert_eq!( 107 | sorbet.get_executable_args(&fake_worktree), 108 | custom_args, 109 | "Should use custom arguments when config and settings are present" 110 | ); 111 | } 112 | 113 | #[test] 114 | fn test_executable_args_with_config_no_custom_settings() { 115 | let sorbet = Sorbet::new(); 116 | let mut fake_worktree = FakeWorktree::new("/path/to/project".to_string()); 117 | 118 | fake_worktree.add_file("sorbet/config".to_string(), Ok("--dir\n.".to_string())); 119 | fake_worktree.add_lsp_binary_setting(Sorbet::SERVER_ID.to_string(), Ok(None)); 120 | 121 | let expected_default_args = vec![ 122 | "tc".to_string(), 123 | "--lsp".to_string(), 124 | "--enable-experimental-lsp-document-highlight".to_string(), 125 | ]; 126 | assert_eq!( 127 | sorbet.get_executable_args(&fake_worktree), 128 | expected_default_args, 129 | "Should use default arguments when config is present but no custom settings" 130 | ); 131 | } 132 | 133 | #[test] 134 | fn test_executable_args_with_config_lsp_settings_is_empty_struct() { 135 | let sorbet = Sorbet::new(); 136 | let mut fake_worktree = FakeWorktree::new("/path/to/project".to_string()); 137 | 138 | fake_worktree.add_file("sorbet/config".to_string(), Ok("--dir\n.".to_string())); 139 | fake_worktree.add_lsp_binary_setting( 140 | Sorbet::SERVER_ID.to_string(), 141 | Ok(Some(LspBinarySettings::default())), 142 | ); 143 | 144 | let expected_default_args = vec![ 145 | "tc".to_string(), 146 | "--lsp".to_string(), 147 | "--enable-experimental-lsp-document-highlight".to_string(), 148 | ]; 149 | assert_eq!( 150 | sorbet.get_executable_args(&fake_worktree), 151 | expected_default_args, 152 | "Should use default arguments when config is present and LSP settings have no arguments" 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/language_servers/steep.rs: -------------------------------------------------------------------------------- 1 | use super::{language_server::WorktreeLike, LanguageServer}; 2 | use zed_extension_api::{self as zed}; 3 | 4 | pub struct Steep {} 5 | 6 | impl LanguageServer for Steep { 7 | const SERVER_ID: &str = "steep"; 8 | const EXECUTABLE_NAME: &str = "steep"; 9 | const GEM_NAME: &str = "steep"; 10 | 11 | fn get_executable_args(&self, _worktree: &T) -> Vec { 12 | vec!["langserver".to_string()] 13 | } 14 | 15 | fn language_server_command( 16 | &mut self, 17 | language_server_id: &zed::LanguageServerId, 18 | worktree: &zed::Worktree, 19 | ) -> zed::Result { 20 | let lsp_settings = 21 | zed::settings::LspSettings::for_worktree(language_server_id.as_ref(), worktree)?; 22 | 23 | let require_root_steepfile = lsp_settings 24 | .settings 25 | .as_ref() 26 | .and_then(|settings| settings["require_root_steepfile"].as_bool()) 27 | .unwrap_or(true); 28 | 29 | if require_root_steepfile && worktree.read_text_file("Steepfile").is_err() { 30 | return Err("Steep language server requires a Steepfile in the project root. You can disable this requirement by setting 'require_root_steepfile': false in your LSP settings.".to_string()); 31 | } 32 | 33 | let binary = self.language_server_binary(language_server_id, worktree)?; 34 | 35 | zed::set_language_server_installation_status( 36 | language_server_id, 37 | &zed::LanguageServerInstallationStatus::None, 38 | ); 39 | 40 | Ok(zed::Command { 41 | command: binary.path, 42 | args: binary.args.unwrap_or(self.get_executable_args(worktree)), 43 | env: binary.env.unwrap_or_default(), 44 | }) 45 | } 46 | } 47 | 48 | impl Steep { 49 | pub fn new() -> Self { 50 | Self {} 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use crate::language_servers::{language_server::FakeWorktree, LanguageServer, Steep}; 57 | 58 | #[test] 59 | fn test_server_id() { 60 | assert_eq!(Steep::SERVER_ID, "steep"); 61 | } 62 | 63 | #[test] 64 | fn test_executable_name() { 65 | assert_eq!(Steep::EXECUTABLE_NAME, "steep"); 66 | } 67 | 68 | #[test] 69 | fn test_executable_args() { 70 | let steep = Steep::new(); 71 | let mock_worktree = FakeWorktree::new("/path/to/project".to_string()); 72 | 73 | assert_eq!( 74 | steep.get_executable_args(&mock_worktree), 75 | vec!["langserver"] 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ruby.rs: -------------------------------------------------------------------------------- 1 | mod bundler; 2 | mod command_executor; 3 | mod gemset; 4 | mod language_servers; 5 | 6 | use language_servers::{LanguageServer, Rubocop, RubyLsp, Solargraph, Sorbet, Steep}; 7 | use zed_extension_api::{self as zed}; 8 | 9 | #[derive(Default)] 10 | struct RubyExtension { 11 | solargraph: Option, 12 | ruby_lsp: Option, 13 | rubocop: Option, 14 | sorbet: Option, 15 | steep: Option, 16 | } 17 | 18 | impl zed::Extension for RubyExtension { 19 | fn new() -> Self { 20 | Self::default() 21 | } 22 | 23 | fn language_server_command( 24 | &mut self, 25 | language_server_id: &zed::LanguageServerId, 26 | worktree: &zed::Worktree, 27 | ) -> zed::Result { 28 | match language_server_id.as_ref() { 29 | Solargraph::SERVER_ID => { 30 | let solargraph = self.solargraph.get_or_insert_with(Solargraph::new); 31 | solargraph.language_server_command(language_server_id, worktree) 32 | } 33 | RubyLsp::SERVER_ID => { 34 | let ruby_lsp = self.ruby_lsp.get_or_insert_with(RubyLsp::new); 35 | ruby_lsp.language_server_command(language_server_id, worktree) 36 | } 37 | Rubocop::SERVER_ID => { 38 | let rubocop = self.rubocop.get_or_insert_with(Rubocop::new); 39 | rubocop.language_server_command(language_server_id, worktree) 40 | } 41 | Sorbet::SERVER_ID => { 42 | let sorbet = self.sorbet.get_or_insert_with(Sorbet::new); 43 | sorbet.language_server_command(language_server_id, worktree) 44 | } 45 | Steep::SERVER_ID => { 46 | let steep = self.steep.get_or_insert_with(Steep::new); 47 | steep.language_server_command(language_server_id, worktree) 48 | } 49 | language_server_id => Err(format!("unknown language server: {language_server_id}")), 50 | } 51 | } 52 | 53 | fn language_server_initialization_options( 54 | &mut self, 55 | language_server_id: &zed::LanguageServerId, 56 | worktree: &zed::Worktree, 57 | ) -> zed::Result> { 58 | let initialization_options = 59 | zed::settings::LspSettings::for_worktree(language_server_id.as_ref(), worktree) 60 | .ok() 61 | .and_then(|lsp_settings| lsp_settings.initialization_options.clone()) 62 | .unwrap_or_default(); 63 | 64 | Ok(Some(zed::serde_json::json!(initialization_options))) 65 | } 66 | 67 | fn label_for_completion( 68 | &self, 69 | language_server_id: &zed::LanguageServerId, 70 | completion: zed::lsp::Completion, 71 | ) -> Option { 72 | match language_server_id.as_ref() { 73 | Solargraph::SERVER_ID => self.solargraph.as_ref()?.label_for_completion(completion), 74 | RubyLsp::SERVER_ID => self.ruby_lsp.as_ref()?.label_for_completion(completion), 75 | _ => None, 76 | } 77 | } 78 | 79 | fn label_for_symbol( 80 | &self, 81 | language_server_id: &zed::LanguageServerId, 82 | symbol: zed::lsp::Symbol, 83 | ) -> Option { 84 | match language_server_id.as_ref() { 85 | Solargraph::SERVER_ID => self.solargraph.as_ref()?.label_for_symbol(symbol), 86 | RubyLsp::SERVER_ID => self.ruby_lsp.as_ref()?.label_for_symbol(symbol), 87 | _ => None, 88 | } 89 | } 90 | } 91 | 92 | zed_extension_api::register_extension!(RubyExtension); 93 | --------------------------------------------------------------------------------