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