├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── flake.lock ├── flake.nix ├── src ├── lib.rs ├── main.rs ├── server.rs └── snippets │ ├── config.rs │ ├── external.rs │ ├── mod.rs │ └── vscode.rs └── tests └── basic.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "cargo" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - v[0-9]+.* 10 | 11 | jobs: 12 | create-release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: taiki-e/create-gh-release-action@v1 17 | with: 18 | # (optional) Path to changelog. 19 | changelog: CHANGELOG.md 20 | # (required) GitHub token for creating GitHub Releases. 21 | token: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | upload-assets: 24 | needs: create-release 25 | strategy: 26 | matrix: 27 | include: 28 | - target: x86_64-unknown-linux-gnu 29 | os: ubuntu-latest 30 | - target: x86_64-apple-darwin 31 | os: macos-latest 32 | - target: aarch64-apple-darwin 33 | os: macos-latest 34 | - target: x86_64-pc-windows-msvc 35 | os: windows-latest 36 | runs-on: ${{ matrix.os }} 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: taiki-e/upload-rust-binary-action@v1 40 | with: 41 | # (required) Comma-separated list of binary names (non-extension portion of filename) to build and upload. 42 | # Note that glob pattern is not supported yet. 43 | bin: simple-completion-language-server 44 | # (optional) Target triple, default is host triple. 45 | # This is optional but it is recommended that this always be set to 46 | # clarify which target you are building for if macOS is included in 47 | # the matrix because GitHub Actions changed the default architecture 48 | # of macos-latest since macos-14. 49 | target: ${{ matrix.target }} 50 | # (optional) On which platform to distribute the `.tar.gz` file. 51 | # [default value: unix] 52 | # [possible values: all, unix, windows, none] 53 | tar: unix 54 | # (optional) On which platform to distribute the `.zip` file. 55 | # [default value: windows] 56 | # [possible values: all, unix, windows, none] 57 | zip: windows 58 | # (required) GitHub token for uploading assets to GitHub Releases. 59 | token: ${{ secrets.GITHUB_TOKEN }} 60 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: tests suite 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: clippy&test 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: dtolnay/rust-toolchain@stable 12 | with: 13 | components: clippy 14 | - run: cargo clippy --all-features 15 | - run: cargo test --all-features 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /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 = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "anyhow" 31 | version = "1.0.98" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 34 | 35 | [[package]] 36 | name = "async-trait" 37 | version = "0.1.83" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" 40 | dependencies = [ 41 | "proc-macro2", 42 | "quote", 43 | "syn", 44 | ] 45 | 46 | [[package]] 47 | name = "auto_impl" 48 | version = "1.2.0" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" 51 | dependencies = [ 52 | "proc-macro2", 53 | "quote", 54 | "syn", 55 | ] 56 | 57 | [[package]] 58 | name = "autocfg" 59 | version = "1.4.0" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 62 | 63 | [[package]] 64 | name = "backtrace" 65 | version = "0.3.74" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 68 | dependencies = [ 69 | "addr2line", 70 | "cfg-if", 71 | "libc", 72 | "miniz_oxide", 73 | "object", 74 | "rustc-demangle", 75 | "windows-targets", 76 | ] 77 | 78 | [[package]] 79 | name = "biblatex" 80 | version = "0.10.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "a35a7317fcbdbef94b60d0dd0a658711a936accfce4a631fea4bf8e527eff3c2" 83 | dependencies = [ 84 | "numerals", 85 | "paste", 86 | "strum", 87 | "unicode-normalization", 88 | "unscanny", 89 | ] 90 | 91 | [[package]] 92 | name = "bitflags" 93 | version = "1.3.2" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 96 | 97 | [[package]] 98 | name = "bitflags" 99 | version = "2.6.0" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 102 | 103 | [[package]] 104 | name = "bytes" 105 | version = "1.9.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" 108 | 109 | [[package]] 110 | name = "caseless" 111 | version = "0.2.2" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8" 114 | dependencies = [ 115 | "unicode-normalization", 116 | ] 117 | 118 | [[package]] 119 | name = "cfg-if" 120 | version = "1.0.0" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 123 | 124 | [[package]] 125 | name = "crossbeam-channel" 126 | version = "0.5.14" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" 129 | dependencies = [ 130 | "crossbeam-utils", 131 | ] 132 | 133 | [[package]] 134 | name = "crossbeam-utils" 135 | version = "0.8.21" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 138 | 139 | [[package]] 140 | name = "dashmap" 141 | version = "5.5.3" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" 144 | dependencies = [ 145 | "cfg-if", 146 | "hashbrown 0.14.5", 147 | "lock_api", 148 | "once_cell", 149 | "parking_lot_core", 150 | ] 151 | 152 | [[package]] 153 | name = "deranged" 154 | version = "0.3.11" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 157 | dependencies = [ 158 | "powerfmt", 159 | ] 160 | 161 | [[package]] 162 | name = "displaydoc" 163 | version = "0.2.5" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 166 | dependencies = [ 167 | "proc-macro2", 168 | "quote", 169 | "syn", 170 | ] 171 | 172 | [[package]] 173 | name = "equivalent" 174 | version = "1.0.1" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 177 | 178 | [[package]] 179 | name = "etcetera" 180 | version = "0.10.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6" 183 | dependencies = [ 184 | "cfg-if", 185 | "home", 186 | "windows-sys", 187 | ] 188 | 189 | [[package]] 190 | name = "form_urlencoded" 191 | version = "1.2.1" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 194 | dependencies = [ 195 | "percent-encoding", 196 | ] 197 | 198 | [[package]] 199 | name = "futures" 200 | version = "0.3.31" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 203 | dependencies = [ 204 | "futures-channel", 205 | "futures-core", 206 | "futures-io", 207 | "futures-sink", 208 | "futures-task", 209 | "futures-util", 210 | ] 211 | 212 | [[package]] 213 | name = "futures-channel" 214 | version = "0.3.31" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 217 | dependencies = [ 218 | "futures-core", 219 | "futures-sink", 220 | ] 221 | 222 | [[package]] 223 | name = "futures-core" 224 | version = "0.3.31" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 227 | 228 | [[package]] 229 | name = "futures-io" 230 | version = "0.3.31" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 233 | 234 | [[package]] 235 | name = "futures-macro" 236 | version = "0.3.31" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 239 | dependencies = [ 240 | "proc-macro2", 241 | "quote", 242 | "syn", 243 | ] 244 | 245 | [[package]] 246 | name = "futures-sink" 247 | version = "0.3.31" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 250 | 251 | [[package]] 252 | name = "futures-task" 253 | version = "0.3.31" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 256 | 257 | [[package]] 258 | name = "futures-util" 259 | version = "0.3.31" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 262 | dependencies = [ 263 | "futures-channel", 264 | "futures-core", 265 | "futures-io", 266 | "futures-macro", 267 | "futures-sink", 268 | "futures-task", 269 | "memchr", 270 | "pin-project-lite", 271 | "pin-utils", 272 | "slab", 273 | ] 274 | 275 | [[package]] 276 | name = "gimli" 277 | version = "0.31.1" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 280 | 281 | [[package]] 282 | name = "hashbrown" 283 | version = "0.14.5" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 286 | 287 | [[package]] 288 | name = "hashbrown" 289 | version = "0.15.2" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 292 | 293 | [[package]] 294 | name = "heck" 295 | version = "0.5.0" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 298 | 299 | [[package]] 300 | name = "home" 301 | version = "0.5.11" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 304 | dependencies = [ 305 | "windows-sys", 306 | ] 307 | 308 | [[package]] 309 | name = "httparse" 310 | version = "1.9.5" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" 313 | 314 | [[package]] 315 | name = "icu_collections" 316 | version = "1.5.0" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 319 | dependencies = [ 320 | "displaydoc", 321 | "yoke", 322 | "zerofrom", 323 | "zerovec", 324 | ] 325 | 326 | [[package]] 327 | name = "icu_locid" 328 | version = "1.5.0" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 331 | dependencies = [ 332 | "displaydoc", 333 | "litemap", 334 | "tinystr", 335 | "writeable", 336 | "zerovec", 337 | ] 338 | 339 | [[package]] 340 | name = "icu_locid_transform" 341 | version = "1.5.0" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 344 | dependencies = [ 345 | "displaydoc", 346 | "icu_locid", 347 | "icu_locid_transform_data", 348 | "icu_provider", 349 | "tinystr", 350 | "zerovec", 351 | ] 352 | 353 | [[package]] 354 | name = "icu_locid_transform_data" 355 | version = "1.5.0" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 358 | 359 | [[package]] 360 | name = "icu_normalizer" 361 | version = "1.5.0" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 364 | dependencies = [ 365 | "displaydoc", 366 | "icu_collections", 367 | "icu_normalizer_data", 368 | "icu_properties", 369 | "icu_provider", 370 | "smallvec", 371 | "utf16_iter", 372 | "utf8_iter", 373 | "write16", 374 | "zerovec", 375 | ] 376 | 377 | [[package]] 378 | name = "icu_normalizer_data" 379 | version = "1.5.0" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 382 | 383 | [[package]] 384 | name = "icu_properties" 385 | version = "1.5.1" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 388 | dependencies = [ 389 | "displaydoc", 390 | "icu_collections", 391 | "icu_locid_transform", 392 | "icu_properties_data", 393 | "icu_provider", 394 | "tinystr", 395 | "zerovec", 396 | ] 397 | 398 | [[package]] 399 | name = "icu_properties_data" 400 | version = "1.5.0" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 403 | 404 | [[package]] 405 | name = "icu_provider" 406 | version = "1.5.0" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 409 | dependencies = [ 410 | "displaydoc", 411 | "icu_locid", 412 | "icu_provider_macros", 413 | "stable_deref_trait", 414 | "tinystr", 415 | "writeable", 416 | "yoke", 417 | "zerofrom", 418 | "zerovec", 419 | ] 420 | 421 | [[package]] 422 | name = "icu_provider_macros" 423 | version = "1.5.0" 424 | source = "registry+https://github.com/rust-lang/crates.io-index" 425 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 426 | dependencies = [ 427 | "proc-macro2", 428 | "quote", 429 | "syn", 430 | ] 431 | 432 | [[package]] 433 | name = "idna" 434 | version = "1.0.3" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 437 | dependencies = [ 438 | "idna_adapter", 439 | "smallvec", 440 | "utf8_iter", 441 | ] 442 | 443 | [[package]] 444 | name = "idna_adapter" 445 | version = "1.2.0" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 448 | dependencies = [ 449 | "icu_normalizer", 450 | "icu_properties", 451 | ] 452 | 453 | [[package]] 454 | name = "indexmap" 455 | version = "2.7.0" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" 458 | dependencies = [ 459 | "equivalent", 460 | "hashbrown 0.15.2", 461 | ] 462 | 463 | [[package]] 464 | name = "itoa" 465 | version = "1.0.14" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 468 | 469 | [[package]] 470 | name = "lazy_static" 471 | version = "1.5.0" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 474 | 475 | [[package]] 476 | name = "libc" 477 | version = "0.2.168" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" 480 | 481 | [[package]] 482 | name = "litemap" 483 | version = "0.7.4" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 486 | 487 | [[package]] 488 | name = "lock_api" 489 | version = "0.4.12" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 492 | dependencies = [ 493 | "autocfg", 494 | "scopeguard", 495 | ] 496 | 497 | [[package]] 498 | name = "log" 499 | version = "0.4.22" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 502 | 503 | [[package]] 504 | name = "lsp-types" 505 | version = "0.94.1" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" 508 | dependencies = [ 509 | "bitflags 1.3.2", 510 | "serde", 511 | "serde_json", 512 | "serde_repr", 513 | "url", 514 | ] 515 | 516 | [[package]] 517 | name = "matchers" 518 | version = "0.1.0" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 521 | dependencies = [ 522 | "regex-automata 0.1.10", 523 | ] 524 | 525 | [[package]] 526 | name = "memchr" 527 | version = "2.7.4" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 530 | 531 | [[package]] 532 | name = "miniz_oxide" 533 | version = "0.8.2" 534 | source = "registry+https://github.com/rust-lang/crates.io-index" 535 | checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" 536 | dependencies = [ 537 | "adler2", 538 | ] 539 | 540 | [[package]] 541 | name = "nu-ansi-term" 542 | version = "0.46.0" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 545 | dependencies = [ 546 | "overload", 547 | "winapi", 548 | ] 549 | 550 | [[package]] 551 | name = "num-conv" 552 | version = "0.1.0" 553 | source = "registry+https://github.com/rust-lang/crates.io-index" 554 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 555 | 556 | [[package]] 557 | name = "numerals" 558 | version = "0.1.4" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "e25be21376a772d15f97ae789845340a9651d3c4246ff5ebb6a2b35f9c37bd31" 561 | 562 | [[package]] 563 | name = "object" 564 | version = "0.36.5" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" 567 | dependencies = [ 568 | "memchr", 569 | ] 570 | 571 | [[package]] 572 | name = "once_cell" 573 | version = "1.20.2" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 576 | 577 | [[package]] 578 | name = "overload" 579 | version = "0.1.1" 580 | source = "registry+https://github.com/rust-lang/crates.io-index" 581 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 582 | 583 | [[package]] 584 | name = "parking_lot_core" 585 | version = "0.9.10" 586 | source = "registry+https://github.com/rust-lang/crates.io-index" 587 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 588 | dependencies = [ 589 | "cfg-if", 590 | "libc", 591 | "redox_syscall", 592 | "smallvec", 593 | "windows-targets", 594 | ] 595 | 596 | [[package]] 597 | name = "paste" 598 | version = "1.0.15" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 601 | 602 | [[package]] 603 | name = "percent-encoding" 604 | version = "2.3.1" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 607 | 608 | [[package]] 609 | name = "pin-project" 610 | version = "1.1.7" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" 613 | dependencies = [ 614 | "pin-project-internal", 615 | ] 616 | 617 | [[package]] 618 | name = "pin-project-internal" 619 | version = "1.1.7" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" 622 | dependencies = [ 623 | "proc-macro2", 624 | "quote", 625 | "syn", 626 | ] 627 | 628 | [[package]] 629 | name = "pin-project-lite" 630 | version = "0.2.15" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" 633 | 634 | [[package]] 635 | name = "pin-utils" 636 | version = "0.1.0" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 639 | 640 | [[package]] 641 | name = "powerfmt" 642 | version = "0.2.0" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 645 | 646 | [[package]] 647 | name = "proc-macro2" 648 | version = "1.0.92" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 651 | dependencies = [ 652 | "unicode-ident", 653 | ] 654 | 655 | [[package]] 656 | name = "quote" 657 | version = "1.0.37" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 660 | dependencies = [ 661 | "proc-macro2", 662 | ] 663 | 664 | [[package]] 665 | name = "redox_syscall" 666 | version = "0.5.8" 667 | source = "registry+https://github.com/rust-lang/crates.io-index" 668 | checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" 669 | dependencies = [ 670 | "bitflags 2.6.0", 671 | ] 672 | 673 | [[package]] 674 | name = "regex" 675 | version = "1.11.1" 676 | source = "registry+https://github.com/rust-lang/crates.io-index" 677 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 678 | dependencies = [ 679 | "aho-corasick", 680 | "memchr", 681 | "regex-automata 0.4.9", 682 | "regex-syntax 0.8.5", 683 | ] 684 | 685 | [[package]] 686 | name = "regex-automata" 687 | version = "0.1.10" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 690 | dependencies = [ 691 | "regex-syntax 0.6.29", 692 | ] 693 | 694 | [[package]] 695 | name = "regex-automata" 696 | version = "0.4.9" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 699 | dependencies = [ 700 | "aho-corasick", 701 | "memchr", 702 | "regex-syntax 0.8.5", 703 | ] 704 | 705 | [[package]] 706 | name = "regex-cursor" 707 | version = "0.1.5" 708 | source = "registry+https://github.com/rust-lang/crates.io-index" 709 | checksum = "0497c781d2f982ae8284d2932aee6a877e58a4541daa5e8fadc18cc75c23a61d" 710 | dependencies = [ 711 | "log", 712 | "memchr", 713 | "regex-automata 0.4.9", 714 | "regex-syntax 0.8.5", 715 | "ropey", 716 | ] 717 | 718 | [[package]] 719 | name = "regex-syntax" 720 | version = "0.6.29" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 723 | 724 | [[package]] 725 | name = "regex-syntax" 726 | version = "0.8.5" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 729 | 730 | [[package]] 731 | name = "ropey" 732 | version = "1.6.1" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5" 735 | dependencies = [ 736 | "smallvec", 737 | "str_indices", 738 | ] 739 | 740 | [[package]] 741 | name = "rustc-demangle" 742 | version = "0.1.24" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 745 | 746 | [[package]] 747 | name = "rustversion" 748 | version = "1.0.18" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" 751 | 752 | [[package]] 753 | name = "ryu" 754 | version = "1.0.18" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 757 | 758 | [[package]] 759 | name = "scopeguard" 760 | version = "1.2.0" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 763 | 764 | [[package]] 765 | name = "serde" 766 | version = "1.0.219" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 769 | dependencies = [ 770 | "serde_derive", 771 | ] 772 | 773 | [[package]] 774 | name = "serde_derive" 775 | version = "1.0.219" 776 | source = "registry+https://github.com/rust-lang/crates.io-index" 777 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 778 | dependencies = [ 779 | "proc-macro2", 780 | "quote", 781 | "syn", 782 | ] 783 | 784 | [[package]] 785 | name = "serde_json" 786 | version = "1.0.140" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 789 | dependencies = [ 790 | "itoa", 791 | "memchr", 792 | "ryu", 793 | "serde", 794 | ] 795 | 796 | [[package]] 797 | name = "serde_repr" 798 | version = "0.1.19" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" 801 | dependencies = [ 802 | "proc-macro2", 803 | "quote", 804 | "syn", 805 | ] 806 | 807 | [[package]] 808 | name = "serde_spanned" 809 | version = "0.6.8" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 812 | dependencies = [ 813 | "serde", 814 | ] 815 | 816 | [[package]] 817 | name = "sharded-slab" 818 | version = "0.1.7" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 821 | dependencies = [ 822 | "lazy_static", 823 | ] 824 | 825 | [[package]] 826 | name = "simple-completion-language-server" 827 | version = "0.1.0" 828 | dependencies = [ 829 | "aho-corasick", 830 | "anyhow", 831 | "biblatex", 832 | "caseless", 833 | "etcetera", 834 | "regex-cursor", 835 | "ropey", 836 | "serde", 837 | "serde_json", 838 | "test-log", 839 | "tokio", 840 | "toml", 841 | "tower-lsp", 842 | "tracing", 843 | "tracing-appender", 844 | "tracing-subscriber", 845 | "xshell", 846 | ] 847 | 848 | [[package]] 849 | name = "slab" 850 | version = "0.4.9" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 853 | dependencies = [ 854 | "autocfg", 855 | ] 856 | 857 | [[package]] 858 | name = "smallvec" 859 | version = "1.13.2" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 862 | 863 | [[package]] 864 | name = "stable_deref_trait" 865 | version = "1.2.0" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 868 | 869 | [[package]] 870 | name = "str_indices" 871 | version = "0.4.4" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" 874 | 875 | [[package]] 876 | name = "strum" 877 | version = "0.26.3" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 880 | dependencies = [ 881 | "strum_macros", 882 | ] 883 | 884 | [[package]] 885 | name = "strum_macros" 886 | version = "0.26.4" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 889 | dependencies = [ 890 | "heck", 891 | "proc-macro2", 892 | "quote", 893 | "rustversion", 894 | "syn", 895 | ] 896 | 897 | [[package]] 898 | name = "syn" 899 | version = "2.0.90" 900 | source = "registry+https://github.com/rust-lang/crates.io-index" 901 | checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" 902 | dependencies = [ 903 | "proc-macro2", 904 | "quote", 905 | "unicode-ident", 906 | ] 907 | 908 | [[package]] 909 | name = "synstructure" 910 | version = "0.13.1" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 913 | dependencies = [ 914 | "proc-macro2", 915 | "quote", 916 | "syn", 917 | ] 918 | 919 | [[package]] 920 | name = "test-log" 921 | version = "0.2.17" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "e7f46083d221181166e5b6f6b1e5f1d499f3a76888826e6cb1d057554157cd0f" 924 | dependencies = [ 925 | "test-log-macros", 926 | "tracing-subscriber", 927 | ] 928 | 929 | [[package]] 930 | name = "test-log-macros" 931 | version = "0.2.16" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "5999e24eaa32083191ba4e425deb75cdf25efefabe5aaccb7446dd0d4122a3f5" 934 | dependencies = [ 935 | "proc-macro2", 936 | "quote", 937 | "syn", 938 | ] 939 | 940 | [[package]] 941 | name = "thiserror" 942 | version = "1.0.69" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 945 | dependencies = [ 946 | "thiserror-impl", 947 | ] 948 | 949 | [[package]] 950 | name = "thiserror-impl" 951 | version = "1.0.69" 952 | source = "registry+https://github.com/rust-lang/crates.io-index" 953 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 954 | dependencies = [ 955 | "proc-macro2", 956 | "quote", 957 | "syn", 958 | ] 959 | 960 | [[package]] 961 | name = "thread_local" 962 | version = "1.1.8" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 965 | dependencies = [ 966 | "cfg-if", 967 | "once_cell", 968 | ] 969 | 970 | [[package]] 971 | name = "time" 972 | version = "0.3.37" 973 | source = "registry+https://github.com/rust-lang/crates.io-index" 974 | checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" 975 | dependencies = [ 976 | "deranged", 977 | "itoa", 978 | "num-conv", 979 | "powerfmt", 980 | "serde", 981 | "time-core", 982 | "time-macros", 983 | ] 984 | 985 | [[package]] 986 | name = "time-core" 987 | version = "0.1.2" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 990 | 991 | [[package]] 992 | name = "time-macros" 993 | version = "0.2.19" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" 996 | dependencies = [ 997 | "num-conv", 998 | "time-core", 999 | ] 1000 | 1001 | [[package]] 1002 | name = "tinystr" 1003 | version = "0.7.6" 1004 | source = "registry+https://github.com/rust-lang/crates.io-index" 1005 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1006 | dependencies = [ 1007 | "displaydoc", 1008 | "zerovec", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "tinyvec" 1013 | version = "1.8.0" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" 1016 | dependencies = [ 1017 | "tinyvec_macros", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "tinyvec_macros" 1022 | version = "0.1.1" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1025 | 1026 | [[package]] 1027 | name = "tokio" 1028 | version = "1.45.1" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" 1031 | dependencies = [ 1032 | "backtrace", 1033 | "pin-project-lite", 1034 | "tokio-macros", 1035 | ] 1036 | 1037 | [[package]] 1038 | name = "tokio-macros" 1039 | version = "2.5.0" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1042 | dependencies = [ 1043 | "proc-macro2", 1044 | "quote", 1045 | "syn", 1046 | ] 1047 | 1048 | [[package]] 1049 | name = "tokio-util" 1050 | version = "0.7.13" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" 1053 | dependencies = [ 1054 | "bytes", 1055 | "futures-core", 1056 | "futures-sink", 1057 | "pin-project-lite", 1058 | "tokio", 1059 | ] 1060 | 1061 | [[package]] 1062 | name = "toml" 1063 | version = "0.8.22" 1064 | source = "registry+https://github.com/rust-lang/crates.io-index" 1065 | checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" 1066 | dependencies = [ 1067 | "serde", 1068 | "serde_spanned", 1069 | "toml_datetime", 1070 | "toml_edit", 1071 | ] 1072 | 1073 | [[package]] 1074 | name = "toml_datetime" 1075 | version = "0.6.9" 1076 | source = "registry+https://github.com/rust-lang/crates.io-index" 1077 | checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" 1078 | dependencies = [ 1079 | "serde", 1080 | ] 1081 | 1082 | [[package]] 1083 | name = "toml_edit" 1084 | version = "0.22.26" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" 1087 | dependencies = [ 1088 | "indexmap", 1089 | "serde", 1090 | "serde_spanned", 1091 | "toml_datetime", 1092 | "toml_write", 1093 | "winnow", 1094 | ] 1095 | 1096 | [[package]] 1097 | name = "toml_write" 1098 | version = "0.1.1" 1099 | source = "registry+https://github.com/rust-lang/crates.io-index" 1100 | checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" 1101 | 1102 | [[package]] 1103 | name = "tower" 1104 | version = "0.4.13" 1105 | source = "registry+https://github.com/rust-lang/crates.io-index" 1106 | checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 1107 | dependencies = [ 1108 | "futures-core", 1109 | "futures-util", 1110 | "pin-project", 1111 | "pin-project-lite", 1112 | "tower-layer", 1113 | "tower-service", 1114 | ] 1115 | 1116 | [[package]] 1117 | name = "tower-layer" 1118 | version = "0.3.3" 1119 | source = "registry+https://github.com/rust-lang/crates.io-index" 1120 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1121 | 1122 | [[package]] 1123 | name = "tower-lsp" 1124 | version = "0.20.0" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "d4ba052b54a6627628d9b3c34c176e7eda8359b7da9acd497b9f20998d118508" 1127 | dependencies = [ 1128 | "async-trait", 1129 | "auto_impl", 1130 | "bytes", 1131 | "dashmap", 1132 | "futures", 1133 | "httparse", 1134 | "lsp-types", 1135 | "memchr", 1136 | "serde", 1137 | "serde_json", 1138 | "tokio", 1139 | "tokio-util", 1140 | "tower", 1141 | "tower-lsp-macros", 1142 | "tracing", 1143 | ] 1144 | 1145 | [[package]] 1146 | name = "tower-lsp-macros" 1147 | version = "0.9.0" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" 1150 | dependencies = [ 1151 | "proc-macro2", 1152 | "quote", 1153 | "syn", 1154 | ] 1155 | 1156 | [[package]] 1157 | name = "tower-service" 1158 | version = "0.3.3" 1159 | source = "registry+https://github.com/rust-lang/crates.io-index" 1160 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1161 | 1162 | [[package]] 1163 | name = "tracing" 1164 | version = "0.1.41" 1165 | source = "registry+https://github.com/rust-lang/crates.io-index" 1166 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1167 | dependencies = [ 1168 | "pin-project-lite", 1169 | "tracing-attributes", 1170 | "tracing-core", 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "tracing-appender" 1175 | version = "0.2.3" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" 1178 | dependencies = [ 1179 | "crossbeam-channel", 1180 | "thiserror", 1181 | "time", 1182 | "tracing-subscriber", 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "tracing-attributes" 1187 | version = "0.1.28" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 1190 | dependencies = [ 1191 | "proc-macro2", 1192 | "quote", 1193 | "syn", 1194 | ] 1195 | 1196 | [[package]] 1197 | name = "tracing-core" 1198 | version = "0.1.33" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1201 | dependencies = [ 1202 | "once_cell", 1203 | "valuable", 1204 | ] 1205 | 1206 | [[package]] 1207 | name = "tracing-log" 1208 | version = "0.2.0" 1209 | source = "registry+https://github.com/rust-lang/crates.io-index" 1210 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 1211 | dependencies = [ 1212 | "log", 1213 | "once_cell", 1214 | "tracing-core", 1215 | ] 1216 | 1217 | [[package]] 1218 | name = "tracing-subscriber" 1219 | version = "0.3.19" 1220 | source = "registry+https://github.com/rust-lang/crates.io-index" 1221 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 1222 | dependencies = [ 1223 | "matchers", 1224 | "nu-ansi-term", 1225 | "once_cell", 1226 | "regex", 1227 | "sharded-slab", 1228 | "smallvec", 1229 | "thread_local", 1230 | "tracing", 1231 | "tracing-core", 1232 | "tracing-log", 1233 | ] 1234 | 1235 | [[package]] 1236 | name = "unicode-ident" 1237 | version = "1.0.14" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 1240 | 1241 | [[package]] 1242 | name = "unicode-normalization" 1243 | version = "0.1.24" 1244 | source = "registry+https://github.com/rust-lang/crates.io-index" 1245 | checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" 1246 | dependencies = [ 1247 | "tinyvec", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "unscanny" 1252 | version = "0.1.0" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" 1255 | 1256 | [[package]] 1257 | name = "url" 1258 | version = "2.5.4" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1261 | dependencies = [ 1262 | "form_urlencoded", 1263 | "idna", 1264 | "percent-encoding", 1265 | "serde", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "utf16_iter" 1270 | version = "1.0.5" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1273 | 1274 | [[package]] 1275 | name = "utf8_iter" 1276 | version = "1.0.4" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1279 | 1280 | [[package]] 1281 | name = "valuable" 1282 | version = "0.1.0" 1283 | source = "registry+https://github.com/rust-lang/crates.io-index" 1284 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 1285 | 1286 | [[package]] 1287 | name = "winapi" 1288 | version = "0.3.9" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1291 | dependencies = [ 1292 | "winapi-i686-pc-windows-gnu", 1293 | "winapi-x86_64-pc-windows-gnu", 1294 | ] 1295 | 1296 | [[package]] 1297 | name = "winapi-i686-pc-windows-gnu" 1298 | version = "0.4.0" 1299 | source = "registry+https://github.com/rust-lang/crates.io-index" 1300 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1301 | 1302 | [[package]] 1303 | name = "winapi-x86_64-pc-windows-gnu" 1304 | version = "0.4.0" 1305 | source = "registry+https://github.com/rust-lang/crates.io-index" 1306 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1307 | 1308 | [[package]] 1309 | name = "windows-sys" 1310 | version = "0.59.0" 1311 | source = "registry+https://github.com/rust-lang/crates.io-index" 1312 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1313 | dependencies = [ 1314 | "windows-targets", 1315 | ] 1316 | 1317 | [[package]] 1318 | name = "windows-targets" 1319 | version = "0.52.6" 1320 | source = "registry+https://github.com/rust-lang/crates.io-index" 1321 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1322 | dependencies = [ 1323 | "windows_aarch64_gnullvm", 1324 | "windows_aarch64_msvc", 1325 | "windows_i686_gnu", 1326 | "windows_i686_gnullvm", 1327 | "windows_i686_msvc", 1328 | "windows_x86_64_gnu", 1329 | "windows_x86_64_gnullvm", 1330 | "windows_x86_64_msvc", 1331 | ] 1332 | 1333 | [[package]] 1334 | name = "windows_aarch64_gnullvm" 1335 | version = "0.52.6" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1338 | 1339 | [[package]] 1340 | name = "windows_aarch64_msvc" 1341 | version = "0.52.6" 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" 1343 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1344 | 1345 | [[package]] 1346 | name = "windows_i686_gnu" 1347 | version = "0.52.6" 1348 | source = "registry+https://github.com/rust-lang/crates.io-index" 1349 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1350 | 1351 | [[package]] 1352 | name = "windows_i686_gnullvm" 1353 | version = "0.52.6" 1354 | source = "registry+https://github.com/rust-lang/crates.io-index" 1355 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1356 | 1357 | [[package]] 1358 | name = "windows_i686_msvc" 1359 | version = "0.52.6" 1360 | source = "registry+https://github.com/rust-lang/crates.io-index" 1361 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1362 | 1363 | [[package]] 1364 | name = "windows_x86_64_gnu" 1365 | version = "0.52.6" 1366 | source = "registry+https://github.com/rust-lang/crates.io-index" 1367 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1368 | 1369 | [[package]] 1370 | name = "windows_x86_64_gnullvm" 1371 | version = "0.52.6" 1372 | source = "registry+https://github.com/rust-lang/crates.io-index" 1373 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1374 | 1375 | [[package]] 1376 | name = "windows_x86_64_msvc" 1377 | version = "0.52.6" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1380 | 1381 | [[package]] 1382 | name = "winnow" 1383 | version = "0.7.7" 1384 | source = "registry+https://github.com/rust-lang/crates.io-index" 1385 | checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5" 1386 | dependencies = [ 1387 | "memchr", 1388 | ] 1389 | 1390 | [[package]] 1391 | name = "write16" 1392 | version = "1.0.0" 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" 1394 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 1395 | 1396 | [[package]] 1397 | name = "writeable" 1398 | version = "0.5.5" 1399 | source = "registry+https://github.com/rust-lang/crates.io-index" 1400 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 1401 | 1402 | [[package]] 1403 | name = "xshell" 1404 | version = "0.2.7" 1405 | source = "registry+https://github.com/rust-lang/crates.io-index" 1406 | checksum = "9e7290c623014758632efe00737145b6867b66292c42167f2ec381eb566a373d" 1407 | dependencies = [ 1408 | "xshell-macros", 1409 | ] 1410 | 1411 | [[package]] 1412 | name = "xshell-macros" 1413 | version = "0.2.7" 1414 | source = "registry+https://github.com/rust-lang/crates.io-index" 1415 | checksum = "32ac00cd3f8ec9c1d33fb3e7958a82df6989c42d747bd326c822b1d625283547" 1416 | 1417 | [[package]] 1418 | name = "yoke" 1419 | version = "0.7.5" 1420 | source = "registry+https://github.com/rust-lang/crates.io-index" 1421 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 1422 | dependencies = [ 1423 | "serde", 1424 | "stable_deref_trait", 1425 | "yoke-derive", 1426 | "zerofrom", 1427 | ] 1428 | 1429 | [[package]] 1430 | name = "yoke-derive" 1431 | version = "0.7.5" 1432 | source = "registry+https://github.com/rust-lang/crates.io-index" 1433 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 1434 | dependencies = [ 1435 | "proc-macro2", 1436 | "quote", 1437 | "syn", 1438 | "synstructure", 1439 | ] 1440 | 1441 | [[package]] 1442 | name = "zerofrom" 1443 | version = "0.1.5" 1444 | source = "registry+https://github.com/rust-lang/crates.io-index" 1445 | checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 1446 | dependencies = [ 1447 | "zerofrom-derive", 1448 | ] 1449 | 1450 | [[package]] 1451 | name = "zerofrom-derive" 1452 | version = "0.1.5" 1453 | source = "registry+https://github.com/rust-lang/crates.io-index" 1454 | checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 1455 | dependencies = [ 1456 | "proc-macro2", 1457 | "quote", 1458 | "syn", 1459 | "synstructure", 1460 | ] 1461 | 1462 | [[package]] 1463 | name = "zerovec" 1464 | version = "0.10.4" 1465 | source = "registry+https://github.com/rust-lang/crates.io-index" 1466 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 1467 | dependencies = [ 1468 | "yoke", 1469 | "zerofrom", 1470 | "zerovec-derive", 1471 | ] 1472 | 1473 | [[package]] 1474 | name = "zerovec-derive" 1475 | version = "0.10.3" 1476 | source = "registry+https://github.com/rust-lang/crates.io-index" 1477 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 1478 | dependencies = [ 1479 | "proc-macro2", 1480 | "quote", 1481 | "syn", 1482 | ] 1483 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "simple-completion-language-server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [[bin]] 7 | name = "simple-completion-language-server" 8 | path = "src/main.rs" 9 | 10 | [features] 11 | default = [] 12 | citation = ["regex-cursor", "biblatex"] 13 | 14 | [dependencies] 15 | anyhow = "1.0" 16 | ropey = "1.6" 17 | aho-corasick = "1.1" 18 | tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-std", "macros"] } 19 | tower-lsp = { version = "0.20", features = ["runtime-tokio"] } 20 | serde = { version = "1", features = ["serde_derive"] } 21 | serde_json = { version = "1" } 22 | caseless = "0.2" 23 | toml = "0.8" 24 | etcetera = "0.10" 25 | xshell = "0.2" 26 | 27 | # citation completion 28 | regex-cursor = { version = "0.1", optional = true } 29 | biblatex = { version = "0.10", optional = true } 30 | 31 | tracing = "0.1" 32 | tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } 33 | tracing-appender = "0.2" 34 | 35 | [dev-dependencies] 36 | test-log = { version = "0.2", default-features = false, features = ["trace"] } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Evgeniy Tatarkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

