diff --git a/Cargo.lock b/Cargo.lock index 8db69b4..7bc1306 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,8 @@ dependencies = [ "anv-viz", "clap", "miette", + "notify", + "notify-debouncer-mini", "serde_json", "thiserror", ] @@ -126,6 +128,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "anv-lsp" +version = "0.1.0" +dependencies = [ + "anv-core", + "anv-semantics", + "anv-syntax", + "anv-types", + "dashmap", + "ropey", + "serde", + "serde_json", + "tokio", + "tower-lsp", +] + [[package]] name = "anv-semantics" version = "0.1.0" @@ -178,6 +196,28 @@ dependencies = [ "object 0.32.2", ] +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -214,12 +254,24 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + [[package]] name = "cc" version = "1.2.51" @@ -298,6 +350,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -317,6 +382,17 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "errno" version = "0.3.14" @@ -327,6 +403,24 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "find-msvc-tools" version = "0.1.6" @@ -339,6 +433,113 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "gimli" version = "0.32.3" @@ -361,6 +562,143 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + [[package]] name = "is_ci" version = "1.2.0" @@ -379,6 +717,26 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -391,12 +749,44 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall 0.7.0", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "logos" version = "0.14.4" @@ -430,6 +820,19 @@ dependencies = [ "logos-codegen", ] +[[package]] +name = "lsp-types" +version = "0.94.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde_json", + "serde_repr", + "url", +] + [[package]] name = "memchr" version = "2.7.6" @@ -475,6 +878,58 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "notify" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" +dependencies = [ + "bitflags 2.10.0", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.52.0", +] + +[[package]] +name = "notify-debouncer-mini" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaa5a66d07ed97dce782be94dcf5ab4d1b457f4243f7566c7557f15cabc8c799" +dependencies = [ + "log", + "notify", + "notify-types", + "tempfile", +] + +[[package]] +name = "notify-types" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" +dependencies = [ + "instant", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -503,34 +958,104 @@ dependencies = [ ] [[package]] -name = "once_cell" -version = "1.21.3" +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", + "rand", + "serde", +] + +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] -name = "once_cell_polyfill" -version = "1.70.2" +name = "pin-utils" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "ordered-float" -version = "4.6.0" +name = "potential_utf" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ - "num-traits", - "rand", - "serde", + "zerovec", ] -[[package]] -name = "owo-colors" -version = "4.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" - [[package]] name = "pretty_assertions" version = "1.4.1" @@ -569,6 +1094,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -588,12 +1119,40 @@ dependencies = [ "serde", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "ropey" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5" +dependencies = [ + "smallvec", + "str_indices", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -615,13 +1174,28 @@ version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", "windows-sys 0.61.2", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "semver" version = "1.0.27" @@ -671,12 +1245,45 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "smol_str" version = "0.2.2" @@ -686,6 +1293,22 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "stacker" version = "0.1.22" @@ -699,6 +1322,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "str_indices" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" + [[package]] name = "strsim" version = "0.11.1" @@ -737,6 +1366,30 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "terminal_size" version = "0.4.3" @@ -777,6 +1430,148 @@ dependencies = [ "syn", ] +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-lsp" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba052b54a6627628d9b3c34c176e7eda8359b7da9acd497b9f20998d118508" +dependencies = [ + "async-trait", + "auto_impl", + "bytes", + "dashmap", + "futures", + "httparse", + "lsp-types", + "memchr", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tower", + "tower-lsp-macros", + "tracing", +] + +[[package]] +name = "tower-lsp-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -801,6 +1596,24 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -813,12 +1626,55 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -975,12 +1831,47 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.31" @@ -1001,6 +1892,60 @@ dependencies = [ "syn", ] +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.5" diff --git a/Cargo.toml b/Cargo.toml index 8dadcc1..ff64c11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/anv-ir", "crates/anv-viz", "crates/anv-cli", + "crates/anv-lsp", ] [workspace.package] diff --git a/crates/anv-cli/Cargo.toml b/crates/anv-cli/Cargo.toml index 964e48e..d8dc063 100644 --- a/crates/anv-cli/Cargo.toml +++ b/crates/anv-cli/Cargo.toml @@ -27,3 +27,5 @@ clap = { workspace = true } miette = { workspace = true } thiserror = { workspace = true } serde_json = { workspace = true } +notify = "7.0" +notify-debouncer-mini = "0.5" diff --git a/crates/anv-cli/src/main.rs b/crates/anv-cli/src/main.rs index 61c13ae..05f1c97 100644 --- a/crates/anv-cli/src/main.rs +++ b/crates/anv-cli/src/main.rs @@ -17,9 +17,12 @@ use anv_types::check; use anv_viz::{RinkRenderer, SvgOptions, TimelineRenderer}; use clap::{Parser, Subcommand}; use miette::{Diagnostic, NamedSource, Report, SourceSpan}; +use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode}; use std::fs; use std::path::PathBuf; use std::process::ExitCode; +use std::sync::mpsc::channel; +use std::time::Duration; /// Anvomidav - A domain-specific language for figure skating choreography. #[derive(Parser)] @@ -50,7 +53,8 @@ enum Commands { EXAMPLES: anv check program.anv anv check *.anv --verbose - anv check short.anv free.anv")] + anv check short.anv free.anv + anv check *.anv --watch (watch for changes)")] Check { /// Source files to check #[arg(required = true)] @@ -59,6 +63,10 @@ EXAMPLES: /// Show detailed output #[arg(short, long)] verbose: bool, + + /// Watch for file changes and re-check automatically + #[arg(short, long)] + watch: bool, }, /// Parse a source file and print the AST @@ -92,7 +100,8 @@ EXAMPLES: EXAMPLES: anv fmt program.anv - anv fmt *.anv --check (verify formatting without changes)")] + anv fmt *.anv --check (verify formatting without changes) + anv fmt *.anv --watch (watch for changes)")] Fmt { /// Source files to format files: Vec, @@ -100,6 +109,10 @@ EXAMPLES: /// Check if files are formatted without modifying them #[arg(long)] check: bool, + + /// Watch for file changes and re-format automatically + #[arg(short, long)] + watch: bool, }, /// Create a new Anvomidav project @@ -262,66 +275,214 @@ fn main() -> ExitCode { } } -fn run(cli: Cli) -> miette::Result<()> { - match cli.command { - Commands::Check { files, verbose } => { - let mut has_errors = false; +/// Check files for errors, returning true if all passed. +fn check_files(files: &[PathBuf], verbose: bool) -> bool { + let mut has_errors = false; + + for (file_idx, path) in files.iter().enumerate() { + if verbose { + eprintln!("Checking {}...", path.display()); + } + + let source = match fs::read_to_string(path) { + Ok(s) => s, + Err(e) => { + eprintln!("Failed to read {}: {}", path.display(), e); + has_errors = true; + continue; + } + }; + + let file_id = FileId(file_idx as u32); - for (file_idx, path) in files.iter().enumerate() { - if verbose { - eprintln!("Checking {}...", path.display()); + // Parse + let program = match parse(&source, file_id) { + Ok(p) => p, + Err(errors) => { + has_errors = true; + for err in errors { + let span: SourceSpan = (err.span.start, err.span.end - err.span.start).into(); + let report = Report::new(CliError::ParseError { + src: NamedSource::new(path.display().to_string(), source.clone()), + span, + message: err.message.clone(), + label: err.label.clone().unwrap_or_else(|| "here".into()), + help: err.help.clone(), + }); + eprintln!("{:?}", report); + } + continue; + } + }; + + // Type check + if let Err(diagnostics) = check(&program, file_id) { + has_errors = true; + for diag in diagnostics.iter() { + let code_str = diag.code.as_ref().map(|c| c.to_string()).unwrap_or_default(); + eprintln!( + "{}:{}: {} [{}]", + path.display(), + diag.labels.first().map(|l| l.span.start).unwrap_or(0), + diag.message, + code_str + ); + for note in &diag.notes { + eprintln!(" note: {:?}", note); } + } + } + } - let source = fs::read_to_string(path).map_err(|e| CliError::ReadError { - path: path.display().to_string(), - source: e, - })?; - - let file_id = FileId(file_idx as u32); - - // Parse - let program = match parse(&source, file_id) { - Ok(p) => p, - Err(errors) => { - has_errors = true; - for err in errors { - let span: SourceSpan = (err.span.start, err.span.end - err.span.start).into(); - let report = Report::new(CliError::ParseError { - src: NamedSource::new(path.display().to_string(), source.clone()), - span, - message: err.message.clone(), - label: err.label.clone().unwrap_or_else(|| "here".into()), - help: err.help.clone(), - }); - eprintln!("{:?}", report); - } - continue; - } - }; + if !has_errors && verbose { + eprintln!("All checks passed!"); + } - // Type check - if let Err(diagnostics) = check(&program, file_id) { - has_errors = true; - for diag in diagnostics.iter() { - let code_str = diag.code.as_ref().map(|c| c.to_string()).unwrap_or_default(); - eprintln!( - "{}:{}: {} [{}]", - path.display(), - diag.labels.first().map(|l| l.span.start).unwrap_or(0), - diag.message, - code_str - ); - for note in &diag.notes { - eprintln!(" note: {:?}", note); - } - } + !has_errors +} + +/// Format files, returning true if all formatted successfully. +fn format_files(files: &[PathBuf], check_only: bool) -> bool { + if files.is_empty() { + eprintln!("No files specified"); + return true; + } + + let mut success = true; + + for path in files { + let source = match fs::read_to_string(path) { + Ok(s) => s, + Err(e) => { + eprintln!("Failed to read {}: {}", path.display(), e); + success = false; + continue; + } + }; + + // Format the source + let formatted = match anv_syntax::format::format(&source) { + Ok(f) => f, + Err(errors) => { + for err in errors { + let span: SourceSpan = (err.span.start, err.span.end - err.span.start).into(); + let report = Report::new(CliError::ParseError { + src: NamedSource::new(path.display().to_string(), source.clone()), + span, + message: err.message.clone(), + label: err.label.clone().unwrap_or_else(|| "here".into()), + help: err.help.clone(), + }); + eprintln!("{:?}", report); } + success = false; + continue; } + }; - if has_errors { - Err(miette::miette!("check failed with errors"))?; - } else if verbose { - eprintln!("All checks passed!"); + let changed = source != formatted; + + if check_only { + if changed { + eprintln!("{}: would be reformatted", path.display()); + success = false; + } else { + eprintln!("{}: ok", path.display()); + } + } else if changed { + if let Err(e) = fs::write(path, &formatted) { + eprintln!("Failed to write {}: {}", path.display(), e); + success = false; + } else { + eprintln!("{}: formatted", path.display()); + } + } else { + eprintln!("{}: unchanged", path.display()); + } + } + + success +} + +/// Watch files for changes and run a callback when they change. +fn watch_files(files: &[PathBuf], mut on_change: F) -> miette::Result<()> +where + F: FnMut() -> bool, +{ + eprintln!("Watching for changes... (press Ctrl+C to stop)"); + + // Run once initially + on_change(); + + // Set up file watcher + let (tx, rx) = channel(); + let mut debouncer = new_debouncer(Duration::from_millis(300), tx) + .map_err(|e| miette::miette!("Failed to create watcher: {}", e))?; + + // Watch parent directories of all files + let mut watched_dirs = std::collections::HashSet::new(); + for path in files { + if let Some(parent) = path.parent() { + let parent = if parent.as_os_str().is_empty() { + PathBuf::from(".") + } else { + parent.to_path_buf() + }; + if watched_dirs.insert(parent.clone()) { + debouncer + .watcher() + .watch(&parent, RecursiveMode::NonRecursive) + .map_err(|e| miette::miette!("Failed to watch {}: {}", parent.display(), e))?; + } + } + } + + // Process events + loop { + match rx.recv() { + Ok(Ok(events)) => { + // Check if any of our files changed + let our_files: std::collections::HashSet<_> = files + .iter() + .filter_map(|p| p.canonicalize().ok()) + .collect(); + + let changed = events.iter().any(|event| { + event + .path + .canonicalize() + .map(|p| our_files.contains(&p)) + .unwrap_or(false) + }); + + if changed { + eprintln!("\n--- File changed, re-running... ---\n"); + on_change(); + } + } + Ok(Err(error)) => { + eprintln!("Watch error: {}", error); + } + Err(e) => { + eprintln!("Watch channel error: {}", e); + break; + } + } + } + + Ok(()) +} + +fn run(cli: Cli) -> miette::Result<()> { + match cli.command { + Commands::Check { files, verbose, watch } => { + if watch { + watch_files(&files, || check_files(&files, verbose))?; + } else { + let success = check_files(&files, verbose); + if !success { + return Err(miette::miette!("check failed with errors")); + } } Ok(()) @@ -384,36 +545,13 @@ fn run(cli: Cli) -> miette::Result<()> { Ok(()) } - Commands::Fmt { files, check } => { - if files.is_empty() { - eprintln!("No files specified"); - return Ok(()); - } - - for path in &files { - let source = fs::read_to_string(path).map_err(|e| CliError::ReadError { - path: path.display().to_string(), - source: e, - })?; - - // TODO: Implement formatter - // For now, just verify the file parses - let _program = parse(&source, FileId(0)).map_err(|errors| { - let err = &errors[0]; - let span: SourceSpan = (err.span.start, err.span.end - err.span.start).into(); - CliError::ParseError { - src: NamedSource::new(path.display().to_string(), source.clone()), - span, - message: err.message.clone(), - label: err.label.clone().unwrap_or_else(|| "here".into()), - help: err.help.clone(), - } - })?; - - if check { - eprintln!("{}: would reformat", path.display()); - } else { - eprintln!("{}: formatted (no-op, formatter not implemented)", path.display()); + Commands::Fmt { files, check: check_only, watch } => { + if watch { + watch_files(&files, || format_files(&files, check_only))?; + } else { + let success = format_files(&files, check_only); + if !success && check_only { + return Err(miette::miette!("Some files would be reformatted")); } } diff --git a/crates/anv-lsp/Cargo.toml b/crates/anv-lsp/Cargo.toml new file mode 100644 index 0000000..fc3b905 --- /dev/null +++ b/crates/anv-lsp/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "anv-lsp" +version = "0.1.0" +edition = "2021" +description = "Language Server Protocol implementation for Anvomidav" +license = "MIT OR AGPL-3.0-or-later" +repository = "https://github.com/hyperpolymath/anvomidav" + +[[bin]] +name = "anv-lsp" +path = "src/main.rs" + +[dependencies] +anv-core = { path = "../anv-core" } +anv-syntax = { path = "../anv-syntax" } +anv-types = { path = "../anv-types" } +anv-semantics = { path = "../anv-semantics" } + +tower-lsp = "0.20" +tokio = { version = "1", features = ["full"] } +serde = { workspace = true } +serde_json = { workspace = true } +dashmap = "5" +ropey = "1" diff --git a/crates/anv-lsp/src/main.rs b/crates/anv-lsp/src/main.rs new file mode 100644 index 0000000..81de525 --- /dev/null +++ b/crates/anv-lsp/src/main.rs @@ -0,0 +1,512 @@ +// SPDX-FileCopyrightText: 2025 hyperpolymath +// SPDX-License-Identifier: MIT OR AGPL-3.0-or-later + +//! Anvomidav Language Server Protocol implementation. + +use dashmap::DashMap; +use ropey::Rope; +use tower_lsp::jsonrpc::Result; +use tower_lsp::lsp_types::*; +use tower_lsp::{Client, LanguageServer, LspService, Server}; + +use anv_core::source::FileId; +use anv_semantics::Discipline; +use anv_syntax::parse; + +/// Document state stored by the server. +struct Document { + /// Document content as rope for efficient editing. + content: Rope, + /// Current version. + version: i32, +} + +/// The Anvomidav language server. +struct AnvomidavServer { + /// LSP client handle. + client: Client, + /// Open documents. + documents: DashMap, +} + +impl AnvomidavServer { + fn new(client: Client) -> Self { + Self { + client, + documents: DashMap::new(), + } + } + + /// Validate a document and publish diagnostics. + async fn validate_document(&self, uri: &Url) { + let Some(doc) = self.documents.get(uri) else { + return; + }; + + let source = doc.content.to_string(); + let mut diagnostics = Vec::new(); + + // Parse the document + match parse(&source, FileId(0)) { + Ok(program) => { + // Type check + if let Err(type_diags) = anv_types::check(&program, FileId(0)) { + for diag in type_diags.iter() { + // Get the first label's span for the range, or default to start + let range = if let Some(label) = diag.labels.first() { + self.span_to_range(&source, label.span.start as usize, label.span.end as usize) + } else { + Range::new(Position::new(0, 0), Position::new(0, 1)) + }; + + diagnostics.push(Diagnostic { + range, + severity: Some(DiagnosticSeverity::ERROR), + code: Some(NumberOrString::String("type-error".into())), + source: Some("anvomidav".into()), + message: diag.message.clone(), + ..Default::default() + }); + } + } + + // Semantic validation (ISU rules) + let result = anv_semantics::validate_program(&program, Discipline::MenSingles); + for err in result.errors { + diagnostics.push(Diagnostic { + range: Range::new(Position::new(0, 0), Position::new(0, 1)), + severity: Some(DiagnosticSeverity::ERROR), + code: Some(NumberOrString::String("isu-rule".into())), + source: Some("anvomidav".into()), + message: format!("{}", err), + ..Default::default() + }); + } + for warn in result.warnings { + diagnostics.push(Diagnostic { + range: Range::new(Position::new(0, 0), Position::new(0, 1)), + severity: Some(DiagnosticSeverity::WARNING), + code: Some(NumberOrString::String("isu-warning".into())), + source: Some("anvomidav".into()), + message: format!("{}", warn), + ..Default::default() + }); + } + } + Err(parse_errors) => { + for err in parse_errors { + let range = self.span_to_range(&source, err.span.start, err.span.end); + let mut diagnostic = Diagnostic { + range, + severity: Some(DiagnosticSeverity::ERROR), + code: Some(NumberOrString::String("parse-error".into())), + source: Some("anvomidav".into()), + message: err.message, + ..Default::default() + }; + + // Add related information for help text + if let Some(help) = err.help { + diagnostic.related_information = Some(vec![DiagnosticRelatedInformation { + location: Location { + uri: uri.clone(), + range, + }, + message: help, + }]); + } + + diagnostics.push(diagnostic); + } + } + } + + self.client + .publish_diagnostics(uri.clone(), diagnostics, None) + .await; + } + + /// Convert byte offsets to LSP range. + fn span_to_range(&self, source: &str, start: usize, end: usize) -> Range { + let start_pos = self.offset_to_position(source, start); + let end_pos = self.offset_to_position(source, end); + Range::new(start_pos, end_pos) + } + + /// Convert byte offset to LSP position. + fn offset_to_position(&self, source: &str, offset: usize) -> Position { + let mut line = 0u32; + let mut col = 0u32; + for (i, ch) in source.char_indices() { + if i >= offset { + break; + } + if ch == '\n' { + line += 1; + col = 0; + } else { + col += 1; + } + } + Position::new(line, col) + } +} + +#[tower_lsp::async_trait] +impl LanguageServer for AnvomidavServer { + async fn initialize(&self, _: InitializeParams) -> Result { + Ok(InitializeResult { + capabilities: ServerCapabilities { + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::FULL), + save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions { + include_text: Some(true), + })), + ..Default::default() + }, + )), + hover_provider: Some(HoverProviderCapability::Simple(true)), + completion_provider: Some(CompletionOptions { + trigger_characters: Some(vec![" ".into(), ":".into()]), + ..Default::default() + }), + definition_provider: Some(OneOf::Left(true)), + document_symbol_provider: Some(OneOf::Left(true)), + semantic_tokens_provider: Some( + SemanticTokensServerCapabilities::SemanticTokensOptions( + SemanticTokensOptions { + legend: SemanticTokensLegend { + token_types: vec![ + SemanticTokenType::KEYWORD, + SemanticTokenType::TYPE, + SemanticTokenType::FUNCTION, + SemanticTokenType::VARIABLE, + SemanticTokenType::NUMBER, + SemanticTokenType::STRING, + SemanticTokenType::COMMENT, + SemanticTokenType::OPERATOR, + ], + token_modifiers: vec![], + }, + full: Some(SemanticTokensFullOptions::Bool(true)), + range: Some(false), + ..Default::default() + }, + ), + ), + ..Default::default() + }, + server_info: Some(ServerInfo { + name: "anvomidav-lsp".into(), + version: Some(env!("CARGO_PKG_VERSION").into()), + }), + }) + } + + async fn initialized(&self, _: InitializedParams) { + self.client + .log_message(MessageType::INFO, "Anvomidav LSP initialized") + .await; + } + + async fn shutdown(&self) -> Result<()> { + Ok(()) + } + + async fn did_open(&self, params: DidOpenTextDocumentParams) { + let uri = params.text_document.uri; + let content = Rope::from_str(¶ms.text_document.text); + let version = params.text_document.version; + + self.documents.insert(uri.clone(), Document { content, version }); + self.validate_document(&uri).await; + } + + async fn did_change(&self, params: DidChangeTextDocumentParams) { + let uri = params.text_document.uri; + + if let Some(mut doc) = self.documents.get_mut(&uri) { + // Full sync - replace entire content + if let Some(change) = params.content_changes.into_iter().next() { + doc.content = Rope::from_str(&change.text); + doc.version = params.text_document.version; + } + } + + self.validate_document(&uri).await; + } + + async fn did_save(&self, params: DidSaveTextDocumentParams) { + self.validate_document(¶ms.text_document.uri).await; + } + + async fn did_close(&self, params: DidCloseTextDocumentParams) { + self.documents.remove(¶ms.text_document.uri); + } + + async fn hover(&self, params: HoverParams) -> Result> { + let uri = ¶ms.text_document_position_params.text_document.uri; + let position = params.text_document_position_params.position; + + let Some(doc) = self.documents.get(uri) else { + return Ok(None); + }; + + let source = doc.content.to_string(); + + // Find the word at the cursor position + let offset = self.position_to_offset(&source, position); + let word = self.word_at_offset(&source, offset); + + // Provide hover info for keywords and elements + let info = match word.as_str() { + // Element keywords + "jump" => Some("**Jump Element**\n\nSyntax: `jump `\n\nRotations: single, double, triple, quad\nKinds: axel, lutz, flip, loop, salchow, toe_loop, euler"), + "spin" => Some("**Spin Element**\n\nSyntax: `spin [level]`\n\nPositions: upright, sit, camel, layback, biellmann\nLevels: B, L1, L2, L3, L4"), + "step" => Some("**Step Sequence**\n\nSyntax: `step [level]`\n\nPatterns: straight, circular, serpentine, diagonal, midline"), + "lift" => Some("**Lift Element** (Pairs)\n\nSyntax: `lift [level]`\n\nGroups: Gr1, Gr2, Gr3, Gr4, Gr5"), + "throw" => Some("**Throw Jump** (Pairs)\n\nSyntax: `throw `"), + "twist" => Some("**Twist Lift** (Pairs)\n\nSyntax: `twist [level]`"), + "death_spiral" => Some("**Death Spiral** (Pairs)\n\nSyntax: `death_spiral [level]`\n\nEdges: LFO, LFI, LBO, LBI, RFO, RFI, RBO, RBI"), + "choreographic" => Some("**Choreographic Element**\n\nSyntax: `choreographic `\n\nKinds: spiral, spread, ina, hydroblading, pivot"), + + // Segment kinds + "short" => Some("**Short Program**\n\n2:40 duration (+/- 10 seconds)\nRequired elements based on discipline"), + "free" => Some("**Free Skating**\n\n4:00 duration (+/- 10 seconds)\nWell-balanced program requirements"), + "rhythm" => Some("**Rhythm Dance** (Ice Dance)\n\n2:50 duration (+/- 10 seconds)\nRequired pattern dance sequences"), + "exhibition" => Some("**Exhibition/Gala**\n\nNo time limits or element requirements\nPure artistic expression"), + + // Rotations + "triple" => Some("**Triple** (3 rotations)"), + "quad" => Some("**Quadruple** (4 rotations)\n\nHighest difficulty rotation count"), + + _ => None, + }; + + Ok(info.map(|text| Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: text.to_string(), + }), + range: None, + })) + } + + async fn completion(&self, params: CompletionParams) -> Result> { + let uri = ¶ms.text_document_position.text_document.uri; + let position = params.text_document_position.position; + + let Some(doc) = self.documents.get(uri) else { + return Ok(None); + }; + + let source = doc.content.to_string(); + let offset = self.position_to_offset(&source, position); + + // Get context for completion + let line_start = source[..offset].rfind('\n').map(|i| i + 1).unwrap_or(0); + let line = &source[line_start..offset]; + + let mut items = Vec::new(); + + // Context-aware completions + if line.trim().is_empty() || line.ends_with('{') { + // Top-level or block start + items.extend(vec![ + self.completion_item("sequence", CompletionItemKind::KEYWORD, "sequence { }"), + self.completion_item("jump", CompletionItemKind::KEYWORD, "jump triple "), + self.completion_item("spin", CompletionItemKind::KEYWORD, "spin camel "), + self.completion_item("step", CompletionItemKind::KEYWORD, "step circular "), + self.completion_item("choreographic", CompletionItemKind::KEYWORD, "choreographic spiral"), + ]); + } + + if line.contains("jump") || line.contains("throw") { + // After jump/throw keyword + items.extend(vec![ + self.completion_item("single", CompletionItemKind::ENUM_MEMBER, "single"), + self.completion_item("double", CompletionItemKind::ENUM_MEMBER, "double"), + self.completion_item("triple", CompletionItemKind::ENUM_MEMBER, "triple"), + self.completion_item("quad", CompletionItemKind::ENUM_MEMBER, "quad"), + ]); + } + + if line.contains("triple") || line.contains("quad") || line.contains("double") || line.contains("single") { + // After rotation + items.extend(vec![ + self.completion_item("axel", CompletionItemKind::ENUM_MEMBER, "axel"), + self.completion_item("lutz", CompletionItemKind::ENUM_MEMBER, "lutz"), + self.completion_item("flip", CompletionItemKind::ENUM_MEMBER, "flip"), + self.completion_item("loop", CompletionItemKind::ENUM_MEMBER, "loop"), + self.completion_item("salchow", CompletionItemKind::ENUM_MEMBER, "salchow"), + self.completion_item("toe_loop", CompletionItemKind::ENUM_MEMBER, "toe_loop"), + ]); + } + + if line.contains("spin") { + // Spin positions + items.extend(vec![ + self.completion_item("upright", CompletionItemKind::ENUM_MEMBER, "upright"), + self.completion_item("sit", CompletionItemKind::ENUM_MEMBER, "sit"), + self.completion_item("camel", CompletionItemKind::ENUM_MEMBER, "camel"), + self.completion_item("layback", CompletionItemKind::ENUM_MEMBER, "layback"), + ]); + } + + if line.contains("segment") && line.contains(":") { + // Segment kinds + items.extend(vec![ + self.completion_item("short", CompletionItemKind::ENUM_MEMBER, "short"), + self.completion_item("free", CompletionItemKind::ENUM_MEMBER, "free"), + self.completion_item("rhythm", CompletionItemKind::ENUM_MEMBER, "rhythm"), + self.completion_item("exhibition", CompletionItemKind::ENUM_MEMBER, "exhibition"), + ]); + } + + // Level completions + items.extend(vec![ + self.completion_item("L1", CompletionItemKind::ENUM_MEMBER, "L1"), + self.completion_item("L2", CompletionItemKind::ENUM_MEMBER, "L2"), + self.completion_item("L3", CompletionItemKind::ENUM_MEMBER, "L3"), + self.completion_item("L4", CompletionItemKind::ENUM_MEMBER, "L4"), + ]); + + Ok(Some(CompletionResponse::Array(items))) + } + + async fn document_symbol( + &self, + params: DocumentSymbolParams, + ) -> Result> { + let uri = ¶ms.text_document.uri; + + let Some(doc) = self.documents.get(uri) else { + return Ok(None); + }; + + let source = doc.content.to_string(); + + match parse(&source, FileId(0)) { + Ok(program) => { + let mut symbols = Vec::new(); + + // Program symbol + let program_range = Range::new(Position::new(0, 0), Position::new(0, 10)); + + let mut children = Vec::new(); + + // Add segments as children + for segment in &program.segments { + let segment_range = self.span_to_range( + &source, + segment.span.start as usize, + segment.span.end as usize, + ); + + let mut seq_children = Vec::new(); + for seq in &segment.sequences { + let seq_range = self.span_to_range( + &source, + seq.span.start as usize, + seq.span.end as usize, + ); + let seq_name = seq.name.as_ref() + .map(|n| n.node.clone()) + .unwrap_or_else(|| "sequence".into()); + + #[allow(deprecated)] + seq_children.push(DocumentSymbol { + name: seq_name, + detail: Some(format!("{} elements", seq.elements.len())), + kind: SymbolKind::FUNCTION, + range: seq_range, + selection_range: seq_range, + children: None, + tags: None, + deprecated: None, + }); + } + + #[allow(deprecated)] + children.push(DocumentSymbol { + name: segment.name.node.clone(), + detail: Some(format!("{:?}", segment.kind)), + kind: SymbolKind::CLASS, + range: segment_range, + selection_range: segment_range, + children: Some(seq_children), + tags: None, + deprecated: None, + }); + } + + #[allow(deprecated)] + symbols.push(DocumentSymbol { + name: program.name.node.clone(), + detail: Some("program".into()), + kind: SymbolKind::MODULE, + range: program_range, + selection_range: program_range, + children: Some(children), + tags: None, + deprecated: None, + }); + + Ok(Some(DocumentSymbolResponse::Nested(symbols))) + } + Err(_) => Ok(None), + } + } +} + +impl AnvomidavServer { + fn position_to_offset(&self, source: &str, position: Position) -> usize { + let mut offset = 0; + for (line_num, line) in source.lines().enumerate() { + if line_num == position.line as usize { + offset += position.character as usize; + break; + } + offset += line.len() + 1; // +1 for newline + } + offset.min(source.len()) + } + + fn word_at_offset(&self, source: &str, offset: usize) -> String { + let bytes = source.as_bytes(); + + // Find word start + let mut start = offset; + while start > 0 && (bytes[start - 1].is_ascii_alphanumeric() || bytes[start - 1] == b'_') { + start -= 1; + } + + // Find word end + let mut end = offset; + while end < bytes.len() && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_') { + end += 1; + } + + source[start..end].to_string() + } + + fn completion_item(&self, label: &str, kind: CompletionItemKind, insert: &str) -> CompletionItem { + CompletionItem { + label: label.into(), + kind: Some(kind), + insert_text: Some(insert.into()), + ..Default::default() + } + } +} + +#[tokio::main] +async fn main() { + let stdin = tokio::io::stdin(); + let stdout = tokio::io::stdout(); + + let (service, socket) = LspService::new(AnvomidavServer::new); + Server::new(stdin, stdout, socket).serve(service).await; +} diff --git a/crates/anv-syntax/src/format.rs b/crates/anv-syntax/src/format.rs new file mode 100644 index 0000000..68cb7fa --- /dev/null +++ b/crates/anv-syntax/src/format.rs @@ -0,0 +1,892 @@ +// SPDX-FileCopyrightText: 2025 hyperpolymath +// SPDX-License-Identifier: MIT OR AGPL-3.0-or-later + +//! Formatter for Anvomidav source code. +//! +//! Produces consistent, readable formatting for .anv files. + +use crate::ast::*; + +/// Formatting options. +#[derive(Debug, Clone)] +pub struct FormatOptions { + /// Indentation string (default: 4 spaces). + pub indent: String, + /// Maximum line width before breaking. + pub max_width: usize, + /// Add blank lines between segments. + pub blank_between_segments: bool, + /// Add blank lines between sequences. + pub blank_between_sequences: bool, +} + +impl Default for FormatOptions { + fn default() -> Self { + Self { + indent: " ".to_string(), + max_width: 100, + blank_between_segments: true, + blank_between_sequences: false, + } + } +} + +/// Formatter for Anvomidav programs. +pub struct Formatter { + options: FormatOptions, + output: String, + indent_level: usize, +} + +impl Formatter { + /// Create a new formatter with default options. + pub fn new() -> Self { + Self::with_options(FormatOptions::default()) + } + + /// Create a new formatter with custom options. + pub fn with_options(options: FormatOptions) -> Self { + Self { + options, + output: String::new(), + indent_level: 0, + } + } + + /// Format a program. + pub fn format(&mut self, program: &Program) -> String { + self.output.clear(); + self.format_program(program); + self.output.clone() + } + + fn indent(&mut self) { + for _ in 0..self.indent_level { + self.output.push_str(&self.options.indent); + } + } + + fn newline(&mut self) { + self.output.push('\n'); + } + + fn format_program(&mut self, program: &Program) { + // Doc comments + for doc in &program.docs { + self.output.push_str("/// "); + self.output.push_str(doc); + self.newline(); + } + + // Program declaration + self.output.push_str("program "); + self.output.push_str(&program.name.node); + self.output.push_str(" {"); + self.newline(); + self.indent_level += 1; + + // Imports + for import in &program.imports { + self.format_import(import); + } + + if !program.imports.is_empty() && !program.segments.is_empty() { + self.newline(); + } + + // Functions + for func in &program.functions { + self.format_fn_def(func); + self.newline(); + } + + // Segments + for (i, segment) in program.segments.iter().enumerate() { + if i > 0 && self.options.blank_between_segments { + self.newline(); + } + self.format_segment(segment); + } + + self.indent_level -= 1; + self.output.push('}'); + self.newline(); + } + + fn format_import(&mut self, import: &Import) { + self.indent(); + self.output.push_str("import "); + + let path: Vec<_> = import.path.iter().map(|p| p.node.as_str()).collect(); + self.output.push_str(&path.join("::")); + + if let Some(alias) = &import.alias { + self.output.push_str(" as "); + self.output.push_str(&alias.node); + } + + self.newline(); + } + + fn format_fn_def(&mut self, func: &FnDef) { + self.indent(); + self.output.push_str("fn "); + self.output.push_str(&func.name.node); + self.output.push('('); + + for (i, param) in func.params.iter().enumerate() { + if i > 0 { + self.output.push_str(", "); + } + self.output.push_str(¶m.name.node); + if let Some(ty) = ¶m.ty { + self.output.push_str(": "); + self.format_type(ty); + } + } + + self.output.push(')'); + + if let Some(ret) = &func.return_ty { + self.output.push_str(" -> "); + self.format_type(ret); + } + + self.output.push_str(" = "); + self.format_expr(&func.body); + self.newline(); + } + + fn format_type(&mut self, ty: &TypeExpr) { + match ty { + TypeExpr::Named(name) => { + self.output.push_str(&name.node); + } + TypeExpr::Array(inner) => { + self.output.push('['); + self.format_type(inner); + self.output.push(']'); + } + TypeExpr::Optional(inner) => { + self.format_type(inner); + self.output.push('?'); + } + TypeExpr::Tuple(types) => { + self.output.push('('); + for (i, t) in types.iter().enumerate() { + if i > 0 { + self.output.push_str(", "); + } + self.format_type(t); + } + self.output.push(')'); + } + TypeExpr::App(base, args) => { + self.format_type(base); + self.output.push('<'); + for (i, arg) in args.iter().enumerate() { + if i > 0 { + self.output.push_str(", "); + } + self.format_type(arg); + } + self.output.push('>'); + } + TypeExpr::Fn(params, ret) => { + self.output.push_str("fn("); + for (i, p) in params.iter().enumerate() { + if i > 0 { + self.output.push_str(", "); + } + self.format_type(p); + } + self.output.push_str(") -> "); + self.format_type(ret); + } + TypeExpr::Refinement { base, var, predicate } => { + self.output.push_str("{ "); + self.output.push_str(&var.node); + self.output.push_str(": "); + self.format_type(base); + self.output.push_str(" | "); + self.format_expr(predicate); + self.output.push_str(" }"); + } + } + } + + fn format_expr(&mut self, expr: &Expr) { + match expr { + Expr::Int(n) => { + self.output.push_str(&n.to_string()); + } + Expr::Float(f) => { + self.output.push_str(&f.to_string()); + } + Expr::String(s) => { + self.output.push('"'); + self.output.push_str(s); + self.output.push('"'); + } + Expr::Bool(b) => { + self.output.push_str(if *b { "true" } else { "false" }); + } + Expr::Time(time) => { + self.format_time_expr(time); + } + Expr::Position(pos) => { + self.format_position_expr(pos); + } + Expr::Var(v) => { + self.output.push_str(&v.node); + } + Expr::BinOp(left, op, right) => { + self.format_expr(&left.node); + self.output.push(' '); + self.format_binop(op); + self.output.push(' '); + self.format_expr(&right.node); + } + Expr::UnaryOp(op, expr) => { + match op { + UnaryOp::Neg => self.output.push('-'), + UnaryOp::Not => self.output.push('!'), + } + self.format_expr(&expr.node); + } + Expr::Tuple(exprs) => { + self.output.push('('); + for (i, e) in exprs.iter().enumerate() { + if i > 0 { + self.output.push_str(", "); + } + self.format_expr(&e.node); + } + self.output.push(')'); + } + Expr::Array(exprs) => { + self.output.push('['); + for (i, e) in exprs.iter().enumerate() { + if i > 0 { + self.output.push_str(", "); + } + self.format_expr(&e.node); + } + self.output.push(']'); + } + Expr::Block(stmts) => { + self.output.push_str("{ "); + for (i, stmt) in stmts.iter().enumerate() { + if i > 0 { + self.output.push_str("; "); + } + self.format_stmt(&stmt.node); + } + self.output.push_str(" }"); + } + Expr::If(cond, then_branch, else_branch) => { + self.output.push_str("if "); + self.format_expr(&cond.node); + self.output.push_str(" then "); + self.format_expr(&then_branch.node); + if let Some(else_b) = else_branch { + self.output.push_str(" else "); + self.format_expr(&else_b.node); + } + } + Expr::Let(name, ty, value, body) => { + self.output.push_str("let "); + self.output.push_str(&name.node); + if let Some(t) = ty { + self.output.push_str(": "); + self.format_type(t); + } + self.output.push_str(" = "); + self.format_expr(&value.node); + self.output.push_str(" in "); + self.format_expr(&body.node); + } + Expr::Call(func, args) => { + self.format_expr(&func.node); + self.output.push('('); + for (i, arg) in args.iter().enumerate() { + if i > 0 { + self.output.push_str(", "); + } + self.format_expr(&arg.node); + } + self.output.push(')'); + } + Expr::Field(expr, field) => { + self.format_expr(&expr.node); + self.output.push('.'); + self.output.push_str(&field.node); + } + Expr::Index(expr, idx) => { + self.format_expr(&expr.node); + self.output.push('['); + self.format_expr(&idx.node); + self.output.push(']'); + } + Expr::Match(expr, arms) => { + self.output.push_str("match "); + self.format_expr(&expr.node); + self.output.push_str(" { "); + for (i, arm) in arms.iter().enumerate() { + if i > 0 { + self.output.push_str(", "); + } + self.format_pattern(&arm.pattern); + self.output.push_str(" => "); + self.format_expr(&arm.body.node); + } + self.output.push_str(" }"); + } + Expr::Lambda(params, body) => { + self.output.push_str("|"); + for (i, p) in params.iter().enumerate() { + if i > 0 { + self.output.push_str(", "); + } + self.output.push_str(&p.name.node); + if let Some(t) = &p.ty { + self.output.push_str(": "); + self.format_type(t); + } + } + self.output.push_str("| "); + self.format_expr(&body.node); + } + Expr::Record(fields) => { + self.output.push_str("{ "); + for (i, (name, val)) in fields.iter().enumerate() { + if i > 0 { + self.output.push_str(", "); + } + self.output.push_str(&name.node); + self.output.push_str(": "); + self.format_expr(&val.node); + } + self.output.push_str(" }"); + } + Expr::Element(elem) => { + self.format_element(elem); + } + } + } + + fn format_position_expr(&mut self, pos: &PositionExpr) { + match pos { + PositionExpr::Literal(x, y) => { + self.output.push_str(&format!("({}, {})", x, y)); + } + PositionExpr::Named(name) => { + self.output.push_str(name); + } + PositionExpr::Var(v) => { + self.output.push_str(&v.node); + } + PositionExpr::Relative(base, dx, dy) => { + self.format_position_expr(base); + self.output.push_str(&format!(" + ({}, {})", dx, dy)); + } + } + } + + fn format_pattern(&mut self, _pattern: &Pattern) { + // Simplified pattern formatting + self.output.push_str("_"); + } + + fn format_binop(&mut self, op: &BinOp) { + let s = match op { + BinOp::Add => "+", + BinOp::Sub => "-", + BinOp::Mul => "*", + BinOp::Div => "/", + BinOp::Mod => "%", + BinOp::Eq => "==", + BinOp::NotEq => "!=", + BinOp::Lt => "<", + BinOp::LtEq => "<=", + BinOp::Gt => ">", + BinOp::GtEq => ">=", + BinOp::And => "&&", + BinOp::Or => "||", + }; + self.output.push_str(s); + } + + fn format_stmt(&mut self, stmt: &Stmt) { + match stmt { + Stmt::Expr(e) => self.format_expr(e), + Stmt::Let(name, ty, value) => { + self.output.push_str("let "); + self.output.push_str(&name.node); + if let Some(t) = ty { + self.output.push_str(": "); + self.format_type(t); + } + self.output.push_str(" = "); + self.format_expr(value); + } + Stmt::Assign(target, value) => { + self.format_expr(&target.node); + self.output.push_str(" = "); + self.format_expr(value); + } + Stmt::Return(expr) => { + self.output.push_str("return"); + if let Some(e) = expr { + self.output.push(' '); + self.format_expr(e); + } + } + Stmt::For(name, iter, body) => { + self.output.push_str("for "); + self.output.push_str(&name.node); + self.output.push_str(" in "); + self.format_expr(iter); + self.output.push_str(" { "); + for stmt in body { + self.format_stmt(&stmt.node); + self.output.push_str("; "); + } + self.output.push('}'); + } + Stmt::While(cond, body) => { + self.output.push_str("while "); + self.format_expr(cond); + self.output.push_str(" { "); + for stmt in body { + self.format_stmt(&stmt.node); + self.output.push_str("; "); + } + self.output.push('}'); + } + } + } + + fn format_transition(&mut self, trans: &TransitionElement) { + self.output.push_str("transition"); + if let Some(desc) = &trans.description { + self.output.push(' '); + self.output.push('"'); + self.output.push_str(desc); + self.output.push('"'); + } + } + + fn format_pattern_element(&mut self, pattern: &PatternElement) { + self.output.push_str("pattern "); + self.output.push('"'); + self.output.push_str(&pattern.name); + self.output.push('"'); + } + + fn format_parallel(&mut self, parallel: &ParallelElement) { + self.output.push_str("parallel {"); + self.newline(); + self.indent_level += 1; + for (skater, elements) in ¶llel.branches { + self.indent(); + self.output.push_str(&skater.node); + self.output.push_str(": {"); + self.newline(); + self.indent_level += 1; + for elem in elements { + self.format_element(elem); + } + self.indent_level -= 1; + self.indent(); + self.output.push('}'); + self.newline(); + } + self.indent_level -= 1; + self.indent(); + self.output.push('}'); + } + + fn format_sync(&mut self, sync: &SyncElement) { + self.output.push_str("sync {"); + self.newline(); + self.indent_level += 1; + for elem in &sync.elements { + self.format_element(elem); + } + self.indent_level -= 1; + self.indent(); + self.output.push('}'); + } + + fn format_segment(&mut self, segment: &Segment) { + // Doc comments + for doc in &segment.docs { + self.indent(); + self.output.push_str("/// "); + self.output.push_str(doc); + self.newline(); + } + + self.indent(); + self.output.push_str("segment "); + self.output.push_str(&segment.name.node); + self.output.push_str(": "); + self.format_segment_kind(&segment.kind); + self.output.push_str(" {"); + self.newline(); + self.indent_level += 1; + + for (i, seq) in segment.sequences.iter().enumerate() { + if i > 0 && self.options.blank_between_sequences { + self.newline(); + } + self.format_sequence(seq); + } + + self.indent_level -= 1; + self.indent(); + self.output.push('}'); + self.newline(); + } + + fn format_segment_kind(&mut self, kind: &SegmentKind) { + let s = match kind { + SegmentKind::Short => "short", + SegmentKind::Free => "free", + SegmentKind::Pattern => "pattern", + SegmentKind::Rhythm => "rhythm", + SegmentKind::Exhibition => "exhibition", + }; + self.output.push_str(s); + } + + fn format_sequence(&mut self, seq: &Sequence) { + self.indent(); + self.output.push_str("sequence"); + if let Some(name) = &seq.name { + self.output.push(' '); + self.output.push_str(&name.node); + } + self.output.push_str(" {"); + self.newline(); + self.indent_level += 1; + + for element in &seq.elements { + self.format_element(element); + } + + self.indent_level -= 1; + self.indent(); + self.output.push('}'); + self.newline(); + } + + fn format_element(&mut self, element: &Element) { + self.indent(); + + match &element.kind { + ElementKind::Jump(jump) => { + self.output.push_str("jump "); + self.format_rotations(&jump.rotations); + self.output.push(' '); + self.format_jump_kind(&jump.kind); + } + ElementKind::Spin(spin) => { + self.output.push_str("spin "); + for (i, pos) in spin.positions.iter().enumerate() { + if i > 0 { + self.output.push(' '); + } + self.format_spin_position(&pos.position); + } + if let Some(level) = &spin.level { + self.output.push(' '); + self.format_level(level); + } + } + ElementKind::StepSequence(step) => { + self.output.push_str("step "); + self.format_step_pattern(&step.pattern); + if let Some(level) = &step.level { + self.output.push(' '); + self.format_level(level); + } + } + ElementKind::Lift(lift) => { + self.output.push_str("lift "); + self.format_lift_group(&lift.group); + if let Some(level) = &lift.level { + self.output.push(' '); + self.format_level(level); + } + } + ElementKind::Throw(throw) => { + self.output.push_str("throw "); + self.format_rotations(&throw.rotations); + self.output.push(' '); + self.format_jump_kind(&throw.kind); + } + ElementKind::Twist(twist) => { + self.output.push_str("twist "); + self.format_rotations(&twist.rotations); + if let Some(level) = &twist.level { + self.output.push(' '); + self.format_level(level); + } + } + ElementKind::DeathSpiral(ds) => { + self.output.push_str("death_spiral "); + self.format_edge(&ds.edge); + if let Some(level) = &ds.level { + self.output.push(' '); + self.format_level(level); + } + } + ElementKind::Choreographic(choreo) => { + self.output.push_str("choreographic "); + self.format_choreo_kind(&choreo.kind); + } + ElementKind::Transition(trans) => { + self.format_transition(trans); + } + ElementKind::Pattern(pattern) => { + self.format_pattern_element(pattern); + } + ElementKind::Parallel(parallel) => { + self.format_parallel(parallel); + } + ElementKind::Sync(sync) => { + self.format_sync(sync); + } + } + + // Timing + if let Some(timing) = &element.timing { + self.output.push(' '); + self.format_timing(timing); + } + + self.newline(); + } + + fn format_rotations(&mut self, rot: &anv_core::skating::Rotations) { + use anv_core::skating::Rotations; + let s = match rot { + Rotations::Single => "single", + Rotations::Double => "double", + Rotations::Triple => "triple", + Rotations::Quad => "quad", + }; + self.output.push_str(s); + } + + fn format_jump_kind(&mut self, kind: &anv_core::skating::JumpKind) { + use anv_core::skating::JumpKind; + let s = match kind { + JumpKind::Axel => "axel", + JumpKind::Lutz => "lutz", + JumpKind::Flip => "flip", + JumpKind::Loop => "loop", + JumpKind::Salchow => "salchow", + JumpKind::ToeLoop => "toe_loop", + JumpKind::Euler => "euler", + }; + self.output.push_str(s); + } + + fn format_spin_position(&mut self, pos: &anv_core::skating::SpinPosition) { + use anv_core::skating::SpinPosition; + let s = match pos { + SpinPosition::Upright => "upright", + SpinPosition::Sit => "sit", + SpinPosition::Camel => "camel", + SpinPosition::Layback => "layback", + SpinPosition::Biellmann => "biellmann", + }; + self.output.push_str(s); + } + + fn format_step_pattern(&mut self, pattern: &StepPattern) { + let s = match pattern { + StepPattern::Straight => "straight", + StepPattern::Circular => "circular", + StepPattern::Serpentine => "serpentine", + StepPattern::Diagonal => "diagonal", + StepPattern::Midline => "midline", + }; + self.output.push_str(s); + } + + fn format_lift_group(&mut self, group: &anv_core::skating::LiftGroup) { + use anv_core::skating::LiftGroup; + let s = match group { + LiftGroup::Group1 => "Gr1", + LiftGroup::Group2 => "Gr2", + LiftGroup::Group3 => "Gr3", + LiftGroup::Group4 => "Gr4", + LiftGroup::Group5 => "Gr5", + }; + self.output.push_str(s); + } + + fn format_edge(&mut self, edge: &anv_core::skating::Edge) { + use anv_core::skating::Edge; + let s = match edge { + Edge::LFO => "LFO", + Edge::LFI => "LFI", + Edge::LBO => "LBO", + Edge::LBI => "LBI", + Edge::RFO => "RFO", + Edge::RFI => "RFI", + Edge::RBO => "RBO", + Edge::RBI => "RBI", + }; + self.output.push_str(s); + } + + fn format_choreo_kind(&mut self, kind: &ChoreographicKind) { + let s = match kind { + ChoreographicKind::Spiral => "spiral", + ChoreographicKind::Spread => "spread", + ChoreographicKind::Ina => "ina", + ChoreographicKind::Hydroblading => "hydroblading", + ChoreographicKind::Pivot => "pivot", + ChoreographicKind::Choreographic => "choreographic", + }; + self.output.push_str(s); + } + + fn format_level(&mut self, level: &anv_core::skating::Level) { + use anv_core::skating::Level; + let s = match level { + Level::B => "B", + Level::L1 => "L1", + Level::L2 => "L2", + Level::L3 => "L3", + Level::L4 => "L4", + }; + self.output.push_str(s); + } + + fn format_timing(&mut self, timing: &Timing) { + match timing { + Timing::At(time) => { + self.output.push_str("at "); + self.format_time_expr(time); + } + Timing::Range(start, end) => { + self.format_time_expr(start); + self.output.push_str(".."); + self.format_time_expr(end); + } + Timing::Duration(time) => { + self.output.push_str("duration "); + self.format_time_expr(time); + } + Timing::Beat(n) => { + self.output.push_str("beat "); + self.output.push_str(&n.to_string()); + } + Timing::After(time) => { + self.output.push_str("after "); + self.format_time_expr(time); + } + } + } + + fn format_time_expr(&mut self, time: &TimeExpr) { + match time { + TimeExpr::Literal(f) => { + self.output.push_str(&f.to_string()); + } + TimeExpr::Formatted(s) => { + self.output.push_str(s); + } + TimeExpr::Var(v) => { + self.output.push_str(&v.node); + } + TimeExpr::BinOp(a, op, b) => { + self.format_time_expr(a); + let op_str = match op { + TimeBinOp::Add => " + ", + TimeBinOp::Sub => " - ", + }; + self.output.push_str(op_str); + self.format_time_expr(b); + } + } + } +} + +impl Default for Formatter { + fn default() -> Self { + Self::new() + } +} + +/// Format source code. +pub fn format(source: &str) -> Result> { + let program = crate::parse(source, anv_core::source::FileId(0))?; + let mut formatter = Formatter::new(); + Ok(formatter.format(&program)) +} + +/// Format source code with custom options. +pub fn format_with_options(source: &str, options: FormatOptions) -> Result> { + let program = crate::parse(source, anv_core::source::FileId(0))?; + let mut formatter = Formatter::with_options(options); + Ok(formatter.format(&program)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_simple_program() { + let source = r#"program test { +segment sp: short { +sequence { +jump triple axel +} +} +}"#; + let result = format(source).unwrap(); + assert!(result.contains("program test")); + assert!(result.contains(" segment sp: short")); + assert!(result.contains(" sequence")); + assert!(result.contains(" jump triple axel")); + } + + #[test] + fn test_format_preserves_doc_comments() { + let source = r#"/// This is a test +program test {}"#; + let result = format(source).unwrap(); + assert!(result.starts_with("/// This is a test")); + } + + #[test] + fn test_format_with_timing() { + let source = "program t { segment s: free { sequence { jump triple lutz at 1:30 } } }"; + let result = format(source).unwrap(); + assert!(result.contains("jump triple lutz at 1:30")); + } + + #[test] + fn test_format_spin_with_positions() { + let source = "program t { segment s: free { sequence { spin camel sit upright L4 } } }"; + let result = format(source).unwrap(); + assert!(result.contains("spin camel sit upright L4")); + } + + #[test] + fn test_format_pairs_elements() { + let source = "program t { segment s: short { sequence { lift Gr5 L4 throw triple lutz death_spiral LBI L3 } } }"; + let result = format(source).unwrap(); + assert!(result.contains("lift Gr5 L4")); + assert!(result.contains("throw triple lutz")); + assert!(result.contains("death_spiral LBI L3")); + } +} diff --git a/crates/anv-syntax/src/lib.rs b/crates/anv-syntax/src/lib.rs index 3e37273..da0727a 100644 --- a/crates/anv-syntax/src/lib.rs +++ b/crates/anv-syntax/src/lib.rs @@ -31,6 +31,7 @@ //! ``` pub mod ast; +pub mod format; pub mod parser; pub mod token; diff --git a/editors/helix/README.md b/editors/helix/README.md new file mode 100644 index 0000000..da23320 --- /dev/null +++ b/editors/helix/README.md @@ -0,0 +1,58 @@ +# Anvomidav for Helix + +This directory contains configuration for Anvomidav language support in the Helix editor. + +## Installation + +### 1. Add Language Configuration + +Add the following to your `~/.config/helix/languages.toml`: + +```toml +[[language]] +name = "anvomidav" +scope = "source.anv" +file-types = ["anv"] +comment-token = "//" +indent = { tab-width = 4, unit = " " } +language-servers = ["anvomidav-lsp"] + +[language-server.anvomidav-lsp] +command = "anv-lsp" +``` + +### 2. Add Tree-sitter Grammar + +The tree-sitter grammar needs to be built and installed. + +```bash +# Clone the repository +git clone https://github.com/hyperpolymath/anvomidav +cd anvomidav/tree-sitter-anvomidav + +# Build the grammar (requires tree-sitter CLI) +tree-sitter generate + +# Copy to Helix runtime +mkdir -p ~/.config/helix/runtime/grammars/sources/anvomidav +cp -r src ~/.config/helix/runtime/grammars/sources/anvomidav/ + +# Copy queries +mkdir -p ~/.config/helix/runtime/queries/anvomidav +cp queries/*.scm ~/.config/helix/runtime/queries/anvomidav/ +``` + +### 3. Build the Grammar + +```bash +cd ~/.config/helix +hx --grammar build +``` + +## Verification + +Open a `.anv` file in Helix. You should see: +- Syntax highlighting +- LSP diagnostics +- Hover information +- Completions diff --git a/editors/helix/languages.toml b/editors/helix/languages.toml new file mode 100644 index 0000000..32ef4c4 --- /dev/null +++ b/editors/helix/languages.toml @@ -0,0 +1,20 @@ +# Anvomidav language configuration for Helix +# Copy this to ~/.config/helix/languages.toml + +[[language]] +name = "anvomidav" +scope = "source.anv" +file-types = ["anv"] +roots = [] +comment-token = "//" +indent = { tab-width = 4, unit = " " } +language-servers = ["anvomidav-lsp"] +auto-format = false + +[[grammar]] +name = "anvomidav" +source = { git = "https://github.com/hyperpolymath/anvomidav", rev = "main", subpath = "tree-sitter-anvomidav" } + +[language-server.anvomidav-lsp] +command = "anv-lsp" +args = [] diff --git a/editors/neovim/README.md b/editors/neovim/README.md new file mode 100644 index 0000000..cbe1ea6 --- /dev/null +++ b/editors/neovim/README.md @@ -0,0 +1,70 @@ +# Anvomidav for Neovim + +This directory contains configuration for Anvomidav language support in Neovim. + +## Installation + +### Using lazy.nvim + +```lua +{ + "hyperpolymath/anvomidav", + config = function() + require("anvomidav").setup() + end, + ft = "anvomidav", +} +``` + +### Manual Setup + +1. Copy the files from this directory to your Neovim config +2. Add the following to your init.lua: + +```lua +-- Add filetype detection +vim.filetype.add({ + extension = { + anv = "anvomidav", + }, +}) + +-- Configure LSP +local lspconfig = require("lspconfig") +local configs = require("lspconfig.configs") + +if not configs.anvomidav then + configs.anvomidav = { + default_config = { + cmd = { "anv-lsp" }, + filetypes = { "anvomidav" }, + root_dir = lspconfig.util.root_pattern(".git", "*.anv"), + settings = {}, + }, + } +end + +lspconfig.anvomidav.setup({ + on_attach = function(client, bufnr) + -- Your on_attach function + end, +}) +``` + +### Tree-sitter + +To enable tree-sitter highlighting, add the parser: + +```lua +local parser_config = require("nvim-treesitter.parsers").get_parser_configs() +parser_config.anvomidav = { + install_info = { + url = "https://github.com/hyperpolymath/anvomidav", + files = { "tree-sitter-anvomidav/src/parser.c" }, + branch = "main", + }, + filetype = "anvomidav", +} +``` + +Then run `:TSInstall anvomidav` diff --git a/editors/neovim/ftplugin/anvomidav.lua b/editors/neovim/ftplugin/anvomidav.lua new file mode 100644 index 0000000..38793ea --- /dev/null +++ b/editors/neovim/ftplugin/anvomidav.lua @@ -0,0 +1,13 @@ +-- SPDX-FileCopyrightText: 2025 hyperpolymath +-- SPDX-License-Identifier: MIT OR AGPL-3.0-or-later + +-- Anvomidav filetype plugin + +vim.bo.commentstring = "// %s" +vim.bo.tabstop = 4 +vim.bo.shiftwidth = 4 +vim.bo.expandtab = true + +-- Set up folding +vim.wo.foldmethod = "indent" +vim.wo.foldlevel = 99 diff --git a/editors/neovim/lua/anvomidav/init.lua b/editors/neovim/lua/anvomidav/init.lua new file mode 100644 index 0000000..8139747 --- /dev/null +++ b/editors/neovim/lua/anvomidav/init.lua @@ -0,0 +1,96 @@ +-- SPDX-FileCopyrightText: 2025 hyperpolymath +-- SPDX-License-Identifier: MIT OR AGPL-3.0-or-later + +-- Anvomidav Neovim plugin + +local M = {} + +M.config = { + lsp_path = "anv-lsp", + auto_start = true, + discipline = "singles", +} + +function M.setup(opts) + opts = opts or {} + M.config = vim.tbl_deep_extend("force", M.config, opts) + + -- Register filetype + vim.filetype.add({ + extension = { + anv = "anvomidav", + }, + }) + + -- Setup LSP if lspconfig is available + local ok, lspconfig = pcall(require, "lspconfig") + if ok then + local configs = require("lspconfig.configs") + + if not configs.anvomidav then + configs.anvomidav = { + default_config = { + cmd = { M.config.lsp_path }, + filetypes = { "anvomidav" }, + root_dir = lspconfig.util.root_pattern(".git", "*.anv"), + settings = { + anvomidav = { + discipline = M.config.discipline, + }, + }, + }, + } + end + + if M.config.auto_start then + lspconfig.anvomidav.setup({}) + end + end + + -- Setup treesitter if available + local ts_ok, parsers = pcall(require, "nvim-treesitter.parsers") + if ts_ok then + local parser_config = parsers.get_parser_configs() + parser_config.anvomidav = { + install_info = { + url = "https://github.com/hyperpolymath/anvomidav", + files = { "tree-sitter-anvomidav/src/parser.c" }, + branch = "main", + }, + filetype = "anvomidav", + } + end +end + +-- Commands +function M.restart_lsp() + vim.cmd("LspRestart anvomidav") +end + +function M.show_info() + local bufnr = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local source = table.concat(lines, "\n") + + -- Count elements + local jumps = 0 + local spins = 0 + local steps = 0 + + for line in source:gmatch("[^\n]+") do + if line:match("^%s*jump") then jumps = jumps + 1 end + if line:match("^%s*spin") then spins = spins + 1 end + if line:match("^%s*step") then steps = steps + 1 end + end + + vim.notify(string.format( + "Anvomidav Program Info:\n Jumps: %d\n Spins: %d\n Steps: %d", + jumps, spins, steps + ), vim.log.levels.INFO) +end + +-- Create user commands +vim.api.nvim_create_user_command("AnvRestartLsp", M.restart_lsp, {}) +vim.api.nvim_create_user_command("AnvInfo", M.show_info, {}) + +return M diff --git a/editors/neovim/queries/anvomidav/highlights.scm b/editors/neovim/queries/anvomidav/highlights.scm new file mode 100644 index 0000000..edee82e --- /dev/null +++ b/editors/neovim/queries/anvomidav/highlights.scm @@ -0,0 +1,111 @@ +; SPDX-FileCopyrightText: 2025 hyperpolymath +; SPDX-License-Identifier: MIT OR AGPL-3.0-or-later + +; Anvomidav syntax highlighting queries for tree-sitter + +; === Comments === +(doc_comment) @comment.documentation +(line_comment) @comment + +; === Keywords === +[ + "program" + "segment" + "sequence" + "import" + "fn" + "let" + "in" + "if" + "then" + "else" + "at" + "duration" + "beat" + "as" +] @keyword + +; === Segment kinds === +(segment_kind) @keyword.storage + +; === Element keywords === +[ + "jump" + "spin" + "step" + "lift" + "throw" + "twist" + "death_spiral" + "choreographic" +] @keyword.function + +; === Skating-specific constants === +(rotation) @constant +(jump_kind) @constant.builtin +(spin_position) @constant.builtin +(step_pattern) @constant.builtin +(lift_group) @constant.builtin +(edge) @constant.builtin +(choreographic_kind) @constant.builtin +(level) @constant + +; === Types === +(type) @type + +; === Literals === +(integer) @number +(float) @number.float +(time_literal) @number +(string) @string +(escape_sequence) @string.escape +(boolean) @constant.builtin.boolean + +; === Identifiers === +(identifier) @variable + +; Function names +(function_definition + name: (identifier) @function) + +; Program name +(program_definition + name: (identifier) @namespace) + +; Segment name +(segment + name: (identifier) @label) + +; Sequence name +(sequence + name: (identifier) @label) + +; Parameter names +(parameter + (identifier) @variable.parameter) + +; === Operators === +(binary_operator) @operator +(unary_operator) @operator + +; === Punctuation === +[ + "{" + "}" + "(" + ")" + "[" + "]" +] @punctuation.bracket + +[ + ":" + "::" + ";" + "," +] @punctuation.delimiter + +[ + "->" + "=" +] @punctuation.special diff --git a/editors/vscode/language-configuration.json b/editors/vscode/language-configuration.json new file mode 100644 index 0000000..bc939b5 --- /dev/null +++ b/editors/vscode/language-configuration.json @@ -0,0 +1,34 @@ +{ + "comments": { + "lineComment": "//", + "blockComment": null + }, + "brackets": [ + ["{", "}"], + ["[", "]"], + ["(", ")"] + ], + "autoClosingPairs": [ + { "open": "{", "close": "}" }, + { "open": "[", "close": "]" }, + { "open": "(", "close": ")" }, + { "open": "\"", "close": "\"", "notIn": ["string"] } + ], + "surroundingPairs": [ + ["{", "}"], + ["[", "]"], + ["(", ")"], + ["\"", "\""] + ], + "folding": { + "markers": { + "start": "^\\s*//\\s*#?region\\b", + "end": "^\\s*//\\s*#?endregion\\b" + } + }, + "indentationRules": { + "increaseIndentPattern": "^.*\\{[^}]*$", + "decreaseIndentPattern": "^\\s*\\}" + }, + "wordPattern": "[a-zA-Z_][a-zA-Z0-9_]*" +} diff --git a/editors/vscode/package.json b/editors/vscode/package.json new file mode 100644 index 0000000..d2037c1 --- /dev/null +++ b/editors/vscode/package.json @@ -0,0 +1,104 @@ +{ + "name": "anvomidav", + "displayName": "Anvomidav", + "description": "Language support for Anvomidav figure skating choreography DSL", + "version": "0.1.0", + "publisher": "hyperpolymath", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/hyperpolymath/anvomidav" + }, + "categories": [ + "Programming Languages", + "Linters" + ], + "keywords": [ + "figure skating", + "choreography", + "DSL", + "anvomidav" + ], + "engines": { + "vscode": "^1.75.0" + }, + "activationEvents": [ + "onLanguage:anvomidav" + ], + "main": "./out/extension.js", + "contributes": { + "languages": [ + { + "id": "anvomidav", + "aliases": [ + "Anvomidav", + "anv" + ], + "extensions": [ + ".anv" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "anvomidav", + "scopeName": "source.anv", + "path": "./syntaxes/anvomidav.tmLanguage.json" + } + ], + "configuration": { + "title": "Anvomidav", + "properties": { + "anvomidav.lsp.path": { + "type": "string", + "default": "anv-lsp", + "description": "Path to the Anvomidav language server binary" + }, + "anvomidav.validation.discipline": { + "type": "string", + "enum": [ + "singles", + "pairs", + "ice-dance" + ], + "default": "singles", + "description": "Default discipline for ISU rule validation" + }, + "anvomidav.trace.server": { + "type": "string", + "enum": [ + "off", + "messages", + "verbose" + ], + "default": "off", + "description": "Traces the communication between VS Code and the language server" + } + } + }, + "commands": [ + { + "command": "anvomidav.restart", + "title": "Anvomidav: Restart Language Server" + }, + { + "command": "anvomidav.showInfo", + "title": "Anvomidav: Show Program Info" + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./" + }, + "dependencies": { + "vscode-languageclient": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/vscode": "^1.75.0", + "typescript": "^5.0.0" + } +} diff --git a/editors/vscode/src/extension.js b/editors/vscode/src/extension.js new file mode 100644 index 0000000..3517f88 --- /dev/null +++ b/editors/vscode/src/extension.js @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2025 hyperpolymath +// SPDX-License-Identifier: MIT OR AGPL-3.0-or-later + +/** + * Anvomidav VS Code Extension + * + * Provides language support for the Anvomidav figure skating DSL. + */ + +const vscode = require('vscode'); +const { LanguageClient, TransportKind } = require('vscode-languageclient/node'); + +let client; + +/** + * Activate the extension. + * @param {vscode.ExtensionContext} context + */ +function activate(context) { + const config = vscode.workspace.getConfiguration('anvomidav'); + const serverPath = config.get('lsp.path', 'anv-lsp'); + + const serverOptions = { + run: { + command: serverPath, + transport: TransportKind.stdio + }, + debug: { + command: serverPath, + transport: TransportKind.stdio + } + }; + + const clientOptions = { + documentSelector: [{ scheme: 'file', language: 'anvomidav' }], + synchronize: { + fileEvents: vscode.workspace.createFileSystemWatcher('**/*.anv') + } + }; + + client = new LanguageClient( + 'anvomidav', + 'Anvomidav Language Server', + serverOptions, + clientOptions + ); + + // Register commands + context.subscriptions.push( + vscode.commands.registerCommand('anvomidav.restart', async () => { + if (client) { + await client.stop(); + await client.start(); + vscode.window.showInformationMessage('Anvomidav language server restarted'); + } + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand('anvomidav.showInfo', () => { + const editor = vscode.window.activeTextEditor; + if (editor && editor.document.languageId === 'anvomidav') { + vscode.window.showInformationMessage( + `Anvomidav file: ${editor.document.fileName}` + ); + } + }) + ); + + // Start the client + client.start(); +} + +/** + * Deactivate the extension. + */ +function deactivate() { + if (client) { + return client.stop(); + } +} + +module.exports = { activate, deactivate }; diff --git a/editors/vscode/syntaxes/anvomidav.tmLanguage.json b/editors/vscode/syntaxes/anvomidav.tmLanguage.json new file mode 100644 index 0000000..dcaed15 --- /dev/null +++ b/editors/vscode/syntaxes/anvomidav.tmLanguage.json @@ -0,0 +1,123 @@ +{ + "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", + "name": "Anvomidav", + "scopeName": "source.anv", + "fileTypes": ["anv"], + "patterns": [ + { "include": "#comments" }, + { "include": "#keywords" }, + { "include": "#elements" }, + { "include": "#skating" }, + { "include": "#literals" }, + { "include": "#operators" } + ], + "repository": { + "comments": { + "patterns": [ + { + "name": "comment.line.documentation.anv", + "match": "///.*$" + }, + { + "name": "comment.line.anv", + "match": "//.*$" + } + ] + }, + "keywords": { + "patterns": [ + { + "name": "keyword.control.anv", + "match": "\\b(program|segment|sequence|import|fn|let|in|if|then|else|at|duration|beat)\\b" + }, + { + "name": "keyword.other.segment-kind.anv", + "match": "\\b(short|free|pattern|rhythm|exhibition)\\b" + } + ] + }, + "elements": { + "patterns": [ + { + "name": "keyword.element.anv", + "match": "\\b(jump|spin|step|lift|throw|twist|death_spiral|choreographic)\\b" + } + ] + }, + "skating": { + "patterns": [ + { + "name": "constant.language.rotation.anv", + "match": "\\b(single|double|triple|quad)\\b" + }, + { + "name": "constant.language.jump.anv", + "match": "\\b(axel|lutz|flip|loop|salchow|toe_loop|euler)\\b" + }, + { + "name": "constant.language.spin.anv", + "match": "\\b(upright|sit|camel|layback|biellmann)\\b" + }, + { + "name": "constant.language.step.anv", + "match": "\\b(straight|circular|serpentine|diagonal|midline)\\b" + }, + { + "name": "constant.language.level.anv", + "match": "\\b(B|L1|L2|L3|L4)\\b" + }, + { + "name": "constant.language.lift-group.anv", + "match": "\\b(Gr1|Gr2|Gr3|Gr4|Gr5)\\b" + }, + { + "name": "constant.language.edge.anv", + "match": "\\b(LFO|LFI|LBO|LBI|RFO|RFI|RBO|RBI)\\b" + }, + { + "name": "constant.language.choreo.anv", + "match": "\\b(spiral|spread|ina|hydroblading|pivot)\\b" + } + ] + }, + "literals": { + "patterns": [ + { + "name": "constant.numeric.time.anv", + "match": "\\b[0-9]+:[0-9]+(:[0-9]+)?\\b" + }, + { + "name": "constant.numeric.anv", + "match": "\\b[0-9]+(\\.[0-9]+)?\\b" + }, + { + "name": "string.quoted.double.anv", + "begin": "\"", + "end": "\"", + "patterns": [ + { + "name": "constant.character.escape.anv", + "match": "\\\\." + } + ] + }, + { + "name": "constant.language.boolean.anv", + "match": "\\b(true|false)\\b" + } + ] + }, + "operators": { + "patterns": [ + { + "name": "keyword.operator.anv", + "match": "(->|=>|::|\\+|-|\\*|/|%|==|!=|<=|>=|<|>|&&|\\|\\||!)" + }, + { + "name": "punctuation.anv", + "match": "[{}()\\[\\]:;,@]" + } + ] + } + } +} diff --git a/editors/zed/extension.toml b/editors/zed/extension.toml new file mode 100644 index 0000000..5054910 --- /dev/null +++ b/editors/zed/extension.toml @@ -0,0 +1,15 @@ +[extension] +id = "anvomidav" +name = "Anvomidav" +description = "Language support for Anvomidav figure skating choreography DSL" +version = "0.1.0" +schema_version = 1 +authors = ["hyperpolymath"] +repository = "https://github.com/hyperpolymath/anvomidav" + +[languages.anvomidav] +path = "languages/anvomidav" + +[language_servers.anvomidav] +name = "Anvomidav" +language = "anvomidav" diff --git a/editors/zed/languages/anvomidav/config.toml b/editors/zed/languages/anvomidav/config.toml new file mode 100644 index 0000000..dda3b5d --- /dev/null +++ b/editors/zed/languages/anvomidav/config.toml @@ -0,0 +1,12 @@ +name = "Anvomidav" +grammar = "anvomidav" +path_suffixes = ["anv"] +line_comments = ["//"] +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = false }, + { start = "(", end = ")", close = true, newline = false }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, +] +word_characters = ["_"] diff --git a/editors/zed/languages/anvomidav/highlights.scm b/editors/zed/languages/anvomidav/highlights.scm new file mode 100644 index 0000000..edee82e --- /dev/null +++ b/editors/zed/languages/anvomidav/highlights.scm @@ -0,0 +1,111 @@ +; SPDX-FileCopyrightText: 2025 hyperpolymath +; SPDX-License-Identifier: MIT OR AGPL-3.0-or-later + +; Anvomidav syntax highlighting queries for tree-sitter + +; === Comments === +(doc_comment) @comment.documentation +(line_comment) @comment + +; === Keywords === +[ + "program" + "segment" + "sequence" + "import" + "fn" + "let" + "in" + "if" + "then" + "else" + "at" + "duration" + "beat" + "as" +] @keyword + +; === Segment kinds === +(segment_kind) @keyword.storage + +; === Element keywords === +[ + "jump" + "spin" + "step" + "lift" + "throw" + "twist" + "death_spiral" + "choreographic" +] @keyword.function + +; === Skating-specific constants === +(rotation) @constant +(jump_kind) @constant.builtin +(spin_position) @constant.builtin +(step_pattern) @constant.builtin +(lift_group) @constant.builtin +(edge) @constant.builtin +(choreographic_kind) @constant.builtin +(level) @constant + +; === Types === +(type) @type + +; === Literals === +(integer) @number +(float) @number.float +(time_literal) @number +(string) @string +(escape_sequence) @string.escape +(boolean) @constant.builtin.boolean + +; === Identifiers === +(identifier) @variable + +; Function names +(function_definition + name: (identifier) @function) + +; Program name +(program_definition + name: (identifier) @namespace) + +; Segment name +(segment + name: (identifier) @label) + +; Sequence name +(sequence + name: (identifier) @label) + +; Parameter names +(parameter + (identifier) @variable.parameter) + +; === Operators === +(binary_operator) @operator +(unary_operator) @operator + +; === Punctuation === +[ + "{" + "}" + "(" + ")" + "[" + "]" +] @punctuation.bracket + +[ + ":" + "::" + ";" + "," +] @punctuation.delimiter + +[ + "->" + "=" +] @punctuation.special