on master help sign in
Add language server protocol implementation with diagnostics, completions, and hover
commitc05aa337184d46add00652a5f63c529d6bfdaa80 authoriko <mail@iko.soy> dateTue 14 Apr 2026, 13:53 (6 months ago) parent8a898d7
5 files changed+1379−6
Cargo.lock+762 Cargo.toml+2 src/bin/htmlang-lsp.rs+610 src/lib.rs+3 src/main.rs+2−6
Cargo.lock+762
@@ -2,6 +2,768 @@
22# It is not intended for manual editing.
33version = 4
44
5+[[package]]
6+name = "async-trait"
7+version = "0.1.89"
8+source = "registry+https://github.com/rust-lang/crates.io-index"
9+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
10+dependencies = [
11+ "proc-macro2",
12+ "quote",
13+ "syn",
14+]
15+
16+[[package]]
17+name = "auto_impl"
18+version = "1.3.0"
19+source = "registry+https://github.com/rust-lang/crates.io-index"
20+checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7"
21+dependencies = [
22+ "proc-macro2",
23+ "quote",
24+ "syn",
25+]
26+
27+[[package]]
28+name = "bitflags"
29+version = "1.3.2"
30+source = "registry+https://github.com/rust-lang/crates.io-index"
31+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
32+
33+[[package]]
34+name = "bitflags"
35+version = "2.11.1"
36+source = "registry+https://github.com/rust-lang/crates.io-index"
37+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
38+
39+[[package]]
40+name = "bytes"
41+version = "1.11.1"
42+source = "registry+https://github.com/rust-lang/crates.io-index"
43+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
44+
45+[[package]]
46+name = "cfg-if"
47+version = "1.0.4"
48+source = "registry+https://github.com/rust-lang/crates.io-index"
49+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
50+
51+[[package]]
52+name = "dashmap"
53+version = "5.5.3"
54+source = "registry+https://github.com/rust-lang/crates.io-index"
55+checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
56+dependencies = [
57+ "cfg-if",
58+ "hashbrown",
59+ "lock_api",
60+ "once_cell",
61+ "parking_lot_core",
62+]
63+
64+[[package]]
65+name = "displaydoc"
66+version = "0.2.5"
67+source = "registry+https://github.com/rust-lang/crates.io-index"
68+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
69+dependencies = [
70+ "proc-macro2",
71+ "quote",
72+ "syn",
73+]
74+
75+[[package]]
76+name = "form_urlencoded"
77+version = "1.2.2"
78+source = "registry+https://github.com/rust-lang/crates.io-index"
79+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
80+dependencies = [
81+ "percent-encoding",
82+]
83+
84+[[package]]
85+name = "futures"
86+version = "0.3.32"
87+source = "registry+https://github.com/rust-lang/crates.io-index"
88+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
89+dependencies = [
90+ "futures-channel",
91+ "futures-core",
92+ "futures-io",
93+ "futures-sink",
94+ "futures-task",
95+ "futures-util",
96+]
97+
98+[[package]]
99+name = "futures-channel"
100+version = "0.3.32"
101+source = "registry+https://github.com/rust-lang/crates.io-index"
102+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
103+dependencies = [
104+ "futures-core",
105+ "futures-sink",
106+]
107+
108+[[package]]
109+name = "futures-core"
110+version = "0.3.32"
111+source = "registry+https://github.com/rust-lang/crates.io-index"
112+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
113+
114+[[package]]
115+name = "futures-io"
116+version = "0.3.32"
117+source = "registry+https://github.com/rust-lang/crates.io-index"
118+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
119+
120+[[package]]
121+name = "futures-macro"
122+version = "0.3.32"
123+source = "registry+https://github.com/rust-lang/crates.io-index"
124+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
125+dependencies = [
126+ "proc-macro2",
127+ "quote",
128+ "syn",
129+]
130+
131+[[package]]
132+name = "futures-sink"
133+version = "0.3.32"
134+source = "registry+https://github.com/rust-lang/crates.io-index"
135+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
136+
137+[[package]]
138+name = "futures-task"
139+version = "0.3.32"
140+source = "registry+https://github.com/rust-lang/crates.io-index"
141+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
142+
143+[[package]]
144+name = "futures-util"
145+version = "0.3.32"
146+source = "registry+https://github.com/rust-lang/crates.io-index"
147+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
148+dependencies = [
149+ "futures-channel",
150+ "futures-core",
151+ "futures-io",
152+ "futures-macro",
153+ "futures-sink",
154+ "futures-task",
155+ "memchr",
156+ "pin-project-lite",
157+ "slab",
158+]
159+
160+[[package]]
161+name = "hashbrown"
162+version = "0.14.5"
163+source = "registry+https://github.com/rust-lang/crates.io-index"
164+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
165+
5166[[package]]
6167name = "htmlang"
7168version = "0.1.0"
169+dependencies = [
170+ "tokio",
171+ "tower-lsp",
172+]
173+
174+[[package]]
175+name = "httparse"
176+version = "1.10.1"
177+source = "registry+https://github.com/rust-lang/crates.io-index"
178+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
179+
180+[[package]]
181+name = "icu_collections"
182+version = "2.2.0"
183+source = "registry+https://github.com/rust-lang/crates.io-index"
184+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
185+dependencies = [
186+ "displaydoc",
187+ "potential_utf",
188+ "utf8_iter",
189+ "yoke",
190+ "zerofrom",
191+ "zerovec",
192+]
193+
194+[[package]]
195+name = "icu_locale_core"
196+version = "2.2.0"
197+source = "registry+https://github.com/rust-lang/crates.io-index"
198+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
199+dependencies = [
200+ "displaydoc",
201+ "litemap",
202+ "tinystr",
203+ "writeable",
204+ "zerovec",
205+]
206+
207+[[package]]
208+name = "icu_normalizer"
209+version = "2.2.0"
210+source = "registry+https://github.com/rust-lang/crates.io-index"
211+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
212+dependencies = [
213+ "icu_collections",
214+ "icu_normalizer_data",
215+ "icu_properties",
216+ "icu_provider",
217+ "smallvec",
218+ "zerovec",
219+]
220+
221+[[package]]
222+name = "icu_normalizer_data"
223+version = "2.2.0"
224+source = "registry+https://github.com/rust-lang/crates.io-index"
225+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
226+
227+[[package]]
228+name = "icu_properties"
229+version = "2.2.0"
230+source = "registry+https://github.com/rust-lang/crates.io-index"
231+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
232+dependencies = [
233+ "icu_collections",
234+ "icu_locale_core",
235+ "icu_properties_data",
236+ "icu_provider",
237+ "zerotrie",
238+ "zerovec",
239+]
240+
241+[[package]]
242+name = "icu_properties_data"
243+version = "2.2.0"
244+source = "registry+https://github.com/rust-lang/crates.io-index"
245+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
246+
247+[[package]]
248+name = "icu_provider"
249+version = "2.2.0"
250+source = "registry+https://github.com/rust-lang/crates.io-index"
251+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
252+dependencies = [
253+ "displaydoc",
254+ "icu_locale_core",
255+ "writeable",
256+ "yoke",
257+ "zerofrom",
258+ "zerotrie",
259+ "zerovec",
260+]
261+
262+[[package]]
263+name = "idna"
264+version = "1.1.0"
265+source = "registry+https://github.com/rust-lang/crates.io-index"
266+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
267+dependencies = [
268+ "idna_adapter",
269+ "smallvec",
270+ "utf8_iter",
271+]
272+
273+[[package]]
274+name = "idna_adapter"
275+version = "1.2.1"
276+source = "registry+https://github.com/rust-lang/crates.io-index"
277+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
278+dependencies = [
279+ "icu_normalizer",
280+ "icu_properties",
281+]
282+
283+[[package]]
284+name = "itoa"
285+version = "1.0.18"
286+source = "registry+https://github.com/rust-lang/crates.io-index"
287+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
288+
289+[[package]]
290+name = "libc"
291+version = "0.2.185"
292+source = "registry+https://github.com/rust-lang/crates.io-index"
293+checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
294+
295+[[package]]
296+name = "litemap"
297+version = "0.8.2"
298+source = "registry+https://github.com/rust-lang/crates.io-index"
299+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
300+
301+[[package]]
302+name = "lock_api"
303+version = "0.4.14"
304+source = "registry+https://github.com/rust-lang/crates.io-index"
305+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
306+dependencies = [
307+ "scopeguard",
308+]
309+
310+[[package]]
311+name = "lsp-types"
312+version = "0.94.1"
313+source = "registry+https://github.com/rust-lang/crates.io-index"
314+checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1"
315+dependencies = [
316+ "bitflags 1.3.2",
317+ "serde",
318+ "serde_json",
319+ "serde_repr",
320+ "url",
321+]
322+
323+[[package]]
324+name = "memchr"
325+version = "2.8.0"
326+source = "registry+https://github.com/rust-lang/crates.io-index"
327+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
328+
329+[[package]]
330+name = "once_cell"
331+version = "1.21.4"
332+source = "registry+https://github.com/rust-lang/crates.io-index"
333+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
334+
335+[[package]]
336+name = "parking_lot_core"
337+version = "0.9.12"
338+source = "registry+https://github.com/rust-lang/crates.io-index"
339+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
340+dependencies = [
341+ "cfg-if",
342+ "libc",
343+ "redox_syscall",
344+ "smallvec",
345+ "windows-link",
346+]
347+
348+[[package]]
349+name = "percent-encoding"
350+version = "2.3.2"
351+source = "registry+https://github.com/rust-lang/crates.io-index"
352+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
353+
354+[[package]]
355+name = "pin-project"
356+version = "1.1.11"
357+source = "registry+https://github.com/rust-lang/crates.io-index"
358+checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
359+dependencies = [
360+ "pin-project-internal",
361+]
362+
363+[[package]]
364+name = "pin-project-internal"
365+version = "1.1.11"
366+source = "registry+https://github.com/rust-lang/crates.io-index"
367+checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
368+dependencies = [
369+ "proc-macro2",
370+ "quote",
371+ "syn",
372+]
373+
374+[[package]]
375+name = "pin-project-lite"
376+version = "0.2.17"
377+source = "registry+https://github.com/rust-lang/crates.io-index"
378+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
379+
380+[[package]]
381+name = "potential_utf"
382+version = "0.1.5"
383+source = "registry+https://github.com/rust-lang/crates.io-index"
384+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
385+dependencies = [
386+ "zerovec",
387+]
388+
389+[[package]]
390+name = "proc-macro2"
391+version = "1.0.106"
392+source = "registry+https://github.com/rust-lang/crates.io-index"
393+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
394+dependencies = [
395+ "unicode-ident",
396+]
397+
398+[[package]]
399+name = "quote"
400+version = "1.0.45"
401+source = "registry+https://github.com/rust-lang/crates.io-index"
402+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
403+dependencies = [
404+ "proc-macro2",
405+]
406+
407+[[package]]
408+name = "redox_syscall"
409+version = "0.5.18"
410+source = "registry+https://github.com/rust-lang/crates.io-index"
411+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
412+dependencies = [
413+ "bitflags 2.11.1",
414+]
415+
416+[[package]]
417+name = "scopeguard"
418+version = "1.2.0"
419+source = "registry+https://github.com/rust-lang/crates.io-index"
420+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
421+
422+[[package]]
423+name = "serde"
424+version = "1.0.228"
425+source = "registry+https://github.com/rust-lang/crates.io-index"
426+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
427+dependencies = [
428+ "serde_core",
429+ "serde_derive",
430+]
431+
432+[[package]]
433+name = "serde_core"
434+version = "1.0.228"
435+source = "registry+https://github.com/rust-lang/crates.io-index"
436+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
437+dependencies = [
438+ "serde_derive",
439+]
440+
441+[[package]]
442+name = "serde_derive"
443+version = "1.0.228"
444+source = "registry+https://github.com/rust-lang/crates.io-index"
445+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
446+dependencies = [
447+ "proc-macro2",
448+ "quote",
449+ "syn",
450+]
451+
452+[[package]]
453+name = "serde_json"
454+version = "1.0.149"
455+source = "registry+https://github.com/rust-lang/crates.io-index"
456+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
457+dependencies = [
458+ "itoa",
459+ "memchr",
460+ "serde",
461+ "serde_core",
462+ "zmij",
463+]
464+
465+[[package]]
466+name = "serde_repr"
467+version = "0.1.20"
468+source = "registry+https://github.com/rust-lang/crates.io-index"
469+checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
470+dependencies = [
471+ "proc-macro2",
472+ "quote",
473+ "syn",
474+]
475+
476+[[package]]
477+name = "slab"
478+version = "0.4.12"
479+source = "registry+https://github.com/rust-lang/crates.io-index"
480+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
481+
482+[[package]]
483+name = "smallvec"
484+version = "1.15.1"
485+source = "registry+https://github.com/rust-lang/crates.io-index"
486+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
487+
488+[[package]]
489+name = "stable_deref_trait"
490+version = "1.2.1"
491+source = "registry+https://github.com/rust-lang/crates.io-index"
492+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
493+
494+[[package]]
495+name = "syn"
496+version = "2.0.117"
497+source = "registry+https://github.com/rust-lang/crates.io-index"
498+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
499+dependencies = [
500+ "proc-macro2",
501+ "quote",
502+ "unicode-ident",
503+]
504+
505+[[package]]
506+name = "synstructure"
507+version = "0.13.2"
508+source = "registry+https://github.com/rust-lang/crates.io-index"
509+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
510+dependencies = [
511+ "proc-macro2",
512+ "quote",
513+ "syn",
514+]
515+
516+[[package]]
517+name = "tinystr"
518+version = "0.8.3"
519+source = "registry+https://github.com/rust-lang/crates.io-index"
520+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
521+dependencies = [
522+ "displaydoc",
523+ "zerovec",
524+]
525+
526+[[package]]
527+name = "tokio"
528+version = "1.51.1"
529+source = "registry+https://github.com/rust-lang/crates.io-index"
530+checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c"
531+dependencies = [
532+ "pin-project-lite",
533+ "tokio-macros",
534+]
535+
536+[[package]]
537+name = "tokio-macros"
538+version = "2.7.0"
539+source = "registry+https://github.com/rust-lang/crates.io-index"
540+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
541+dependencies = [
542+ "proc-macro2",
543+ "quote",
544+ "syn",
545+]
546+
547+[[package]]
548+name = "tokio-util"
549+version = "0.7.18"
550+source = "registry+https://github.com/rust-lang/crates.io-index"
551+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
552+dependencies = [
553+ "bytes",
554+ "futures-core",
555+ "futures-sink",
556+ "pin-project-lite",
557+ "tokio",
558+]
559+
560+[[package]]
561+name = "tower"
562+version = "0.4.13"
563+source = "registry+https://github.com/rust-lang/crates.io-index"
564+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
565+dependencies = [
566+ "futures-core",
567+ "futures-util",
568+ "pin-project",
569+ "pin-project-lite",
570+ "tower-layer",
571+ "tower-service",
572+]
573+
574+[[package]]
575+name = "tower-layer"
576+version = "0.3.3"
577+source = "registry+https://github.com/rust-lang/crates.io-index"
578+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
579+
580+[[package]]
581+name = "tower-lsp"
582+version = "0.20.0"
583+source = "registry+https://github.com/rust-lang/crates.io-index"
584+checksum = "d4ba052b54a6627628d9b3c34c176e7eda8359b7da9acd497b9f20998d118508"
585+dependencies = [
586+ "async-trait",
587+ "auto_impl",
588+ "bytes",
589+ "dashmap",
590+ "futures",
591+ "httparse",
592+ "lsp-types",
593+ "memchr",
594+ "serde",
595+ "serde_json",
596+ "tokio",
597+ "tokio-util",
598+ "tower",
599+ "tower-lsp-macros",
600+ "tracing",
601+]
602+
603+[[package]]
604+name = "tower-lsp-macros"
605+version = "0.9.0"
606+source = "registry+https://github.com/rust-lang/crates.io-index"
607+checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa"
608+dependencies = [
609+ "proc-macro2",
610+ "quote",
611+ "syn",
612+]
613+
614+[[package]]
615+name = "tower-service"
616+version = "0.3.3"
617+source = "registry+https://github.com/rust-lang/crates.io-index"
618+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
619+
620+[[package]]
621+name = "tracing"
622+version = "0.1.44"
623+source = "registry+https://github.com/rust-lang/crates.io-index"
624+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
625+dependencies = [
626+ "pin-project-lite",
627+ "tracing-attributes",
628+ "tracing-core",
629+]
630+
631+[[package]]
632+name = "tracing-attributes"
633+version = "0.1.31"
634+source = "registry+https://github.com/rust-lang/crates.io-index"
635+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
636+dependencies = [
637+ "proc-macro2",
638+ "quote",
639+ "syn",
640+]
641+
642+[[package]]
643+name = "tracing-core"
644+version = "0.1.36"
645+source = "registry+https://github.com/rust-lang/crates.io-index"
646+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
647+dependencies = [
648+ "once_cell",
649+]
650+
651+[[package]]
652+name = "unicode-ident"
653+version = "1.0.24"
654+source = "registry+https://github.com/rust-lang/crates.io-index"
655+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
656+
657+[[package]]
658+name = "url"
659+version = "2.5.8"
660+source = "registry+https://github.com/rust-lang/crates.io-index"
661+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
662+dependencies = [
663+ "form_urlencoded",
664+ "idna",
665+ "percent-encoding",
666+ "serde",
667+ "serde_derive",
668+]
669+
670+[[package]]
671+name = "utf8_iter"
672+version = "1.0.4"
673+source = "registry+https://github.com/rust-lang/crates.io-index"
674+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
675+
676+[[package]]
677+name = "windows-link"
678+version = "0.2.1"
679+source = "registry+https://github.com/rust-lang/crates.io-index"
680+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
681+
682+[[package]]
683+name = "writeable"
684+version = "0.6.3"
685+source = "registry+https://github.com/rust-lang/crates.io-index"
686+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
687+
688+[[package]]
689+name = "yoke"
690+version = "0.8.2"
691+source = "registry+https://github.com/rust-lang/crates.io-index"
692+checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
693+dependencies = [
694+ "stable_deref_trait",
695+ "yoke-derive",
696+ "zerofrom",
697+]
698+
699+[[package]]
700+name = "yoke-derive"
701+version = "0.8.2"
702+source = "registry+https://github.com/rust-lang/crates.io-index"
703+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
704+dependencies = [
705+ "proc-macro2",
706+ "quote",
707+ "syn",
708+ "synstructure",
709+]
710+
711+[[package]]
712+name = "zerofrom"
713+version = "0.1.7"
714+source = "registry+https://github.com/rust-lang/crates.io-index"
715+checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
716+dependencies = [
717+ "zerofrom-derive",
718+]
719+
720+[[package]]
721+name = "zerofrom-derive"
722+version = "0.1.7"
723+source = "registry+https://github.com/rust-lang/crates.io-index"
724+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
725+dependencies = [
726+ "proc-macro2",
727+ "quote",
728+ "syn",
729+ "synstructure",
730+]
731+
732+[[package]]
733+name = "zerotrie"
734+version = "0.2.4"
735+source = "registry+https://github.com/rust-lang/crates.io-index"
736+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
737+dependencies = [
738+ "displaydoc",
739+ "yoke",
740+ "zerofrom",
741+]
742+
743+[[package]]
744+name = "zerovec"
745+version = "0.11.6"
746+source = "registry+https://github.com/rust-lang/crates.io-index"
747+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
748+dependencies = [
749+ "yoke",
750+ "zerofrom",
751+ "zerovec-derive",
752+]
753+
754+[[package]]
755+name = "zerovec-derive"
756+version = "0.11.3"
757+source = "registry+https://github.com/rust-lang/crates.io-index"
758+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
759+dependencies = [
760+ "proc-macro2",
761+ "quote",
762+ "syn",
763+]
764+
765+[[package]]
766+name = "zmij"
767+version = "1.0.21"
768+source = "registry+https://github.com/rust-lang/crates.io-index"
769+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
Cargo.toml+2
@@ -4,3 +4,5 @@ version = "0.1.0"
44edition = "2024"
55
66[dependencies]
7+tokio = { version = "1", features = ["io-std", "macros", "rt-multi-thread"] }
8+tower-lsp = "0.20"
src/bin/htmlang-lsp.rsnew file+610
@@ -0,0 +1,610 @@
1+use std::collections::HashMap;
2+use std::sync::Arc;
3+
4+use tokio::sync::RwLock;
5+use tower_lsp::jsonrpc::Result;
6+use tower_lsp::lsp_types::*;
7+use tower_lsp::{Client, LanguageServer, LspService, Server};
8+
9+// ---------------------------------------------------------------------------
10+// Backend
11+// ---------------------------------------------------------------------------
12+
13+struct Backend {
14+ client: Client,
15+ documents: Arc<RwLock<HashMap<Url, String>>>,
16+}
17+
18+impl Backend {
19+ async fn on_change(&self, uri: Url, text: String) {
20+ let diags = match htmlang::parser::parse(&text) {
21+ Ok(_) => vec![],
22+ Err(e) => {
23+ let line = e.line.saturating_sub(1) as u32;
24+ vec![Diagnostic {
25+ range: Range::new(Position::new(line, 0), Position::new(line, 1000)),
26+ severity: Some(DiagnosticSeverity::ERROR),
27+ source: Some("htmlang".into()),
28+ message: e.message,
29+ ..Default::default()
30+ }]
31+ }
32+ };
33+ self.client
34+ .publish_diagnostics(uri.clone(), diags, None)
35+ .await;
36+ self.documents.write().await.insert(uri, text);
37+ }
38+}
39+
40+#[tower_lsp::async_trait]
41+impl LanguageServer for Backend {
42+ async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
43+ Ok(InitializeResult {
44+ capabilities: ServerCapabilities {
45+ text_document_sync: Some(TextDocumentSyncCapability::Kind(
46+ TextDocumentSyncKind::FULL,
47+ )),
48+ completion_provider: Some(CompletionOptions {
49+ trigger_characters: Some(vec![
50+ "@".into(),
51+ "$".into(),
52+ "[".into(),
53+ ",".into(),
54+ ]),
55+ ..Default::default()
56+ }),
57+ hover_provider: Some(HoverProviderCapability::Simple(true)),
58+ ..Default::default()
59+ },
60+ ..Default::default()
61+ })
62+ }
63+
64+ async fn initialized(&self, _: InitializedParams) {}
65+
66+ async fn shutdown(&self) -> Result<()> {
67+ Ok(())
68+ }
69+
70+ async fn did_open(&self, params: DidOpenTextDocumentParams) {
71+ self.on_change(params.text_document.uri, params.text_document.text)
72+ .await;
73+ }
74+
75+ async fn did_change(&self, params: DidChangeTextDocumentParams) {
76+ if let Some(change) = params.content_changes.into_iter().last() {
77+ self.on_change(params.text_document.uri, change.text).await;
78+ }
79+ }
80+
81+ async fn did_close(&self, params: DidCloseTextDocumentParams) {
82+ self.documents
83+ .write()
84+ .await
85+ .remove(&params.text_document.uri);
86+ self.client
87+ .publish_diagnostics(params.text_document.uri, vec![], None)
88+ .await;
89+ }
90+
91+ async fn completion(
92+ &self,
93+ params: CompletionParams,
94+ ) -> Result<Option<CompletionResponse>> {
95+ let uri = &params.text_document_position.text_document.uri;
96+ let pos = params.text_document_position.position;
97+ let docs = self.documents.read().await;
98+ let text = match docs.get(uri) {
99+ Some(t) => t.clone(),
100+ None => return Ok(None),
101+ };
102+ drop(docs);
103+
104+ let items = completions(&text, pos);
105+ Ok(if items.is_empty() {
106+ None
107+ } else {
108+ Some(CompletionResponse::Array(items))
109+ })
110+ }
111+
112+ async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
113+ let uri = &params.text_document_position_params.text_document.uri;
114+ let pos = params.text_document_position_params.position;
115+ let docs = self.documents.read().await;
116+ let text = match docs.get(uri) {
117+ Some(t) => t.clone(),
118+ None => return Ok(None),
119+ };
120+ drop(docs);
121+
122+ Ok(hover_at(&text, pos))
123+ }
124+}
125+
126+// ---------------------------------------------------------------------------
127+// Completions
128+// ---------------------------------------------------------------------------
129+
130+fn completions(text: &str, position: Position) -> Vec<CompletionItem> {
131+ let lines: Vec<&str> = text.lines().collect();
132+ let line = match lines.get(position.line as usize) {
133+ Some(l) => *l,
134+ None => return vec![],
135+ };
136+
137+ let col = (position.character as usize).min(line.len());
138+ let before = &line[..col];
139+
140+ let word_start = find_word_start(before);
141+ let edit_range = Range::new(
142+ Position::new(position.line, word_start as u32),
143+ position,
144+ );
145+
146+ // Inside attribute brackets?
147+ if in_brackets(before) {
148+ let current_word = &before[word_start..];
149+
150+ // $ variable/define reference
151+ if current_word.starts_with('$') {
152+ return variable_completions(text, edit_range);
153+ }
154+
155+ // State prefix (hover:, active:, focus:)
156+ if let Some(colon) = current_word.find(':') {
157+ let prefix = &current_word[..colon];
158+ if matches!(prefix, "hover" | "active" | "focus") {
159+ return state_attr_completions(prefix, edit_range);
160+ }
161+ }
162+
163+ return attr_completions(edit_range);
164+ }
165+
166+ // $ variable reference outside brackets
167+ let current_word = &before[word_start..];
168+ if current_word.starts_with('$') {
169+ return variable_completions(text, edit_range);
170+ }
171+
172+ // @ element/directive or start of line
173+ let trimmed = before.trim_start();
174+ if trimmed.is_empty() || trimmed.starts_with('@') {
175+ let mut items = element_completions(edit_range);
176+ items.extend(directive_completions(edit_range));
177+ items.extend(function_completions(text, edit_range));
178+ return items;
179+ }
180+
181+ vec![]
182+}
183+
184+fn find_word_start(text: &str) -> usize {
185+ let bytes = text.as_bytes();
186+ let mut i = bytes.len();
187+ while i > 0 {
188+ let c = bytes[i - 1];
189+ if c.is_ascii_alphanumeric()
190+ || c == b'@'
191+ || c == b'$'
192+ || c == b'-'
193+ || c == b'_'
194+ || c == b':'
195+ {
196+ i -= 1;
197+ } else {
198+ break;
199+ }
200+ }
201+ i
202+}
203+
204+fn in_brackets(text: &str) -> bool {
205+ let mut depth: i32 = 0;
206+ for ch in text.chars() {
207+ if ch == '[' {
208+ depth += 1;
209+ } else if ch == ']' {
210+ depth -= 1;
211+ }
212+ }
213+ depth > 0
214+}
215+
216+fn item(
217+ label: &str,
218+ kind: CompletionItemKind,
219+ detail: &str,
220+ insert: &str,
221+ range: Range,
222+) -> CompletionItem {
223+ CompletionItem {
224+ label: label.to_string(),
225+ kind: Some(kind),
226+ detail: Some(detail.to_string()),
227+ text_edit: Some(CompletionTextEdit::Edit(TextEdit {
228+ range,
229+ new_text: insert.to_string(),
230+ })),
231+ ..Default::default()
232+ }
233+}
234+
235+fn element_completions(range: Range) -> Vec<CompletionItem> {
236+ [
237+ ("@row", "Horizontal layout (flexbox row)"),
238+ ("@column", "Vertical layout (flexbox column)"),
239+ ("@col", "Vertical layout (short for @column)"),
240+ ("@el", "Generic container"),
241+ ("@text", "Styled inline text (span)"),
242+ ("@paragraph", "Flowing text block (p)"),
243+ ("@p", "Flowing text block (short for @paragraph)"),
244+ ("@image", "Image element"),
245+ ("@img", "Image element (short for @image)"),
246+ ("@link", "Anchor/link element"),
247+ ("@raw", "Raw HTML escape hatch"),
248+ ("@children", "Slot for caller's children (inside @fn)"),
249+ ]
250+ .iter()
251+ .map(|(name, detail)| item(name, CompletionItemKind::KEYWORD, detail, name, range))
252+ .collect()
253+}
254+
255+fn directive_completions(range: Range) -> Vec<CompletionItem> {
256+ [
257+ ("@page", "Set HTML page title", "@page "),
258+ ("@let", "Define a variable", "@let "),
259+ ("@define", "Define an attribute bundle", "@define "),
260+ ("@fn", "Define a reusable function", "@fn "),
261+ ]
262+ .iter()
263+ .map(|(name, detail, insert)| {
264+ item(name, CompletionItemKind::SNIPPET, detail, insert, range)
265+ })
266+ .collect()
267+}
268+
269+fn attr_completions(range: Range) -> Vec<CompletionItem> {
270+ [
271+ // Layout
272+ ("spacing", "Gap between children (px)", true),
273+ ("padding", "Inner padding (1/2/4 values, px)", true),
274+ ("padding-x", "Horizontal padding (px)", true),
275+ ("padding-y", "Vertical padding (px)", true),
276+ // Sizing
277+ ("width", "Width (px | fill | shrink)", true),
278+ ("height", "Height (px | fill | shrink)", true),
279+ ("min-width", "Minimum width (px)", true),
280+ ("max-width", "Maximum width (px)", true),
281+ ("min-height", "Minimum height (px)", true),
282+ ("max-height", "Maximum height (px)", true),
283+ // Alignment
284+ ("center-x", "Center horizontally", false),
285+ ("center-y", "Center vertically", false),
286+ ("align-left", "Align left", false),
287+ ("align-right", "Align right", false),
288+ ("align-top", "Align top", false),
289+ ("align-bottom", "Align bottom", false),
290+ // Style
291+ ("background", "Background color/value", true),
292+ ("color", "Text color", true),
293+ ("border", "Border (width [color])", true),
294+ ("rounded", "Border radius (px)", true),
295+ ("bold", "Bold text", false),
296+ ("italic", "Italic text", false),
297+ ("underline", "Underlined text", false),
298+ ("size", "Font size (px)", true),
299+ ("font", "Font family", true),
300+ ("transition", "CSS transition", true),
301+ ("cursor", "CSS cursor type", true),
302+ ("opacity", "Opacity (0-1)", true),
303+ // Flow
304+ ("wrap", "Enable flex-wrap", false),
305+ // Identity
306+ ("id", "HTML id attribute", true),
307+ ("class", "HTML class attribute", true),
308+ // State prefixes
309+ ("hover:", "Style on hover", false),
310+ ("active:", "Style on active/click", false),
311+ ("focus:", "Style on focus", false),
312+ ]
313+ .iter()
314+ .map(|(name, detail, takes_value)| {
315+ let insert = if *takes_value {
316+ format!("{} ", name)
317+ } else {
318+ name.to_string()
319+ };
320+ item(name, CompletionItemKind::PROPERTY, detail, &insert, range)
321+ })
322+ .collect()
323+}
324+
325+fn state_attr_completions(prefix: &str, range: Range) -> Vec<CompletionItem> {
326+ [
327+ ("background", "Background color/value", true),
328+ ("color", "Text color", true),
329+ ("border", "Border (width [color])", true),
330+ ("rounded", "Border radius (px)", true),
331+ ("bold", "Bold text", false),
332+ ("italic", "Italic text", false),
333+ ("underline", "Underlined text", false),
334+ ("size", "Font size (px)", true),
335+ ("opacity", "Opacity (0-1)", true),
336+ ("cursor", "CSS cursor type", true),
337+ ]
338+ .iter()
339+ .map(|(name, detail, takes_value)| {
340+ let full = format!("{}:{}", prefix, name);
341+ let insert = if *takes_value {
342+ format!("{} ", full)
343+ } else {
344+ full.clone()
345+ };
346+ item(&full, CompletionItemKind::PROPERTY, detail, &insert, range)
347+ })
348+ .collect()
349+}
350+
351+fn variable_completions(text: &str, range: Range) -> Vec<CompletionItem> {
352+ let mut items = Vec::new();
353+
354+ for line in text.lines() {
355+ let trimmed = line.trim();
356+ if let Some(rest) = trimmed.strip_prefix("@let ") {
357+ if let Some((name, value)) = rest.trim().split_once(' ') {
358+ items.push(item(
359+ &format!("${}", name),
360+ CompletionItemKind::VARIABLE,
361+ &format!("= {}", value.trim()),
362+ &format!("${}", name),
363+ range,
364+ ));
365+ }
366+ } else if let Some(rest) = trimmed.strip_prefix("@define ") {
367+ if let Some(bracket) = rest.find('[') {
368+ let name = rest[..bracket].trim();
369+ items.push(item(
370+ &format!("${}", name),
371+ CompletionItemKind::CONSTANT,
372+ "Attribute bundle",
373+ &format!("${}", name),
374+ range,
375+ ));
376+ }
377+ }
378+ }
379+
380+ items
381+}
382+
383+fn function_completions(text: &str, range: Range) -> Vec<CompletionItem> {
384+ let mut items = Vec::new();
385+
386+ for line in text.lines() {
387+ let trimmed = line.trim();
388+ if let Some(rest) = trimmed.strip_prefix("@fn ") {
389+ let parts: Vec<&str> = rest.split_whitespace().collect();
390+ if let Some(name) = parts.first() {
391+ let params = parts[1..].join(" ");
392+ let detail = if params.is_empty() {
393+ "Function".to_string()
394+ } else {
395+ format!("Function({})", params)
396+ };
397+ items.push(item(
398+ &format!("@{}", name),
399+ CompletionItemKind::FUNCTION,
400+ &detail,
401+ &format!("@{}", name),
402+ range,
403+ ));
404+ }
405+ }
406+ }
407+
408+ items
409+}
410+
411+// ---------------------------------------------------------------------------
412+// Hover
413+// ---------------------------------------------------------------------------
414+
415+fn hover_at(text: &str, position: Position) -> Option<Hover> {
416+ let lines: Vec<&str> = text.lines().collect();
417+ let line = lines.get(position.line as usize)?;
418+ let col = (position.character as usize).min(line.len());
419+ let word = word_at(line, col)?;
420+
421+ let doc = if word.starts_with('$') {
422+ hover_variable(text, &word[1..])
423+ } else if word.starts_with('@') {
424+ hover_user_fn(text, &word[1..]).or_else(|| hover_builtin(&word))
425+ } else {
426+ hover_builtin(&word)
427+ }?;
428+
429+ Some(Hover {
430+ contents: HoverContents::Markup(MarkupContent {
431+ kind: MarkupKind::Markdown,
432+ value: doc,
433+ }),
434+ range: None,
435+ })
436+}
437+
438+fn word_at(line: &str, col: usize) -> Option<String> {
439+ let bytes = line.as_bytes();
440+ let mut start = col;
441+ while start > 0 && is_word_byte(bytes[start - 1]) {
442+ start -= 1;
443+ }
444+ let mut end = col;
445+ while end < bytes.len() && is_word_byte(bytes[end]) {
446+ end += 1;
447+ }
448+ if start == end {
449+ return None;
450+ }
451+ Some(line[start..end].to_string())
452+}
453+
454+fn is_word_byte(c: u8) -> bool {
455+ c.is_ascii_alphanumeric() || c == b'@' || c == b'$' || c == b'-' || c == b'_' || c == b':'
456+}
457+
458+fn hover_variable(text: &str, name: &str) -> Option<String> {
459+ for line in text.lines() {
460+ let trimmed = line.trim();
461+ if let Some(rest) = trimmed.strip_prefix("@let ") {
462+ if let Some((n, v)) = rest.trim().split_once(' ') {
463+ if n == name {
464+ return Some(format!("**${}** = `{}`", name, v.trim()));
465+ }
466+ }
467+ }
468+ }
469+
470+ for line in text.lines() {
471+ let trimmed = line.trim();
472+ if let Some(rest) = trimmed.strip_prefix("@define ") {
473+ let rest = rest.trim();
474+ if let Some(bracket) = rest.find('[') {
475+ if rest[..bracket].trim() == name {
476+ return Some(format!(
477+ "**${}** \u{2014} Attribute bundle\n\n`{}`",
478+ name, trimmed
479+ ));
480+ }
481+ }
482+ }
483+ }
484+
485+ for line in text.lines() {
486+ let trimmed = line.trim();
487+ if let Some(rest) = trimmed.strip_prefix("@fn ") {
488+ let parts: Vec<&str> = rest.split_whitespace().collect();
489+ if let Some(fn_name) = parts.first() {
490+ for param in &parts[1..] {
491+ let p = param.strip_prefix('$').unwrap_or(param);
492+ if p == name {
493+ return Some(format!(
494+ "**${}** \u{2014} Parameter of `@{}`",
495+ name, fn_name
496+ ));
497+ }
498+ }
499+ }
500+ }
501+ }
502+
503+ None
504+}
505+
506+fn hover_user_fn(text: &str, name: &str) -> Option<String> {
507+ for line in text.lines() {
508+ let trimmed = line.trim();
509+ if let Some(rest) = trimmed.strip_prefix("@fn ") {
510+ let parts: Vec<&str> = rest.split_whitespace().collect();
511+ if parts.first() == Some(&name) {
512+ let params = &parts[1..];
513+ let params_str = if params.is_empty() {
514+ String::new()
515+ } else {
516+ format!("\n\nParameters: {}", params.join(", "))
517+ };
518+ return Some(format!(
519+ "**@{}** \u{2014} User function{}",
520+ name, params_str
521+ ));
522+ }
523+ }
524+ }
525+ None
526+}
527+
528+fn hover_builtin(word: &str) -> Option<String> {
529+ // Strip state prefix for attribute lookup
530+ let (state, base) = if let Some(rest) = word.strip_prefix("hover:") {
531+ (Some("hover"), rest)
532+ } else if let Some(rest) = word.strip_prefix("active:") {
533+ (Some("active"), rest)
534+ } else if let Some(rest) = word.strip_prefix("focus:") {
535+ (Some("focus"), rest)
536+ } else {
537+ (None, word)
538+ };
539+
540+ let doc = match base {
541+ // Elements
542+ "@row" => "**@row** \u{2014} Horizontal layout\n\nRenders as `<div>` with `display: flex; flex-direction: row`.\n\nChildren are laid out left-to-right.",
543+ "@column" | "@col" => "**@column** \u{2014} Vertical layout\n\nRenders as `<div>` with `display: flex; flex-direction: column`.\n\nChildren are laid out top-to-bottom.",
544+ "@el" => "**@el** \u{2014} Generic container\n\nRenders as `<div>` with column flex layout.",
545+ "@text" => "**@text** \u{2014} Inline text\n\nRenders as `<span>`.\n\nUsage: `@text [bold, size 24] Hello world`",
546+ "@paragraph" | "@p" => "**@paragraph** \u{2014} Text block\n\nRenders as `<p>`.\n\nSupports inline elements: `{@text [bold] word}`",
547+ "@image" | "@img" => "**@image** \u{2014} Image\n\nRenders as `<img>`.\n\nUsage: `@image [width 200] https://example.com/photo.jpg`",
548+ "@link" => "**@link** \u{2014} Hyperlink\n\nRenders as `<a>`.\n\nUsage: `@link [color blue] https://example.com Link text`",
549+ "@raw" => "**@raw** \u{2014} Raw HTML\n\nPasses content through without processing.\n\nUsage: `@raw \"\"\"<div>custom html</div>\"\"\"`",
550+ "@page" => "**@page** \u{2014} Page title\n\nSets the HTML `<title>` and wraps output in a full document.\n\nUsage: `@page My Page Title`",
551+ "@let" => "**@let** \u{2014} Variable\n\nDefines a variable for `$name` substitution.\n\nUsage: `@let primary #3b82f6`",
552+ "@define" => "**@define** \u{2014} Attribute bundle\n\nDefines a reusable set of attributes.\n\nUsage: `@define card-style [padding 20, rounded 8]`\n\nApply with `$card-style` in attribute lists.",
553+ "@fn" => "**@fn** \u{2014} Function\n\nDefines a reusable component.\n\n```\n@fn card $title\n @el [padding 20]\n @text [bold] $title\n @children\n```",
554+ "@children" => "**@children** \u{2014} Children slot\n\nPlaceholder inside `@fn` body replaced with the caller's children.",
555+ // Attributes
556+ "spacing" => "**spacing** `<value>`\n\nGap between children in pixels. Maps to CSS `gap`.",
557+ "padding" => "**padding** `<value>` | `<y> <x>` | `<t> <r> <b> <l>`\n\nInner padding in pixels. Accepts 1, 2, or 4 values.",
558+ "padding-x" => "**padding-x** `<value>`\n\nHorizontal padding (left + right) in pixels.",
559+ "padding-y" => "**padding-y** `<value>`\n\nVertical padding (top + bottom) in pixels.",
560+ "width" => "**width** `<px>` | `fill` | `shrink`\n\n- Number: fixed width in pixels\n- `fill`: expand to fill parent\n- `shrink`: prevent flex shrinking",
561+ "height" => "**height** `<px>` | `fill` | `shrink`\n\n- Number: fixed height in pixels\n- `fill`: expand to fill parent\n- `shrink`: prevent flex shrinking",
562+ "min-width" => "**min-width** `<value>` \u{2014} Minimum width in pixels.",
563+ "max-width" => "**max-width** `<value>` \u{2014} Maximum width in pixels.",
564+ "min-height" => "**min-height** `<value>` \u{2014} Minimum height in pixels.",
565+ "max-height" => "**max-height** `<value>` \u{2014} Maximum height in pixels.",
566+ "center-x" => "**center-x**\n\nCenter horizontally.\n\nIn column parent: `align-self: center`\nOtherwise: auto margins.",
567+ "center-y" => "**center-y**\n\nCenter vertically.\n\nIn row parent: `align-self: center`\nOtherwise: auto margins.",
568+ "align-left" => "**align-left** \u{2014} Align to the left edge.",
569+ "align-right" => "**align-right** \u{2014} Align to the right edge.",
570+ "align-top" => "**align-top** \u{2014} Align to the top edge.",
571+ "align-bottom" => "**align-bottom** \u{2014} Align to the bottom edge.",
572+ "background" => "**background** `<color>` \u{2014} Background color or CSS background value.",
573+ "color" => "**color** `<color>` \u{2014} Text color.",
574+ "border" => "**border** `<width> [color]`\n\nBorder. Width in pixels, color defaults to `currentColor`.",
575+ "rounded" => "**rounded** `<value>` \u{2014} Border radius in pixels.",
576+ "bold" => "**bold** \u{2014} Bold text (`font-weight: bold`).",
577+ "italic" => "**italic** \u{2014} Italic text (`font-style: italic`).",
578+ "underline" => "**underline** \u{2014} Underlined text.",
579+ "size" => "**size** `<value>` \u{2014} Font size in pixels.",
580+ "font" => "**font** `<family>` \u{2014} Font family.",
581+ "transition" => "**transition** `<value>` \u{2014} CSS transition (e.g., `all 0.15s ease`).",
582+ "cursor" => "**cursor** `<value>` \u{2014} CSS cursor (e.g., `pointer`).",
583+ "opacity" => "**opacity** `<value>` \u{2014} Opacity from 0 to 1.",
584+ "wrap" => "**wrap** \u{2014} Enable flex-wrap for children.",
585+ "id" => "**id** `<value>` \u{2014} HTML id attribute.",
586+ "class" => "**class** `<value>` \u{2014} HTML class attribute.",
587+ _ => return None,
588+ };
589+
590+ Some(if let Some(state) = state {
591+ format!("*({} state)* {}", state, doc)
592+ } else {
593+ doc.to_string()
594+ })
595+}
596+
597+// ---------------------------------------------------------------------------
598+// Entry point
599+// ---------------------------------------------------------------------------
600+
601+#[tokio::main]
602+async fn main() {
603+ let stdin = tokio::io::stdin();
604+ let stdout = tokio::io::stdout();
605+ let (service, socket) = LspService::new(|client| Backend {
606+ client,
607+ documents: Arc::new(RwLock::new(HashMap::new())),
608+ });
609+ Server::new(stdin, stdout, socket).serve(service).await;
610+}
src/lib.rsnew file+3
@@ -0,0 +1,3 @@
1+pub mod ast;
2+pub mod codegen;
3+pub mod parser;
src/main.rs+2−6
@@ -3,10 +3,6 @@ use std::fs;
33use std::path::Path;
44use std::process;
55
6−mod ast;
7−mod codegen;
8−mod parser;
9−
106fn main() {
117 let args: Vec<String> = env::args().collect();
128 if args.len() < 2 {
@@ -23,7 +19,7 @@ fn main() {
2319 }
2420 };
2521
26− let doc = match parser::parse(&input) {
22+ let doc = match htmlang::parser::parse(&input) {
2723 Ok(d) => d,
2824 Err(e) => {
2925 eprintln!("parse error: {}", e);
@@ -31,7 +27,7 @@ fn main() {
3127 }
3228 };
3329
34− let html = codegen::generate(&doc);
30+ let html = htmlang::codegen::generate(&doc);
3531
3632 let out_path = Path::new(input_path).with_extension("html");
3733 match fs::write(&out_path, &html) {