simple-completion-language-server

3 |

Allow to use common word completion and snippets for Helix editor

4 |

5 |
6 | 7 | 8 | https://github.com/estin/simple-completion-language-server/assets/520814/10566ad4-d6d1-475b-8561-2e909be0f875 9 | 10 | Based on [comment](https://github.com/helix-editor/helix/pull/3328#issuecomment-1559031060) 11 | 12 | ### Install 13 | 14 | #### From source 15 | 16 | From GitHub: 17 | 18 | ```console 19 | $ cargo install --git https://github.com/estin/simple-completion-language-server.git 20 | ``` 21 | 22 | From local repository: 23 | 24 | ```console 25 | $ git clone https://github.com/estin/simple-completion-language-server.git 26 | $ cd simple-completion-language-server 27 | $ cargo install --path . 28 | ``` 29 | 30 | #### Nix 31 | 32 | You can install `simple-completion-language-server` using the [nix package manager](https://nixos.org/) from [nixpkgs](https://search.nixos.org/packages?channel=unstable&show=simple-completion-language-server&from=0&size=50&sort=relevance&type=packages&query=simple-comple). 33 | 34 | > [!NOTE] 35 | > At the moment the package is only available on the [unstable](https://nixos.org/manual/nixpkgs/unstable/#overview-of-nixpkgs) channel of nixpkgs. 36 | 37 | ```console 38 | # Add it to a temporary shell environment 39 | nix shell nixpkgs#simple-completion-language-server 40 | # Add it to your current profile 41 | nix profile install nixpkgs#simple-completion-language-server 42 | ``` 43 | 44 | > [!NOTE] 45 | > The above instructions assume you have enabled the *experimental features* `nix-command` `flakes`. If you are unsure about what this means or how to check read [here](https://nixos.wiki/wiki/Flakes). 46 | 47 | If you are on [NixOS](https://nixos.org/) or are using [home-manager](https://nix-community.github.io/home-manager/) you can do one of the following: 48 | 49 | ```nix 50 | # NixOS configuration.nix 51 | environment.systemPackages = [pkgs.simple-completion-language-server]; 52 | 53 | # or home-manager config 54 | home.packages = [pkgs.simple-completion-language-server]; 55 | # This will let `hx` know about the location of the binary without putting it in your $PATH 56 | programs.helix.extraPackages = [pkgs.simple-completion-language-server]; 57 | ``` 58 | 59 | ### Configure 60 | 61 | For Helix on `~/.config/helix/languages.toml` 62 | 63 | ```toml 64 | # introduce new language server 65 | [language-server.scls] 66 | command = "simple-completion-language-server" 67 | 68 | [language-server.scls.config] 69 | max_completion_items = 100 # set max completion results len for each group: words, snippets, unicode-input 70 | feature_words = true # enable completion by word 71 | feature_snippets = true # enable snippets 72 | snippets_first = true # completions will return before snippets by default 73 | snippets_inline_by_word_tail = false # suggest snippets by WORD tail, for example text `xsq|` become `x^2|` when snippet `sq` has body `^2` 74 | feature_unicode_input = false # enable "unicode input" 75 | feature_paths = false # enable path completion 76 | feature_citations = false # enable citation completion (only on `citation` feature enabled) 77 | 78 | 79 | # write logs to /tmp/completion.log 80 | [language-server.scls.environment] 81 | RUST_LOG = "info,simple-completion-language-server=info" 82 | LOG_FILE = "/tmp/completion.log" 83 | 84 | # append language server to existed languages 85 | [[language]] 86 | name = "rust" 87 | language-servers = [ "scls", "rust-analyzer" ] 88 | 89 | [[language]] 90 | name = "git-commit" 91 | language-servers = [ "scls" ] 92 | 93 | # etc.. 94 | 95 | # introduce a new language to enable completion on any doc by forcing set language with :set-language stub 96 | [[language]] 97 | name = "stub" 98 | scope = "text.stub" 99 | file-types = [] 100 | shebangs = [] 101 | roots = [] 102 | auto-format = false 103 | language-servers = [ "scls" ] 104 | ``` 105 | 106 | ### Snippets 107 | 108 | Read snippets from dir `~/.config/helix/snippets` or specify snippets path via `SNIPPETS_PATH` env. 109 | 110 | Default lookup directory can be overriden via `SCLS_CONFIG_SUBDIRECTORY` as well (e.g. when SCLS_CONFIG_SUBDIRECTORY = `vim`, SCLS will perform it's lookups in `~/.config/vim` instead) 111 | 112 | Currently, it supports our own `toml` format and vscode `json` (a basic effort). 113 | 114 | Filename used as snippet scope ([language id][1]), filename `snippets.(toml|json)` will not attach scope to snippets. 115 | 116 | For example, snippets with the filename `python.toml` or `python.json` would have a `python` scope. 117 | 118 | Snippets format 119 | 120 | ```toml 121 | [[snippets]] 122 | prefix = "ld" 123 | scope = [ "python" ] # language id https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers 124 | body = 'log.debug("$1")' 125 | description = "log at debug level" 126 | ``` 127 | 128 | ### Use external snippets collections from git repos 129 | 130 | Configure sources in `~/.config/helix/external-snippets.toml` (or via env `EXTERNAL_SNIPPETS_CONFIG`) 131 | 132 | 1. Declare list of sources by key `[[sources]]` 133 | 134 | 2. Optionally, declare paths to load snippets on current source by `[[source.paths]]`. 135 | 136 | Highly recommended to declare concrete paths with scopes. To explicit configure required snippets and its scopes. 137 | 138 | Snippets suggestion filtered out by document scope. 139 | 140 | Scope it's [language id](https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers) and **not file extension** by language server protocol. 141 | 142 | If `[[source.paths]]` isn't specified for source then all files with extensions `.json` and `.toml` would be tried to load. Scope at that case would be equal filename. 143 | To apply snippets the filename must be one of known language id. 144 | 145 | 3. Run commands to fetch and validate snippets 146 | 147 | ```console 148 | 149 | # Clone or update snippets source repos to `~/.config/helix/external-snippets/` 150 | simple-completion-language-server fetch-external-snippets 151 | 152 | # Try to find and parse snippets 153 | simple-completion-language-server validate-snippets 154 | 155 | ``` 156 | 157 | #### Config format for external snippets 158 | 159 | ```toml 160 | # first external source to load snippets 161 | [[sources]] # list of sources to load 162 | name = "source1" # optional name shown on snippet description 163 | git = "https://example.com/source1.git" # git repo with snippets collections 164 | 165 | [[sources.paths]] # explicit list of paths to load on current source 166 | scope = ["scope1", "scope2"] # optional scopes (language id) for current snippets 167 | path = "path-in-repo/snippets1.json" # where snippet file or dir located in repo 168 | 169 | [[sources.paths]] 170 | scope = ["scope3"] 171 | path = "path-in-repo/snippets2.json" 172 | 173 | # next external source to load snippets 174 | [[sources]] 175 | name = "source2" 176 | git = "https://example.com/source2.git" 177 | 178 | [[sources.paths]] 179 | scope = ["scope1"] 180 | path = "path-in-repo-of-source2/snippets1.json" 181 | ``` 182 | 183 | #### Example 184 | 185 | Load python snippets from file https://github.com/rafamadriz/friendly-snippets/blob/main/snippets/python/python.json 186 | 187 | File `~/.config/helix/external-snippets.toml` 188 | 189 | ```toml 190 | [[sources]] 191 | name = "friendly-snippets" 192 | git = "https://github.com/rafamadriz/friendly-snippets.git" 193 | 194 | [[sources.paths]] 195 | scope = ["python"] 196 | path = "snippets/python/python.json" 197 | ``` 198 | 199 | Clone or update snippets source repos to `~/.config/helix/external-snippets/` 200 | 201 | ```console 202 | $ simple-completion-language-server fetch-external-snippets 203 | ``` 204 | 205 | Validate snippets 206 | 207 | ```console 208 | $ simple-completion-language-server validate-snippets 209 | ``` 210 | 211 | ### Unicode input 212 | 213 | Read unicode input config as each file from dir `~/.config/helix/unicode-input` (or specify path via `UNICODE_INPUT_PATH` env). 214 | 215 | Unicode input format (toml key-value), for example `~/.config/helix/unicode-input/base.toml` 216 | 217 | ```toml 218 | alpha = "α" 219 | betta = "β" 220 | gamma = "γ" 221 | fire = "🔥" 222 | ``` 223 | 224 | 225 | Validate unicode input config 226 | 227 | ```console 228 | $ simple-completion-language-server validate-unicode-input 229 | ``` 230 | 231 | ### Citation completion 232 | 233 | Citation keys completion from bibliography file declared in current document. 234 | When completion is triggered with a prefixed `@` (which can be configured via `citation_prefix_trigger` settings), scls will try to extract the bibliography file path from the current document (regex can be configured via the `citation_bibfile_extract_regexp` setting) to parse and use it as a completion source. 235 | 236 | To enable this feature, scls must be compiled with the `--features citation` flag.  237 | 238 | ```console 239 | $ cargo install --features citation --git https://github.com/estin/simple-completion-language-server.git 240 | ``` 241 | 242 | And initialize scls with `feature_citations = true`. 243 | 244 | ```toml 245 | [language-server.scls.config] 246 | max_completion_items = 20 247 | feature_citations = true 248 | ``` 249 | 250 | For more info, please check https://github.com/estin/simple-completion-language-server/issues/78 251 | 252 | ### Similar projects 253 | 254 | - [erasin/hx-lsp](https://github.com/erasin/hx-lsp) 255 | - [metafates/buffer-language-server](https://github.com/metafates/buffer-language-server) 256 | - [rajasegar/helix-snippets-ls](https://github.com/rajasegar/helix-snippets-ls) 257 | - [quantonganh/snippets-ls](https://github.com/quantonganh/snippets-ls) 258 | - [Stanislav-Lapata/snippets-ls](https://github.com/Stanislav-Lapata/snippets-ls) 259 | - ...(please add another useful links here) 260 | 261 | ### Useful snippets collections 262 | 263 | - [rafamadriz/friendly-snippets](https://github.com/rafamadriz/friendly-snippets) 264 | - ...(please add another useful links here) 265 | 266 | [1]: "Known language identifiers" 267 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "naersk": { 22 | "inputs": { 23 | "nixpkgs": [ 24 | "nixpkgs" 25 | ] 26 | }, 27 | "locked": { 28 | "lastModified": 1736429655, 29 | "narHash": "sha256-BwMekRuVlSB9C0QgwKMICiJ5EVbLGjfe4qyueyNQyGI=", 30 | "owner": "nix-community", 31 | "repo": "naersk", 32 | "rev": "0621e47bd95542b8e1ce2ee2d65d6a1f887a13ce", 33 | "type": "github" 34 | }, 35 | "original": { 36 | "owner": "nix-community", 37 | "ref": "master", 38 | "repo": "naersk", 39 | "type": "github" 40 | } 41 | }, 42 | "nixpkgs": { 43 | "locked": { 44 | "lastModified": 1738136902, 45 | "narHash": "sha256-pUvLijVGARw4u793APze3j6mU1Zwdtz7hGkGGkD87qw=", 46 | "owner": "NixOS", 47 | "repo": "nixpkgs", 48 | "rev": "9a5db3142ce450045840cc8d832b13b8a2018e0c", 49 | "type": "github" 50 | }, 51 | "original": { 52 | "owner": "NixOS", 53 | "ref": "nixpkgs-unstable", 54 | "repo": "nixpkgs", 55 | "type": "github" 56 | } 57 | }, 58 | "root": { 59 | "inputs": { 60 | "flake-utils": "flake-utils", 61 | "naersk": "naersk", 62 | "nixpkgs": "nixpkgs" 63 | } 64 | }, 65 | "systems": { 66 | "locked": { 67 | "lastModified": 1681028828, 68 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 69 | "owner": "nix-systems", 70 | "repo": "default", 71 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 72 | "type": "github" 73 | }, 74 | "original": { 75 | "owner": "nix-systems", 76 | "repo": "default", 77 | "type": "github" 78 | } 79 | } 80 | }, 81 | "root": "root", 82 | "version": 7 83 | } 84 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A simple language server protocol for snippets."; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 6 | naersk = { 7 | url = "github:nix-community/naersk/master"; 8 | inputs.nixpkgs.follows = "nixpkgs"; 9 | }; 10 | flake-utils.url = "github:numtide/flake-utils"; 11 | }; 12 | 13 | outputs = { self, nixpkgs, flake-utils, naersk }: 14 | flake-utils.lib.eachDefaultSystem (system: 15 | let 16 | pkgs = import nixpkgs { inherit system; }; 17 | naersk-lib = pkgs.callPackage naersk { }; 18 | in { 19 | defaultPackage = naersk-lib.buildPackage ./.; 20 | devShell = with pkgs; 21 | mkShell { 22 | buildInputs = 23 | [ cargo rustc rustfmt pre-commit rustPackages.clippy ]; 24 | RUST_SRC_PATH = rustPlatform.rustLibSrc; 25 | }; 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use aho_corasick::AhoCorasick; 2 | use anyhow::Result; 3 | use ropey::Rope; 4 | use serde::Deserialize; 5 | use std::borrow::Cow; 6 | use std::collections::{HashMap, HashSet}; 7 | use std::io::prelude::*; 8 | use std::path::PathBuf; 9 | use tokio::sync::{mpsc, oneshot}; 10 | use tower_lsp::lsp_types::*; 11 | 12 | #[cfg(feature = "citation")] 13 | use biblatex::Type; 14 | #[cfg(feature = "citation")] 15 | use regex_cursor::{engines::meta::Regex, Input, RopeyCursor}; 16 | 17 | pub mod server; 18 | pub mod snippets; 19 | 20 | use snippets::{Snippet, UnicodeInputItem}; 21 | 22 | pub struct StartOptions { 23 | pub home_dir: String, 24 | pub external_snippets_config_path: std::path::PathBuf, 25 | pub snippets_path: std::path::PathBuf, 26 | pub unicode_input_path: std::path::PathBuf, 27 | } 28 | 29 | #[derive(Deserialize)] 30 | pub struct BackendSettings { 31 | pub max_completion_items: usize, 32 | pub max_chars_prefix_len: usize, 33 | pub min_chars_prefix_len: usize, 34 | pub snippets_first: bool, 35 | pub snippets_inline_by_word_tail: bool, 36 | // citation 37 | pub citation_prefix_trigger: String, 38 | pub citation_bibfile_extract_regexp: String, 39 | // feature flags 40 | pub feature_words: bool, 41 | pub feature_snippets: bool, 42 | pub feature_unicode_input: bool, 43 | pub feature_paths: bool, 44 | pub feature_citations: bool, 45 | } 46 | 47 | #[derive(Deserialize)] 48 | pub struct PartialBackendSettings { 49 | pub max_completion_items: Option, 50 | pub max_chars_prefix_len: Option, 51 | pub min_chars_prefix_len: Option, 52 | pub max_path_chars: Option, 53 | pub snippets_first: Option, 54 | pub snippets_inline_by_word_tail: Option, 55 | // citation 56 | pub citation_prefix_trigger: Option, 57 | pub citation_bibfile_extract_regexp: Option, 58 | // feature flags 59 | pub feature_words: Option, 60 | pub feature_snippets: Option, 61 | pub feature_unicode_input: Option, 62 | pub feature_paths: Option, 63 | pub feature_citations: Option, 64 | 65 | #[serde(flatten)] 66 | pub extra: Option, 67 | } 68 | 69 | impl Default for BackendSettings { 70 | fn default() -> Self { 71 | BackendSettings { 72 | min_chars_prefix_len: 2, 73 | max_completion_items: 100, 74 | max_chars_prefix_len: 64, 75 | snippets_first: false, 76 | snippets_inline_by_word_tail: false, 77 | citation_prefix_trigger: "@".to_string(), 78 | citation_bibfile_extract_regexp: r#"bibliography:\s*['"\[]*([~\w\./\\-]*)['"\]]*"# 79 | .to_string(), 80 | feature_words: true, 81 | feature_snippets: true, 82 | feature_unicode_input: false, 83 | feature_paths: false, 84 | feature_citations: false, 85 | } 86 | } 87 | } 88 | 89 | impl BackendSettings { 90 | pub fn apply_partial_settings(&self, settings: PartialBackendSettings) -> Self { 91 | Self { 92 | max_completion_items: settings 93 | .max_completion_items 94 | .unwrap_or(self.max_completion_items), 95 | max_chars_prefix_len: settings.max_path_chars.unwrap_or(self.max_chars_prefix_len), 96 | min_chars_prefix_len: settings 97 | .min_chars_prefix_len 98 | .unwrap_or(self.min_chars_prefix_len), 99 | snippets_first: settings.snippets_first.unwrap_or(self.snippets_first), 100 | snippets_inline_by_word_tail: settings 101 | .snippets_inline_by_word_tail 102 | .unwrap_or(self.snippets_inline_by_word_tail), 103 | citation_prefix_trigger: settings 104 | .citation_prefix_trigger 105 | .clone() 106 | .unwrap_or_else(|| self.citation_prefix_trigger.to_owned()), 107 | citation_bibfile_extract_regexp: settings 108 | .citation_prefix_trigger 109 | .clone() 110 | .unwrap_or_else(|| self.citation_bibfile_extract_regexp.to_owned()), 111 | feature_words: settings.feature_words.unwrap_or(self.feature_words), 112 | feature_snippets: settings.feature_snippets.unwrap_or(self.feature_snippets), 113 | feature_unicode_input: settings 114 | .feature_unicode_input 115 | .unwrap_or(self.feature_unicode_input), 116 | feature_paths: settings.feature_paths.unwrap_or(self.feature_paths), 117 | feature_citations: settings.feature_citations.unwrap_or(self.feature_citations), 118 | } 119 | } 120 | } 121 | 122 | #[inline] 123 | pub fn char_is_word(ch: char) -> bool { 124 | ch.is_alphanumeric() || ch == '_' || ch == '-' 125 | } 126 | 127 | #[inline] 128 | pub fn char_is_char_prefix(ch: char) -> bool { 129 | ch != ' ' && ch != '\n' && ch != '\t' 130 | } 131 | 132 | #[inline] 133 | pub fn starts_with(source: &str, s: &str) -> bool { 134 | if s.len() > source.len() { 135 | return false; 136 | } 137 | let Some(part) = source.get(..s.len()) else { 138 | return false; 139 | }; 140 | caseless::default_caseless_match_str(part, s) 141 | } 142 | 143 | enum PathLogic { 144 | Full, 145 | Tilde, 146 | RelativeCurrent, 147 | RelativeParent, 148 | } 149 | 150 | struct PathState<'a> { 151 | logic: PathLogic, 152 | home_dir: &'a str, 153 | current_dir: PathBuf, 154 | parent_dir: Option, 155 | } 156 | 157 | impl From<&str> for PathLogic { 158 | fn from(s: &str) -> Self { 159 | if s.starts_with("~/") { 160 | PathLogic::Tilde 161 | } else if s.starts_with("./") { 162 | PathLogic::RelativeCurrent 163 | } else if s.starts_with("../") { 164 | PathLogic::RelativeParent 165 | } else { 166 | PathLogic::Full 167 | } 168 | } 169 | } 170 | 171 | impl<'a> PathState<'a> { 172 | fn new(s: &'a str, home_dir: &'a str, document_path: &'a str) -> Self { 173 | let logic = PathLogic::from(s); 174 | let current_dir = PathBuf::from(document_path); 175 | let current_dir = current_dir 176 | .parent() 177 | .map(PathBuf::from) 178 | .unwrap_or(current_dir); 179 | Self { 180 | home_dir, 181 | parent_dir: if matches!(logic, PathLogic::RelativeParent) { 182 | current_dir.parent().map(PathBuf::from) 183 | } else { 184 | None 185 | }, 186 | logic, 187 | current_dir, 188 | } 189 | } 190 | 191 | fn expand(&'a self, s: &'a str) -> Cow<'a, str> { 192 | match self.logic { 193 | PathLogic::Full => Cow::Borrowed(s), 194 | PathLogic::Tilde => Cow::Owned(s.replacen('~', self.home_dir, 1)), 195 | PathLogic::RelativeCurrent => { 196 | if let Some(dir) = self.current_dir.to_str() { 197 | tracing::warn!("Can't represent current_dir {:?} as str", self.current_dir); 198 | Cow::Owned(s.replacen(".", dir, 1)) 199 | } else { 200 | Cow::Borrowed(s) 201 | } 202 | } 203 | PathLogic::RelativeParent => { 204 | if let Some(dir) = self.parent_dir.as_ref().and_then(|p| p.to_str()) { 205 | tracing::warn!("Can't represent current_dir {:?} as str", self.current_dir); 206 | Cow::Owned(s.replacen("..", dir, 1)) 207 | } else { 208 | Cow::Borrowed(s) 209 | } 210 | } 211 | } 212 | } 213 | fn fold(&'a self, s: &'a str) -> Cow<'a, str> { 214 | match self.logic { 215 | PathLogic::Full => Cow::Borrowed(s), 216 | PathLogic::Tilde => Cow::Owned(s.replacen(self.home_dir, "~", 1)), 217 | PathLogic::RelativeCurrent => { 218 | if let Some(dir) = self.current_dir.to_str() { 219 | tracing::warn!("Can't represent current_dir {:?} as str", self.current_dir); 220 | Cow::Owned(s.replacen(dir, ".", 1)) 221 | } else { 222 | Cow::Borrowed(s) 223 | } 224 | } 225 | 226 | PathLogic::RelativeParent => { 227 | if let Some(dir) = self.parent_dir.as_ref().and_then(|p| p.to_str()) { 228 | tracing::warn!("Can't represent current_dir {:?} as str", self.current_dir); 229 | Cow::Owned(s.replacen(dir, "..", 1)) 230 | } else { 231 | Cow::Borrowed(s) 232 | } 233 | } 234 | } 235 | } 236 | } 237 | 238 | impl PathLogic {} 239 | 240 | pub struct RopeReader<'a> { 241 | tail: Vec, 242 | chunks: ropey::iter::Chunks<'a>, 243 | } 244 | 245 | impl<'a> RopeReader<'a> { 246 | pub fn new(rope: &'a ropey::Rope) -> Self { 247 | RopeReader { 248 | tail: Vec::new(), 249 | chunks: rope.chunks(), 250 | } 251 | } 252 | } 253 | 254 | impl std::io::Read for RopeReader<'_> { 255 | fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result { 256 | match self.chunks.next() { 257 | Some(chunk) => { 258 | let tail_len = self.tail.len(); 259 | 260 | // write previous tail 261 | if tail_len > 0 { 262 | let tail = self.tail.drain(..); 263 | Write::write_all(&mut buf, tail.as_slice())?; 264 | } 265 | 266 | // find last ending word 267 | let data = if let Some((byte_pos, _)) = chunk 268 | .char_indices() 269 | .rev() 270 | .find(|(_, ch)| !char_is_word(*ch)) 271 | { 272 | if byte_pos != 0 { 273 | Write::write_all(&mut self.tail, &chunk.as_bytes()[byte_pos..])?; 274 | &chunk[0..byte_pos] 275 | } else { 276 | chunk 277 | } 278 | } else { 279 | chunk 280 | }; 281 | Write::write_all(&mut buf, data.as_bytes())?; 282 | Ok(tail_len + data.len()) 283 | } 284 | _ => { 285 | let tail_len = self.tail.len(); 286 | 287 | if tail_len == 0 { 288 | return Ok(0); 289 | } 290 | 291 | // write previous tail 292 | let tail = self.tail.drain(..); 293 | Write::write_all(&mut buf, tail.as_slice())?; 294 | Ok(tail_len) 295 | } 296 | } 297 | } 298 | } 299 | 300 | pub fn ac_searcher(prefix: &str) -> Result { 301 | AhoCorasick::builder() 302 | .ascii_case_insensitive(true) 303 | .build([&prefix]) 304 | .map_err(|e| anyhow::anyhow!("error {e}")) 305 | } 306 | 307 | pub fn search( 308 | prefix: &str, 309 | text: &Rope, 310 | ac: &AhoCorasick, 311 | max_completion_items: usize, 312 | result: &mut HashSet, 313 | ) -> Result<()> { 314 | let searcher = ac.try_stream_find_iter(RopeReader::new(text))?; 315 | 316 | for mat in searcher { 317 | let mat = mat?; 318 | 319 | let Ok(start_char_idx) = text.try_byte_to_char(mat.start()) else { 320 | continue; 321 | }; 322 | let Ok(mat_end) = text.try_byte_to_char(mat.end()) else { 323 | continue; 324 | }; 325 | 326 | // check is word start 327 | if mat.start() > 0 { 328 | let Ok(s) = text.try_byte_to_char(mat.start() - 1) else { 329 | continue; 330 | }; 331 | let Some(ch) = text.get_char(s) else { 332 | continue; 333 | }; 334 | if char_is_word(ch) { 335 | continue; 336 | } 337 | } 338 | 339 | // search word end 340 | let word_end = text 341 | .chars() 342 | .skip(mat_end) 343 | .take_while(|ch| char_is_word(*ch)) 344 | .count(); 345 | 346 | let Ok(word_end) = text.try_char_to_byte(mat_end + word_end) else { 347 | continue; 348 | }; 349 | let Ok(end_char_idx) = text.try_byte_to_char(word_end) else { 350 | continue; 351 | }; 352 | 353 | let item = text.slice(start_char_idx..end_char_idx); 354 | if let Some(item) = item.as_str() { 355 | if item != prefix && starts_with(item, prefix) { 356 | result.insert(item.to_string()); 357 | if result.len() >= max_completion_items { 358 | return Ok(()); 359 | } 360 | } 361 | } 362 | } 363 | 364 | Ok(()) 365 | } 366 | 367 | #[derive(Debug)] 368 | pub enum BackendRequest { 369 | NewDoc(DidOpenTextDocumentParams), 370 | ChangeDoc(DidChangeTextDocumentParams), 371 | ChangeConfiguration(DidChangeConfigurationParams), 372 | SaveDoc(DidSaveTextDocumentParams), 373 | CompletionRequest( 374 | ( 375 | oneshot::Sender>, 376 | CompletionParams, 377 | ), 378 | ), 379 | } 380 | 381 | #[derive(Debug)] 382 | pub enum BackendResponse { 383 | CompletionResponse(CompletionResponse), 384 | } 385 | 386 | pub struct Document { 387 | uri: Url, 388 | text: Rope, 389 | language_id: String, 390 | } 391 | 392 | pub struct BackendState { 393 | home_dir: String, 394 | settings: BackendSettings, 395 | docs: HashMap, 396 | snippets: Vec, 397 | unicode_input: Vec, 398 | max_unicode_input_prefix_len: usize, 399 | max_snippet_input_prefix_len: usize, 400 | rx: mpsc::UnboundedReceiver, 401 | 402 | #[cfg(feature = "citation")] 403 | citation_bibliography_re: Option, 404 | } 405 | 406 | impl BackendState { 407 | pub async fn new( 408 | home_dir: String, 409 | snippets: Vec, 410 | unicode_input: Vec, 411 | ) -> (mpsc::UnboundedSender, Self) { 412 | let (request_tx, request_rx) = mpsc::unbounded_channel::(); 413 | 414 | let settings = BackendSettings::default(); 415 | ( 416 | request_tx, 417 | BackendState { 418 | home_dir, 419 | #[cfg(feature = "citation")] 420 | citation_bibliography_re: Regex::new(&settings.citation_bibfile_extract_regexp) 421 | .map_err(|e| { 422 | tracing::error!("Invalid citation bibliography regex: {e}"); 423 | e 424 | }) 425 | .ok(), 426 | 427 | settings, 428 | docs: HashMap::new(), 429 | max_unicode_input_prefix_len: unicode_input 430 | .iter() 431 | .map(|s| s.prefix.len()) 432 | .max() 433 | .unwrap_or_default(), 434 | max_snippet_input_prefix_len: snippets 435 | .iter() 436 | .map(|s| s.prefix.len()) 437 | .max() 438 | .unwrap_or_default(), 439 | snippets, 440 | unicode_input, 441 | rx: request_rx, 442 | }, 443 | ) 444 | } 445 | 446 | fn save_doc(&mut self, params: DidSaveTextDocumentParams) -> Result<()> { 447 | let Some(doc) = self.docs.get_mut(¶ms.text_document.uri) else { 448 | anyhow::bail!("Document {} not found", params.text_document.uri) 449 | }; 450 | doc.text = if let Some(text) = ¶ms.text { 451 | Rope::from_str(text) 452 | } else { 453 | // Sync read content from file 454 | let file = std::fs::File::open(params.text_document.uri.path())?; 455 | Rope::from_reader(file)? 456 | }; 457 | Ok(()) 458 | } 459 | 460 | fn change_doc(&mut self, params: DidChangeTextDocumentParams) -> Result<()> { 461 | let Some(doc) = self.docs.get_mut(¶ms.text_document.uri) else { 462 | tracing::error!("Doc {} not found", params.text_document.uri); 463 | return Ok(()); 464 | }; 465 | for change in params.clone().content_changes { 466 | let Some(range) = change.range else { continue }; 467 | let start_idx = doc 468 | .text 469 | .try_line_to_char(range.start.line as usize) 470 | .map(|idx| idx + range.start.character as usize); 471 | let end_idx = doc 472 | .text 473 | .try_line_to_char(range.end.line as usize) 474 | .map(|idx| idx + range.end.character as usize) 475 | .and_then(|c| { 476 | if c > doc.text.len_chars() { 477 | Err(ropey::Error::CharIndexOutOfBounds(c, doc.text.len_chars())) 478 | } else { 479 | Ok(c) 480 | } 481 | }); 482 | 483 | match (start_idx, end_idx) { 484 | (Ok(start_idx), Err(_)) => { 485 | doc.text.try_remove(start_idx..)?; 486 | doc.text.try_insert(start_idx, &change.text)?; 487 | } 488 | (Ok(start_idx), Ok(end_idx)) => { 489 | doc.text.try_remove(start_idx..end_idx)?; 490 | doc.text.try_insert(start_idx, &change.text)?; 491 | } 492 | (Err(_), _) => { 493 | *doc = Document { 494 | uri: doc.uri.clone(), 495 | text: Rope::from(change.text), 496 | language_id: doc.language_id.clone(), 497 | } 498 | } 499 | } 500 | } 501 | Ok(()) 502 | } 503 | 504 | fn change_configuration(&mut self, params: DidChangeConfigurationParams) -> Result<()> { 505 | self.settings = self 506 | .settings 507 | .apply_partial_settings(serde_json::from_value(params.settings)?); 508 | 509 | #[cfg(feature = "citation")] 510 | { 511 | self.citation_bibliography_re = 512 | Some(Regex::new(&self.settings.citation_bibfile_extract_regexp)?); 513 | }; 514 | 515 | Ok(()) 516 | } 517 | 518 | fn get_prefix( 519 | &self, 520 | max_chars: usize, 521 | params: &CompletionParams, 522 | ) -> Result<(Option<&str>, Option<&str>, &Document)> { 523 | let Some(doc) = self 524 | .docs 525 | .get(¶ms.text_document_position.text_document.uri) 526 | else { 527 | anyhow::bail!( 528 | "Document {} not found", 529 | params.text_document_position.text_document.uri 530 | ) 531 | }; 532 | 533 | // word prefix 534 | let cursor = doc 535 | .text 536 | .try_line_to_char(params.text_document_position.position.line as usize)? 537 | + params.text_document_position.position.character as usize; 538 | let mut iter = doc 539 | .text 540 | .get_chars_at(cursor) 541 | .ok_or_else(|| anyhow::anyhow!("bounds error"))?; 542 | iter.reverse(); 543 | let offset = iter.take_while(|ch| char_is_word(*ch)).count(); 544 | let start_offset_word = cursor.saturating_sub(offset); 545 | 546 | let len_chars = doc.text.len_chars(); 547 | 548 | if start_offset_word > len_chars || cursor > len_chars { 549 | anyhow::bail!("bounds error") 550 | } 551 | 552 | let mut iter = doc 553 | .text 554 | .get_chars_at(cursor) 555 | .ok_or_else(|| anyhow::anyhow!("bounds error"))?; 556 | iter.reverse(); 557 | let offset = iter 558 | .enumerate() 559 | .take_while(|(i, ch)| *i < max_chars && char_is_char_prefix(*ch)) 560 | .count(); 561 | let start_offset_chars = cursor.saturating_sub(offset); 562 | 563 | if start_offset_chars > len_chars || cursor > len_chars { 564 | anyhow::bail!("bounds error") 565 | } 566 | 567 | let prefix = doc.text.slice(start_offset_word..cursor).as_str(); 568 | let chars_prefix = doc.text.slice(start_offset_chars..cursor).as_str(); 569 | Ok((prefix, chars_prefix, doc)) 570 | } 571 | 572 | fn completion(&self, prefix: &str, current_doc: &Document) -> Result> { 573 | // prepare search pattern 574 | let ac = ac_searcher(prefix)?; 575 | let mut result = HashSet::with_capacity(self.settings.max_completion_items); 576 | 577 | // search in current doc at first 578 | search( 579 | prefix, 580 | ¤t_doc.text, 581 | &ac, 582 | self.settings.max_completion_items, 583 | &mut result, 584 | )?; 585 | if result.len() >= self.settings.max_completion_items { 586 | return Ok(result); 587 | } 588 | 589 | for doc in self.docs.values().filter(|doc| doc.uri != current_doc.uri) { 590 | search( 591 | prefix, 592 | &doc.text, 593 | &ac, 594 | self.settings.max_completion_items, 595 | &mut result, 596 | )?; 597 | if result.len() >= self.settings.max_completion_items { 598 | return Ok(result); 599 | } 600 | } 601 | 602 | Ok(result) 603 | } 604 | 605 | fn words(&self, prefix: &str, doc: &Document) -> impl Iterator { 606 | match self.completion(prefix, doc) { 607 | Ok(words) => words.into_iter(), 608 | Err(e) => { 609 | tracing::error!("On complete by words: {e}"); 610 | HashSet::new().into_iter() 611 | } 612 | } 613 | .map(|word| CompletionItem { 614 | label: word, 615 | kind: Some(CompletionItemKind::TEXT), 616 | ..Default::default() 617 | }) 618 | } 619 | 620 | fn snippets<'a>( 621 | &'a self, 622 | prefix: &'a str, 623 | filter_text_prefix: &'a str, 624 | exact_match: bool, 625 | doc: &'a Document, 626 | params: &'a CompletionParams, 627 | ) -> impl Iterator + 'a { 628 | self.snippets 629 | .iter() 630 | .filter(move |s| { 631 | let filter_by_scope = if let Some(scope) = &s.scope { 632 | scope.is_empty() | scope.contains(&doc.language_id) 633 | } else { 634 | true 635 | }; 636 | if !filter_by_scope { 637 | return false; 638 | } 639 | if exact_match { 640 | caseless::default_caseless_match_str(s.prefix.as_str(), prefix) 641 | } else { 642 | starts_with(s.prefix.as_str(), prefix) 643 | } 644 | }) 645 | .map(move |s| { 646 | let line = params.text_document_position.position.line; 647 | let start = params.text_document_position.position.character - prefix.len() as u32; 648 | let replace_end = params.text_document_position.position.character; 649 | let range = Range { 650 | start: Position { 651 | line, 652 | character: start, 653 | }, 654 | end: Position { 655 | line, 656 | character: replace_end, 657 | }, 658 | }; 659 | CompletionItem { 660 | label: s.prefix.to_owned(), 661 | sort_text: Some(s.prefix.to_string()), 662 | filter_text: Some(if filter_text_prefix.is_empty() { 663 | s.prefix.to_string() 664 | } else { 665 | filter_text_prefix.to_string() 666 | }), 667 | kind: Some(CompletionItemKind::SNIPPET), 668 | detail: Some(s.body.to_string()), 669 | documentation: Some(if let Some(description) = &s.description { 670 | Documentation::MarkupContent(MarkupContent { 671 | kind: MarkupKind::Markdown, 672 | value: format!( 673 | "{description}\n```{}\n{}\n```", 674 | doc.language_id, s.body 675 | ), 676 | }) 677 | } else { 678 | Documentation::String(s.body.to_string()) 679 | }), 680 | text_edit: Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit { 681 | replace: range, 682 | insert: range, 683 | new_text: s.body.to_string(), 684 | })), 685 | insert_text_format: Some(InsertTextFormat::SNIPPET), 686 | ..Default::default() 687 | } 688 | }) 689 | .take(self.settings.max_completion_items) 690 | } 691 | 692 | fn snippets_by_word_tail<'a>( 693 | &'a self, 694 | chars_prefix: &'a str, 695 | doc: &'a Document, 696 | params: &'a CompletionParams, 697 | ) -> impl Iterator + 'a { 698 | let mut chars_snippets: Vec = Vec::new(); 699 | 700 | for index in 0..=chars_prefix.len() { 701 | let Some(part) = chars_prefix.get(index..) else { 702 | continue; 703 | }; 704 | if part.is_empty() { 705 | break; 706 | } 707 | // try to find tail for prefix to start completion 708 | if part.len() > self.max_snippet_input_prefix_len { 709 | continue; 710 | } 711 | if part.len() < self.settings.min_chars_prefix_len { 712 | break; 713 | } 714 | chars_snippets.extend(self.snippets(part, chars_prefix, false, doc, params)); 715 | if chars_snippets.len() >= self.settings.max_completion_items { 716 | break; 717 | } 718 | } 719 | 720 | chars_snippets.into_iter() 721 | } 722 | 723 | fn unicode_input( 724 | &self, 725 | word_prefix: &str, 726 | chars_prefix: &str, 727 | params: &CompletionParams, 728 | ) -> impl Iterator { 729 | let mut chars_snippets: Vec = Vec::new(); 730 | 731 | for index in 0..=chars_prefix.len() { 732 | let Some(part) = chars_prefix.get(index..) else { 733 | continue; 734 | }; 735 | if part.is_empty() { 736 | break; 737 | } 738 | // try to find tail for prefix to start completion 739 | if part.len() > self.max_unicode_input_prefix_len { 740 | continue; 741 | } 742 | if part.len() < self.settings.min_chars_prefix_len { 743 | break; 744 | } 745 | 746 | let items = self 747 | .unicode_input 748 | .iter() 749 | .filter_map(|s| { 750 | if !starts_with(&s.prefix, part) { 751 | return None; 752 | } 753 | tracing::info!( 754 | "Chars prefix: {} index: {}, part: {} {s:?}", 755 | chars_prefix, 756 | index, 757 | part 758 | ); 759 | let line = params.text_document_position.position.line; 760 | let start = 761 | params.text_document_position.position.character - part.len() as u32; 762 | let replace_end = params.text_document_position.position.character; 763 | let range = Range { 764 | start: Position { 765 | line, 766 | character: start, 767 | }, 768 | end: Position { 769 | line, 770 | character: replace_end, 771 | }, 772 | }; 773 | Some(CompletionItem { 774 | label: s.body.to_string(), 775 | filter_text: format!("{word_prefix}{}", s.prefix).into(), 776 | kind: Some(CompletionItemKind::TEXT), 777 | documentation: Documentation::String(s.prefix.to_string()).into(), 778 | text_edit: Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit { 779 | replace: range, 780 | insert: range, 781 | new_text: s.body.to_string(), 782 | })), 783 | ..Default::default() 784 | }) 785 | }) 786 | .take(self.settings.max_completion_items - chars_snippets.len()); 787 | chars_snippets.extend(items); 788 | if chars_snippets.len() >= self.settings.max_completion_items { 789 | break; 790 | } 791 | } 792 | 793 | chars_snippets 794 | .into_iter() 795 | .enumerate() 796 | .map(move |(index, item)| CompletionItem { 797 | sort_text: format!("{:0width$}", index, width = 2).into(), 798 | ..item 799 | }) 800 | } 801 | 802 | fn paths( 803 | &self, 804 | word_prefix: &str, 805 | chars_prefix: &str, 806 | params: &CompletionParams, 807 | current_document: &Document, 808 | ) -> impl Iterator { 809 | // check is it path 810 | if !chars_prefix.contains(std::path::MAIN_SEPARATOR) { 811 | return Vec::new().into_iter(); 812 | } 813 | 814 | let Some(first_char) = chars_prefix.chars().nth(0) else { 815 | return Vec::new().into_iter(); 816 | }; 817 | let Some(last_char) = chars_prefix.chars().last() else { 818 | return Vec::new().into_iter(); 819 | }; 820 | 821 | // sanitize surround chars 822 | let chars_prefix = if first_char.is_alphabetic() 823 | || first_char == std::path::MAIN_SEPARATOR 824 | || first_char == '~' 825 | || first_char == '.' 826 | { 827 | chars_prefix 828 | } else { 829 | &chars_prefix[1..] 830 | }; 831 | 832 | let chars_prefix_len = chars_prefix.len() as u32; 833 | let document_path = current_document.uri.path(); 834 | let path_state = PathState::new(chars_prefix, &self.home_dir, document_path); 835 | 836 | let chars_prefix = path_state.expand(chars_prefix); 837 | 838 | // build path 839 | let path = std::path::Path::new(chars_prefix.as_ref()); 840 | 841 | // normalize filename 842 | let (filename, parent_dir) = if last_char == std::path::MAIN_SEPARATOR { 843 | (String::new(), path) 844 | } else { 845 | let Some(filename) = path.file_name().and_then(|f| f.to_str()) else { 846 | return Vec::new().into_iter(); 847 | }; 848 | let Some(parent_dir) = path.parent() else { 849 | return Vec::new().into_iter(); 850 | }; 851 | (filename.to_lowercase(), parent_dir) 852 | }; 853 | 854 | let items = match parent_dir.read_dir() { 855 | Ok(items) => items, 856 | Err(e) => { 857 | tracing::warn!("On read dir {parent_dir:?}: {e}"); 858 | return Vec::new().into_iter(); 859 | } 860 | }; 861 | 862 | items 863 | .into_iter() 864 | .filter_map(|item| item.ok()) 865 | .filter_map(|item| { 866 | // convert to regular &str 867 | let fname = item.file_name(); 868 | let item_filename = fname.to_str()?; 869 | let item_filename = item_filename.to_lowercase(); 870 | if !filename.is_empty() && !item_filename.starts_with(&filename) { 871 | return None; 872 | } 873 | 874 | // use full path 875 | let path = item.path(); 876 | let full_path = path.to_str()?; 877 | 878 | // fold back 879 | let full_path = path_state.fold(full_path); 880 | 881 | let line = params.text_document_position.position.line; 882 | let start = params.text_document_position.position.character - chars_prefix_len; 883 | let replace_end = params.text_document_position.position.character; 884 | let range = Range { 885 | start: Position { 886 | line, 887 | character: start, 888 | }, 889 | end: Position { 890 | line, 891 | character: replace_end, 892 | }, 893 | }; 894 | Some(CompletionItem { 895 | label: full_path.to_string(), 896 | sort_text: Some(full_path.to_string()), 897 | filter_text: Some(format!("{word_prefix}{full_path}")), 898 | kind: Some(if path.is_dir() { 899 | CompletionItemKind::FOLDER 900 | } else { 901 | CompletionItemKind::FILE 902 | }), 903 | text_edit: Some(CompletionTextEdit::InsertAndReplace(InsertReplaceEdit { 904 | replace: range, 905 | insert: range, 906 | new_text: full_path.to_string(), 907 | })), 908 | ..Default::default() 909 | }) 910 | }) 911 | .take(self.settings.max_completion_items) 912 | .collect::>() 913 | .into_iter() 914 | } 915 | 916 | #[cfg(feature = "citation")] 917 | fn citations<'a>( 918 | &'a self, 919 | word_prefix: &str, 920 | chars_prefix: &str, 921 | doc: &'a Document, 922 | params: &CompletionParams, 923 | ) -> impl Iterator { 924 | let mut items: Vec = Vec::new(); 925 | 926 | tracing::debug!("Citation word_prefix: {word_prefix}, chars_prefix: {chars_prefix}"); 927 | 928 | let Some(re) = &self.citation_bibliography_re else { 929 | tracing::warn!("Citation bibliography regex empty or invalid"); 930 | return Vec::new().into_iter(); 931 | }; 932 | 933 | let Some(slice) = doc.text.get_slice(..) else { 934 | tracing::warn!("Failed to get rope slice"); 935 | return Vec::new().into_iter(); 936 | }; 937 | 938 | let cursor = RopeyCursor::new(slice); 939 | 940 | for span in re 941 | .captures_iter(Input::new(cursor)) 942 | .filter_map(|c| c.get_group(1)) 943 | { 944 | if items.len() >= self.settings.max_completion_items { 945 | break; 946 | } 947 | 948 | let Some(path) = slice.get_byte_slice(span.start..span.end) else { 949 | tracing::error!("Failed to get path by span"); 950 | continue; 951 | }; 952 | 953 | // TODO any ways get &str from whole RopeSlice 954 | let path = path.to_string(); 955 | 956 | let path = if path.contains("~") { 957 | path.replacen('~', &self.home_dir, 1) 958 | } else { 959 | path 960 | }; 961 | 962 | // TODO read and parse only if file changed 963 | tracing::debug!("Citation try to read: {path}"); 964 | let bib = match std::fs::read_to_string(&path) { 965 | Err(e) => { 966 | tracing::error!("Failed to read file {path}: {e}"); 967 | continue; 968 | } 969 | Ok(r) => r, 970 | }; 971 | 972 | let bib = match biblatex::Bibliography::parse(&bib) { 973 | Err(e) => { 974 | tracing::error!("Failed to parse bib file {path}: {e}"); 975 | continue; 976 | } 977 | Ok(r) => r, 978 | }; 979 | 980 | items.extend( 981 | bib.iter() 982 | .filter_map(|b| { 983 | let matched = starts_with(&b.key, word_prefix); 984 | tracing::debug!( 985 | "Citation from file: {path} prefix: {word_prefix} key: {} - match: {}", 986 | b.key, 987 | matched, 988 | ); 989 | if !matched { 990 | return None; 991 | } 992 | let line = params.text_document_position.position.line; 993 | let start = params.text_document_position.position.character 994 | - word_prefix.len() as u32; 995 | let replace_end = params.text_document_position.position.character; 996 | let range = Range { 997 | start: Position { 998 | line, 999 | character: start, 1000 | }, 1001 | end: Position { 1002 | line, 1003 | character: replace_end, 1004 | }, 1005 | }; 1006 | let documentation = { 1007 | let entry_type = b.entry_type.to_string(); 1008 | let title = b 1009 | .title() 1010 | .ok()? 1011 | .iter() 1012 | .map(|chunk| chunk.v.get()) 1013 | .collect::>() 1014 | .join(""); 1015 | let authors = b 1016 | .author() 1017 | .ok()? 1018 | .into_iter() 1019 | .map(|person| person.to_string()) 1020 | .collect::>() 1021 | .join(","); 1022 | 1023 | let date = match b.date() { 1024 | Ok(d) => match d { 1025 | biblatex::PermissiveType::Typed(date) => date.to_chunks(), 1026 | biblatex::PermissiveType::Chunks(v) => v, 1027 | } 1028 | .iter() 1029 | .map(|chunk| chunk.v.get()) 1030 | .collect::>() 1031 | .join(""), 1032 | Err(e) => { 1033 | tracing::error!("On parse date field on entry {b:?}: {e}"); 1034 | String::new() 1035 | } 1036 | }; 1037 | 1038 | Some(format!( 1039 | "# {title:?}\n*{authors}*\n\n{entry_type}{}", 1040 | if date.is_empty() { 1041 | date 1042 | } else { 1043 | format!(", {date}") 1044 | } 1045 | )) 1046 | }; 1047 | Some(CompletionItem { 1048 | label: format!("@{}", b.key), 1049 | sort_text: Some(word_prefix.to_string()), 1050 | filter_text: Some(word_prefix.to_string()), 1051 | kind: Some(CompletionItemKind::REFERENCE), 1052 | text_edit: Some(CompletionTextEdit::InsertAndReplace( 1053 | InsertReplaceEdit { 1054 | replace: range, 1055 | insert: range, 1056 | new_text: b.key.to_string(), 1057 | }, 1058 | )), 1059 | documentation: Some(Documentation::MarkupContent(MarkupContent { 1060 | kind: MarkupKind::Markdown, 1061 | value: documentation.unwrap_or_else(|| { 1062 | format!( 1063 | "'''{}'''\n\n*fallback to biblatex format*", 1064 | b.to_biblatex_string() 1065 | ) 1066 | }), 1067 | })), 1068 | ..Default::default() 1069 | }) 1070 | }) 1071 | .take(self.settings.max_completion_items - items.len()), 1072 | ); 1073 | } 1074 | 1075 | items.into_iter() 1076 | } 1077 | 1078 | pub async fn start(mut self) { 1079 | loop { 1080 | let Some(cmd) = self.rx.recv().await else { 1081 | continue; 1082 | }; 1083 | 1084 | match cmd { 1085 | BackendRequest::NewDoc(params) => { 1086 | self.docs.insert( 1087 | params.text_document.uri.clone(), 1088 | Document { 1089 | uri: params.text_document.uri, 1090 | text: Rope::from_str(¶ms.text_document.text), 1091 | language_id: params.text_document.language_id, 1092 | }, 1093 | ); 1094 | } 1095 | BackendRequest::SaveDoc(params) => { 1096 | if let Err(e) = self.save_doc(params) { 1097 | tracing::error!("Error on save doc: {e}"); 1098 | } 1099 | } 1100 | BackendRequest::ChangeDoc(params) => { 1101 | if let Err(e) = self.change_doc(params) { 1102 | tracing::error!("Error on change doc: {e}"); 1103 | } 1104 | } 1105 | BackendRequest::ChangeConfiguration(params) => { 1106 | if let Err(e) = self.change_configuration(params) { 1107 | tracing::error!("Error on change configuration: {e}"); 1108 | } 1109 | } 1110 | BackendRequest::CompletionRequest((tx, params)) => { 1111 | let now = std::time::Instant::now(); 1112 | 1113 | let Ok((prefix, chars_prefix, doc)) = 1114 | self.get_prefix(self.settings.max_chars_prefix_len, ¶ms) 1115 | else { 1116 | if tx 1117 | .send(Err(anyhow::anyhow!("Failed to get prefix"))) 1118 | .is_err() 1119 | { 1120 | tracing::error!("Error on send completion response"); 1121 | } 1122 | continue; 1123 | }; 1124 | 1125 | let Some(chars_prefix) = chars_prefix else { 1126 | if tx 1127 | .send(Err(anyhow::anyhow!("Failed to get char prefix"))) 1128 | .is_err() 1129 | { 1130 | tracing::error!("Error on send completion response"); 1131 | } 1132 | continue; 1133 | }; 1134 | 1135 | if chars_prefix.is_empty() || chars_prefix.starts_with(' ') { 1136 | if tx 1137 | .send(Ok(BackendResponse::CompletionResponse( 1138 | CompletionResponse::Array(Vec::new()), 1139 | ))) 1140 | .is_err() 1141 | { 1142 | tracing::error!("Error on send completion response"); 1143 | } 1144 | continue; 1145 | }; 1146 | 1147 | let base_completion = || { 1148 | Vec::new() 1149 | .into_iter() 1150 | // snippets first 1151 | .chain( 1152 | match ( 1153 | self.settings.feature_snippets, 1154 | self.settings.snippets_inline_by_word_tail, 1155 | self.settings.snippets_first, 1156 | prefix, 1157 | ) { 1158 | (true, true, true, _) if !chars_prefix.is_empty() => { 1159 | Some(self.snippets_by_word_tail(chars_prefix, doc, ¶ms)) 1160 | } 1161 | _ => None, 1162 | } 1163 | .into_iter() 1164 | .flatten(), 1165 | ) 1166 | .chain( 1167 | match ( 1168 | self.settings.feature_snippets, 1169 | self.settings.snippets_inline_by_word_tail, 1170 | self.settings.snippets_first, 1171 | prefix, 1172 | ) { 1173 | (true, false, true, Some(prefix)) if !prefix.is_empty() => { 1174 | Some(self.snippets(prefix, "", true, doc, ¶ms)) 1175 | } 1176 | _ => None, 1177 | } 1178 | .into_iter() 1179 | .flatten(), 1180 | ) 1181 | // words 1182 | .chain( 1183 | if let Some(prefix) = prefix { 1184 | if self.settings.feature_words { 1185 | Some(self.words(prefix, doc)) 1186 | } else { 1187 | None 1188 | } 1189 | } else { 1190 | None 1191 | } 1192 | .into_iter() 1193 | .flatten(), 1194 | ) 1195 | // snippets last 1196 | .chain( 1197 | match ( 1198 | self.settings.feature_snippets, 1199 | self.settings.snippets_inline_by_word_tail, 1200 | self.settings.snippets_first, 1201 | prefix, 1202 | ) { 1203 | (true, true, false, _) if !chars_prefix.is_empty() => { 1204 | Some(self.snippets_by_word_tail(chars_prefix, doc, ¶ms)) 1205 | } 1206 | _ => None, 1207 | } 1208 | .into_iter() 1209 | .flatten(), 1210 | ) 1211 | .chain( 1212 | match ( 1213 | self.settings.feature_snippets, 1214 | self.settings.snippets_inline_by_word_tail, 1215 | self.settings.snippets_first, 1216 | prefix, 1217 | ) { 1218 | (true, false, false, Some(prefix)) if !prefix.is_empty() => { 1219 | Some(self.snippets(prefix, "", false, doc, ¶ms)) 1220 | } 1221 | _ => None, 1222 | } 1223 | .into_iter() 1224 | .flatten(), 1225 | ) 1226 | .chain( 1227 | if self.settings.feature_unicode_input { 1228 | Some(self.unicode_input( 1229 | prefix.unwrap_or_default(), 1230 | chars_prefix, 1231 | ¶ms, 1232 | )) 1233 | } else { 1234 | None 1235 | } 1236 | .into_iter() 1237 | .flatten(), 1238 | ) 1239 | .chain( 1240 | if self.settings.feature_paths { 1241 | Some(self.paths( 1242 | prefix.unwrap_or_default(), 1243 | chars_prefix, 1244 | ¶ms, 1245 | doc, 1246 | )) 1247 | } else { 1248 | None 1249 | } 1250 | .into_iter() 1251 | .flatten(), 1252 | ) 1253 | .collect() 1254 | }; 1255 | 1256 | #[cfg(feature = "citation")] 1257 | let results: Vec = if self.settings.feature_citations 1258 | & chars_prefix.contains(&self.settings.citation_prefix_trigger) 1259 | { 1260 | self.citations(prefix.unwrap_or_default(), chars_prefix, doc, ¶ms) 1261 | .collect() 1262 | } else { 1263 | base_completion() 1264 | }; 1265 | 1266 | #[cfg(not(feature = "citation"))] 1267 | let results: Vec = base_completion(); 1268 | 1269 | tracing::debug!( 1270 | "completion request by prefix: {prefix:?} chars prefix: {chars_prefix:?} took {:.2}ms with {} result items", 1271 | now.elapsed().as_millis(), 1272 | results.len(), 1273 | ); 1274 | 1275 | let response = 1276 | BackendResponse::CompletionResponse(CompletionResponse::Array(results)); 1277 | 1278 | if tx.send(Ok(response)).is_err() { 1279 | tracing::error!("Error on send completion response"); 1280 | } 1281 | } 1282 | }; 1283 | } 1284 | } 1285 | } 1286 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use etcetera::base_strategy::{choose_base_strategy, BaseStrategy}; 2 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 3 | use xshell::{cmd, Shell}; 4 | 5 | use simple_completion_language_server::{ 6 | server, 7 | snippets::config::{load_snippets, load_unicode_input_from_path}, 8 | snippets::external::ExternalSnippets, 9 | StartOptions, 10 | }; 11 | 12 | async fn serve(start_options: &StartOptions) { 13 | let _guard = if let Ok(log_file) = &std::env::var("LOG_FILE") { 14 | let log_file = std::path::Path::new(log_file); 15 | let file_appender = tracing_appender::rolling::never( 16 | log_file 17 | .parent() 18 | .expect("Failed to parse LOG_FILE parent part"), 19 | log_file 20 | .file_name() 21 | .expect("Failed to parse LOG_FILE file_name part"), 22 | ); 23 | let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); 24 | tracing_subscriber::registry() 25 | .with(tracing_subscriber::EnvFilter::new( 26 | std::env::var("RUST_LOG") 27 | .unwrap_or_else(|_| "info,simple-completion-language-server=info".into()), 28 | )) 29 | .with(tracing_subscriber::fmt::layer().with_writer(non_blocking)) 30 | .init(); 31 | Some(_guard) 32 | } else { 33 | None 34 | }; 35 | 36 | let stdin = tokio::io::stdin(); 37 | let stdout = tokio::io::stdout(); 38 | 39 | let snippets = load_snippets(start_options).unwrap_or_else(|e| { 40 | tracing::error!("On read snippets: {e}"); 41 | Vec::new() 42 | }); 43 | 44 | let unicode_input = load_unicode_input_from_path(&start_options.unicode_input_path) 45 | .unwrap_or_else(|e| { 46 | tracing::error!("On read 'unicode input' config: {e}"); 47 | Default::default() 48 | }); 49 | 50 | server::start( 51 | stdin, 52 | stdout, 53 | snippets, 54 | unicode_input, 55 | start_options.home_dir.clone(), 56 | ) 57 | .await; 58 | } 59 | 60 | fn help() { 61 | println!( 62 | "usage: 63 | simple-completion-language-server fetch-external-snippets 64 | Fetch external snippets (git clone or git pull). 65 | 66 | simple-completion-language-server validate-snippets 67 | Read all snippets to ensure correctness. 68 | 69 | simple-completion-language-server validate-unicode-input 70 | Read all unicode-input to ensure correctness. 71 | 72 | simple-completion-language-server 73 | Start language server protocol on stdin+stdout." 74 | ); 75 | } 76 | 77 | fn fetch_external_snippets(start_options: &StartOptions) -> anyhow::Result<()> { 78 | tracing::info!( 79 | "Try read config from: {:?}", 80 | start_options.external_snippets_config_path 81 | ); 82 | 83 | let path = std::path::Path::new(&start_options.external_snippets_config_path); 84 | 85 | if !path.exists() { 86 | return Ok(()); 87 | } 88 | 89 | let Some(base_path) = path.parent() else { 90 | anyhow::bail!("Failed to get base path") 91 | }; 92 | 93 | let base_path = base_path.join("external-snippets"); 94 | 95 | let content = std::fs::read_to_string(path)?; 96 | 97 | let sources = toml::from_str::(&content) 98 | .map(|sc| sc.sources) 99 | .map_err(|e| anyhow::anyhow!(e))?; 100 | 101 | let sh = Shell::new()?; 102 | for source in sources { 103 | let git_repo = &source.git; 104 | let destination_path = base_path.join(source.destination_path()?); 105 | 106 | // TODO don't fetch full history? 107 | if destination_path.exists() { 108 | sh.change_dir(&destination_path); 109 | tracing::info!("Try update: {:?}", destination_path); 110 | cmd!(sh, "git pull --rebase").run()?; 111 | } else { 112 | tracing::info!("Try clone {} to {:?}", git_repo, destination_path); 113 | sh.create_dir(&destination_path)?; 114 | cmd!(sh, "git clone {git_repo} {destination_path}").run()?; 115 | } 116 | } 117 | 118 | Ok(()) 119 | } 120 | 121 | fn validate_snippets(start_options: &StartOptions) -> anyhow::Result<()> { 122 | let snippets = load_snippets(start_options)?; 123 | tracing::info!("Successful. Total: {}", snippets.len()); 124 | Ok(()) 125 | } 126 | 127 | fn validate_unicode_input(start_options: &StartOptions) -> anyhow::Result<()> { 128 | let unicode_input = load_unicode_input_from_path(&start_options.unicode_input_path)?; 129 | tracing::info!("Successful. Total: {}", unicode_input.len()); 130 | Ok(()) 131 | } 132 | 133 | #[tokio::main] 134 | async fn main() { 135 | let args: Vec = std::env::args().collect(); 136 | 137 | let strategy = choose_base_strategy().expect("Unable to find the config directory!"); 138 | let mut config_dir = strategy.config_dir(); 139 | let config_subdirectory_name = 140 | std::env::var("SCLS_CONFIG_SUBDIRECTORY").unwrap_or_else(|_| "helix".to_owned()); 141 | config_dir.push(config_subdirectory_name); 142 | 143 | let start_options = StartOptions { 144 | home_dir: etcetera::home_dir() 145 | .expect("Unable to get home dir!") 146 | .to_str() 147 | .expect("Unable to get home dir as string!") 148 | .to_string(), 149 | snippets_path: std::env::var("SNIPPETS_PATH") 150 | .map(std::path::PathBuf::from) 151 | .unwrap_or_else(|_| { 152 | let mut filepath = config_dir.clone(); 153 | filepath.push("snippets"); 154 | filepath 155 | }), 156 | external_snippets_config_path: std::env::var("EXTERNAL_SNIPPETS_CONFIG") 157 | .map(std::path::PathBuf::from) 158 | .unwrap_or_else(|_| { 159 | let mut filepath = config_dir.clone(); 160 | filepath.push("external-snippets.toml"); 161 | filepath 162 | }), 163 | unicode_input_path: std::env::var("UNICODE_INPUT_PATH") 164 | .map(std::path::PathBuf::from) 165 | .unwrap_or_else(|_| { 166 | let mut filepath = config_dir.clone(); 167 | filepath.push("unicode-input"); 168 | filepath 169 | }), 170 | }; 171 | 172 | match args.len() { 173 | 2.. => { 174 | tracing_subscriber::registry() 175 | .with(tracing_subscriber::EnvFilter::new( 176 | std::env::var("RUST_LOG") 177 | .unwrap_or_else(|_| "info,simple-completion-language-server=info".into()), 178 | )) 179 | .with(tracing_subscriber::fmt::layer()) 180 | .init(); 181 | 182 | let cmd = args[1].parse::().expect("command required"); 183 | 184 | if cmd.contains("-h") || cmd.contains("help") { 185 | help(); 186 | return; 187 | } 188 | 189 | match cmd.as_str() { 190 | "fetch-external-snippets" => fetch_external_snippets(&start_options) 191 | .expect("Failed to fetch external snippets"), 192 | "validate-snippets" => { 193 | validate_snippets(&start_options).expect("Failed to validate snippets") 194 | } 195 | "validate-unicode-input" => validate_unicode_input(&start_options) 196 | .expect("Failed to validate 'unicode input' config"), 197 | _ => help(), 198 | } 199 | } 200 | _ => serve(&start_options).await, 201 | }; 202 | } 203 | -------------------------------------------------------------------------------- /src/server.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | snippets::{Snippet, UnicodeInputItem}, 3 | BackendRequest, BackendResponse, BackendState, 4 | }; 5 | use tokio::io::{AsyncRead, AsyncWrite}; 6 | use tokio::sync::{mpsc, oneshot}; 7 | use tower_lsp::jsonrpc::Result; 8 | use tower_lsp::lsp_types::*; 9 | use tower_lsp::{Client, LanguageServer, LspService, Server}; 10 | 11 | #[derive(Debug)] 12 | pub struct Backend { 13 | client: Client, 14 | tx: mpsc::UnboundedSender, 15 | _task: tokio::task::JoinHandle<()>, 16 | } 17 | 18 | impl Backend { 19 | async fn log_info(&self, message: &str) { 20 | tracing::info!(message); 21 | self.client.log_message(MessageType::INFO, message).await; 22 | } 23 | async fn log_err(&self, message: &str) { 24 | tracing::error!(message); 25 | self.client.log_message(MessageType::ERROR, message).await; 26 | } 27 | async fn send_request(&self, request: BackendRequest) -> anyhow::Result<()> { 28 | if self.tx.send(request).is_err() { 29 | self.log_err("error on send request").await; 30 | anyhow::bail!("Failed to send request"); 31 | } 32 | Ok(()) 33 | } 34 | } 35 | 36 | #[tower_lsp::async_trait] 37 | impl LanguageServer for Backend { 38 | async fn initialize(&self, _: InitializeParams) -> Result { 39 | Ok(InitializeResult { 40 | capabilities: ServerCapabilities { 41 | position_encoding: Some(PositionEncodingKind::UTF32), 42 | text_document_sync: Some(TextDocumentSyncCapability::Kind( 43 | TextDocumentSyncKind::INCREMENTAL, 44 | )), 45 | completion_provider: Some(CompletionOptions { 46 | resolve_provider: Some(false), 47 | trigger_characters: Some(vec![std::path::MAIN_SEPARATOR_STR.to_string()]), 48 | ..CompletionOptions::default() 49 | }), 50 | ..Default::default() 51 | }, 52 | ..Default::default() 53 | }) 54 | } 55 | 56 | async fn initialized(&self, _: InitializedParams) { 57 | self.log_info("server initialized!").await; 58 | } 59 | 60 | async fn shutdown(&self) -> Result<()> { 61 | Ok(()) 62 | } 63 | 64 | async fn did_open(&self, params: DidOpenTextDocumentParams) { 65 | let _ = self.send_request(BackendRequest::NewDoc(params)).await; 66 | } 67 | 68 | async fn did_save(&self, params: DidSaveTextDocumentParams) { 69 | tracing::debug!("Did save: {params:?}"); 70 | let _ = self.send_request(BackendRequest::SaveDoc(params)).await; 71 | } 72 | 73 | async fn did_change(&self, params: DidChangeTextDocumentParams) { 74 | tracing::debug!("Did change: {params:?}"); 75 | let _ = self.send_request(BackendRequest::ChangeDoc(params)).await; 76 | } 77 | 78 | async fn did_change_configuration(&self, params: DidChangeConfigurationParams) { 79 | self.log_info(&format!("Did change configuration: {params:?}")) 80 | .await; 81 | let _ = self 82 | .send_request(BackendRequest::ChangeConfiguration(params)) 83 | .await; 84 | } 85 | 86 | async fn completion(&self, params: CompletionParams) -> Result> { 87 | tracing::debug!("Completion: {params:?}"); 88 | let (tx, rx) = oneshot::channel::>(); 89 | 90 | self.send_request(BackendRequest::CompletionRequest((tx, params))) 91 | .await 92 | .map_err(|_| tower_lsp::jsonrpc::Error::internal_error())?; 93 | 94 | let Ok(result) = rx.await else { 95 | self.log_err("Error on receive completion response").await; 96 | return Err(tower_lsp::jsonrpc::Error::internal_error()); 97 | }; 98 | 99 | match result { 100 | Ok(BackendResponse::CompletionResponse(r)) => Ok(Some(r)), 101 | Err(e) => { 102 | self.log_err(&format!("Completion error: {e}")).await; 103 | return Err(tower_lsp::jsonrpc::Error::internal_error()); 104 | } 105 | } 106 | } 107 | 108 | // mock completionItem/resolve 109 | async fn completion_resolve(&self, params: CompletionItem) -> Result { 110 | Ok(params) 111 | } 112 | } 113 | 114 | pub async fn start( 115 | read: I, 116 | write: O, 117 | snippets: Vec, 118 | unicode_input: Vec, 119 | home_dir: String, 120 | ) where 121 | I: AsyncRead + Unpin, 122 | O: AsyncWrite, 123 | { 124 | let (tx, backend_state) = BackendState::new(home_dir, snippets, unicode_input).await; 125 | 126 | let task = tokio::spawn(backend_state.start()); 127 | 128 | let (service, socket) = LspService::new(|client| Backend { 129 | client, 130 | tx, 131 | _task: task, 132 | }); 133 | Server::new(read, write, socket).serve(service).await; 134 | } 135 | -------------------------------------------------------------------------------- /src/snippets/config.rs: -------------------------------------------------------------------------------- 1 | use crate::snippets::external::ExternalSnippets; 2 | use crate::snippets::vscode::VSSnippetsConfig; 3 | use crate::StartOptions; 4 | use anyhow::Result; 5 | use serde::Deserialize; 6 | use std::collections::{BTreeMap, HashMap}; 7 | use std::fs; 8 | use std::path::Path; 9 | 10 | #[derive(Deserialize)] 11 | pub struct SnippetsConfig { 12 | pub snippets: Vec, 13 | } 14 | 15 | #[derive(Debug, Deserialize)] 16 | pub struct Snippet { 17 | pub scope: Option>, 18 | pub prefix: String, 19 | pub body: String, 20 | pub description: Option, 21 | } 22 | 23 | #[derive(Debug, Deserialize)] 24 | pub struct UnicodeInputItem { 25 | pub prefix: String, 26 | pub body: String, 27 | } 28 | 29 | #[derive(Deserialize)] 30 | pub struct UnicodeInputConfig { 31 | #[serde(flatten)] 32 | pub inner: BTreeMap, 33 | } 34 | 35 | fn walk_dir(dir: &Path, filter: &F, files: &mut Vec) 36 | where 37 | F: Fn(&Path) -> bool, 38 | { 39 | if let Ok(entries) = fs::read_dir(dir) { 40 | for entry in entries.flatten() { 41 | let path = entry.path(); 42 | if path.is_dir() { 43 | walk_dir(&path, filter, files); 44 | } else if filter(&path) { 45 | files.push(path); 46 | } 47 | } 48 | } 49 | } 50 | 51 | pub fn load_snippets(start_options: &StartOptions) -> Result> { 52 | let mut snippets = load_snippets_from_path(&start_options.snippets_path, &None)?; 53 | 54 | tracing::info!( 55 | "Try read config from: {:?}", 56 | start_options.external_snippets_config_path 57 | ); 58 | 59 | let path = std::path::Path::new(&start_options.external_snippets_config_path); 60 | 61 | if path.exists() { 62 | let Some(base_path) = path.parent() else { 63 | anyhow::bail!("Failed to get base path") 64 | }; 65 | 66 | let base_path = base_path.join("external-snippets"); 67 | 68 | let content = std::fs::read_to_string(path)?; 69 | 70 | let sources = toml::from_str::(&content) 71 | .map(|sc| sc.sources) 72 | .map_err(|e| anyhow::anyhow!(e))?; 73 | 74 | for source in sources { 75 | let source_name = source.name.as_ref().unwrap_or(&source.git); 76 | 77 | let source_dst = source.destination_path()?; 78 | 79 | if source.paths.is_empty() { 80 | let mut files = Vec::new(); 81 | walk_dir( 82 | &base_path.join(&source_dst), 83 | &|filepath| { 84 | let Some(ext) = filepath.extension() else { 85 | return false; 86 | }; 87 | ext == "json" || ext == "toml" 88 | }, 89 | &mut files, 90 | ); 91 | snippets.extend( 92 | files 93 | .into_iter() 94 | .filter_map(|filepath| match load_snippets_from_path(&filepath, &None) { 95 | Ok(s) => s 96 | .into_iter() 97 | .map(|mut s| { 98 | s.description = Some(format!( 99 | "{source_name}\n\n{}", 100 | s.description.unwrap_or_default(), 101 | )); 102 | s 103 | }) 104 | .into(), 105 | Err(e) => { 106 | tracing::warn!( 107 | "Skip. failed to load snippets from: {filepath:?} - {e}" 108 | ); 109 | None 110 | } 111 | }) 112 | .flatten() 113 | .collect::>(), 114 | ); 115 | } else { 116 | for item in &source.paths { 117 | snippets.extend( 118 | load_snippets_from_path( 119 | &base_path.join(&source_dst).join(&item.path), 120 | &item.scope, 121 | )? 122 | .into_iter() 123 | .map(|mut s| { 124 | s.description = Some(format!( 125 | "{source_name}\n\n{}", 126 | s.description.unwrap_or_default(), 127 | )); 128 | s 129 | }) 130 | .collect::>(), 131 | ); 132 | } 133 | } 134 | } 135 | } 136 | 137 | snippets.sort_unstable_by(|a, b| a.prefix.cmp(&b.prefix)); 138 | 139 | Ok(snippets) 140 | } 141 | 142 | pub fn load_snippets_from_file( 143 | path: &std::path::PathBuf, 144 | scope: &Option>, 145 | ) -> Result> { 146 | let scope = if scope.is_none() { 147 | path.file_stem() 148 | .and_then(|v| v.to_str()) 149 | .filter(|v| *v != "snippets") 150 | .map(|v| vec![v.to_string()]) 151 | } else { 152 | scope.clone() 153 | }; 154 | 155 | tracing::info!("Try load snippets from: {path:?} for scope: {scope:?}"); 156 | 157 | let content = std::fs::read_to_string(path)?; 158 | 159 | let result = match path.extension().and_then(|v| v.to_str()) { 160 | Some("toml") => toml::from_str::(&content) 161 | .map(|sc| sc.snippets) 162 | .map_err(|e| anyhow::anyhow!(e)), 163 | Some("json") => serde_json::from_str::(&content) 164 | .map(|s| { 165 | s.snippets 166 | .into_iter() 167 | .map(|(prefix, snippet)| { 168 | if snippet.prefix.is_some() { 169 | return snippet; 170 | } 171 | snippet.prefix(prefix) 172 | }) 173 | .flat_map(Into::>::into) 174 | .collect() 175 | }) 176 | .map_err(|e| anyhow::anyhow!(e)), 177 | _ => { 178 | anyhow::bail!("Unsupported snipptes format: {path:?}") 179 | } 180 | }; 181 | 182 | let snippets = result?; 183 | 184 | if let Some(scope) = scope { 185 | // add global scope to each snippet 186 | Ok(snippets 187 | .into_iter() 188 | .map(|mut s| { 189 | s.scope = Some(if let Some(mut v) = s.scope { 190 | // TODO unique scope items 191 | v.extend(scope.clone()); 192 | v 193 | } else { 194 | scope.clone() 195 | }); 196 | s 197 | }) 198 | .collect()) 199 | } else { 200 | Ok(snippets) 201 | } 202 | } 203 | 204 | pub fn load_snippets_from_path( 205 | snippets_path: &std::path::PathBuf, 206 | scope: &Option>, 207 | ) -> Result> { 208 | if snippets_path.is_file() { 209 | return load_snippets_from_file(snippets_path, scope); 210 | } 211 | 212 | let mut snippets = Vec::new(); 213 | match std::fs::read_dir(snippets_path) { 214 | Ok(entries) => { 215 | for entry in entries { 216 | let Ok(entry) = entry else { continue }; 217 | 218 | let path = entry.path(); 219 | if path.is_dir() { 220 | continue; 221 | }; 222 | 223 | match load_snippets_from_file(&path, scope) { 224 | Ok(r) => snippets.extend(r), 225 | Err(e) => { 226 | tracing::error!("On read snippets from {path:?}: {e}"); 227 | continue; 228 | } 229 | } 230 | } 231 | } 232 | Err(e) => tracing::error!("On read dir {snippets_path:?}: {e}"), 233 | } 234 | 235 | Ok(snippets) 236 | } 237 | 238 | pub fn load_unicode_input_from_file(path: &std::path::PathBuf) -> Result> { 239 | tracing::info!("Try load 'unicode input' config from: {path:?}"); 240 | 241 | let content = std::fs::read_to_string(path)?; 242 | 243 | let result = match path.extension().and_then(|v| v.to_str()) { 244 | Some("toml") => toml::from_str::>(&content) 245 | .map(|sc| sc.into_iter().collect())?, 246 | _ => { 247 | anyhow::bail!("Unsupported unicode format: {path:?}") 248 | } 249 | }; 250 | 251 | Ok(result) 252 | } 253 | 254 | pub fn load_unicode_input_from_path( 255 | filepath: &std::path::PathBuf, 256 | ) -> Result> { 257 | let result = if filepath.is_file() { 258 | load_unicode_input_from_file(filepath)? 259 | } else { 260 | let mut result = Vec::new(); 261 | match std::fs::read_dir(filepath) { 262 | Ok(entries) => { 263 | for entry in entries { 264 | let Ok(entry) = entry else { continue }; 265 | 266 | let path = entry.path(); 267 | if path.is_dir() { 268 | continue; 269 | }; 270 | 271 | match load_unicode_input_from_file(&path) { 272 | Ok(r) => result.extend(r), 273 | Err(e) => { 274 | tracing::error!("On read 'unicode input' config from {path:?}: {e}"); 275 | continue; 276 | } 277 | } 278 | } 279 | } 280 | Err(e) => tracing::error!("On read dir {filepath:?}: {e}"), 281 | } 282 | result 283 | }; 284 | 285 | // sort items from largest to smallest by prefix 286 | let mut items = result.into_iter().collect::>(); 287 | items.sort_unstable_by(|(a, _), (b, _)| (a.len(), a).cmp(&(b.len(), b))); 288 | items.reverse(); 289 | 290 | Ok(items 291 | .into_iter() 292 | .map(|(prefix, body)| UnicodeInputItem { prefix, body }) 293 | .collect()) 294 | } 295 | -------------------------------------------------------------------------------- /src/snippets/external.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use serde::Deserialize; 3 | use std::str::FromStr; 4 | 5 | #[derive(Debug, Deserialize)] 6 | pub struct ExternalSnippets { 7 | pub sources: Vec, 8 | } 9 | 10 | #[derive(Debug, Deserialize)] 11 | pub struct SnippetSource { 12 | pub name: Option, 13 | pub git: String, 14 | #[serde(default)] 15 | pub paths: Vec, 16 | } 17 | 18 | #[derive(Debug, Deserialize)] 19 | pub struct SourcePath { 20 | pub scope: Option>, 21 | pub path: String, 22 | } 23 | 24 | impl SnippetSource { 25 | pub fn destination_path(&self) -> Result { 26 | // TODO may be use Url crate? 27 | // normalize url 28 | let url = self 29 | .git 30 | .split('?') 31 | .nth(0) 32 | .ok_or_else(|| anyhow::anyhow!("Failed to parse {}", self.git))?; 33 | let source = url 34 | .split("://") 35 | .nth(1) 36 | .ok_or_else(|| anyhow::anyhow!("Failed to parse {}", self.git))?; 37 | 38 | Ok(std::path::PathBuf::from_str(source)?) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/snippets/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod external; 3 | pub mod vscode; 4 | 5 | pub use config::{Snippet, SnippetsConfig, UnicodeInputItem}; 6 | -------------------------------------------------------------------------------- /src/snippets/vscode.rs: -------------------------------------------------------------------------------- 1 | use crate::Snippet; 2 | use serde::Deserialize; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Deserialize)] 6 | pub struct VSSnippetsConfig { 7 | #[serde(flatten)] 8 | pub snippets: HashMap, 9 | } 10 | 11 | #[derive(Deserialize)] 12 | #[serde(untagged)] 13 | pub enum VSCodeSnippetValue { 14 | Single(String), 15 | List(Vec), 16 | } 17 | 18 | #[derive(Deserialize)] 19 | pub struct VSCodeSnippet { 20 | pub scope: Option, 21 | pub prefix: Option, 22 | pub body: VSCodeSnippetValue, 23 | pub description: Option, 24 | } 25 | 26 | impl VSCodeSnippet { 27 | pub fn prefix(self, prefix: String) -> Self { 28 | Self { 29 | prefix: Some(VSCodeSnippetValue::Single(prefix)), 30 | ..self 31 | } 32 | } 33 | } 34 | 35 | impl std::fmt::Display for VSCodeSnippetValue { 36 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 37 | write!( 38 | f, 39 | "{}", 40 | match self { 41 | VSCodeSnippetValue::Single(v) => v.to_owned(), 42 | VSCodeSnippetValue::List(v) => v.join("\n"), 43 | } 44 | ) 45 | } 46 | } 47 | 48 | impl From for Vec { 49 | fn from(value: VSCodeSnippet) -> Vec { 50 | let scope = value 51 | .scope 52 | .map(|v| v.split(',').map(String::from).collect()); 53 | let body = value.body.to_string(); 54 | let description = value.description.map(|v| v.to_string()); 55 | 56 | match value.prefix { 57 | Some(VSCodeSnippetValue::Single(prefix)) => { 58 | vec![Snippet { 59 | scope, 60 | prefix, 61 | body, 62 | description, 63 | }] 64 | } 65 | Some(VSCodeSnippetValue::List(prefixes)) => prefixes 66 | .into_iter() 67 | .map(|prefix| Snippet { 68 | scope: scope.clone(), 69 | prefix, 70 | body: body.clone(), 71 | description: description.clone(), 72 | }) 73 | .collect(), 74 | None => Vec::new(), 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/basic.rs: -------------------------------------------------------------------------------- 1 | use simple_completion_language_server::{ac_searcher, search, server, snippets, RopeReader}; 2 | use std::io::Read; 3 | 4 | use std::pin::Pin; 5 | use std::str::FromStr; 6 | use std::task::{Context, Poll}; 7 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 8 | use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; 9 | use tower_lsp::{jsonrpc, lsp_types}; 10 | 11 | pub struct AsyncIn(UnboundedReceiver); 12 | pub struct AsyncOut(UnboundedSender); 13 | 14 | fn encode_message(content_type: Option<&str>, message: &str) -> String { 15 | let content_type = content_type 16 | .map(|ty| format!("\r\nContent-Type: {ty}")) 17 | .unwrap_or_default(); 18 | 19 | format!( 20 | "Content-Length: {}{}\r\n\r\n{}", 21 | message.len(), 22 | content_type, 23 | message 24 | ) 25 | } 26 | 27 | impl AsyncRead for AsyncIn { 28 | fn poll_read( 29 | self: Pin<&mut Self>, 30 | cx: &mut Context<'_>, 31 | buf: &mut ReadBuf<'_>, 32 | ) -> Poll> { 33 | let rx = self.get_mut(); 34 | match rx.0.poll_recv(cx) { 35 | Poll::Ready(Some(v)) => { 36 | tracing::debug!("read value: {:?}", v); 37 | buf.put_slice(v.as_bytes()); 38 | Poll::Ready(Ok(())) 39 | } 40 | _ => Poll::Pending, 41 | } 42 | } 43 | } 44 | 45 | impl AsyncWrite for AsyncOut { 46 | fn poll_write( 47 | self: Pin<&mut Self>, 48 | _cx: &mut Context<'_>, 49 | buf: &[u8], 50 | ) -> Poll> { 51 | let tx = self.get_mut(); 52 | let value = String::from_utf8(buf.to_vec()).unwrap(); 53 | tracing::debug!("write value: {value:?}"); 54 | let _ = tx.0.send(value); 55 | Poll::Ready(Ok(buf.len())) 56 | } 57 | 58 | fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { 59 | Poll::Ready(Ok(())) 60 | } 61 | 62 | fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { 63 | Poll::Ready(Ok(())) 64 | } 65 | } 66 | 67 | struct TestContext { 68 | pub request_tx: UnboundedSender, 69 | pub response_rx: UnboundedReceiver, 70 | pub _server: tokio::task::JoinHandle<()>, 71 | pub _client: tokio::task::JoinHandle<()>, 72 | } 73 | 74 | impl Default for TestContext { 75 | fn default() -> Self { 76 | TestContext::new(Default::default(), Default::default(), Default::default()) 77 | } 78 | } 79 | 80 | impl TestContext { 81 | pub fn new( 82 | snippets: Vec, 83 | unicode_input: Vec, 84 | home_dir: String, 85 | ) -> Self { 86 | let (request_tx, rx) = mpsc::unbounded_channel::(); 87 | let (tx, mut client_response_rx) = mpsc::unbounded_channel::(); 88 | let (client_tx, response_rx) = mpsc::unbounded_channel::(); 89 | 90 | let async_in = AsyncIn(rx); 91 | let async_out = AsyncOut(tx); 92 | 93 | let server = tokio::spawn(async move { 94 | server::start(async_in, async_out, snippets, unicode_input, home_dir).await 95 | }); 96 | 97 | let client = tokio::spawn(async move { 98 | loop { 99 | let Some(response) = client_response_rx.recv().await else { 100 | continue; 101 | }; 102 | if client_tx.send(response).is_err() { 103 | tracing::error!("Failed to pass client response"); 104 | } 105 | } 106 | }); 107 | 108 | Self { 109 | request_tx, 110 | response_rx, 111 | _server: server, 112 | _client: client, 113 | } 114 | } 115 | 116 | pub async fn send_all(&mut self, messages: &[&str]) -> anyhow::Result<()> { 117 | for message in messages { 118 | self.send(&jsonrpc::Request::from_str(message)?).await?; 119 | } 120 | Ok(()) 121 | } 122 | 123 | pub async fn send(&mut self, request: &jsonrpc::Request) -> anyhow::Result<()> { 124 | self.request_tx 125 | .send(encode_message(None, &serde_json::to_string(request)?))?; 126 | Ok(()) 127 | } 128 | 129 | pub async fn recv( 130 | &mut self, 131 | ) -> anyhow::Result { 132 | // TODO split response for single messages 133 | loop { 134 | let response = self 135 | .response_rx 136 | .recv() 137 | .await 138 | .ok_or_else(|| anyhow::anyhow!("empty response"))?; 139 | // decode response 140 | let payload = response.split('\n').next_back().unwrap_or_default(); 141 | 142 | // skip log messages 143 | if payload.contains("window/logMessage") { 144 | tracing::debug!("log: {payload}"); 145 | continue; 146 | } 147 | let response = serde_json::from_str::(payload)?; 148 | let (_id, result) = response.into_parts(); 149 | return Ok(serde_json::from_value(result?)?); 150 | } 151 | } 152 | 153 | pub async fn request( 154 | &mut self, 155 | request: &jsonrpc::Request, 156 | ) -> anyhow::Result { 157 | self.send(request).await?; 158 | self.recv().await 159 | } 160 | 161 | pub async fn initialize(&mut self) -> anyhow::Result<()> { 162 | let request = jsonrpc::Request::build("initialize") 163 | .id(1) 164 | .params(serde_json::json!({"capabilities":{}})) 165 | .finish(); 166 | 167 | let _ = self 168 | .request::(&request) 169 | .await?; 170 | 171 | Ok(()) 172 | } 173 | } 174 | 175 | #[test_log::test] 176 | fn chunks_by_words() -> anyhow::Result<()> { 177 | let rope = ropey::Rope::from_iter((0..1000).map(|_| "word ".to_string())); 178 | 179 | let mut reader = RopeReader::new(&rope); 180 | 181 | let mut buf = vec![0; 1000 * 5]; 182 | 183 | let mut whole_text = String::new(); 184 | 185 | loop { 186 | let n = reader.read(&mut buf)?; 187 | 188 | if n == 0 { 189 | break; 190 | } 191 | 192 | let text = std::str::from_utf8(&buf[..n])?; 193 | 194 | whole_text.push_str(text); 195 | 196 | let text = text.trim(); 197 | if text.is_empty() { 198 | continue; 199 | } 200 | 201 | let last_word = text.split(" ").last().unwrap(); 202 | 203 | assert_eq!(last_word, "word"); 204 | } 205 | let words = whole_text.trim().split(" "); 206 | assert_eq!(words.clone().count(), 1000); 207 | 208 | Ok(()) 209 | } 210 | 211 | #[test_log::test] 212 | fn words_search() -> anyhow::Result<()> { 213 | let text = r#"Word 求 btask_timeout HANDLERS["loggers"]"#; 214 | let doc = ropey::Rope::from_str(text); 215 | let mut words = std::collections::HashSet::new(); 216 | 217 | let prefix = "BTA"; 218 | search(prefix, &doc, &ac_searcher(prefix)?, 10, &mut words)?; 219 | assert_eq!( 220 | words.iter().next().map(|v| v.as_str()), 221 | Some("btask_timeout") 222 | ); 223 | 224 | words.clear(); 225 | 226 | let prefix = "logge"; 227 | search(prefix, &doc, &ac_searcher(prefix)?, 10, &mut words)?; 228 | assert_eq!(words.iter().next().map(|v| v.as_str()), Some("loggers")); 229 | 230 | Ok(()) 231 | } 232 | 233 | #[test_log::test(tokio::test)] 234 | async fn initialize() -> anyhow::Result<()> { 235 | let mut context = TestContext::default(); 236 | 237 | let request = jsonrpc::Request::build("initialize") 238 | .id(1) 239 | .params(serde_json::json!({"capabilities":{}})) 240 | .finish(); 241 | 242 | let response = context 243 | .request::(&request) 244 | .await?; 245 | 246 | assert_eq!( 247 | response.capabilities.completion_provider, 248 | Some(lsp_types::CompletionOptions { 249 | resolve_provider: Some(false), 250 | trigger_characters: Some(vec![std::path::MAIN_SEPARATOR_STR.to_string()]), 251 | ..lsp_types::CompletionOptions::default() 252 | }) 253 | ); 254 | assert_eq!( 255 | response.capabilities.text_document_sync, 256 | Some(lsp_types::TextDocumentSyncCapability::Kind( 257 | lsp_types::TextDocumentSyncKind::INCREMENTAL, 258 | )) 259 | ); 260 | 261 | Ok(()) 262 | } 263 | 264 | #[test_log::test(tokio::test)] 265 | async fn completion() -> anyhow::Result<()> { 266 | let mut context = TestContext::default(); 267 | context.initialize().await?; 268 | context.send_all(&[ 269 | r#"{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"python","text":"hello\nhe","uri":"file:///tmp/main.py","version":0}}}"#, 270 | r#"{"jsonrpc":"2.0","method":"textDocument/completion","params":{"position":{"character":2,"line":1},"textDocument":{"uri":"file:///tmp/main.py"}},"id":3}"# 271 | ]).await?; 272 | 273 | let response = context.recv::().await?; 274 | 275 | let lsp_types::CompletionResponse::Array(items) = response else { 276 | anyhow::bail!("completion array expected") 277 | }; 278 | 279 | assert_eq!(items.len(), 1); 280 | assert_eq!( 281 | items.into_iter().map(|i| i.label).collect::>(), 282 | vec!["hello"] 283 | ); 284 | 285 | context.send_all(&[ 286 | r#"{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"python","text":"hello\nel","uri":"file:///tmp/main2.py","version":0}}}"#, 287 | r#"{"jsonrpc":"2.0","method":"textDocument/completion","params":{"position":{"character":2,"line":1},"textDocument":{"uri":"file:///tmp/main2.py"}},"id":3}"# 288 | ]).await?; 289 | 290 | let response = context.recv::().await?; 291 | 292 | let lsp_types::CompletionResponse::Array(items) = response else { 293 | anyhow::bail!("completion array expected") 294 | }; 295 | 296 | assert_eq!(items.len(), 0); 297 | 298 | Ok(()) 299 | } 300 | 301 | #[test_log::test(tokio::test)] 302 | async fn completion_by_quoted_word() -> anyhow::Result<()> { 303 | let mut context = TestContext::default(); 304 | context.initialize().await?; 305 | context.send_all(&[ 306 | r#"{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"python","text":"function(\"hello\")\nhe","uri":"file:///tmp/main.py","version":0}}}"#, 307 | r#"{"jsonrpc":"2.0","method":"textDocument/completion","params":{"position":{"character":2,"line":1},"textDocument":{"uri":"file:///tmp/main.py"}},"id":3}"# 308 | ]).await?; 309 | 310 | let response = context.recv::().await?; 311 | 312 | let lsp_types::CompletionResponse::Array(items) = response else { 313 | anyhow::bail!("completion array expected") 314 | }; 315 | 316 | assert_eq!(items.len(), 1); 317 | assert_eq!( 318 | items.into_iter().map(|i| i.label).collect::>(), 319 | vec!["hello"] 320 | ); 321 | 322 | Ok(()) 323 | } 324 | 325 | #[test_log::test(tokio::test)] 326 | async fn snippets() -> anyhow::Result<()> { 327 | let mut context = TestContext::new( 328 | vec![ 329 | snippets::Snippet { 330 | scope: Some(vec!["python".to_string()]), 331 | prefix: "ma".to_string(), 332 | body: "def main(): pass".to_string(), 333 | description: None, 334 | }, 335 | snippets::Snippet { 336 | scope: Some(vec!["c".to_string()]), 337 | prefix: "ma".to_string(), 338 | body: "malloc".to_string(), 339 | description: None, 340 | }, 341 | ], 342 | Default::default(), 343 | Default::default(), 344 | ); 345 | context.initialize().await?; 346 | context.send_all(&[ 347 | r#"{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"python","text":"ma","uri":"file:///tmp/main.py","version":0}}}"#, 348 | r#"{"jsonrpc":"2.0","method":"textDocument/completion","params":{"position":{"character":2,"line":0},"textDocument":{"uri":"file:///tmp/main.py"}},"id":3}"# 349 | ]).await?; 350 | 351 | let response = context.recv::().await?; 352 | 353 | let lsp_types::CompletionResponse::Array(items) = response else { 354 | anyhow::bail!("completion array expected") 355 | }; 356 | 357 | assert_eq!(items.len(), 1); 358 | assert_eq!( 359 | items 360 | .into_iter() 361 | .filter_map(|i| i.text_edit.and_then(|l| match l { 362 | lsp_types::CompletionTextEdit::InsertAndReplace(t) => Some(t.new_text), 363 | _ => None, 364 | })) 365 | .collect::>(), 366 | vec!["def main(): pass"] 367 | ); 368 | 369 | Ok(()) 370 | } 371 | 372 | #[test_log::test(tokio::test)] 373 | async fn snippets_inline_by_word_tail() -> anyhow::Result<()> { 374 | let mut context = TestContext::new( 375 | vec![snippets::Snippet { 376 | scope: Some(vec!["python".to_string()]), 377 | prefix: "sq".to_string(), 378 | body: "^2".to_string(), 379 | description: None, 380 | }], 381 | Default::default(), 382 | Default::default(), 383 | ); 384 | context.initialize().await?; 385 | 386 | let request = jsonrpc::Request::from_str(&serde_json::to_string(&serde_json::json!( 387 | { 388 | "jsonrpc": "2.0", 389 | "method": "workspace/didChangeConfiguration", 390 | "params": { 391 | "settings": { 392 | "snippets_inline_by_word_tail": true, 393 | "feature_snippets": true, 394 | "feature_citations": false, 395 | "feature_words": false, 396 | "feature_unicode_input": false, 397 | "feature_paths": false, 398 | } 399 | } 400 | } 401 | ))?)?; 402 | context.send(&request).await?; 403 | 404 | context.send_all(&[ 405 | r#"{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"python","text":"xsq","uri":"file:///tmp/main.py","version":0}}}"#, 406 | r#"{"jsonrpc":"2.0","method":"textDocument/completion","params":{"position":{"character":3,"line":0},"textDocument":{"uri":"file:///tmp/main.py"}},"id":3}"# 407 | ]).await?; 408 | 409 | let response = context.recv::().await?; 410 | 411 | let lsp_types::CompletionResponse::Array(items) = response else { 412 | anyhow::bail!("completion array expected") 413 | }; 414 | 415 | assert_eq!(items.len(), 1); 416 | assert_eq!( 417 | items 418 | .into_iter() 419 | .filter_map(|i| i.text_edit.and_then(|l| match l { 420 | lsp_types::CompletionTextEdit::InsertAndReplace(t) => Some(t.new_text), 421 | _ => None, 422 | })) 423 | .collect::>(), 424 | vec!["^2"] 425 | ); 426 | 427 | Ok(()) 428 | } 429 | 430 | #[test_log::test(tokio::test)] 431 | async fn unicode_input() -> anyhow::Result<()> { 432 | let mut context = TestContext::new( 433 | Default::default(), 434 | vec![ 435 | snippets::UnicodeInputItem { 436 | prefix: "alpha".to_string(), 437 | body: "α".to_string(), 438 | }, 439 | snippets::UnicodeInputItem { 440 | prefix: "betta".to_string(), 441 | body: "β".to_string(), 442 | }, 443 | ], 444 | Default::default(), 445 | ); 446 | context.initialize().await?; 447 | 448 | let request = jsonrpc::Request::from_str(&serde_json::to_string(&serde_json::json!( 449 | { 450 | "jsonrpc": "2.0", 451 | "method": "workspace/didChangeConfiguration", 452 | "params": { 453 | "settings": { 454 | "snippets_inline_by_word_tail": false, 455 | "feature_snippets": false, 456 | "feature_citations": false, 457 | "feature_words": false, 458 | "feature_unicode_input": true, 459 | "feature_paths": false, 460 | } 461 | } 462 | } 463 | ))?)?; 464 | context.send(&request).await?; 465 | 466 | context.send_all(&[ 467 | r#"{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"python","text":"α+bet","uri":"file:///tmp/main.py","version":0}}}"#, 468 | r#"{"jsonrpc":"2.0","method":"textDocument/completion","params":{"position":{"character":5,"line":0},"textDocument":{"uri":"file:///tmp/main.py"}},"id":3}"# 469 | ]).await?; 470 | 471 | let response = context.recv::().await?; 472 | 473 | let lsp_types::CompletionResponse::Array(items) = response else { 474 | anyhow::bail!("completion array expected") 475 | }; 476 | 477 | assert_eq!( 478 | items 479 | .into_iter() 480 | .filter_map(|i| match i.text_edit { 481 | Some(lsp_types::CompletionTextEdit::InsertAndReplace(te)) => Some(te.new_text), 482 | _ => None, 483 | }) 484 | .collect::>(), 485 | vec!["β"] 486 | ); 487 | 488 | Ok(()) 489 | } 490 | 491 | #[test_log::test(tokio::test)] 492 | async fn paths() -> anyhow::Result<()> { 493 | std::fs::create_dir_all("/tmp/scls-test/sub-folder")?; 494 | 495 | let mut context = TestContext::new(Default::default(), Default::default(), "/tmp".to_string()); 496 | context.initialize().await?; 497 | 498 | let request = jsonrpc::Request::from_str(&serde_json::to_string(&serde_json::json!( 499 | { 500 | "jsonrpc": "2.0", 501 | "method": "workspace/didChangeConfiguration", 502 | "params": { 503 | "settings": { 504 | "snippets_inline_by_word_tail": false, 505 | "feature_snippets": false, 506 | "feature_citations": false, 507 | "feature_words": false, 508 | "feature_unicode_input": false, 509 | "feature_paths": true, 510 | } 511 | } 512 | } 513 | ))?)?; 514 | context.send(&request).await?; 515 | 516 | context.send_all(&[ 517 | r#"{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"python","text":"/tmp/scls-test/","uri":"file:///tmp/main.py","version":0}}}"#, 518 | r#"{"jsonrpc":"2.0","method":"textDocument/completion","params":{"position":{"character":15,"line":0},"textDocument":{"uri":"file:///tmp/main.py"}},"id":3}"# 519 | ]).await?; 520 | 521 | let response = context.recv::().await?; 522 | 523 | let lsp_types::CompletionResponse::Array(items) = response else { 524 | anyhow::bail!("completion array expected") 525 | }; 526 | 527 | assert_eq!( 528 | items 529 | .into_iter() 530 | .filter_map(|i| match i.text_edit { 531 | Some(lsp_types::CompletionTextEdit::InsertAndReplace(te)) => Some(te.new_text), 532 | _ => None, 533 | }) 534 | .collect::>(), 535 | vec!["/tmp/scls-test/sub-folder"] 536 | ); 537 | 538 | context.send_all(&[ 539 | r#"{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"python","text":"/tmp/scls-test/su","uri":"file:///tmp/main2.py","version":0}}}"#, 540 | r#"{"jsonrpc":"2.0","method":"textDocument/completion","params":{"position":{"character":17,"line":0},"textDocument":{"uri":"file:///tmp/main2.py"}},"id":3}"# 541 | ]).await?; 542 | 543 | let response = context.recv::().await?; 544 | 545 | let lsp_types::CompletionResponse::Array(items) = response else { 546 | anyhow::bail!("completion array expected") 547 | }; 548 | 549 | assert_eq!( 550 | items 551 | .into_iter() 552 | .filter_map(|i| match i.text_edit { 553 | Some(lsp_types::CompletionTextEdit::InsertAndReplace(te)) => Some(te.new_text), 554 | _ => None, 555 | }) 556 | .collect::>(), 557 | vec!["/tmp/scls-test/sub-folder"] 558 | ); 559 | 560 | context.send_all(&[ 561 | r#"{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"python","text":"~/scls-test/su","uri":"file:///tmp/main3.py","version":0}}}"#, 562 | r#"{"jsonrpc":"2.0","method":"textDocument/completion","params":{"position":{"character":14,"line":0},"textDocument":{"uri":"file:///tmp/main3.py"}},"id":3}"# 563 | ]).await?; 564 | 565 | let response = context.recv::().await?; 566 | 567 | let lsp_types::CompletionResponse::Array(items) = response else { 568 | anyhow::bail!("completion array expected") 569 | }; 570 | 571 | assert_eq!( 572 | items 573 | .into_iter() 574 | .filter_map(|i| match i.text_edit { 575 | Some(lsp_types::CompletionTextEdit::InsertAndReplace(te)) => Some(te.new_text), 576 | _ => None, 577 | }) 578 | .collect::>(), 579 | vec!["~/scls-test/sub-folder"] 580 | ); 581 | 582 | context.send_all(&[ 583 | r#"{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"python","text":"./scls-test/su","uri":"file:///tmp/main4.py","version":0}}}"#, 584 | r#"{"jsonrpc":"2.0","method":"textDocument/completion","params":{"position":{"character":14,"line":0},"textDocument":{"uri":"file:///tmp/main4.py"}},"id":3}"# 585 | ]).await?; 586 | 587 | let response = context.recv::().await?; 588 | 589 | let lsp_types::CompletionResponse::Array(items) = response else { 590 | anyhow::bail!("completion array expected") 591 | }; 592 | 593 | assert_eq!( 594 | items 595 | .into_iter() 596 | .filter_map(|i| match i.text_edit { 597 | Some(lsp_types::CompletionTextEdit::InsertAndReplace(te)) => Some(te.new_text), 598 | _ => None, 599 | }) 600 | .collect::>(), 601 | vec!["./scls-test/sub-folder"] 602 | ); 603 | 604 | context.send_all(&[ 605 | r#"{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"python","text":"../scls-test/su","uri":"file:///tmp/scls-test/main5.py","version":0}}}"#, 606 | r#"{"jsonrpc":"2.0","method":"textDocument/completion","params":{"position":{"character":15,"line":0},"textDocument":{"uri":"file:///tmp/scls-test/main5.py"}},"id":3}"# 607 | ]).await?; 608 | 609 | let response = context.recv::().await?; 610 | 611 | let lsp_types::CompletionResponse::Array(items) = response else { 612 | anyhow::bail!("completion array expected") 613 | }; 614 | 615 | assert_eq!( 616 | items 617 | .into_iter() 618 | .filter_map(|i| match i.text_edit { 619 | Some(lsp_types::CompletionTextEdit::InsertAndReplace(te)) => Some(te.new_text), 620 | _ => None, 621 | }) 622 | .collect::>(), 623 | vec!["../scls-test/sub-folder"] 624 | ); 625 | 626 | Ok(()) 627 | } 628 | 629 | #[test_log::test(tokio::test)] 630 | #[cfg(feature = "citation")] 631 | async fn citations() -> anyhow::Result<()> { 632 | std::fs::create_dir_all("/tmp/scls-test-citation")?; 633 | 634 | let doc = r#" 635 | --- 636 | bibliography: "/tmp/scls-test-citation/test.bib" # could also be surrounded by brackets instead of quotation marks 637 | --- 638 | 639 | # Heading 640 | @b 641 | "#; 642 | 643 | let bib = r#" 644 | @online{irfanullah_open_acces_global_south_2021, 645 | author = {Irfanullah, Haseeb}, 646 | title = {{Open Access and Global South}}, 647 | subtitle = {It is More Than a Matter of Inclusion}, 648 | date = {2021-02-08}, 649 | urldate = {2024-08-04}, 650 | language = {english}, 651 | url = {https://web.archive.org/web/20240303223926/https://scholarlykitchen.sspnet.org/2021/01/28/open-access-and-global-south-it-is-more-than-a-matter-of-inclusion/}, 652 | } 653 | 654 | @article{brainard_pay-to-publ_model_open_acces_2024, 655 | author = {Brainard, Jeffrey}, 656 | title = {{Is the pay-to-publish model for open access pricing scientists 657 | out?}}, 658 | journal = {American Association for the Advancement of Science}, 659 | volume = {385}, 660 | issue = {6708}, 661 | date = {2024-08-01}, 662 | urldate = {2024-08-04}, 663 | doi = {10.1126/science.zp80ua9}, 664 | } 665 | 666 | @article{brembs_replacing_academic_journals_2023, 667 | author = {Brembs, Björn and Huneman, Philippe and Schönbrodt, Felix and 668 | Nilsonne, Gustav and Susi, Toma and Siems, Renke and Perakakis, 669 | Pandelis and Trachana, Varvara and Ma, Lai and Rodriguez-Cuadrado, 670 | Sara}, 671 | title = {Replacing academic journals}, 672 | year = {2023}, 673 | month = may, 674 | doi = {10.5281/zenodo.7974116}, 675 | } 676 | "#; 677 | 678 | std::fs::write("/tmp/scls-test-citation/test.bib", bib)?; 679 | 680 | let mut context = TestContext::default(); 681 | context.initialize().await?; 682 | 683 | let request = jsonrpc::Request::from_str(&serde_json::to_string(&serde_json::json!( 684 | { 685 | "jsonrpc": "2.0", 686 | "method": "workspace/didChangeConfiguration", 687 | "params": { 688 | "settings": { 689 | "feature_citations": true, 690 | "feature_words": false, 691 | "feature_snippets": false, 692 | "feature_unicode_input": false, 693 | "feature_paths": false, 694 | } 695 | } 696 | } 697 | ))?)?; 698 | context.send(&request).await?; 699 | 700 | context 701 | .send_all(&[ 702 | &serde_json::to_string(&serde_json::json!( 703 | { 704 | "jsonrpc": "2.0", 705 | "method": "textDocument/didOpen", 706 | "params": { 707 | "textDocument": { 708 | "languageId": "markdown", 709 | "text": doc, 710 | "uri": "file:///tmp/doc.md", 711 | "version":0 712 | } 713 | } 714 | } 715 | ))?, 716 | &serde_json::to_string(&serde_json::json!( 717 | { 718 | "jsonrpc": "2.0", 719 | "id": 3, 720 | "method": "textDocument/completion", 721 | "params": { 722 | "position": { 723 | "character": 2, 724 | "line": doc.lines().count() - 1 725 | }, 726 | "textDocument": { 727 | "uri": "file:///tmp/doc.md" 728 | } 729 | } 730 | } 731 | ))?, 732 | ]) 733 | .await?; 734 | 735 | let response = context.recv::().await?; 736 | 737 | let lsp_types::CompletionResponse::Array(items) = response else { 738 | anyhow::bail!("completion array expected") 739 | }; 740 | 741 | assert_eq!(items.len(), 2); 742 | 743 | assert_eq!( 744 | items 745 | .into_iter() 746 | .filter_map(|i| match i.text_edit { 747 | Some(lsp_types::CompletionTextEdit::InsertAndReplace(te)) => Some(te.new_text), 748 | _ => None, 749 | }) 750 | .collect::>(), 751 | vec![ 752 | "brainard_pay-to-publ_model_open_acces_2024", 753 | "brembs_replacing_academic_journals_2023" 754 | ] 755 | ); 756 | Ok(()) 757 | } 758 | --------------------------------------------------------------------------------