A human-friendly DSL for ATProto Lexicons
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Initial LSP support

+1555 -50
+280 -14
Cargo.lock
··· 122 122 checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 123 123 124 124 [[package]] 125 + name = "auto_impl" 126 + version = "1.3.0" 127 + source = "registry+https://github.com/rust-lang/crates.io-index" 128 + checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" 129 + dependencies = [ 130 + "proc-macro2", 131 + "quote", 132 + "syn 2.0.106", 133 + ] 134 + 135 + [[package]] 125 136 name = "autocfg" 126 137 version = "1.5.0" 127 138 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 156 167 version = "0.22.1" 157 168 source = "registry+https://github.com/rust-lang/crates.io-index" 158 169 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 170 + 171 + [[package]] 172 + name = "bitflags" 173 + version = "1.3.2" 174 + source = "registry+https://github.com/rust-lang/crates.io-index" 175 + checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 159 176 160 177 [[package]] 161 178 name = "bitflags" ··· 362 379 ] 363 380 364 381 [[package]] 382 + name = "dashmap" 383 + version = "5.5.3" 384 + source = "registry+https://github.com/rust-lang/crates.io-index" 385 + checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" 386 + dependencies = [ 387 + "cfg-if", 388 + "hashbrown 0.14.5", 389 + "lock_api", 390 + "once_cell", 391 + "parking_lot_core", 392 + ] 393 + 394 + [[package]] 365 395 name = "data-encoding" 366 396 version = "2.9.0" 367 397 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 477 507 ] 478 508 479 509 [[package]] 510 + name = "futures" 511 + version = "0.3.31" 512 + source = "registry+https://github.com/rust-lang/crates.io-index" 513 + checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 514 + dependencies = [ 515 + "futures-channel", 516 + "futures-core", 517 + "futures-io", 518 + "futures-sink", 519 + "futures-task", 520 + "futures-util", 521 + ] 522 + 523 + [[package]] 480 524 name = "futures-channel" 481 525 version = "0.3.31" 482 526 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 499 543 checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 500 544 501 545 [[package]] 546 + name = "futures-macro" 547 + version = "0.3.31" 548 + source = "registry+https://github.com/rust-lang/crates.io-index" 549 + checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 550 + dependencies = [ 551 + "proc-macro2", 552 + "quote", 553 + "syn 2.0.106", 554 + ] 555 + 556 + [[package]] 502 557 name = "futures-sink" 503 558 version = "0.3.31" 504 559 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 516 571 source = "registry+https://github.com/rust-lang/crates.io-index" 517 572 checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 518 573 dependencies = [ 574 + "futures-channel", 519 575 "futures-core", 520 576 "futures-io", 577 + "futures-macro", 521 578 "futures-sink", 522 579 "futures-task", 523 580 "memchr", ··· 599 656 "cfg-if", 600 657 "crunchy", 601 658 ] 659 + 660 + [[package]] 661 + name = "hashbrown" 662 + version = "0.14.5" 663 + source = "registry+https://github.com/rust-lang/crates.io-index" 664 + checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 602 665 603 666 [[package]] 604 667 name = "hashbrown" ··· 940 1003 checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" 941 1004 dependencies = [ 942 1005 "equivalent", 943 - "hashbrown", 1006 + "hashbrown 0.16.0", 944 1007 ] 945 1008 946 1009 [[package]] ··· 964 1027 source = "registry+https://github.com/rust-lang/crates.io-index" 965 1028 checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" 966 1029 dependencies = [ 967 - "bitflags", 1030 + "bitflags 2.9.4", 968 1031 "cfg-if", 969 1032 "libc", 970 1033 ] ··· 1036 1099 ] 1037 1100 1038 1101 [[package]] 1102 + name = "lazy_static" 1103 + version = "1.5.0" 1104 + source = "registry+https://github.com/rust-lang/crates.io-index" 1105 + checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1106 + 1107 + [[package]] 1039 1108 name = "libc" 1040 1109 version = "0.2.176" 1041 1110 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1084 1153 ] 1085 1154 1086 1155 [[package]] 1156 + name = "lsp-types" 1157 + version = "0.94.1" 1158 + source = "registry+https://github.com/rust-lang/crates.io-index" 1159 + checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" 1160 + dependencies = [ 1161 + "bitflags 1.3.2", 1162 + "serde", 1163 + "serde_json", 1164 + "serde_repr", 1165 + "url", 1166 + ] 1167 + 1168 + [[package]] 1169 + name = "matchers" 1170 + version = "0.2.0" 1171 + source = "registry+https://github.com/rust-lang/crates.io-index" 1172 + checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" 1173 + dependencies = [ 1174 + "regex-automata", 1175 + ] 1176 + 1177 + [[package]] 1087 1178 name = "memchr" 1088 1179 version = "2.7.6" 1089 1180 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1206 1297 ] 1207 1298 1208 1299 [[package]] 1209 - name = "mlf-codegen-java" 1210 - version = "0.1.0" 1211 - 1212 - [[package]] 1213 1300 name = "mlf-codegen-rust" 1214 1301 version = "0.1.0" 1215 1302 dependencies = [ ··· 1242 1329 ] 1243 1330 1244 1331 [[package]] 1332 + name = "mlf-lsp" 1333 + version = "0.1.0" 1334 + dependencies = [ 1335 + "mlf-diagnostics", 1336 + "mlf-lang", 1337 + "serde", 1338 + "serde_json", 1339 + "tokio", 1340 + "tower-lsp", 1341 + "tracing", 1342 + "tracing-subscriber", 1343 + ] 1344 + 1345 + [[package]] 1245 1346 name = "mlf-playground-wasm" 1246 1347 version = "0.1.0" 1247 1348 dependencies = [ ··· 1318 1419 ] 1319 1420 1320 1421 [[package]] 1422 + name = "nu-ansi-term" 1423 + version = "0.50.3" 1424 + source = "registry+https://github.com/rust-lang/crates.io-index" 1425 + checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" 1426 + dependencies = [ 1427 + "windows-sys 0.61.1", 1428 + ] 1429 + 1430 + [[package]] 1321 1431 name = "num-conv" 1322 1432 version = "0.1.0" 1323 1433 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1359 1469 source = "registry+https://github.com/rust-lang/crates.io-index" 1360 1470 checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" 1361 1471 dependencies = [ 1362 - "bitflags", 1472 + "bitflags 2.9.4", 1363 1473 "cfg-if", 1364 1474 "foreign-types", 1365 1475 "libc", ··· 1433 1543 checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 1434 1544 1435 1545 [[package]] 1546 + name = "pin-project" 1547 + version = "1.1.10" 1548 + source = "registry+https://github.com/rust-lang/crates.io-index" 1549 + checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" 1550 + dependencies = [ 1551 + "pin-project-internal", 1552 + ] 1553 + 1554 + [[package]] 1555 + name = "pin-project-internal" 1556 + version = "1.1.10" 1557 + source = "registry+https://github.com/rust-lang/crates.io-index" 1558 + checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" 1559 + dependencies = [ 1560 + "proc-macro2", 1561 + "quote", 1562 + "syn 2.0.106", 1563 + ] 1564 + 1565 + [[package]] 1436 1566 name = "pin-project-lite" 1437 1567 version = "0.2.16" 1438 1568 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1564 1694 source = "registry+https://github.com/rust-lang/crates.io-index" 1565 1695 checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" 1566 1696 dependencies = [ 1567 - "bitflags", 1697 + "bitflags 2.9.4", 1568 1698 ] 1569 1699 1570 1700 [[package]] ··· 1629 1759 "sync_wrapper", 1630 1760 "tokio", 1631 1761 "tokio-native-tls", 1632 - "tower", 1762 + "tower 0.5.2", 1633 1763 "tower-http", 1634 1764 "tower-service", 1635 1765 "url", ··· 1670 1800 source = "registry+https://github.com/rust-lang/crates.io-index" 1671 1801 checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 1672 1802 dependencies = [ 1673 - "bitflags", 1803 + "bitflags 2.9.4", 1674 1804 "errno", 1675 1805 "libc", 1676 1806 "linux-raw-sys", ··· 1752 1882 source = "registry+https://github.com/rust-lang/crates.io-index" 1753 1883 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1754 1884 dependencies = [ 1755 - "bitflags", 1885 + "bitflags 2.9.4", 1756 1886 "core-foundation", 1757 1887 "core-foundation-sys", 1758 1888 "libc", ··· 1825 1955 ] 1826 1956 1827 1957 [[package]] 1958 + name = "serde_repr" 1959 + version = "0.1.20" 1960 + source = "registry+https://github.com/rust-lang/crates.io-index" 1961 + checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" 1962 + dependencies = [ 1963 + "proc-macro2", 1964 + "quote", 1965 + "syn 2.0.106", 1966 + ] 1967 + 1968 + [[package]] 1828 1969 name = "serde_spanned" 1829 1970 version = "0.6.9" 1830 1971 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1857 1998 ] 1858 1999 1859 2000 [[package]] 2001 + name = "sharded-slab" 2002 + version = "0.1.7" 2003 + source = "registry+https://github.com/rust-lang/crates.io-index" 2004 + checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 2005 + dependencies = [ 2006 + "lazy_static", 2007 + ] 2008 + 2009 + [[package]] 1860 2010 name = "shlex" 1861 2011 version = "1.3.0" 1862 2012 source = "registry+https://github.com/rust-lang/crates.io-index" 1863 2013 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 2014 + 2015 + [[package]] 2016 + name = "signal-hook-registry" 2017 + version = "1.4.6" 2018 + source = "registry+https://github.com/rust-lang/crates.io-index" 2019 + checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" 2020 + dependencies = [ 2021 + "libc", 2022 + ] 1864 2023 1865 2024 [[package]] 1866 2025 name = "slab" ··· 2000 2159 source = "registry+https://github.com/rust-lang/crates.io-index" 2001 2160 checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 2002 2161 dependencies = [ 2003 - "bitflags", 2162 + "bitflags 2.9.4", 2004 2163 "core-foundation", 2005 2164 "system-configuration-sys", 2006 2165 ] ··· 2089 2248 ] 2090 2249 2091 2250 [[package]] 2251 + name = "thread_local" 2252 + version = "1.1.9" 2253 + source = "registry+https://github.com/rust-lang/crates.io-index" 2254 + checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" 2255 + dependencies = [ 2256 + "cfg-if", 2257 + ] 2258 + 2259 + [[package]] 2092 2260 name = "time" 2093 2261 version = "0.3.44" 2094 2262 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2154 2322 "io-uring", 2155 2323 "libc", 2156 2324 "mio", 2325 + "parking_lot", 2157 2326 "pin-project-lite", 2327 + "signal-hook-registry", 2158 2328 "slab", 2159 2329 "socket2 0.6.0", 2330 + "tokio-macros", 2160 2331 "windows-sys 0.59.0", 2161 2332 ] 2162 2333 2163 2334 [[package]] 2335 + name = "tokio-macros" 2336 + version = "2.5.0" 2337 + source = "registry+https://github.com/rust-lang/crates.io-index" 2338 + checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 2339 + dependencies = [ 2340 + "proc-macro2", 2341 + "quote", 2342 + "syn 2.0.106", 2343 + ] 2344 + 2345 + [[package]] 2164 2346 name = "tokio-native-tls" 2165 2347 version = "0.3.1" 2166 2348 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2236 2418 2237 2419 [[package]] 2238 2420 name = "tower" 2421 + version = "0.4.13" 2422 + source = "registry+https://github.com/rust-lang/crates.io-index" 2423 + checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 2424 + dependencies = [ 2425 + "futures-core", 2426 + "futures-util", 2427 + "pin-project", 2428 + "pin-project-lite", 2429 + "tower-layer", 2430 + "tower-service", 2431 + ] 2432 + 2433 + [[package]] 2434 + name = "tower" 2239 2435 version = "0.5.2" 2240 2436 source = "registry+https://github.com/rust-lang/crates.io-index" 2241 2437 checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" ··· 2255 2451 source = "registry+https://github.com/rust-lang/crates.io-index" 2256 2452 checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" 2257 2453 dependencies = [ 2258 - "bitflags", 2454 + "bitflags 2.9.4", 2259 2455 "bytes", 2260 2456 "futures-util", 2261 2457 "http", 2262 2458 "http-body", 2263 2459 "iri-string", 2264 2460 "pin-project-lite", 2265 - "tower", 2461 + "tower 0.5.2", 2266 2462 "tower-layer", 2267 2463 "tower-service", 2268 2464 ] ··· 2272 2468 version = "0.3.3" 2273 2469 source = "registry+https://github.com/rust-lang/crates.io-index" 2274 2470 checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 2471 + 2472 + [[package]] 2473 + name = "tower-lsp" 2474 + version = "0.20.0" 2475 + source = "registry+https://github.com/rust-lang/crates.io-index" 2476 + checksum = "d4ba052b54a6627628d9b3c34c176e7eda8359b7da9acd497b9f20998d118508" 2477 + dependencies = [ 2478 + "async-trait", 2479 + "auto_impl", 2480 + "bytes", 2481 + "dashmap", 2482 + "futures", 2483 + "httparse", 2484 + "lsp-types", 2485 + "memchr", 2486 + "serde", 2487 + "serde_json", 2488 + "tokio", 2489 + "tokio-util", 2490 + "tower 0.4.13", 2491 + "tower-lsp-macros", 2492 + "tracing", 2493 + ] 2494 + 2495 + [[package]] 2496 + name = "tower-lsp-macros" 2497 + version = "0.9.0" 2498 + source = "registry+https://github.com/rust-lang/crates.io-index" 2499 + checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" 2500 + dependencies = [ 2501 + "proc-macro2", 2502 + "quote", 2503 + "syn 2.0.106", 2504 + ] 2275 2505 2276 2506 [[package]] 2277 2507 name = "tower-service" ··· 2308 2538 checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 2309 2539 dependencies = [ 2310 2540 "once_cell", 2541 + "valuable", 2542 + ] 2543 + 2544 + [[package]] 2545 + name = "tracing-log" 2546 + version = "0.2.0" 2547 + source = "registry+https://github.com/rust-lang/crates.io-index" 2548 + checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 2549 + dependencies = [ 2550 + "log", 2551 + "once_cell", 2552 + "tracing-core", 2553 + ] 2554 + 2555 + [[package]] 2556 + name = "tracing-subscriber" 2557 + version = "0.3.20" 2558 + source = "registry+https://github.com/rust-lang/crates.io-index" 2559 + checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" 2560 + dependencies = [ 2561 + "matchers", 2562 + "nu-ansi-term", 2563 + "once_cell", 2564 + "regex-automata", 2565 + "sharded-slab", 2566 + "smallvec", 2567 + "thread_local", 2568 + "tracing", 2569 + "tracing-core", 2570 + "tracing-log", 2311 2571 ] 2312 2572 2313 2573 [[package]] ··· 2399 2659 version = "0.2.2" 2400 2660 source = "registry+https://github.com/rust-lang/crates.io-index" 2401 2661 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 2662 + 2663 + [[package]] 2664 + name = "valuable" 2665 + version = "0.1.1" 2666 + source = "registry+https://github.com/rust-lang/crates.io-index" 2667 + checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 2402 2668 2403 2669 [[package]] 2404 2670 name = "vcpkg"
+8 -2
Cargo.toml
··· 1 1 [workspace] 2 2 resolver = "3" 3 - members = ["codegen-plugins/mlf-codegen-go", "codegen-plugins/mlf-codegen-java", "codegen-plugins/mlf-codegen-rust","codegen-plugins/mlf-codegen-typescript", 4 - "mlf-cli", "mlf-codegen", "mlf-diagnostics", 3 + members = [ 4 + "codegen-plugins/mlf-codegen-go", 5 + "codegen-plugins/mlf-codegen-rust", 6 + "codegen-plugins/mlf-codegen-typescript", 7 + "mlf-cli", 8 + "mlf-codegen", 9 + "mlf-diagnostics", 5 10 "mlf-lang", 11 + "mlf-lsp", 6 12 "mlf-validation", "mlf-wasm", 7 13 "tree-sitter-mlf", 8 14 "website/mlf-playground-wasm"]
+56
README.md
··· 26 26 27 27 Right now you can only install mlf from source: 28 28 29 + ### CLI Tool 30 + 29 31 ```bash 30 32 # Install with all code generators (default: TypeScript, Go, Rust) 31 33 cargo install --path mlf-cli --all-features ··· 36 38 # Install with JSON generation only 37 39 cargo install --path mlf-cli --no-default-features 38 40 ``` 41 + 42 + ### Language Server 43 + 44 + For editor integration with real-time diagnostics: 45 + 46 + ```bash 47 + # Build the language server 48 + cargo build --release -p mlf-lsp 49 + 50 + # The binary will be at: target/release/mlf-lsp 51 + ``` 52 + 53 + #### Editor Setup 54 + 55 + **VS Code**: Create an extension configuration: 56 + ```json 57 + { 58 + "languageServer": { 59 + "module": "/path/to/mlf-lsp", 60 + "args": [], 61 + "filetypes": ["mlf"] 62 + } 63 + } 64 + ``` 65 + 66 + **Neovim**: Add to your LSP config: 67 + ```lua 68 + local lspconfig = require('lspconfig') 69 + local configs = require('lspconfig.configs') 70 + 71 + configs.mlf = { 72 + default_config = { 73 + cmd = { '/path/to/mlf-lsp' }, 74 + filetypes = { 'mlf' }, 75 + root_dir = lspconfig.util.root_pattern('mlf.toml', '.git'), 76 + }, 77 + } 78 + 79 + lspconfig.mlf.setup{} 80 + ``` 81 + 82 + **Helix**: Add to `languages.toml`: 83 + ```toml 84 + [[language]] 85 + name = "mlf" 86 + scope = "source.mlf" 87 + file-types = ["mlf"] 88 + language-servers = ["mlf-lsp"] 89 + 90 + [language-server.mlf-lsp] 91 + command = "/path/to/mlf-lsp" 92 + ``` 93 + 94 + See [mlf-lsp/README.md](mlf-lsp/README.md) for more details. 39 95 40 96 ## Documentation 41 97
-6
codegen-plugins/mlf-codegen-java/Cargo.toml
··· 1 - [package] 2 - name = "mlf-codegen-java" 3 - version = "0.1.0" 4 - edition = "2024" 5 - 6 - [dependencies]
-14
codegen-plugins/mlf-codegen-java/src/lib.rs
··· 1 - pub fn add(left: u64, right: u64) -> u64 { 2 - left + right 3 - } 4 - 5 - #[cfg(test)] 6 - mod tests { 7 - use super::*; 8 - 9 - #[test] 10 - fn it_works() { 11 - let result = add(2, 2); 12 - assert_eq!(result, 4); 13 - } 14 - }
+110
mlf-diagnostics/src/lib.rs
··· 216 216 name, namespace_suffix 217 217 ) 218 218 } 219 + ValidationError::CircularImport { cycle, .. } => { 220 + write!(f, "Circular import detected: {}", cycle.join(" → ")) 221 + } 222 + ValidationError::UnusedImport { name, .. } => { 223 + write!(f, "Unused import '{}'", name) 224 + } 219 225 } 220 226 } 221 227 ··· 230 236 ValidationError::AmbiguousMain { .. } => "mlf::ambiguous_main", 231 237 ValidationError::MultipleMain { .. } => "mlf::multiple_main", 232 238 ValidationError::ConflictNotAllowed { .. } => "mlf::conflict_not_allowed", 239 + ValidationError::CircularImport { .. } => "mlf::circular_import", 240 + ValidationError::UnusedImport { .. } => "mlf::unused_import", 233 241 } 234 242 } 235 243 ··· 301 309 span.start..span.end, 302 310 format!("'{}' conflicts with another definition", name), 303 311 )], 312 + ValidationError::CircularImport { span, cycle, .. } => vec![LabeledSpan::at( 313 + span.start..span.end, 314 + format!("Import creates a cycle: {}", cycle.join(" → ")), 315 + )], 316 + ValidationError::UnusedImport { span, name, .. } => vec![LabeledSpan::at( 317 + span.start..span.end, 318 + format!("Import '{}' is never used", name), 319 + )], 304 320 } 305 321 } 306 322 ··· 331 347 "Numeric constraints (minimum, maximum) can only be applied to integer or number types.", 332 348 )) 333 349 } 350 + ValidationError::InvalidConstraint { message, .. } 351 + if message.contains("Blob constraint on non-blob") => 352 + { 353 + Some(Box::new( 354 + "Blob constraints (accept, maxSize) can only be applied to blob types.", 355 + )) 356 + } 357 + ValidationError::InvalidConstraint { message, .. } 358 + if message.contains("Length constraint") => 359 + { 360 + Some(Box::new( 361 + "Length constraints (minLength, maxLength) can be applied to strings or arrays.", 362 + )) 363 + } 364 + ValidationError::InvalidConstraint { message, .. } 365 + if message.contains("Union type must have at least one member") => 366 + { 367 + Some(Box::new( 368 + "Union types must contain at least one type. Add a type to the union or use a different type.", 369 + )) 370 + } 334 371 ValidationError::ConstraintTooPermissive { message, .. } if message.contains("maxLength") => { 335 372 Some(Box::new( 336 373 "When refining a constrained type, maxLength can only decrease (become more restrictive).", ··· 341 378 "When refining a constrained type, minLength can only increase (become more restrictive).", 342 379 )) 343 380 } 381 + ValidationError::ConstraintTooPermissive { message, .. } if message.contains("maximum") => { 382 + Some(Box::new( 383 + "When refining a constrained type, maximum can only decrease (become more restrictive).", 384 + )) 385 + } 386 + ValidationError::ConstraintTooPermissive { message, .. } if message.contains("minimum") => { 387 + Some(Box::new( 388 + "When refining a constrained type, minimum can only increase (become more restrictive).", 389 + )) 390 + } 391 + ValidationError::ConstraintTooPermissive { message, .. } if message.contains("maxGraphemes") => { 392 + Some(Box::new( 393 + "When refining a constrained type, maxGraphemes can only decrease (become more restrictive).", 394 + )) 395 + } 396 + ValidationError::ConstraintTooPermissive { message, .. } if message.contains("minGraphemes") => { 397 + Some(Box::new( 398 + "When refining a constrained type, minGraphemes can only increase (become more restrictive).", 399 + )) 400 + } 401 + ValidationError::ConstraintTooPermissive { message, .. } if message.contains("maxSize") => { 402 + Some(Box::new( 403 + "When refining a constrained type, maxSize can only decrease (become more restrictive).", 404 + )) 405 + } 406 + ValidationError::ConstraintTooPermissive { message, .. } if message.contains("enum") => { 407 + Some(Box::new( 408 + "When refining a constrained type, enum values must be a subset of the base enum.", 409 + )) 410 + } 411 + ValidationError::DuplicateDefinition { .. } => { 412 + Some(Box::new( 413 + "Each name can only be defined once in a module. Consider renaming one of the items or using @main annotation if they match the namespace suffix.", 414 + )) 415 + } 416 + ValidationError::ReservedName { name, .. } if name == "main" => { 417 + Some(Box::new( 418 + "The name 'main' is reserved and cannot be used as an item name. Use @main annotation on an item instead.", 419 + )) 420 + } 421 + ValidationError::ReservedName { name, .. } if name == "defs" => { 422 + Some(Box::new( 423 + "The name 'defs' is reserved for future use and cannot be used as an item name.", 424 + )) 425 + } 426 + ValidationError::AmbiguousMain { .. } => { 427 + Some(Box::new( 428 + "When multiple items have the same name as the namespace suffix, use @main to mark which one is the primary definition.", 429 + )) 430 + } 431 + ValidationError::MultipleMain { .. } => { 432 + Some(Box::new( 433 + "Only one item can be marked with @main. Remove the @main annotation from all but one item.", 434 + )) 435 + } 436 + ValidationError::ConflictNotAllowed { namespace_suffix, .. } => { 437 + Some(Box::new(format!( 438 + "Name conflicts are only allowed when the item name matches the namespace suffix ('{}').", 439 + namespace_suffix 440 + ))) 441 + } 442 + ValidationError::CircularImport { .. } => { 443 + Some(Box::new( 444 + "Circular imports are not allowed. Reorganize your modules to break the cycle.", 445 + )) 446 + } 447 + ValidationError::UnusedImport { .. } => { 448 + Some(Box::new( 449 + "This import is never used. Consider removing it to keep the code clean.", 450 + )) 451 + } 344 452 _ => None, 345 453 } 346 454 } ··· 356 464 ValidationError::AmbiguousMain { module_namespace, .. } => module_namespace, 357 465 ValidationError::MultipleMain { module_namespace, .. } => module_namespace, 358 466 ValidationError::ConflictNotAllowed { module_namespace, .. } => module_namespace, 467 + ValidationError::CircularImport { module_namespace, .. } => module_namespace, 468 + ValidationError::UnusedImport { module_namespace, .. } => module_namespace, 359 469 } 360 470 } 361 471
+2
mlf-lang/src/error.rs
··· 21 21 AmbiguousMain { name: String, namespace_suffix: String, first_span: Span, second_span: Span, module_namespace: String }, 22 22 MultipleMain { name: String, first_span: Span, second_span: Span, module_namespace: String }, 23 23 ConflictNotAllowed { name: String, namespace_suffix: String, span: Span, module_namespace: String }, 24 + CircularImport { cycle: Vec<String>, span: Span, module_namespace: String }, 25 + UnusedImport { name: String, span: Span, module_namespace: String }, 24 26 } 25 27 26 28 #[derive(Debug, Clone, Default)]
-14
mlf-lang/src/workspace.rs
··· 895 895 annotations.iter().any(|ann| ann.name.name == "main") 896 896 } 897 897 898 - fn is_main_eligible_item(item: &Item) -> bool { 899 - matches!(item, Item::Record(_) | Item::Query(_) | Item::Procedure(_) | Item::Subscription(_) | Item::DefType(_)) 900 - } 901 - 902 898 fn build_symbol_table(namespace: &str, lexicon: &Lexicon) -> Result<SymbolTable, ValidationErrors> { 903 899 let mut symbols = SymbolTable { 904 900 types: BTreeMap::new(), ··· 1421 1417 module_namespace: current_namespace.to_string(), 1422 1418 }); 1423 1419 Err(errors) 1424 - } 1425 - } 1426 - 1427 - impl Symbol { 1428 - fn span(&self) -> Span { 1429 - match self { 1430 - Symbol::Record { span, .. } => *span, 1431 - Symbol::Alias { span, .. } => *span, 1432 - Symbol::Token { span, .. } => *span, 1433 - } 1434 1420 } 1435 1421 } 1436 1422
+27
mlf-lsp/Cargo.toml
··· 1 + [package] 2 + name = "mlf-lsp" 3 + version = "0.1.0" 4 + edition = "2024" 5 + description = "Language Server Protocol implementation for MLF" 6 + 7 + [dependencies] 8 + mlf-lang = { path = "../mlf-lang" } 9 + mlf-diagnostics = { path = "../mlf-diagnostics" } 10 + 11 + # LSP 12 + tower-lsp = "0.20" 13 + tokio = { version = "1", features = ["full"] } 14 + serde = { version = "1", features = ["derive"] } 15 + serde_json = "1" 16 + 17 + # Logging 18 + tracing = "0.1" 19 + tracing-subscriber = { version = "0.3", features = ["env-filter"] } 20 + 21 + [lib] 22 + name = "mlf_lsp" 23 + path = "src/lib.rs" 24 + 25 + [[bin]] 26 + name = "mlf-lsp" 27 + path = "src/main.rs"
+145
mlf-lsp/README.md
··· 1 + # mlf-lsp 2 + 3 + Language Server Protocol (LSP) implementation for Matt's Lexicon Format (MLF). 4 + 5 + ## Features 6 + 7 + ### Currently Implemented 8 + 9 + - **Diagnostics**: Real-time syntax error reporting as you type 10 + - **Document Synchronization**: Tracks open/changed/closed MLF files 11 + - **Hover Information**: Shows type information, documentation comments, and field details on hover 12 + - **Completion**: Auto-complete for keywords, primitive types, defined types, and constraint names 13 + - **Go to Definition**: Jump to type definitions across files using workspace resolution 14 + - **Workspace Support**: Automatically builds a workspace from open files with cross-file type resolution 15 + - **Logging**: Server activity logging for debugging 16 + 17 + ### Planned Features 18 + 19 + - **Formatting**: Auto-format MLF code from AST 20 + - **Rename**: Rename symbols across files 21 + - **Find References**: Find all usages of a symbol 22 + - **Code Actions**: Quick fixes and refactoring 23 + - **Signature Help**: Parameter hints for queries/procedures 24 + - **Semantic Tokens**: Enhanced syntax highlighting 25 + - **Document Symbols**: Outline view of definitions 26 + - **Workspace Validation**: Multi-file validation with workspace support 27 + 28 + ## Usage 29 + 30 + ### Running the Server 31 + 32 + ```bash 33 + cargo build --release -p mlf-lsp 34 + ./target/release/mlf-lsp 35 + ``` 36 + 37 + The server communicates over stdin/stdout using the LSP protocol. 38 + 39 + ### Editor Integration 40 + 41 + #### VS Code 42 + 43 + Create a VS Code extension that launches the LSP server: 44 + 45 + ```json 46 + { 47 + "languageServer": { 48 + "module": "/path/to/mlf-lsp", 49 + "args": [], 50 + "filetypes": ["mlf"] 51 + } 52 + } 53 + ``` 54 + 55 + #### Neovim 56 + 57 + Configure with `nvim-lspconfig`: 58 + 59 + ```lua 60 + local lspconfig = require('lspconfig') 61 + local configs = require('lspconfig.configs') 62 + 63 + configs.mlf = { 64 + default_config = { 65 + cmd = { '/path/to/mlf-lsp' }, 66 + filetypes = { 'mlf' }, 67 + root_dir = lspconfig.util.root_pattern('mlf.toml', '.git'), 68 + }, 69 + } 70 + 71 + lspconfig.mlf.setup{} 72 + ``` 73 + 74 + #### Helix 75 + 76 + Add to your `languages.toml`: 77 + 78 + ```toml 79 + [[language]] 80 + name = "mlf" 81 + scope = "source.mlf" 82 + file-types = ["mlf"] 83 + language-servers = ["mlf-lsp"] 84 + 85 + [language-server.mlf-lsp] 86 + command = "/path/to/mlf-lsp" 87 + ``` 88 + 89 + ## Development 90 + 91 + The LSP server is built using: 92 + - **tower-lsp**: High-level LSP framework 93 + - **tokio**: Async runtime 94 + - **mlf-lang**: MLF parser and AST 95 + - **mlf-diagnostics**: Error reporting 96 + 97 + ### Architecture 98 + 99 + ``` 100 + mlf-lsp/ 101 + ├── src/ 102 + │ ├── lib.rs # Library exports 103 + │ ├── main.rs # Binary entry point 104 + │ └── server.rs # LSP server implementation 105 + └── Cargo.toml 106 + ``` 107 + 108 + ### Adding Features 109 + 110 + 1. **Hover**: Implement position-based type lookup in the AST 111 + 2. **Completion**: Build a context-aware symbol table from the workspace 112 + 3. **Go to Definition**: Track symbol definitions and references 113 + 4. **Formatting**: Generate MLF source from the AST with consistent formatting 114 + 115 + ## Testing 116 + 117 + ```bash 118 + # Run tests 119 + cargo test -p mlf-lsp 120 + 121 + # Test with LSP inspector 122 + npm install -g @vscode/language-server-inspector 123 + lsp-inspector --command "/path/to/mlf-lsp" 124 + ``` 125 + 126 + ## Logging 127 + 128 + Set the `RUST_LOG` environment variable to control logging: 129 + 130 + ```bash 131 + RUST_LOG=debug mlf-lsp 132 + RUST_LOG=mlf_lsp=trace mlf-lsp 133 + ``` 134 + 135 + Logs are written to stderr. 136 + 137 + ## Contributing 138 + 139 + The LSP server is in early development. Contributions are welcome! 140 + 141 + Priority features: 142 + 1. Hover information with type details 143 + 2. Completion for types and keywords 144 + 3. Go to definition for imports and references 145 + 4. Formatting using mlf-lang AST
+4
mlf-lsp/src/lib.rs
··· 1 + pub mod server; 2 + pub mod utils; 3 + 4 + pub use server::MlfLanguageServer;
+21
mlf-lsp/src/main.rs
··· 1 + use mlf_lsp::MlfLanguageServer; 2 + use tower_lsp::{LspService, Server}; 3 + use tracing_subscriber::EnvFilter; 4 + 5 + #[tokio::main] 6 + async fn main() { 7 + // Initialize logging 8 + tracing_subscriber::fmt() 9 + .with_env_filter(EnvFilter::from_default_env()) 10 + .with_writer(std::io::stderr) 11 + .init(); 12 + 13 + tracing::info!("Starting MLF Language Server"); 14 + 15 + let stdin = tokio::io::stdin(); 16 + let stdout = tokio::io::stdout(); 17 + 18 + let (service, socket) = LspService::new(|client| MlfLanguageServer::new(client)); 19 + 20 + Server::new(stdin, stdout, socket).serve(service).await; 21 + }
+711
mlf-lsp/src/server.rs
··· 1 + use std::collections::HashMap; 2 + use std::path::PathBuf; 3 + use mlf_lang::ast::*; 4 + use mlf_lang::Workspace; 5 + use tower_lsp::jsonrpc::Result; 6 + use tower_lsp::lsp_types::*; 7 + use tower_lsp::{Client, LanguageServer}; 8 + 9 + use crate::utils::*; 10 + 11 + pub struct MlfLanguageServer { 12 + client: Client, 13 + documents: tokio::sync::RwLock<HashMap<Url, DocumentState>>, 14 + workspace: tokio::sync::RwLock<Option<Workspace>>, 15 + } 16 + 17 + struct DocumentState { 18 + text: String, 19 + lexicon: Option<Lexicon>, 20 + namespace: Option<String>, 21 + } 22 + 23 + impl MlfLanguageServer { 24 + pub fn new(client: Client) -> Self { 25 + Self { 26 + client, 27 + documents: tokio::sync::RwLock::new(HashMap::new()), 28 + workspace: tokio::sync::RwLock::new(None), 29 + } 30 + } 31 + 32 + async fn parse_document(&self, uri: &Url, text: &str) { 33 + // Parse the document 34 + match mlf_lang::parser::parse_lexicon(text) { 35 + Ok(lexicon) => { 36 + // Extract namespace from file path 37 + let namespace = extract_namespace_from_uri(uri); 38 + 39 + // Store parsed state 40 + self.documents.write().await.insert( 41 + uri.clone(), 42 + DocumentState { 43 + text: text.to_string(), 44 + lexicon: Some(lexicon.clone()), 45 + namespace: namespace.clone(), 46 + }, 47 + ); 48 + 49 + // Update workspace 50 + self.update_workspace(uri, lexicon, namespace).await; 51 + 52 + // Clear diagnostics on success 53 + self.client 54 + .publish_diagnostics(uri.clone(), vec![], None) 55 + .await; 56 + } 57 + Err(err) => { 58 + // Store failed state 59 + self.documents.write().await.insert( 60 + uri.clone(), 61 + DocumentState { 62 + text: text.to_string(), 63 + lexicon: None, 64 + namespace: None, 65 + }, 66 + ); 67 + 68 + // Convert parse error to LSP diagnostic 69 + let diagnostic = Diagnostic { 70 + range: Range { 71 + start: Position { 72 + line: 0, 73 + character: 0, 74 + }, 75 + end: Position { 76 + line: 0, 77 + character: 0, 78 + }, 79 + }, 80 + severity: Some(DiagnosticSeverity::ERROR), 81 + code: None, 82 + code_description: None, 83 + source: Some("mlf".to_string()), 84 + message: format!("{:?}", err), 85 + related_information: None, 86 + tags: None, 87 + data: None, 88 + }; 89 + 90 + self.client 91 + .publish_diagnostics(uri.clone(), vec![diagnostic], None) 92 + .await; 93 + } 94 + } 95 + } 96 + 97 + async fn update_workspace(&self, _uri: &Url, lexicon: Lexicon, namespace: Option<String>) { 98 + if let Some(ns) = namespace { 99 + let mut workspace_guard = self.workspace.write().await; 100 + 101 + // Initialize workspace if needed 102 + if workspace_guard.is_none() { 103 + match Workspace::with_std() { 104 + Ok(ws) => *workspace_guard = Some(ws), 105 + Err(_) => { 106 + *workspace_guard = Some(Workspace::new()); 107 + } 108 + } 109 + } 110 + 111 + // Add or update module in workspace 112 + if let Some(workspace) = workspace_guard.as_mut() { 113 + let _ = workspace.add_module(ns.clone(), lexicon); 114 + let _ = workspace.resolve(); 115 + } 116 + } 117 + } 118 + 119 + async fn find_definition_in_workspace( 120 + &self, 121 + target_name: &str, 122 + current_namespace: &str, 123 + path: &Path, 124 + ) -> Option<(Url, mlf_lang::span::Span)> { 125 + let workspace_guard = self.workspace.read().await; 126 + let workspace = workspace_guard.as_ref()?; 127 + 128 + // Resolve the reference to find which namespace it's in 129 + let target_namespace = workspace.resolve_reference_namespace(path, current_namespace)?; 130 + 131 + // Find the document with that namespace 132 + let documents = self.documents.read().await; 133 + for (doc_uri, doc_state) in documents.iter() { 134 + if let Some(doc_ns) = &doc_state.namespace { 135 + if doc_ns == &target_namespace { 136 + if let Some(lexicon) = &doc_state.lexicon { 137 + // Find the item in this lexicon 138 + for item in &lexicon.items { 139 + if get_item_name(item) == target_name { 140 + let def_span = match item { 141 + Item::Record(r) => r.name.span, 142 + Item::InlineType(i) => i.name.span, 143 + Item::DefType(d) => d.name.span, 144 + Item::Token(t) => t.name.span, 145 + Item::Query(q) => q.name.span, 146 + Item::Procedure(p) => p.name.span, 147 + Item::Subscription(s) => s.name.span, 148 + Item::Use(_) => continue, 149 + }; 150 + 151 + return Some((doc_uri.clone(), def_span)); 152 + } 153 + } 154 + } 155 + } 156 + } 157 + } 158 + 159 + None 160 + } 161 + } 162 + 163 + /// Extract namespace from file URI 164 + /// Tries to find mlf.toml to determine project root, otherwise uses filename 165 + fn extract_namespace_from_uri(uri: &Url) -> Option<String> { 166 + let path = PathBuf::from(uri.path()); 167 + 168 + // Get the file stem (name without extension) 169 + let file_stem = path.file_stem()?.to_str()?; 170 + 171 + // Try to find project root by looking for mlf.toml 172 + let mut current = path.parent()?; 173 + let mut components = vec![file_stem.to_string()]; 174 + 175 + loop { 176 + // Check if mlf.toml exists here 177 + if current.join("mlf.toml").exists() { 178 + // Found project root, components already collected 179 + break; 180 + } 181 + 182 + // Add current directory name to components 183 + if let Some(dir_name) = current.file_name() { 184 + if let Some(name_str) = dir_name.to_str() { 185 + // Skip common directories that shouldn't be part of namespace 186 + if name_str != "src" && name_str != "lexicons" && name_str != "schemas" { 187 + components.insert(0, name_str.to_string()); 188 + } 189 + } 190 + } 191 + 192 + // Move up one directory 193 + current = current.parent()?; 194 + 195 + // Stop at root or after reasonable depth 196 + if current.parent().is_none() || components.len() > 10 { 197 + break; 198 + } 199 + } 200 + 201 + Some(components.join(".")) 202 + } 203 + 204 + #[tower_lsp::async_trait] 205 + impl LanguageServer for MlfLanguageServer { 206 + async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> { 207 + Ok(InitializeResult { 208 + capabilities: ServerCapabilities { 209 + text_document_sync: Some(TextDocumentSyncCapability::Kind( 210 + TextDocumentSyncKind::FULL, 211 + )), 212 + hover_provider: Some(HoverProviderCapability::Simple(true)), 213 + completion_provider: Some(CompletionOptions { 214 + resolve_provider: Some(false), 215 + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), 216 + ..Default::default() 217 + }), 218 + definition_provider: Some(OneOf::Left(true)), 219 + document_formatting_provider: Some(OneOf::Left(true)), 220 + document_symbol_provider: Some(OneOf::Left(true)), 221 + ..Default::default() 222 + }, 223 + server_info: Some(ServerInfo { 224 + name: "mlf-lsp".to_string(), 225 + version: Some(env!("CARGO_PKG_VERSION").to_string()), 226 + }), 227 + ..Default::default() 228 + }) 229 + } 230 + 231 + async fn initialized(&self, _: InitializedParams) { 232 + self.client 233 + .log_message(MessageType::INFO, "MLF Language Server initialized") 234 + .await; 235 + } 236 + 237 + async fn shutdown(&self) -> Result<()> { 238 + Ok(()) 239 + } 240 + 241 + async fn did_open(&self, params: DidOpenTextDocumentParams) { 242 + let uri = params.text_document.uri; 243 + let text = params.text_document.text; 244 + 245 + self.client 246 + .log_message(MessageType::INFO, format!("Opening document: {}", uri)) 247 + .await; 248 + 249 + // Parse and send diagnostics 250 + self.parse_document(&uri, &text).await; 251 + } 252 + 253 + async fn did_change(&self, params: DidChangeTextDocumentParams) { 254 + let uri = params.text_document.uri; 255 + 256 + if let Some(change) = params.content_changes.into_iter().next() { 257 + let text = change.text; 258 + 259 + // Parse and send diagnostics 260 + self.parse_document(&uri, &text).await; 261 + } 262 + } 263 + 264 + async fn did_close(&self, params: DidCloseTextDocumentParams) { 265 + // Remove document from storage 266 + self.documents.write().await.remove(&params.text_document.uri); 267 + 268 + // TODO: Could rebuild workspace without this module 269 + } 270 + 271 + async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> { 272 + let uri = params.text_document_position_params.text_document.uri; 273 + let position = params.text_document_position_params.position; 274 + 275 + let documents = self.documents.read().await; 276 + if let Some(doc_state) = documents.get(&uri) { 277 + if let Some(lexicon) = &doc_state.lexicon { 278 + // Convert position to offset 279 + if let Some(offset) = position_to_offset(&doc_state.text, position) { 280 + // Find the item at this position 281 + if let Some(item) = find_item_at_offset(lexicon, offset) { 282 + let name = get_item_name(item); 283 + let docs = get_item_docs(item); 284 + 285 + let mut contents = vec![]; 286 + 287 + // Add item kind and name 288 + let kind = match item { 289 + Item::Record(_) => "record", 290 + Item::InlineType(_) => "inline type", 291 + Item::DefType(_) => "def type", 292 + Item::Token(_) => "token", 293 + Item::Query(_) => "query", 294 + Item::Procedure(_) => "procedure", 295 + Item::Subscription(_) => "subscription", 296 + Item::Use(_) => "use", 297 + }; 298 + 299 + contents.push(MarkedString::LanguageString(LanguageString { 300 + language: "mlf".to_string(), 301 + value: format!("{} {}", kind, name), 302 + })); 303 + 304 + // Add documentation if available 305 + if !docs.is_empty() { 306 + contents.push(MarkedString::String(docs.join("\n"))); 307 + } 308 + 309 + // Add type information for fields 310 + match item { 311 + Item::Record(r) => { 312 + if let Some(field) = find_field_at_offset(&r.fields, offset) { 313 + let field_type = format_type(&field.ty); 314 + let opt = if field.optional { "optional" } else { "required" }; 315 + contents.push(MarkedString::String( 316 + format!("Field: {} ({})", field_type, opt) 317 + )); 318 + } 319 + } 320 + Item::InlineType(i) => { 321 + contents.push(MarkedString::String( 322 + format!("Type: {}", format_type(&i.ty)) 323 + )); 324 + } 325 + Item::DefType(d) => { 326 + contents.push(MarkedString::String( 327 + format!("Type: {}", format_type(&d.ty)) 328 + )); 329 + } 330 + Item::Query(q) => { 331 + if let Some(field) = find_field_at_offset(&q.params, offset) { 332 + let field_type = format_type(&field.ty); 333 + let opt = if field.optional { "optional" } else { "required" }; 334 + contents.push(MarkedString::String( 335 + format!("Parameter: {} ({})", field_type, opt) 336 + )); 337 + } 338 + } 339 + Item::Procedure(p) => { 340 + if let Some(field) = find_field_at_offset(&p.params, offset) { 341 + let field_type = format_type(&field.ty); 342 + let opt = if field.optional { "optional" } else { "required" }; 343 + contents.push(MarkedString::String( 344 + format!("Parameter: {} ({})", field_type, opt) 345 + )); 346 + } 347 + } 348 + _ => {} 349 + } 350 + 351 + return Ok(Some(Hover { 352 + contents: HoverContents::Array(contents), 353 + range: None, 354 + })); 355 + } 356 + 357 + // Check if we're hovering over a type reference 358 + for item in &lexicon.items { 359 + let type_to_check = match item { 360 + Item::Record(r) => { 361 + if let Some(field) = find_field_at_offset(&r.fields, offset) { 362 + Some(&field.ty) 363 + } else { 364 + None 365 + } 366 + } 367 + Item::InlineType(i) => Some(&i.ty), 368 + Item::DefType(d) => Some(&d.ty), 369 + Item::Query(q) => { 370 + find_field_at_offset(&q.params, offset).map(|f| &f.ty) 371 + } 372 + Item::Procedure(p) => { 373 + find_field_at_offset(&p.params, offset).map(|f| &f.ty) 374 + } 375 + _ => None, 376 + }; 377 + 378 + if let Some(ty) = type_to_check { 379 + if let Some(type_ref) = find_type_at_offset(ty, offset) { 380 + if let Type::Reference { path, .. } = type_ref { 381 + return Ok(Some(Hover { 382 + contents: HoverContents::Scalar( 383 + MarkedString::LanguageString(LanguageString { 384 + language: "mlf".to_string(), 385 + value: format!("type {}", path.to_string()), 386 + }) 387 + ), 388 + range: None, 389 + })); 390 + } 391 + } 392 + } 393 + } 394 + } 395 + } 396 + } 397 + 398 + Ok(None) 399 + } 400 + 401 + async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> { 402 + let uri = params.text_document_position.text_document.uri; 403 + let position = params.text_document_position.position; 404 + 405 + let documents = self.documents.read().await; 406 + if let Some(doc_state) = documents.get(&uri) { 407 + let mut completions = vec![]; 408 + 409 + // Always provide keywords 410 + let keywords = vec![ 411 + ("record", CompletionItemKind::KEYWORD, "Define a record type"), 412 + ("inline type", CompletionItemKind::KEYWORD, "Define an inline type"), 413 + ("def type", CompletionItemKind::KEYWORD, "Define a def type"), 414 + ("token", CompletionItemKind::KEYWORD, "Define a token"), 415 + ("query", CompletionItemKind::KEYWORD, "Define a query"), 416 + ("procedure", CompletionItemKind::KEYWORD, "Define a procedure"), 417 + ("subscription", CompletionItemKind::KEYWORD, "Define a subscription"), 418 + ("use", CompletionItemKind::KEYWORD, "Import types from another module"), 419 + ("constrained", CompletionItemKind::KEYWORD, "Add constraints to a type"), 420 + ]; 421 + 422 + for (label, kind, detail) in keywords { 423 + completions.push(CompletionItem { 424 + label: label.to_string(), 425 + kind: Some(kind), 426 + detail: Some(detail.to_string()), 427 + ..Default::default() 428 + }); 429 + } 430 + 431 + // Primitive types 432 + let primitives = vec![ 433 + "null", "boolean", "integer", "string", "bytes", "blob", 434 + ]; 435 + 436 + for prim in primitives { 437 + completions.push(CompletionItem { 438 + label: prim.to_string(), 439 + kind: Some(CompletionItemKind::TYPE_PARAMETER), 440 + detail: Some("Primitive type".to_string()), 441 + ..Default::default() 442 + }); 443 + } 444 + 445 + // If we have a parsed lexicon, provide type completions from current file 446 + if let Some(lexicon) = &doc_state.lexicon { 447 + for item in &lexicon.items { 448 + let (name, kind, detail) = match item { 449 + Item::Record(r) => (r.name.name.as_str(), CompletionItemKind::CLASS, "record"), 450 + Item::InlineType(i) => (i.name.name.as_str(), CompletionItemKind::TYPE_PARAMETER, "inline type"), 451 + Item::DefType(d) => (d.name.name.as_str(), CompletionItemKind::TYPE_PARAMETER, "def type"), 452 + Item::Token(t) => (t.name.name.as_str(), CompletionItemKind::ENUM, "token"), 453 + Item::Query(q) => (q.name.name.as_str(), CompletionItemKind::FUNCTION, "query"), 454 + Item::Procedure(p) => (p.name.name.as_str(), CompletionItemKind::FUNCTION, "procedure"), 455 + Item::Subscription(s) => (s.name.name.as_str(), CompletionItemKind::EVENT, "subscription"), 456 + Item::Use(_) => continue, 457 + }; 458 + 459 + completions.push(CompletionItem { 460 + label: name.to_string(), 461 + kind: Some(kind), 462 + detail: Some(detail.to_string()), 463 + ..Default::default() 464 + }); 465 + } 466 + } 467 + 468 + // Check if we're at a field position to provide constraint completions 469 + if let Some(offset) = position_to_offset(&doc_state.text, position) { 470 + // Check if "constrained {" appears before cursor 471 + if doc_state.text[..offset].ends_with("constrained {") 472 + || doc_state.text[..offset].contains("constrained {") { 473 + let constraint_items = vec![ 474 + ("maxLength", "Maximum string/array length"), 475 + ("minLength", "Minimum string/array length"), 476 + ("maxGraphemes", "Maximum grapheme count"), 477 + ("minGraphemes", "Minimum grapheme count"), 478 + ("format", "String format (e.g., 'datetime', 'uri')"), 479 + ("enum", "Enumerated values"), 480 + ("knownValues", "Known values"), 481 + ("minimum", "Minimum numeric value"), 482 + ("maximum", "Maximum numeric value"), 483 + ("accept", "Accepted MIME types for blobs"), 484 + ("maxSize", "Maximum blob size in bytes"), 485 + ("default", "Default value"), 486 + ("const", "Constant value"), 487 + ]; 488 + 489 + for (label, detail) in constraint_items { 490 + completions.push(CompletionItem { 491 + label: label.to_string(), 492 + kind: Some(CompletionItemKind::PROPERTY), 493 + detail: Some(detail.to_string()), 494 + insert_text: Some(format!("{}: ", label)), 495 + ..Default::default() 496 + }); 497 + } 498 + } 499 + } 500 + 501 + return Ok(Some(CompletionResponse::Array(completions))); 502 + } 503 + 504 + Ok(None) 505 + } 506 + 507 + async fn goto_definition( 508 + &self, 509 + params: GotoDefinitionParams, 510 + ) -> Result<Option<GotoDefinitionResponse>> { 511 + let uri = params.text_document_position_params.text_document.uri; 512 + let position = params.text_document_position_params.position; 513 + 514 + self.client 515 + .log_message( 516 + MessageType::INFO, 517 + format!("Go to definition request at {}:{}:{}", uri, position.line, position.character), 518 + ) 519 + .await; 520 + 521 + // Extract needed data from document state 522 + let (text, lexicon, current_namespace) = { 523 + let documents = self.documents.read().await; 524 + if let Some(doc_state) = documents.get(&uri) { 525 + ( 526 + doc_state.text.clone(), 527 + doc_state.lexicon.clone(), 528 + doc_state.namespace.clone(), 529 + ) 530 + } else { 531 + return Ok(None); 532 + } 533 + }; 534 + 535 + if let Some(lexicon) = lexicon { 536 + if let Some(offset) = position_to_offset(&text, position) { 537 + // Find type reference at this position 538 + for item in &lexicon.items { 539 + let type_to_check = match item { 540 + Item::Record(r) => { 541 + find_field_at_offset(&r.fields, offset).map(|f| &f.ty) 542 + } 543 + Item::InlineType(i) => Some(&i.ty), 544 + Item::DefType(d) => Some(&d.ty), 545 + Item::Query(q) => { 546 + find_field_at_offset(&q.params, offset).map(|f| &f.ty) 547 + } 548 + Item::Procedure(p) => { 549 + find_field_at_offset(&p.params, offset).map(|f| &f.ty) 550 + } 551 + _ => None, 552 + }; 553 + 554 + if let Some(ty) = type_to_check { 555 + if let Some(Type::Reference { path, .. }) = find_type_at_offset(ty, offset) { 556 + // Find the definition of this type 557 + let target_name = if path.segments.len() == 1 { 558 + &path.segments[0].name 559 + } else { 560 + &path.segments.last().unwrap().name 561 + }; 562 + 563 + // First try to find in current file 564 + for target_item in &lexicon.items { 565 + if get_item_name(target_item) == target_name { 566 + let def_span = match target_item { 567 + Item::Record(r) => r.name.span, 568 + Item::InlineType(i) => i.name.span, 569 + Item::DefType(d) => d.name.span, 570 + Item::Token(t) => t.name.span, 571 + Item::Query(q) => q.name.span, 572 + Item::Procedure(p) => p.name.span, 573 + Item::Subscription(s) => s.name.span, 574 + Item::Use(_) => continue, 575 + }; 576 + 577 + let range = span_to_range(&text, def_span); 578 + 579 + return Ok(Some(GotoDefinitionResponse::Scalar( 580 + Location { 581 + uri: uri.clone(), 582 + range, 583 + }, 584 + ))); 585 + } 586 + } 587 + 588 + // Not found in current file - try workspace 589 + if let Some(ref current_ns) = current_namespace { 590 + if let Some((def_uri, def_span)) = 591 + self.find_definition_in_workspace(target_name, current_ns, path).await { 592 + 593 + // Get text for span conversion 594 + let documents = self.documents.read().await; 595 + if let Some(target_doc) = documents.get(&def_uri) { 596 + let range = span_to_range(&target_doc.text, def_span); 597 + 598 + return Ok(Some(GotoDefinitionResponse::Scalar( 599 + Location { 600 + uri: def_uri, 601 + range, 602 + }, 603 + ))); 604 + } 605 + } 606 + } 607 + } 608 + } 609 + } 610 + } 611 + } 612 + 613 + Ok(None) 614 + } 615 + 616 + async fn document_symbol( 617 + &self, 618 + params: DocumentSymbolParams, 619 + ) -> Result<Option<DocumentSymbolResponse>> { 620 + let uri = params.text_document.uri; 621 + 622 + let documents = self.documents.read().await; 623 + if let Some(doc_state) = documents.get(&uri) { 624 + if let Some(lexicon) = &doc_state.lexicon { 625 + let mut symbols = vec![]; 626 + 627 + for item in &lexicon.items { 628 + let (name, kind, range) = match item { 629 + Item::Record(r) => ( 630 + &r.name.name, 631 + SymbolKind::STRUCT, 632 + span_to_range(&doc_state.text, r.span), 633 + ), 634 + Item::InlineType(i) => ( 635 + &i.name.name, 636 + SymbolKind::TYPE_PARAMETER, 637 + span_to_range(&doc_state.text, i.span), 638 + ), 639 + Item::DefType(d) => ( 640 + &d.name.name, 641 + SymbolKind::TYPE_PARAMETER, 642 + span_to_range(&doc_state.text, d.span), 643 + ), 644 + Item::Token(t) => ( 645 + &t.name.name, 646 + SymbolKind::ENUM, 647 + span_to_range(&doc_state.text, t.span), 648 + ), 649 + Item::Query(q) => ( 650 + &q.name.name, 651 + SymbolKind::FUNCTION, 652 + span_to_range(&doc_state.text, q.span), 653 + ), 654 + Item::Procedure(p) => ( 655 + &p.name.name, 656 + SymbolKind::FUNCTION, 657 + span_to_range(&doc_state.text, p.span), 658 + ), 659 + Item::Subscription(s) => ( 660 + &s.name.name, 661 + SymbolKind::EVENT, 662 + span_to_range(&doc_state.text, s.span), 663 + ), 664 + Item::Use(_) => continue, 665 + }; 666 + 667 + #[allow(deprecated)] 668 + symbols.push(DocumentSymbol { 669 + name: name.clone(), 670 + detail: None, 671 + kind, 672 + tags: None, 673 + deprecated: None, 674 + range, 675 + selection_range: range, 676 + children: None, 677 + }); 678 + } 679 + 680 + return Ok(Some(DocumentSymbolResponse::Nested(symbols))); 681 + } 682 + } 683 + 684 + Ok(None) 685 + } 686 + 687 + async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> { 688 + let uri = params.text_document.uri; 689 + 690 + let text = { 691 + let documents = self.documents.read().await; 692 + documents.get(&uri).map(|d| d.text.clone()) 693 + }; 694 + 695 + if let Some(text) = text { 696 + // Parse and re-format 697 + match mlf_lang::parser::parse_lexicon(&text) { 698 + Ok(_lexicon) => { 699 + // TODO: Generate formatted MLF from AST 700 + return Ok(None); 701 + } 702 + Err(_) => { 703 + // Don't format invalid documents 704 + return Ok(None); 705 + } 706 + } 707 + } 708 + 709 + Ok(None) 710 + } 711 + }
+191
mlf-lsp/src/utils.rs
··· 1 + use mlf_lang::ast::*; 2 + use mlf_lang::span::{Span, Spanned}; 3 + use tower_lsp::lsp_types::Position; 4 + 5 + /// Convert LSP Position to byte offset in text 6 + pub fn position_to_offset(text: &str, position: Position) -> Option<usize> { 7 + let mut offset = 0; 8 + let mut line = 0; 9 + let mut character = 0; 10 + 11 + for ch in text.chars() { 12 + if line == position.line as usize && character == position.character as usize { 13 + return Some(offset); 14 + } 15 + 16 + if ch == '\n' { 17 + line += 1; 18 + character = 0; 19 + } else { 20 + character += 1; 21 + } 22 + 23 + offset += ch.len_utf8(); 24 + } 25 + 26 + // Handle end of file 27 + if line == position.line as usize && character == position.character as usize { 28 + Some(offset) 29 + } else { 30 + None 31 + } 32 + } 33 + 34 + /// Convert byte offset to LSP Position 35 + pub fn offset_to_position(text: &str, offset: usize) -> Position { 36 + let mut line = 0; 37 + let mut character = 0; 38 + let mut current_offset = 0; 39 + 40 + for ch in text.chars() { 41 + if current_offset >= offset { 42 + break; 43 + } 44 + 45 + if ch == '\n' { 46 + line += 1; 47 + character = 0; 48 + } else { 49 + character += 1; 50 + } 51 + 52 + current_offset += ch.len_utf8(); 53 + } 54 + 55 + Position { line: line as u32, character: character as u32 } 56 + } 57 + 58 + /// Convert MLF Span to LSP Range 59 + pub fn span_to_range(text: &str, span: Span) -> tower_lsp::lsp_types::Range { 60 + let start = offset_to_position(text, span.start); 61 + let end = offset_to_position(text, span.end); 62 + tower_lsp::lsp_types::Range { start, end } 63 + } 64 + 65 + /// Check if an offset is within a span 66 + pub fn offset_in_span(offset: usize, span: Span) -> bool { 67 + offset >= span.start && offset < span.end 68 + } 69 + 70 + /// Find the item at a given offset 71 + pub fn find_item_at_offset(lexicon: &Lexicon, offset: usize) -> Option<&Item> { 72 + lexicon.items.iter().find(|item| { 73 + let span = match item { 74 + Item::Record(r) => r.span, 75 + Item::InlineType(i) => i.span, 76 + Item::DefType(d) => d.span, 77 + Item::Token(t) => t.span, 78 + Item::Query(q) => q.span, 79 + Item::Procedure(p) => p.span, 80 + Item::Subscription(s) => s.span, 81 + Item::Use(u) => u.span, 82 + }; 83 + offset_in_span(offset, span) 84 + }) 85 + } 86 + 87 + /// Find the type reference at a given offset 88 + pub fn find_type_at_offset(ty: &Type, offset: usize) -> Option<&Type> { 89 + if !offset_in_span(offset, ty.span()) { 90 + return None; 91 + } 92 + 93 + match ty { 94 + Type::Reference { .. } => Some(ty), 95 + Type::Array { inner, .. } => find_type_at_offset(inner, offset), 96 + Type::Union { types, .. } => { 97 + for t in types { 98 + if let Some(found) = find_type_at_offset(t, offset) { 99 + return Some(found); 100 + } 101 + } 102 + None 103 + } 104 + Type::Object { fields, .. } => { 105 + for field in fields { 106 + if offset_in_span(offset, field.span) { 107 + if offset_in_span(offset, field.name.span) { 108 + return None; // On field name, not type 109 + } 110 + return find_type_at_offset(&field.ty, offset); 111 + } 112 + } 113 + None 114 + } 115 + Type::Parenthesized { inner, .. } => find_type_at_offset(inner, offset), 116 + Type::Constrained { base, .. } => find_type_at_offset(base, offset), 117 + _ => None, 118 + } 119 + } 120 + 121 + /// Find field at offset within a record/query/procedure 122 + pub fn find_field_at_offset(fields: &[Field], offset: usize) -> Option<&Field> { 123 + fields.iter().find(|field| offset_in_span(offset, field.span)) 124 + } 125 + 126 + /// Get the item name 127 + pub fn get_item_name(item: &Item) -> &str { 128 + match item { 129 + Item::Record(r) => &r.name.name, 130 + Item::InlineType(i) => &i.name.name, 131 + Item::DefType(d) => &d.name.name, 132 + Item::Token(t) => &t.name.name, 133 + Item::Query(q) => &q.name.name, 134 + Item::Procedure(p) => &p.name.name, 135 + Item::Subscription(s) => &s.name.name, 136 + Item::Use(_) => "", 137 + } 138 + } 139 + 140 + /// Get documentation from an item 141 + pub fn get_item_docs(item: &Item) -> Vec<String> { 142 + let docs = match item { 143 + Item::Record(r) => &r.docs, 144 + Item::InlineType(i) => &i.docs, 145 + Item::DefType(d) => &d.docs, 146 + Item::Token(t) => &t.docs, 147 + Item::Query(q) => &q.docs, 148 + Item::Procedure(p) => &p.docs, 149 + Item::Subscription(s) => &s.docs, 150 + Item::Use(_) => return vec![], 151 + }; 152 + 153 + docs.iter().map(|doc| doc.text.clone()).collect() 154 + } 155 + 156 + /// Format a type as a string for display 157 + pub fn format_type(ty: &Type) -> String { 158 + match ty { 159 + Type::Primitive { kind, .. } => format!("{:?}", kind).to_lowercase(), 160 + Type::Reference { path, .. } => path.to_string(), 161 + Type::Array { inner, .. } => format!("{}[]", format_type(inner)), 162 + Type::Union { types, closed, .. } => { 163 + let type_str = types 164 + .iter() 165 + .map(|t| format_type(t)) 166 + .collect::<Vec<_>>() 167 + .join(" | "); 168 + if *closed { 169 + format!("{} | !", type_str) 170 + } else { 171 + type_str 172 + } 173 + } 174 + Type::Object { fields, .. } => { 175 + let fields_str = fields 176 + .iter() 177 + .map(|f| { 178 + let opt = if f.optional { "" } else { "!" }; 179 + format!("{}{}: {}", f.name.name, opt, format_type(&f.ty)) 180 + }) 181 + .collect::<Vec<_>>() 182 + .join(", "); 183 + format!("{{ {} }}", fields_str) 184 + } 185 + Type::Parenthesized { inner, .. } => format!("({})", format_type(inner)), 186 + Type::Constrained { base, constraints, .. } => { 187 + format!("{} constrained {{ {} constraints }}", format_type(base), constraints.len()) 188 + } 189 + Type::Unknown { .. } => "unknown".to_string(), 190 + } 191 + }