diff --git a/Cargo.lock b/Cargo.lock index 171bb73..b8a6d4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,378 @@ # It is not intended for manual editing. version = 4 +[[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 = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[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 = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[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 = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "litemap" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + [[package]] name = "nuwiki-core" version = "0.1.0" @@ -11,11 +383,451 @@ name = "nuwiki-ls" version = "0.1.0" dependencies = [ "nuwiki-lsp", + "tokio", ] [[package]] name = "nuwiki-lsp" version = "0.1.0" dependencies = [ + "async-trait", + "dashmap 6.1.0", "nuwiki-core", + "tokio", + "tower-lsp", ] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[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", + "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.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "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 = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "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 = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +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 5.5.3", + "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.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/README.md b/README.md index 241c1f0..6c9adb9 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,9 @@ A Vim/Neovim plugin providing full vimwiki syntax support, implemented as a Rust-based language server (LSP). -> **Status:** Phase 5 (HTML renderer) complete. End-to-end source → AST → HTML -> works via `VimwikiSyntax` + `HtmlRenderer`. LSP and editor glue still pending. +> **Status:** Phase 6 (LSP foundation) complete. `nuwiki-ls` speaks LSP over +> stdio: didOpen/didChange/didClose, parse diagnostics, nested document +> outline. Editor glue, semantic tokens, and navigation still pending. See [`SPEC.md`](./SPEC.md) for the full project specification. @@ -18,8 +19,8 @@ See [`SPEC.md`](./SPEC.md) for the full project specification. | 3 | Vimwiki Lexer | ✅ done | | 4 | Vimwiki Parser | ✅ done | | 5 | Renderer | ✅ done | -| 6 | LSP Foundation | ⏳ next | -| 7 | Semantic Tokens | ⏳ | +| 6 | LSP Foundation | ✅ done | +| 7 | Semantic Tokens | ⏳ next | | 8 | Navigation | ⏳ | | 9 | Editor Glue | ⏳ | | 10 | CI/CD release pipeline | ⏳ | diff --git a/SPEC.md b/SPEC.md index 196f534..2bd6c22 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,7 +1,7 @@ # nuwiki — Project Specification > Last updated: 2026-05-10 -> Status: Phase 5 (Renderer) complete — moving to Phase 6 (LSP Foundation) +> Status: Phase 6 (LSP Foundation) complete — moving to Phase 7 (Semantic Tokens) --- diff --git a/crates/nuwiki-ls/Cargo.toml b/crates/nuwiki-ls/Cargo.toml index 992e687..2721a47 100644 --- a/crates/nuwiki-ls/Cargo.toml +++ b/crates/nuwiki-ls/Cargo.toml @@ -18,3 +18,4 @@ path = "src/main.rs" [dependencies] nuwiki-lsp = { path = "../nuwiki-lsp", version = "0.1.0" } +tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-std"] } diff --git a/crates/nuwiki-ls/src/main.rs b/crates/nuwiki-ls/src/main.rs index f328e4d..6055399 100644 --- a/crates/nuwiki-ls/src/main.rs +++ b/crates/nuwiki-ls/src/main.rs @@ -1 +1,6 @@ -fn main() {} +//! `nuwiki-ls` — language-server binary. Speaks LSP over stdio. + +#[tokio::main] +async fn main() -> std::io::Result<()> { + nuwiki_lsp::run_stdio().await +} diff --git a/crates/nuwiki-lsp/Cargo.toml b/crates/nuwiki-lsp/Cargo.toml index 711a709..60210c0 100644 --- a/crates/nuwiki-lsp/Cargo.toml +++ b/crates/nuwiki-lsp/Cargo.toml @@ -14,3 +14,7 @@ workspace = true [dependencies] nuwiki-core = { path = "../nuwiki-core", version = "0.1.0" } +tower-lsp = "0.20" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-std", "sync"] } +dashmap = "6" +async-trait = "0.1" diff --git a/crates/nuwiki-lsp/src/lib.rs b/crates/nuwiki-lsp/src/lib.rs index 30c443a..52fc5b2 100644 --- a/crates/nuwiki-lsp/src/lib.rs +++ b/crates/nuwiki-lsp/src/lib.rs @@ -1,4 +1,413 @@ //! LSP protocol bridge for nuwiki. //! -//! Translates between LSP requests/notifications and `nuwiki-core` AST -//! operations. Must never depend on `nuwiki-ls` (see SPEC.md §6.2). +//! `tower-lsp` server that maintains a `DocumentStore` (SPEC §6.10) and +//! exposes the Phase 6 feature set: +//! +//! - `initialize` / `initialized` / `shutdown` +//! - `textDocument/didOpen`, `didChange` (full sync), `didClose` +//! - `textDocument/publishDiagnostics` from `BlockNode::Error` nodes +//! - `textDocument/documentSymbol` — nested outline from headings +//! +//! Position encoding is negotiated as UTF-8 when the client supports LSP +//! 3.17+ encodings (SPEC §6.11). When the client only supports the legacy +//! UTF-16 default, positions get translated through a per-line lookup. +//! +//! Re-parses are full per change (incremental parsing deferred post-v1). + +use std::io; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +use dashmap::DashMap; +use tokio::io::{AsyncRead, AsyncWrite}; +use tower_lsp::jsonrpc::Result as LspResult; +use tower_lsp::lsp_types::{ + Diagnostic, DiagnosticSeverity, DidChangeTextDocumentParams, DidCloseTextDocumentParams, + DidOpenTextDocumentParams, DocumentSymbol, DocumentSymbolParams, DocumentSymbolResponse, + InitializeParams, InitializeResult, InitializedParams, MessageType, OneOf, + PositionEncodingKind, ServerCapabilities, ServerInfo, SymbolKind, TextDocumentSyncCapability, + TextDocumentSyncKind, Url, +}; +use tower_lsp::{Client, LanguageServer, LspService, Server}; + +use nuwiki_core::ast::{ + BlockNode, BlockquoteNode, DocumentNode, ErrorNode, HeadingNode, InlineNode, ListItemNode, + ListNode, Span, +}; +use nuwiki_core::syntax::vimwiki::VimwikiSyntax; +use nuwiki_core::syntax::SyntaxRegistry; + +/// Run the LSP server over the given async reader/writer pair. The binary +/// (`nuwiki-ls`) just calls this with stdin/stdout. +pub async fn run(reader: I, writer: O) +where + I: AsyncRead + Unpin, + O: AsyncWrite + Unpin, +{ + let (service, socket) = LspService::new(Backend::new); + Server::new(reader, writer, socket).serve(service).await; +} + +/// Convenience: run on stdin/stdout. Fails only if stdio handles are +/// unavailable. +pub async fn run_stdio() -> io::Result<()> { + let stdin = tokio::io::stdin(); + let stdout = tokio::io::stdout(); + run(stdin, stdout).await; + Ok(()) +} + +#[derive(Debug)] +struct DocumentState { + text: String, + ast: DocumentNode, + /// Last version we observed from the client. Held per SPEC §6.10 even + /// though the foundation phase doesn't yet need to read it back. + #[allow(dead_code)] + version: i32, +} + +struct Backend { + client: Client, + documents: Arc>, + registry: Arc, + /// True after `initialize` if the client opted into UTF-8 encoding. + /// Otherwise we keep the LSP 3.16 default of UTF-16. + use_utf8: Arc, +} + +impl Backend { + fn new(client: Client) -> Self { + let mut registry = SyntaxRegistry::new(); + registry.register(VimwikiSyntax::new()); + Self { + client, + documents: Arc::new(DashMap::new()), + registry: Arc::new(registry), + use_utf8: Arc::new(AtomicBool::new(false)), + } + } + + async fn update_document(&self, uri: Url, text: String, version: i32) { + // For Phase 6 we always treat unknown buffers as vimwiki — if/when + // multi-syntax dispatch lands the pick should follow the URI's ext. + let plugin = match self.registry.get("vimwiki") { + Some(p) => p, + None => { + self.client + .log_message(MessageType::ERROR, "vimwiki plugin missing") + .await; + return; + } + }; + let ast = plugin.parse(&text); + let diagnostics = ast_diagnostics(&ast, &text, self.use_utf8.load(Ordering::Relaxed)); + self.documents + .insert(uri.clone(), DocumentState { text, ast, version }); + self.client + .publish_diagnostics(uri, diagnostics, Some(version)) + .await; + } +} + +#[tower_lsp::async_trait] +impl LanguageServer for Backend { + async fn initialize(&self, params: InitializeParams) -> LspResult { + // SPEC §6.11: prefer UTF-8 if the client advertises support. + let supports_utf8 = params + .capabilities + .general + .as_ref() + .and_then(|g| g.position_encodings.as_ref()) + .map(|encs| encs.iter().any(|e| *e == PositionEncodingKind::UTF8)) + .unwrap_or(false); + self.use_utf8.store(supports_utf8, Ordering::Relaxed); + + let position_encoding = if supports_utf8 { + Some(PositionEncodingKind::UTF8) + } else { + None // server defaults to UTF-16 per LSP 3.16. + }; + + Ok(InitializeResult { + capabilities: ServerCapabilities { + position_encoding, + text_document_sync: Some(TextDocumentSyncCapability::Kind( + TextDocumentSyncKind::FULL, + )), + document_symbol_provider: Some(OneOf::Left(true)), + ..ServerCapabilities::default() + }, + server_info: Some(ServerInfo { + name: env!("CARGO_PKG_NAME").into(), + version: Some(env!("CARGO_PKG_VERSION").into()), + }), + }) + } + + async fn initialized(&self, _params: InitializedParams) { + let enc = if self.use_utf8.load(Ordering::Relaxed) { + "utf-8" + } else { + "utf-16" + }; + self.client + .log_message( + MessageType::INFO, + format!("nuwiki-lsp initialized (positionEncoding={enc})"), + ) + .await; + } + + async fn shutdown(&self) -> LspResult<()> { + Ok(()) + } + + async fn did_open(&self, params: DidOpenTextDocumentParams) { + let uri = params.text_document.uri; + let text = params.text_document.text; + let version = params.text_document.version; + self.update_document(uri, text, version).await; + } + + async fn did_change(&self, params: DidChangeTextDocumentParams) { + let uri = params.text_document.uri; + let version = params.text_document.version; + // Full sync: the *last* content change carries the whole document. + if let Some(change) = params.content_changes.into_iter().last() { + self.update_document(uri, change.text, version).await; + } + } + + async fn did_close(&self, params: DidCloseTextDocumentParams) { + self.documents.remove(¶ms.text_document.uri); + // Clear lingering diagnostics so closed buffers don't keep red squiggles. + self.client + .publish_diagnostics(params.text_document.uri, Vec::new(), None) + .await; + } + + async fn document_symbol( + &self, + params: DocumentSymbolParams, + ) -> LspResult> { + let uri = ¶ms.text_document.uri; + let Some(doc) = self.documents.get(uri) else { + return Ok(None); + }; + let symbols = + headings_to_symbols(&doc.ast, &doc.text, self.use_utf8.load(Ordering::Relaxed)); + Ok(Some(DocumentSymbolResponse::Nested(symbols))) + } +} + +// ===== Pure helpers ===== + +/// Convert one of our `Position` values into an LSP `Position`. +/// +/// In UTF-8 encoding mode (negotiated during `initialize`) the byte column +/// is the LSP `character`. In UTF-16 mode the LSP `character` is a UTF-16 +/// code-unit offset, so we walk the source line to translate. +pub fn to_lsp_position( + pos: &nuwiki_core::ast::Position, + text: &str, + utf8: bool, +) -> tower_lsp::lsp_types::Position { + let character = if utf8 { + pos.column + } else { + utf16_column(text, pos.line, pos.column) + }; + tower_lsp::lsp_types::Position { + line: pos.line, + character, + } +} + +pub fn to_lsp_range(span: &Span, text: &str, utf8: bool) -> tower_lsp::lsp_types::Range { + tower_lsp::lsp_types::Range { + start: to_lsp_position(&span.start, text, utf8), + end: to_lsp_position(&span.end, text, utf8), + } +} + +/// Convert a byte-offset column on `line` into the corresponding UTF-16 +/// code-unit count from the start of that line. Out-of-range inputs are +/// clamped to the line length, matching the LSP spec's "if the character +/// value is greater than the length of the line it is clipped to the +/// length". +fn utf16_column(text: &str, line: u32, byte_col: u32) -> u32 { + let mut current_line = 0u32; + let mut line_start = 0usize; + let bytes = text.as_bytes(); + while current_line < line && line_start < bytes.len() { + if let Some(nl) = bytes[line_start..].iter().position(|b| *b == b'\n') { + line_start += nl + 1; + current_line += 1; + } else { + return 0; + } + } + let line_end = bytes[line_start..] + .iter() + .position(|b| *b == b'\n') + .map(|i| line_start + i) + .unwrap_or(bytes.len()); + let target = (line_start + byte_col as usize).min(line_end); + text[line_start..target] + .chars() + .map(char::len_utf16) + .sum::() as u32 +} + +pub fn ast_diagnostics(ast: &DocumentNode, text: &str, utf8: bool) -> Vec { + let mut out = Vec::new(); + walk_blocks_for_errors(&ast.children, text, utf8, &mut out); + out +} + +fn walk_blocks_for_errors(blocks: &[BlockNode], text: &str, utf8: bool, out: &mut Vec) { + for block in blocks { + match block { + BlockNode::Error(err) => out.push(error_to_diagnostic(err, text, utf8)), + BlockNode::Blockquote(BlockquoteNode { children, .. }) => { + walk_blocks_for_errors(children, text, utf8, out); + } + BlockNode::List(ListNode { items, .. }) => { + for item in items { + walk_list_item_for_errors(item, text, utf8, out); + } + } + _ => {} + } + } +} + +fn walk_list_item_for_errors( + item: &ListItemNode, + text: &str, + utf8: bool, + out: &mut Vec, +) { + if let Some(sub) = &item.sublist { + for sub_item in &sub.items { + walk_list_item_for_errors(sub_item, text, utf8, out); + } + } + let _ = (text, utf8, out); +} + +fn error_to_diagnostic(err: &ErrorNode, text: &str, utf8: bool) -> Diagnostic { + Diagnostic { + range: to_lsp_range(&err.span, text, utf8), + severity: Some(DiagnosticSeverity::ERROR), + message: err.message.clone(), + source: Some("nuwiki".into()), + ..Diagnostic::default() + } +} + +pub fn headings_to_symbols(ast: &DocumentNode, text: &str, utf8: bool) -> Vec { + let headings: Vec<&HeadingNode> = ast + .children + .iter() + .filter_map(|b| match b { + BlockNode::Heading(h) => Some(h), + _ => None, + }) + .collect(); + + #[allow(deprecated)] + fn build<'a>( + headings: &mut std::slice::Iter<'a, &'a HeadingNode>, + text: &str, + utf8: bool, + parent_level: u8, + peeked: &mut Option<&'a HeadingNode>, + ) -> Vec { + let mut out = Vec::new(); + loop { + let h = match peeked.take().or_else(|| headings.next().copied()) { + Some(h) => h, + None => return out, + }; + if h.level <= parent_level { + *peeked = Some(h); + return out; + } + let children = build(headings, text, utf8, h.level, peeked); + let title = inline_to_text(&h.children); + out.push(DocumentSymbol { + name: if title.is_empty() { + "(empty heading)".into() + } else { + title + }, + detail: None, + kind: SymbolKind::STRING, + tags: None, + deprecated: None, + range: to_lsp_range(&h.span, text, utf8), + selection_range: to_lsp_range(&h.span, text, utf8), + children: if children.is_empty() { + None + } else { + Some(children) + }, + }); + } + } + + let mut iter = headings.iter(); + let mut peeked: Option<&HeadingNode> = None; + build(&mut iter, text, utf8, 0, &mut peeked) +} + +fn inline_to_text(inlines: &[InlineNode]) -> String { + let mut out = String::new(); + inline_to_text_into(inlines, &mut out); + out.trim().to_string() +} + +fn inline_to_text_into(inlines: &[InlineNode], out: &mut String) { + for n in inlines { + match n { + InlineNode::Text(t) => out.push_str(&t.content), + InlineNode::Bold(b) => inline_to_text_into(&b.children, out), + InlineNode::Italic(i) => inline_to_text_into(&i.children, out), + InlineNode::BoldItalic(bi) => inline_to_text_into(&bi.children, out), + InlineNode::Strikethrough(s) => inline_to_text_into(&s.children, out), + InlineNode::Code(c) => out.push_str(&c.content), + InlineNode::Superscript(s) => inline_to_text_into(&s.children, out), + InlineNode::Subscript(s) => inline_to_text_into(&s.children, out), + InlineNode::MathInline(m) => out.push_str(&m.content), + InlineNode::Keyword(k) => out.push_str(keyword_str(k.keyword)), + InlineNode::Color(c) => inline_to_text_into(&c.children, out), + InlineNode::WikiLink(w) => match &w.description { + Some(d) => inline_to_text_into(d, out), + None => { + if let Some(p) = &w.target.path { + out.push_str(p); + } + } + }, + InlineNode::ExternalLink(e) => match &e.description { + Some(d) => inline_to_text_into(d, out), + None => out.push_str(&e.url), + }, + InlineNode::Transclusion(t) => out.push_str(t.alt.as_deref().unwrap_or(&t.url)), + InlineNode::RawUrl(r) => out.push_str(&r.url), + } + } +} + +fn keyword_str(k: nuwiki_core::ast::Keyword) -> &'static str { + match k { + nuwiki_core::ast::Keyword::Todo => "TODO", + nuwiki_core::ast::Keyword::Done => "DONE", + nuwiki_core::ast::Keyword::Started => "STARTED", + nuwiki_core::ast::Keyword::Fixme => "FIXME", + nuwiki_core::ast::Keyword::Fixed => "FIXED", + nuwiki_core::ast::Keyword::Xxx => "XXX", + } +} diff --git a/crates/nuwiki-lsp/tests/lsp_helpers.rs b/crates/nuwiki-lsp/tests/lsp_helpers.rs new file mode 100644 index 0000000..043e58d --- /dev/null +++ b/crates/nuwiki-lsp/tests/lsp_helpers.rs @@ -0,0 +1,255 @@ +//! Tests for the pure helpers that the LSP backend uses to translate +//! between `nuwiki-core` ASTs and the LSP wire format. The async +//! request-handling loop is exercised at integration time in Phase 8+ +//! (navigation), so this file stays focused on conversion correctness. + +use nuwiki_core::ast::{ + inline::InlineNode, BlockNode, BlockquoteNode, DocumentNode, ErrorNode, ParagraphNode, + Position, Span, TextNode, +}; +use nuwiki_core::syntax::vimwiki::VimwikiSyntax; +use nuwiki_core::syntax::SyntaxPlugin; +use nuwiki_lsp::{ast_diagnostics, headings_to_symbols, to_lsp_position, to_lsp_range}; +use tower_lsp::lsp_types::{DiagnosticSeverity, DocumentSymbolResponse, SymbolKind}; + +// ===== Position / Range conversion ===== + +#[test] +fn position_passthrough_in_utf8_mode() { + let pos = Position { + line: 3, + column: 7, + offset: 0, + }; + let lsp = to_lsp_position(&pos, "ignored", true); + assert_eq!(lsp.line, 3); + assert_eq!(lsp.character, 7); +} + +#[test] +fn position_translates_to_utf16_for_multibyte_chars() { + // "héllo" in UTF-8: h(1) é(2 bytes) l(1) l(1) o(1) — total 6 bytes. + // In UTF-16 each char is one code unit (BMP), so byte col 3 ("after é") + // maps to UTF-16 char col 2. + let line = "héllo\nworld\n"; + let pos = Position { + line: 0, + column: 3, + offset: 3, + }; + let lsp = to_lsp_position(&pos, line, false); + assert_eq!(lsp.line, 0); + assert_eq!(lsp.character, 2); +} + +#[test] +fn position_handles_astral_plane_in_utf16() { + // 🦀 is U+1F980, two UTF-16 code units (surrogate pair), four UTF-8 bytes. + let text = "🦀x\n"; + // After the crab + x, byte col is 5, UTF-16 col is 3 (2 surrogates + 1 BMP). + let pos = Position { + line: 0, + column: 5, + offset: 5, + }; + let lsp = to_lsp_position(&pos, text, false); + assert_eq!(lsp.character, 3); +} + +#[test] +fn range_passthrough_in_utf8_mode() { + let span = Span::new( + Position { + line: 0, + column: 0, + offset: 0, + }, + Position { + line: 0, + column: 5, + offset: 5, + }, + ); + let r = to_lsp_range(&span, "hello\n", true); + assert_eq!(r.start.line, 0); + assert_eq!(r.start.character, 0); + assert_eq!(r.end.character, 5); +} + +// ===== Diagnostics ===== + +#[test] +fn ast_diagnostics_yields_one_per_error_node() { + let span_a = Span::new( + Position { + line: 0, + column: 0, + offset: 0, + }, + Position { + line: 0, + column: 4, + offset: 4, + }, + ); + let span_b = Span::new( + Position { + line: 2, + column: 0, + offset: 10, + }, + Position { + line: 2, + column: 3, + offset: 13, + }, + ); + let doc = DocumentNode { + span: Span::default(), + metadata: Default::default(), + children: vec![ + BlockNode::Error(ErrorNode { + span: span_a, + raw: "first".into(), + message: "first parse failure".into(), + }), + BlockNode::Paragraph(ParagraphNode { + span: Span::default(), + children: vec![InlineNode::Text(TextNode { + span: Span::default(), + content: "ok".into(), + })], + }), + BlockNode::Error(ErrorNode { + span: span_b, + raw: "second".into(), + message: "second parse failure".into(), + }), + ], + }; + + let diags = ast_diagnostics(&doc, "ignored", true); + assert_eq!(diags.len(), 2); + assert_eq!(diags[0].message, "first parse failure"); + assert_eq!(diags[0].severity, Some(DiagnosticSeverity::ERROR)); + assert_eq!(diags[0].source.as_deref(), Some("nuwiki")); + assert_eq!(diags[0].range.start.line, 0); + assert_eq!(diags[1].range.start.line, 2); +} + +#[test] +fn ast_diagnostics_descends_into_blockquotes() { + let span = Span::new( + Position { + line: 1, + column: 2, + offset: 0, + }, + Position { + line: 1, + column: 6, + offset: 4, + }, + ); + let inner_error = BlockNode::Error(ErrorNode { + span, + raw: "x".into(), + message: "nested".into(), + }); + let bq = BlockNode::Blockquote(BlockquoteNode { + span: Span::default(), + children: vec![inner_error], + }); + let doc = DocumentNode { + span: Span::default(), + metadata: Default::default(), + children: vec![bq], + }; + + let diags = ast_diagnostics(&doc, "", true); + assert_eq!(diags.len(), 1); + assert_eq!(diags[0].message, "nested"); +} + +// ===== Document symbols ===== + +fn parse(src: &str) -> DocumentNode { + VimwikiSyntax::new().parse(src) +} + +#[test] +fn flat_outline_when_all_headings_same_level() { + let doc = parse("= A =\n= B =\n= C =\n"); + let syms = headings_to_symbols(&doc, "", true); + assert_eq!(syms.len(), 3); + let names: Vec<&str> = syms.iter().map(|s| s.name.as_str()).collect(); + assert_eq!(names, vec!["A", "B", "C"]); + for s in &syms { + assert!(s.children.is_none() || s.children.as_ref().unwrap().is_empty()); + assert_eq!(s.kind, SymbolKind::STRING); + } +} + +#[test] +fn nested_outline_follows_heading_levels() { + let src = "\ += Top = +== Sub A == +=== Deeper === +== Sub B == += Other = +"; + let doc = parse(src); + let syms = headings_to_symbols(&doc, "", true); + assert_eq!(syms.len(), 2); + + // Top contains Sub A and Sub B; Sub A contains Deeper. + let top = &syms[0]; + assert_eq!(top.name, "Top"); + let top_kids = top.children.as_ref().expect("Top should have children"); + assert_eq!(top_kids.len(), 2); + assert_eq!(top_kids[0].name, "Sub A"); + assert_eq!(top_kids[1].name, "Sub B"); + + let sub_a_kids = top_kids[0] + .children + .as_ref() + .expect("Sub A should have children"); + assert_eq!(sub_a_kids.len(), 1); + assert_eq!(sub_a_kids[0].name, "Deeper"); + + assert_eq!(syms[1].name, "Other"); +} + +#[test] +fn heading_title_strips_inline_markers() { + let doc = parse("= Hello *world* and `code` =\n"); + let syms = headings_to_symbols(&doc, "", true); + assert_eq!(syms.len(), 1); + assert_eq!(syms[0].name, "Hello world and code"); +} + +#[test] +fn empty_heading_falls_back_to_placeholder_name() { + let doc = parse("= =\n"); + let syms = headings_to_symbols(&doc, "", true); + if let Some(s) = syms.first() { + assert!(!s.name.is_empty()); + } +} + +// ===== End-to-end through the registry ===== + +#[test] +fn outline_symbols_match_registry_dispatch() { + let src = "= A =\n== B ==\n"; + let plugin = VimwikiSyntax::new(); + let doc = plugin.parse(src); + let syms = headings_to_symbols(&doc, src, true); + assert_eq!(syms.len(), 1); + assert_eq!(syms[0].name, "A"); + let kids = syms[0].children.as_ref().unwrap(); + assert_eq!(kids[0].name, "B"); + // Sanity: symbols can be wrapped in DocumentSymbolResponse::Nested. + let _resp = DocumentSymbolResponse::Nested(syms); +}