diff --git a/Cargo.lock b/Cargo.lock index 75bcadb3b..f1f2eb94c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,7 +65,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.32", + "syn 2.0.111", ] [[package]] @@ -182,7 +182,16 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.111", +] + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", ] [[package]] @@ -191,6 +200,27 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aether_lsp_utils" +version = "0.0.0" +source = "git+https://github.com/posit-dev/air?rev=f959e32eee91#f959e32eee91654f04a44a32e97321ef5d510e93" +dependencies = [ + "anyhow", + "biome_line_index", + "biome_text_size", + "dissimilar", + "itertools 0.13.0", + "line_ending", + "settings", + "tower-lsp", +] + [[package]] name = "ahash" version = "0.8.7" @@ -213,6 +243,40 @@ dependencies = [ "memchr", ] +[[package]] +name = "air_r_factory" +version = "0.0.0" +source = "git+https://github.com/posit-dev/air?rev=f959e32eee91#f959e32eee91654f04a44a32e97321ef5d510e93" +dependencies = [ + "air_r_syntax", + "biome_rowan", +] + +[[package]] +name = "air_r_parser" +version = "0.0.0" +source = "git+https://github.com/posit-dev/air?rev=f959e32eee91#f959e32eee91654f04a44a32e97321ef5d510e93" +dependencies = [ + "air_r_factory", + "air_r_syntax", + "biome_parser", + "biome_rowan", + "biome_unicode_table", + "serde", + "tracing", + "tree-sitter", + "tree-sitter-r", +] + +[[package]] +name = "air_r_syntax" +version = "0.0.0" +source = "git+https://github.com/posit-dev/air?rev=f959e32eee91#f959e32eee91654f04a44a32e97321ef5d510e93" +dependencies = [ + "biome_rowan", + "serde", +] + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -281,11 +345,15 @@ name = "ark" version = "0.1.220" dependencies = [ "actix-web", + "aether_lsp_utils", + "air_r_parser", + "air_r_syntax", "amalthea", "anyhow", "assert_matches", "async-trait", "base64 0.21.0", + "biome_line_index", "bus", "cc", "cfg-if", @@ -301,7 +369,7 @@ dependencies = [ "home", "http 0.2.9", "insta", - "itertools", + "itertools 0.10.5", "libc", "libr", "log", @@ -313,13 +381,13 @@ dependencies = [ "reqwest", "reqwest-middleware", "reqwest-retry", - "ropey", "rust-embed", "rustc-hash", "scraper", "serde", "serde_json", "stdext", + "streaming-iterator", "strum 0.26.2", "strum_macros 0.26.4", "tempfile", @@ -353,7 +421,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.111", ] [[package]] @@ -374,6 +442,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide 0.8.9", + "object", + "rustc-demangle", + "windows-link", +] + [[package]] name = "base64" version = "0.21.0" @@ -386,6 +469,127 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "biome_console" +version = "0.5.7" +source = "git+https://github.com/biomejs/biome?rev=c13fc60726883781e4530a4437724273b560c8e0#c13fc60726883781e4530a4437724273b560c8e0" +dependencies = [ + "biome_markup", + "biome_text_size", + "serde", + "termcolor", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "biome_diagnostics" +version = "0.5.7" +source = "git+https://github.com/biomejs/biome?rev=c13fc60726883781e4530a4437724273b560c8e0#c13fc60726883781e4530a4437724273b560c8e0" +dependencies = [ + "backtrace", + "biome_console", + "biome_diagnostics_categories", + "biome_diagnostics_macros", + "biome_rowan", + "biome_text_edit", + "biome_text_size", + "enumflags2", + "serde", + "serde_json", + "termcolor", + "terminal_size", + "unicode-width", +] + +[[package]] +name = "biome_diagnostics_categories" +version = "0.5.7" +source = "git+https://github.com/biomejs/biome?rev=c13fc60726883781e4530a4437724273b560c8e0#c13fc60726883781e4530a4437724273b560c8e0" +dependencies = [ + "quote", + "serde", +] + +[[package]] +name = "biome_diagnostics_macros" +version = "0.5.7" +source = "git+https://github.com/biomejs/biome?rev=c13fc60726883781e4530a4437724273b560c8e0#c13fc60726883781e4530a4437724273b560c8e0" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "biome_line_index" +version = "0.1.0" +source = "git+https://github.com/biomejs/biome?rev=c13fc60726883781e4530a4437724273b560c8e0#c13fc60726883781e4530a4437724273b560c8e0" +dependencies = [ + "biome_text_size", + "rustc-hash", +] + +[[package]] +name = "biome_markup" +version = "0.5.7" +source = "git+https://github.com/biomejs/biome?rev=c13fc60726883781e4530a4437724273b560c8e0#c13fc60726883781e4530a4437724273b560c8e0" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", +] + +[[package]] +name = "biome_parser" +version = "0.5.7" +source = "git+https://github.com/biomejs/biome?rev=c13fc60726883781e4530a4437724273b560c8e0#c13fc60726883781e4530a4437724273b560c8e0" +dependencies = [ + "biome_console", + "biome_diagnostics", + "biome_rowan", + "biome_unicode_table", + "drop_bomb", + "enumflags2", + "unicode-bom", +] + +[[package]] +name = "biome_rowan" +version = "0.5.7" +source = "git+https://github.com/biomejs/biome?rev=c13fc60726883781e4530a4437724273b560c8e0#c13fc60726883781e4530a4437724273b560c8e0" +dependencies = [ + "biome_text_edit", + "biome_text_size", + "hashbrown 0.15.5", + "rustc-hash", + "serde", +] + +[[package]] +name = "biome_text_edit" +version = "0.5.7" +source = "git+https://github.com/biomejs/biome?rev=c13fc60726883781e4530a4437724273b560c8e0#c13fc60726883781e4530a4437724273b560c8e0" +dependencies = [ + "biome_text_size", + "serde", + "similar", +] + +[[package]] +name = "biome_text_size" +version = "0.5.7" +source = "git+https://github.com/biomejs/biome?rev=c13fc60726883781e4530a4437724273b560c8e0#c13fc60726883781e4530a4437724273b560c8e0" +dependencies = [ + "serde", +] + +[[package]] +name = "biome_unicode_table" +version = "0.5.7" +source = "git+https://github.com/biomejs/biome?rev=c13fc60726883781e4530a4437724273b560c8e0#c13fc60726883781e4530a4437724273b560c8e0" + [[package]] name = "bitflags" version = "1.3.2" @@ -428,6 +632,17 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata 0.4.5", + "serde", +] + [[package]] name = "bumpalo" version = "3.12.2" @@ -707,7 +922,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.32", + "syn 2.0.111", ] [[package]] @@ -718,7 +933,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" dependencies = [ "darling_core", "quote", - "syn 2.0.32", + "syn 2.0.111", ] [[package]] @@ -799,6 +1014,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "dissimilar" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8975ffdaa0ef3661bfe02dbdcc06c9f829dfafe6a3c474de366a8d5e44276921" + +[[package]] +name = "drop_bomb" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1" + [[package]] name = "dtoa" version = "0.4.8" @@ -867,11 +1094,32 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "env_logger" -version = "0.10.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" dependencies = [ "humantime", "is-terminal", @@ -888,12 +1136,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -921,7 +1169,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.7.1", ] [[package]] @@ -1014,7 +1262,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.111", ] [[package]] @@ -1099,6 +1347,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + [[package]] name = "h2" version = "0.3.26" @@ -1111,7 +1365,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.9", - "indexmap 2.1.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -1127,7 +1381,7 @@ dependencies = [ "ctor", "embed-resource", "harp-macros", - "itertools", + "itertools 0.10.5", "libc", "libloading", "libr", @@ -1148,7 +1402,7 @@ name = "harp-macros" version = "0.1.0" dependencies = [ "quote", - "syn 2.0.32", + "syn 2.0.111", ] [[package]] @@ -1159,9 +1413,15 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "hashbrown" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -1186,9 +1446,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" @@ -1287,9 +1547,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "humantime" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" @@ -1387,12 +1647,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.16.1", ] [[package]] @@ -1439,17 +1699,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "io-lifetimes" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" -dependencies = [ - "hermit-abi 0.3.1", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ipnet" version = "2.8.0" @@ -1458,14 +1707,13 @@ checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" [[package]] name = "is-terminal" -version = "0.4.7" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix 0.37.19", - "windows-sys 0.48.0", + "hermit-abi 0.5.2", + "libc", + "windows-sys 0.59.0", ] [[package]] @@ -1477,6 +1725,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" @@ -1545,9 +1802,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" @@ -1569,6 +1826,15 @@ dependencies = [ "paste", ] +[[package]] +name = "line_ending" +version = "0.0.0" +source = "git+https://github.com/posit-dev/air?rev=f959e32eee91#f959e32eee91654f04a44a32e97321ef5d510e93" +dependencies = [ + "memchr", + "settings", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1577,15 +1843,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.7" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "local-channel" @@ -1616,15 +1882,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.18" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lsp-types" -version = "0.94.0" +version = "0.94.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" dependencies = [ "bitflags 1.3.2", "serde", @@ -1717,6 +1983,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "0.8.6" @@ -1818,6 +2093,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -1999,7 +2283,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.111", ] [[package]] @@ -2062,6 +2346,27 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", +] + [[package]] name = "proc-macro-hack" version = "0.5.20+deprecated" @@ -2070,18 +2375,18 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -2339,16 +2644,6 @@ dependencies = [ "rand 0.8.5", ] -[[package]] -name = "ropey" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53ce7a2c43a32e50d666e33c5a80251b31147bb4b49024bcab11fb6f20c671ed" -dependencies = [ - "smallvec", - "str_indices", -] - [[package]] name = "rust-embed" version = "8.2.0" @@ -2369,7 +2664,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.32", + "syn 2.0.111", "walkdir", ] @@ -2383,6 +2678,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -2400,29 +2701,28 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.19" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "errno", - "io-lifetimes", "libc", - "linux-raw-sys 0.3.7", - "windows-sys 0.48.0", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", ] [[package]] name = "rustix" -version = "0.38.37" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags 2.4.0", "errno", "libc", - "linux-raw-sys 0.4.14", - "windows-sys 0.52.0", + "linux-raw-sys 0.11.0", + "windows-sys 0.59.0", ] [[package]] @@ -2494,34 +2794,46 @@ checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +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.193" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.111", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ - "indexmap 1.9.3", + "indexmap 2.12.1", "itoa", + "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -2532,7 +2844,7 @@ checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.111", ] [[package]] @@ -2581,7 +2893,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.111", ] [[package]] @@ -2594,6 +2906,11 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "settings" +version = "0.0.0" +source = "git+https://github.com/posit-dev/air?rev=f959e32eee91#f959e32eee91654f04a44a32e97321ef5d510e93" + [[package]] name = "sha1" version = "0.10.5" @@ -2642,9 +2959,13 @@ dependencies = [ [[package]] name = "similar" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +dependencies = [ + "bstr", + "unicode-segmentation", +] [[package]] name = "siphasher" @@ -2708,10 +3029,10 @@ dependencies = [ ] [[package]] -name = "str_indices" -version = "0.4.1" +name = "streaming-iterator" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f026164926842ec52deb1938fae44f83dfdb82d0a5b0270c5bd5935ab74d6dd" +checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" [[package]] name = "string_cache" @@ -2780,7 +3101,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.32", + "syn 2.0.111", ] [[package]] @@ -2802,9 +3123,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.32" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -2865,13 +3186,23 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.2.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix 1.1.2", + "windows-sys 0.60.2", +] + [[package]] name = "thiserror" version = "1.0.40" @@ -2889,7 +3220,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.111", ] [[package]] @@ -2986,7 +3317,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.111", ] [[package]] @@ -3055,7 +3386,7 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.12.1", "serde", "serde_spanned", "toml_datetime", @@ -3086,9 +3417,8 @@ checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-lsp" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b38fb0e6ce037835174256518aace3ca621c4f96383c56bb846cfc11b341910" +version = "0.20.0" +source = "git+https://github.com/lionel-/tower-lsp?branch=bugfix%2Fpatches#49ef549eaa9f74b71e212cf513283af4ad748a81" dependencies = [ "async-trait", "auto_impl", @@ -3109,13 +3439,12 @@ dependencies = [ [[package]] name = "tower-lsp-macros" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34723c06344244474fdde365b76aebef8050bf6be61a935b91ee9ff7c4e91157" +version = "0.9.0" +source = "git+https://github.com/lionel-/tower-lsp?branch=bugfix%2Fpatches#49ef549eaa9f74b71e212cf513283af4ad748a81" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.111", ] [[package]] @@ -3156,7 +3485,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.111", ] [[package]] @@ -3210,13 +3539,14 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.23.0" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20f4cd3642c47a85052a887d86704f4eac272969f61b686bdd3f772122aabaff" +checksum = "a5387dffa7ffc7d2dae12b50c6f7aab8ff79d6210147c6613561fc3d474c6f75" dependencies = [ "cc", "regex", "regex-syntax 0.8.4", + "streaming-iterator", "tree-sitter-language", ] @@ -3228,8 +3558,8 @@ checksum = "2545046bd1473dac6c626659cc2567c6c0ff302fc8b84a56c4243378276f7f57" [[package]] name = "tree-sitter-r" -version = "1.2.0" -source = "git+https://github.com/r-lib/tree-sitter-r?rev=95aff097aa927a66bb357f715b58cde821be8867#95aff097aa927a66bb357f715b58cde821be8867" +version = "1.1.0" +source = "git+https://github.com/r-lib/tree-sitter-r?rev=daa26a2ff0d9546e9125c7d8bcec046027f02070#daa26a2ff0d9546e9125c7d8bcec046027f02070" dependencies = [ "cc", "tree-sitter-language", @@ -3262,6 +3592,12 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +[[package]] +name = "unicode-bom" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eec5d1121208364f6793f7d2e222bf75a915c19557537745b195b253dd64217" + [[package]] name = "unicode-ident" version = "1.0.8" @@ -3277,11 +3613,17 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "url" @@ -3407,7 +3749,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -3441,7 +3783,7 @@ checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.111", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3517,6 +3859,12 @@ dependencies = [ "windows-targets 0.48.0", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-registry" version = "0.2.0" @@ -3583,6 +3931,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -3622,13 +3979,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -3647,6 +4021,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -3665,6 +4045,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -3683,12 +4069,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -3707,6 +4105,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -3725,6 +4129,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -3743,6 +4153,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -3761,6 +4177,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.4.6" @@ -3827,7 +4249,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.32", + "syn 2.0.111", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 869e6d455..bed8fd92b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,11 @@ rust-version = "1.85" edition = "2021" license = "MIT" authors = ["Posit Software, PBC"] + +[workspace.dependencies] +biome_line_index = { git = "https://github.com/biomejs/biome", rev = "c13fc60726883781e4530a4437724273b560c8e0" } +aether_lsp_utils = { git = "https://github.com/posit-dev/air", rev = "f959e32eee91" } +aether_parser = { git = "https://github.com/posit-dev/air", package = "air_r_parser", rev = "f959e32eee91" } +aether_syntax = { git = "https://github.com/posit-dev/air", package = "air_r_syntax", rev = "f959e32eee91" } +# For https://github.com/ebkalderon/tower-lsp/pull/428 +tower-lsp = { branch = "bugfix/patches", git = "https://github.com/lionel-/tower-lsp" } diff --git a/crates/ark/Cargo.toml b/crates/ark/Cargo.toml index fee82de9c..42d049977 100644 --- a/crates/ark/Cargo.toml +++ b/crates/ark/Cargo.toml @@ -16,12 +16,16 @@ amalthea = { path = "../amalthea" } anyhow = "1.0.80" async-trait = "0.1.66" base64 = "0.21.0" +biome_line_index.workspace = true bus = "2.3.0" cfg-if = "1.0.0" crossbeam = { version = "0.8.2", features = ["crossbeam-channel"] } ctor = "0.1.26" dap = { git = "https://github.com/sztomi/dap-rs", branch = "main" } dashmap = "5.4.0" +aether_parser.workspace = true +aether_syntax.workspace = true +aether_lsp_utils.workspace = true ego-tree = "0.6.2" harp = { path = "../harp" } http = "0.2.9" @@ -38,16 +42,16 @@ regex = "1.10.0" reqwest = { version = "0.12.5", default-features = false, features = ["json"] } reqwest-retry = "0.6.1" reqwest-middleware = "0.3.3" -ropey = "1.6.0" rust-embed = "8.0.0" scraper = "0.15.0" serde = { version = "1.0.183", features = ["derive"] } serde_json = { version = "1.0.94", features = ["preserve_order"] } stdext = { path = "../stdext" } +streaming-iterator = "0.1.9" tokio = { version = "1.26.0", features = ["full"] } -tower-lsp = "0.19.0" -tree-sitter = "0.23.0" -tree-sitter-r = { git = "https://github.com/r-lib/tree-sitter-r", rev = "95aff097aa927a66bb357f715b58cde821be8867" } +tower-lsp.workspace = true +tree-sitter = "0.24.7" +tree-sitter-r = { git = "https://github.com/r-lib/tree-sitter-r", rev = "daa26a2ff0d9546e9125c7d8bcec046027f02070" } uuid = "1.3.0" url = "2.4.1" walkdir = "2" diff --git a/crates/ark/src/lsp/backend.rs b/crates/ark/src/lsp/backend.rs index 1974545e2..fa2dda02d 100644 --- a/crates/ark/src/lsp/backend.rs +++ b/crates/ark/src/lsp/backend.rs @@ -584,7 +584,7 @@ pub fn start_lsp( fn new_jsonrpc_error(message: String) -> jsonrpc::Error { jsonrpc::Error { code: jsonrpc::ErrorCode::ServerError(-1), - message, + message: message.into(), data: None, } } diff --git a/crates/ark/src/lsp/code_action.rs b/crates/ark/src/lsp/code_action.rs index 65220082d..e48659b7c 100644 --- a/crates/ark/src/lsp/code_action.rs +++ b/crates/ark/src/lsp/code_action.rs @@ -15,7 +15,7 @@ use url::Url; use crate::lsp::capabilities::Capabilities; use crate::lsp::code_action::roxygen::roxygen_documentation; -use crate::lsp::documents::Document; +use crate::lsp::document::Document; mod roxygen; diff --git a/crates/ark/src/lsp/code_action/roxygen.rs b/crates/ark/src/lsp/code_action/roxygen.rs index 9f2b07f55..486015522 100644 --- a/crates/ark/src/lsp/code_action/roxygen.rs +++ b/crates/ark/src/lsp/code_action/roxygen.rs @@ -5,9 +5,8 @@ use crate::lsp::capabilities::Capabilities; use crate::lsp::code_action::code_action; use crate::lsp::code_action::code_action_workspace_text_edit; use crate::lsp::code_action::CodeActions; -use crate::lsp::documents::Document; -use crate::lsp::encoding::convert_point_to_position; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::document::Document; +use crate::lsp::traits::node::NodeExt; use crate::treesitter::BinaryOperatorType; use crate::treesitter::NodeTypeExt; @@ -64,8 +63,8 @@ pub(crate) fn roxygen_documentation( // Fairly simple detection of existing `#'` on the previous line (but starting at the // same `column` offset), which tells us not to provide this code action - if let Some(previous_line) = document.contents.get_line(position.row.saturating_sub(1)) { - if let Some(previous_line) = previous_line.get_byte_slice(position.column..) { + if let Some(previous_line) = document.get_line(position.row.saturating_sub(1)) { + if let Some(previous_line) = previous_line.get(position.column..) { let mut previous_line = previous_line.bytes(); if previous_line @@ -91,11 +90,7 @@ pub(crate) fn roxygen_documentation( for child in parameters.children_by_field_name("parameter", &mut cursor) { let parameter_name = child.child_by_field_name("name")?; - let parameter_name = document - .contents - .node_slice(¶meter_name) - .ok()? - .to_string(); + let parameter_name = parameter_name.node_to_string(&document.contents).ok()?; parameter_names.push(parameter_name); } @@ -105,8 +100,10 @@ pub(crate) fn roxygen_documentation( // We insert the documentation string at the start position of the function name. // This handles the indentation of the first documentation line, and makes new line // handling trivial (we just add a new line to every documentation line). - let position = convert_point_to_position(&document.contents, position); - let range = tower_lsp::lsp_types::Range::new(position, position); + let position = document + .lsp_position_from_tree_sitter_point(position) + .ok()?; + let range = lsp_types::Range::new(position, position); let edit = lsp_types::TextEdit::new(range, documentation); let edit = code_action_workspace_text_edit(uri.clone(), document.version, vec![edit], capabilities); @@ -175,7 +172,7 @@ mod tests { use crate::lsp::capabilities::Capabilities; use crate::lsp::code_action::roxygen::roxygen_documentation; use crate::lsp::code_action::CodeActions; - use crate::lsp::documents::Document; + use crate::lsp::document::Document; fn point_range(point: Point, byte: usize) -> Range { Range { diff --git a/crates/ark/src/lsp/completions/completion_context.rs b/crates/ark/src/lsp/completions/completion_context.rs index 0601791f3..ffb897902 100644 --- a/crates/ark/src/lsp/completions/completion_context.rs +++ b/crates/ark/src/lsp/completions/completion_context.rs @@ -20,7 +20,7 @@ pub(crate) struct CompletionContext<'a> { pub(crate) state: &'a WorldState, pipe_root_cell: OnceCell>, containing_call_cell: OnceCell>>, - function_context_cell: OnceCell, + function_context_cell: OnceCell>, } impl<'a> CompletionContext<'a> { @@ -54,8 +54,10 @@ impl<'a> CompletionContext<'a> { .get_or_init(|| node_find_containing_call(self.document_context.node)) } - pub fn function_context(&self) -> &FunctionContext { + pub fn function_context(&self) -> anyhow::Result<&FunctionContext> { self.function_context_cell .get_or_init(|| FunctionContext::new(&self.document_context)) + .as_ref() + .map_err(|err| anyhow::anyhow!("{err:?}")) } } diff --git a/crates/ark/src/lsp/completions/completion_item.rs b/crates/ark/src/lsp/completions/completion_item.rs index fc7acdea5..770dd67b3 100644 --- a/crates/ark/src/lsp/completions/completion_item.rs +++ b/crates/ark/src/lsp/completions/completion_item.rs @@ -43,8 +43,7 @@ use crate::lsp::completions::function_context::FunctionRefUsage; use crate::lsp::completions::types::CompletionData; use crate::lsp::completions::types::PromiseStrategy; use crate::lsp::document_context::DocumentContext; -use crate::lsp::encoding::convert_point_to_position; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::traits::node::NodeExt; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; @@ -102,7 +101,7 @@ pub(super) fn completion_item_from_assignment( let lhs = node.child_by_field_name("lhs").into_result()?; let rhs = node.child_by_field_name("rhs").into_result()?; - let label = context.document.contents.node_slice(&lhs)?.to_string(); + let label = lhs.node_as_str(&context.document.contents)?.to_string(); // TODO: Resolve functions that exist in-document here. let mut item = completion_item(label.clone(), CompletionData::ScopeVariable { @@ -125,11 +124,7 @@ pub(super) fn completion_item_from_assignment( // benefit from the logic in completion_item_from_function() :( if rhs.node_type() == NodeType::FunctionDefinition { if let Some(parameters) = rhs.child_by_field_name("parameters") { - let parameters = context - .document - .contents - .node_slice(¶meters)? - .to_string(); + let parameters = parameters.node_as_str(&context.document.contents)?; item.detail = Some(join!(label, parameters)); } @@ -651,7 +646,9 @@ fn completion_item_from_dot_dot_dot( item.kind = Some(CompletionItemKind::FIELD); - let position = convert_point_to_position(&context.document.contents, context.point); + let position = context + .document + .lsp_position_from_tree_sitter_point(context.point)?; let range = Range { start: position, diff --git a/crates/ark/src/lsp/completions/function_context.rs b/crates/ark/src/lsp/completions/function_context.rs index 597139194..9ce9666da 100644 --- a/crates/ark/src/lsp/completions/function_context.rs +++ b/crates/ark/src/lsp/completions/function_context.rs @@ -5,14 +5,14 @@ // // +use stdext::result::ResultExt; +use tower_lsp::lsp_types; use tower_lsp::lsp_types::Range; use tree_sitter::Node; use crate::lsp::document_context::DocumentContext; -use crate::lsp::encoding::convert_point_to_position; -use crate::lsp::encoding::convert_tree_sitter_range_to_lsp_range; +use crate::lsp::traits::node::NodeExt; use crate::treesitter::node_find_parent_call; -use crate::treesitter::node_text; use crate::treesitter::BinaryOperatorType; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; @@ -54,25 +54,24 @@ pub(crate) enum ArgumentsStatus { } impl FunctionContext { - pub(crate) fn new(document_context: &DocumentContext) -> Self { + pub(crate) fn new(document_context: &DocumentContext) -> anyhow::Result { let completion_node = document_context.node; let Some(effective_function_node) = get_effective_function_node(completion_node) else { // We shouldn't ever attempt to instantiate a FunctionContext or // function-flavored CompletionItem in this degenerate case, but we // return a dummy FunctionContext just to be safe. - let node_end = convert_point_to_position( - &document_context.document.contents, - completion_node.range().end_point, - ); + let node_end = document_context + .document + .lsp_position_from_tree_sitter_point(completion_node.range().end_point)?; - return Self { + return Ok(Self { name: String::new(), - range: tower_lsp::lsp_types::Range::new(node_end, node_end), + range: lsp_types::Range::new(node_end, node_end), usage: FunctionRefUsage::Call, arguments_status: ArgumentsStatus::Absent, cursor_is_at_end: true, - }; + }); }; let usage = determine_function_usage( @@ -93,7 +92,7 @@ impl FunctionContext { cursor.row == node_range.end_point.row && cursor.column == node_range.end_point.column; let name = match function_name_node { - Some(node) => node_text(&node, &document_context.document.contents).unwrap_or_default(), + Some(node) => node.node_to_string(&document_context.document.contents)?, None => String::new(), }; @@ -107,26 +106,26 @@ impl FunctionContext { "FunctionContext created with name: '{name}', usage: {usage:?}, arguments: {arguments_status:?}, cursor at end: {is_cursor_at_end}" ); - Self { + Ok(Self { name, range: match function_name_node { - Some(node) => convert_tree_sitter_range_to_lsp_range( - &document_context.document.contents, - node.range(), - ), + Some(node) => document_context + .document + .lsp_range_from_tree_sitter_range(node.range())?, None => { // Create a zero-width range at the end of the effective_function_node - let node_end = convert_point_to_position( - &document_context.document.contents, - effective_function_node.range().end_point, - ); - tower_lsp::lsp_types::Range::new(node_end, node_end) + let node_end = document_context + .document + .lsp_position_from_tree_sitter_point( + effective_function_node.range().end_point, + )?; + lsp_types::Range::new(node_end, node_end) }, }, usage, arguments_status, cursor_is_at_end: is_cursor_at_end, - } + }) } } @@ -165,7 +164,7 @@ static FUNCTIONS_EXPECTING_A_FUNCTION_REFERENCE: &[&str] = &[ "str", ]; -fn is_inside_special_function(node: &Node, contents: &ropey::Rope) -> bool { +fn is_inside_special_function(node: &Node, contents: &str) -> bool { let Some(call_node) = node_find_parent_call(node) else { return false; }; @@ -174,9 +173,12 @@ fn is_inside_special_function(node: &Node, contents: &ropey::Rope) -> bool { return false; }; - let call_name = node_text(&call_name_node, contents).unwrap_or_default(); + let call_name = call_name_node + .node_as_str(contents) + .log_err() + .unwrap_or_default(); - FUNCTIONS_EXPECTING_A_FUNCTION_REFERENCE.contains(&call_name.as_str()) + FUNCTIONS_EXPECTING_A_FUNCTION_REFERENCE.contains(&call_name) } /// Checks if the node is inside a help operator context like `?foo` or `method?foo` @@ -226,7 +228,7 @@ fn determine_arguments_status(function_container_node: &Node) -> ArgumentsStatus } } -fn determine_function_usage(node: &Node, contents: &ropey::Rope) -> FunctionRefUsage { +fn determine_function_usage(node: &Node, contents: &str) -> FunctionRefUsage { if is_inside_special_function(node, contents) || is_inside_help_operator(node) { FunctionRefUsage::Value } else { diff --git a/crates/ark/src/lsp/completions/provide.rs b/crates/ark/src/lsp/completions/provide.rs index bc1b06aa9..44e6e64f7 100644 --- a/crates/ark/src/lsp/completions/provide.rs +++ b/crates/ark/src/lsp/completions/provide.rs @@ -12,7 +12,7 @@ use crate::lsp::completions::sources::composite; use crate::lsp::completions::sources::unique; use crate::lsp::document_context::DocumentContext; use crate::lsp::state::WorldState; -use crate::treesitter::node_text; +use crate::lsp::traits::node::NodeExt; use crate::treesitter::NodeTypeExt; // Entry point for completions. @@ -23,7 +23,9 @@ pub(crate) fn provide_completions( ) -> anyhow::Result> { log::info!( "provide_completions() - Completion node text: '{node_text}', Node type: '{node_type:?}'", - node_text = node_text(&document_context.node, &document_context.document.contents) + node_text = document_context + .node + .node_as_str(&document_context.document.contents) .unwrap_or_default(), node_type = document_context.node.node_type() ); diff --git a/crates/ark/src/lsp/completions/sources/composite.rs b/crates/ark/src/lsp/completions/sources/composite.rs index 889810a7e..a72810845 100644 --- a/crates/ark/src/lsp/completions/sources/composite.rs +++ b/crates/ark/src/lsp/completions/sources/composite.rs @@ -220,8 +220,8 @@ mod tests { use crate::lsp::completions::completion_context::CompletionContext; use crate::lsp::completions::sources::composite::get_completions; use crate::lsp::completions::sources::composite::is_identifier_like; + use crate::lsp::document::Document; use crate::lsp::document_context::DocumentContext; - use crate::lsp::documents::Document; use crate::lsp::state::WorldState; use crate::r_task; use crate::treesitter::NodeType; diff --git a/crates/ark/src/lsp/completions/sources/composite/call.rs b/crates/ark/src/lsp/completions/sources/composite/call.rs index 303ba4dd6..1bb3f623c 100644 --- a/crates/ark/src/lsp/completions/sources/composite/call.rs +++ b/crates/ark/src/lsp/completions/sources/composite/call.rs @@ -22,7 +22,7 @@ use crate::lsp::completions::sources::utils::CallNodePositionType; use crate::lsp::completions::sources::CompletionSource; use crate::lsp::document_context::DocumentContext; use crate::lsp::indexer; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::traits::node::NodeExt; pub(super) struct CallSource; @@ -75,11 +75,7 @@ fn completions_from_call( return Ok(None); }; - let callee = document_context - .document - .contents - .node_slice(&callee)? - .to_string(); + let callee = callee.node_as_str(&document_context.document.contents)?; // - Prefer `root` as the first argument if it exists // - Then fall back to looking it up, if possible @@ -125,7 +121,7 @@ fn get_first_argument(context: &DocumentContext, node: &Node) -> anyhow::Result< return Ok(None); }; - let text = context.document.contents.node_slice(&value)?.to_string(); + let text = value.node_as_str(&context.document.contents)?; let options = RParseEvalOptions { forbid_function_calls: true, @@ -133,7 +129,7 @@ fn get_first_argument(context: &DocumentContext, node: &Node) -> anyhow::Result< }; // Try to evaluate the first argument - let value = harp::parse_eval(text.as_str(), options); + let value = harp::parse_eval(text, options); // If we get an `UnsafeEvaluationError` here from setting // `forbid_function_calls`, we don't even log that one, as that is @@ -285,8 +281,8 @@ mod tests { use crate::fixtures::point_from_cursor; use crate::lsp::completions::completion_context::CompletionContext; use crate::lsp::completions::sources::composite::call::completions_from_call; + use crate::lsp::document::Document; use crate::lsp::document_context::DocumentContext; - use crate::lsp::documents::Document; use crate::lsp::state::WorldState; use crate::r_task; diff --git a/crates/ark/src/lsp/completions/sources/composite/document.rs b/crates/ark/src/lsp/completions/sources/composite/document.rs index d6f2bc087..3ea34a26d 100644 --- a/crates/ark/src/lsp/completions/sources/composite/document.rs +++ b/crates/ark/src/lsp/completions/sources/composite/document.rs @@ -16,8 +16,8 @@ use crate::lsp::completions::sources::utils::filter_out_dot_prefixes; use crate::lsp::completions::sources::CompletionSource; use crate::lsp::document_context::DocumentContext; use crate::lsp::traits::cursor::TreeCursorExt; +use crate::lsp::traits::node::NodeExt; use crate::lsp::traits::point::PointExt; -use crate::lsp::traits::rope::RopeExt; use crate::treesitter::BinaryOperatorType; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; @@ -170,7 +170,7 @@ fn completions_from_document_function_arguments( continue; } - let parameter = context.document.contents.node_slice(&node)?.to_string(); + let parameter = node.node_as_str(&context.document.contents)?.to_string(); match completion_item_from_scope_parameter(parameter.as_str(), context) { Ok(item) => completions.push(item), Err(err) => log::error!("{err:?}"), @@ -186,7 +186,9 @@ fn call_uses_nse(node: &Node, context: &DocumentContext) -> bool { let lhs = node.child(0).into_result()?; lhs.is_identifier_or_string().into_result()?; - let value = context.document.contents.node_slice(&lhs)?.to_string(); + let value = lhs + .node_as_str(&context.document.contents)? + .to_string(); matches!(value.as_str(), "expression" | "local" | "quote" | "enquote" | "substitute" | "with" | "within").into_result()?; Ok(()) diff --git a/crates/ark/src/lsp/completions/sources/composite/keyword.rs b/crates/ark/src/lsp/completions/sources/composite/keyword.rs index 1f786ca70..1a19b734c 100644 --- a/crates/ark/src/lsp/completions/sources/composite/keyword.rs +++ b/crates/ark/src/lsp/completions/sources/composite/keyword.rs @@ -176,13 +176,14 @@ fn add_keyword_snippets(completions: &mut Vec) { #[cfg(test)] mod tests { use tower_lsp::lsp_types::CompletionItemLabelDetails; + use tower_lsp::lsp_types::{self}; #[test] fn test_presence_bare_keywords() { let completions = super::completions_from_keywords().unwrap().unwrap(); let keyword_completions: Vec<_> = completions .iter() - .filter(|item| item.kind == Some(tower_lsp::lsp_types::CompletionItemKind::KEYWORD)) + .filter(|item| item.kind == Some(lsp_types::CompletionItemKind::KEYWORD)) .collect(); for keyword in super::BARE_KEYWORDS { @@ -209,7 +210,7 @@ mod tests { let completions = super::completions_from_keywords().unwrap().unwrap(); let snippet_completions: Vec<_> = completions .iter() - .filter(|item| item.kind == Some(tower_lsp::lsp_types::CompletionItemKind::SNIPPET)) + .filter(|item| item.kind == Some(lsp_types::CompletionItemKind::SNIPPET)) .collect(); let snippet_labels: Vec<&str> = super::KEYWORD_SNIPPETS diff --git a/crates/ark/src/lsp/completions/sources/composite/pipe.rs b/crates/ark/src/lsp/completions/sources/composite/pipe.rs index b1a1fa211..fd8448f53 100644 --- a/crates/ark/src/lsp/completions/sources/composite/pipe.rs +++ b/crates/ark/src/lsp/completions/sources/composite/pipe.rs @@ -15,7 +15,7 @@ use crate::lsp::completions::completion_context::CompletionContext; use crate::lsp::completions::sources::utils::completions_from_object_names; use crate::lsp::completions::sources::CompletionSource; use crate::lsp::document_context::DocumentContext; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::traits::node::NodeExt; use crate::treesitter::NodeTypeExt; pub(super) struct PipeSource; @@ -137,7 +137,7 @@ fn find_pipe_root_name(context: &DocumentContext, node: &Node) -> anyhow::Result } // Try to evaluate the left-hand side - let root = context.document.contents.node_slice(&lhs)?.to_string(); + let root = lhs.node_as_str(&context.document.contents)?.to_string(); Ok(Some(root)) } @@ -166,8 +166,8 @@ mod tests { use crate::fixtures::point_from_cursor; use crate::lsp::completions::sources::composite::pipe::find_pipe_root; + use crate::lsp::document::Document; use crate::lsp::document_context::DocumentContext; - use crate::lsp::documents::Document; use crate::r_task; use crate::treesitter::node_find_containing_call; diff --git a/crates/ark/src/lsp/completions/sources/composite/search_path.rs b/crates/ark/src/lsp/completions/sources/composite/search_path.rs index 273cd7951..319e819ff 100644 --- a/crates/ark/src/lsp/completions/sources/composite/search_path.rs +++ b/crates/ark/src/lsp/completions/sources/composite/search_path.rs @@ -103,7 +103,7 @@ fn completions_from_search_path( env, name, promise_strategy, - context.function_context(), + context.function_context()?, ) { Ok(item) => completions.push(item), Err(err) => { diff --git a/crates/ark/src/lsp/completions/sources/composite/subset.rs b/crates/ark/src/lsp/completions/sources/composite/subset.rs index a74b550c6..7fff45d6f 100644 --- a/crates/ark/src/lsp/completions/sources/composite/subset.rs +++ b/crates/ark/src/lsp/completions/sources/composite/subset.rs @@ -12,7 +12,7 @@ use crate::lsp::completions::sources::common::subset::is_within_subset_delimiter use crate::lsp::completions::sources::utils::completions_from_evaluated_object_names; use crate::lsp::completions::sources::CompletionSource; use crate::lsp::document_context::DocumentContext; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::traits::node::NodeExt; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; @@ -80,7 +80,7 @@ pub(crate) fn completions_from_subset( return Ok(Some(vec![])); }; - let text = context.document.contents.node_slice(&child)?.to_string(); + let text = child.node_as_str(&context.document.contents)?.to_string(); completions_from_evaluated_object_names(&text, ENQUOTE, node.node_type()) } @@ -92,8 +92,8 @@ mod tests { use crate::fixtures::package_is_installed; use crate::lsp::completions::sources::composite::subset::completions_from_subset; + use crate::lsp::document::Document; use crate::lsp::document_context::DocumentContext; - use crate::lsp::documents::Document; use crate::r_task; #[test] diff --git a/crates/ark/src/lsp/completions/sources/composite/workspace.rs b/crates/ark/src/lsp/completions/sources/composite/workspace.rs index 1a02efacb..3d677da47 100644 --- a/crates/ark/src/lsp/completions/sources/composite/workspace.rs +++ b/crates/ark/src/lsp/completions/sources/composite/workspace.rs @@ -5,8 +5,6 @@ // // -use log::*; -use stdext::*; use tower_lsp::lsp_types::CompletionItem; use tower_lsp::lsp_types::Documentation; use tower_lsp::lsp_types::MarkupContent; @@ -18,7 +16,7 @@ use crate::lsp::completions::completion_item::completion_item_from_variable; use crate::lsp::completions::sources::utils::filter_out_dot_prefixes; use crate::lsp::completions::sources::CompletionSource; use crate::lsp::indexer; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::traits::node::NodeExt; use crate::lsp::traits::string::StringExt; use crate::treesitter::node_in_string; use crate::treesitter::NodeTypeExt; @@ -64,7 +62,7 @@ fn completions_from_workspace( let mut completions = vec![]; let token = if node.is_identifier() { - context.document.contents.node_slice(&node)?.to_string() + node.node_as_str(&context.document.contents)?.to_string() } else { "".to_string() }; @@ -78,14 +76,21 @@ fn completions_from_workspace( match &entry.data { indexer::IndexEntryData::Function { name, .. } => { - let mut completion = unwrap!(completion_item_from_function( - name, - None, - completion_context.function_context(), - ), Err(error) => { - error!("{:?}", error); - return; - }); + let fun_context = match completion_context.function_context() { + Ok(fun_context) => fun_context, + Err(err) => { + log::error!("{:?}", err); + return; + }, + }; + + let mut completion = match completion_item_from_function(name, None, fun_context) { + Ok(completion) => completion, + Err(err) => { + log::error!("{:?}", err); + return; + }, + }; // Add some metadata about where the completion was found let mut path = uri.as_str().to_owned(); diff --git a/crates/ark/src/lsp/completions/sources/unique/colon.rs b/crates/ark/src/lsp/completions/sources/unique/colon.rs index 286b38fda..5cf7e6794 100644 --- a/crates/ark/src/lsp/completions/sources/unique/colon.rs +++ b/crates/ark/src/lsp/completions/sources/unique/colon.rs @@ -10,7 +10,7 @@ use tower_lsp::lsp_types::CompletionItem; use crate::lsp::completions::completion_context::CompletionContext; use crate::lsp::completions::sources::CompletionSource; use crate::lsp::document_context::DocumentContext; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::traits::node::NodeExt; pub(super) struct SingleColonSource; @@ -30,7 +30,9 @@ impl CompletionSource for SingleColonSource { // Don't provide completions if on a single `:`, which typically precedes // a `::` or `:::`. It means we don't provide completions for `1:` but we // accept that. -fn completions_from_single_colon(context: &DocumentContext) -> anyhow::Result>> { +fn completions_from_single_colon( + context: &DocumentContext, +) -> anyhow::Result>> { if is_single_colon(context) { // Return an empty vector to signal that we are done Ok(Some(vec![])) @@ -41,8 +43,8 @@ fn completions_from_single_colon(context: &DocumentContext) -> anyhow::Result bool { - let Ok(slice) = context.document.contents.node_slice(&context.node) else { + let Ok(text) = context.node.node_as_str(&context.document.contents) else { return false; }; - slice.eq(":") + text.eq(":") } diff --git a/crates/ark/src/lsp/completions/sources/unique/comment.rs b/crates/ark/src/lsp/completions/sources/unique/comment.rs index 9c05332a4..43b6f2330 100644 --- a/crates/ark/src/lsp/completions/sources/unique/comment.rs +++ b/crates/ark/src/lsp/completions/sources/unique/comment.rs @@ -22,7 +22,7 @@ use crate::lsp::completions::completion_item::completion_item; use crate::lsp::completions::sources::CompletionSource; use crate::lsp::completions::types::CompletionData; use crate::lsp::document_context::DocumentContext; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::traits::node::NodeExt; use crate::treesitter::NodeTypeExt; pub(super) struct CommentSource; @@ -40,7 +40,9 @@ impl CompletionSource for CommentSource { } } -fn completions_from_comment(context: &DocumentContext) -> anyhow::Result>> { +fn completions_from_comment( + context: &DocumentContext, +) -> anyhow::Result>> { let node = context.node; if !node.is_comment() { @@ -49,8 +51,8 @@ fn completions_from_comment(context: &DocumentContext) -> anyhow::Result = vec![]; @@ -142,7 +144,7 @@ fn inject_roxygen_comment_after_newline(x: &str) -> String { fn test_comment() { use tree_sitter::Point; - use crate::lsp::documents::Document; + use crate::lsp::document::Document; use crate::r_task; r_task(|| { @@ -167,7 +169,7 @@ fn test_roxygen_comment() { use libr::LOGICAL_ELT; use tree_sitter::Point; - use crate::lsp::documents::Document; + use crate::lsp::document::Document; use crate::r_task; r_task(|| unsafe { diff --git a/crates/ark/src/lsp/completions/sources/unique/custom.rs b/crates/ark/src/lsp/completions/sources/unique/custom.rs index 7e18ebdd8..325ac2bdf 100644 --- a/crates/ark/src/lsp/completions/sources/unique/custom.rs +++ b/crates/ark/src/lsp/completions/sources/unique/custom.rs @@ -15,6 +15,7 @@ use libr::VECSXP; use libr::VECTOR_ELT; use stdext::unwrap; use stdext::IntoResult; +use tower_lsp::lsp_types; use tower_lsp::lsp_types::CompletionItem; use crate::lsp; @@ -81,12 +82,12 @@ fn completions_from_custom_source( // Extract the parameter text. let parameter = match parameter.label.clone() { - tower_lsp::lsp_types::ParameterLabel::LabelOffsets([start, end]) => { + lsp_types::ParameterLabel::LabelOffsets([start, end]) => { let label = signature.label.as_str(); let substring = label.get((start as usize)..(end as usize)); substring.unwrap().to_string() }, - tower_lsp::lsp_types::ParameterLabel::Simple(string) => string, + lsp_types::ParameterLabel::Simple(string) => string, }; // Parameter text typically contains the parameter name and its default value if there is one. @@ -210,7 +211,7 @@ mod tests { use crate::lsp::completions::completion_context::CompletionContext; use crate::lsp::completions::sources::unique::custom::completions_from_custom_source; use crate::lsp::document_context::DocumentContext; - use crate::lsp::documents::Document; + use crate::lsp::document::Document; use crate::lsp::state::WorldState; use crate::r_task; diff --git a/crates/ark/src/lsp/completions/sources/unique/extractor.rs b/crates/ark/src/lsp/completions/sources/unique/extractor.rs index 585cc450b..ec5cba642 100644 --- a/crates/ark/src/lsp/completions/sources/unique/extractor.rs +++ b/crates/ark/src/lsp/completions/sources/unique/extractor.rs @@ -21,7 +21,7 @@ use crate::lsp::completions::completion_item::completion_item_from_data_variable use crate::lsp::completions::sources::utils::set_sort_text_by_first_appearance; use crate::lsp::completions::sources::CompletionSource; use crate::lsp::document_context::DocumentContext; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::traits::node::NodeExt; use crate::treesitter::ExtractOperatorType; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; @@ -99,9 +99,9 @@ fn completions_from_extractor( }; // Extract out its name from the document - let text = context.document.contents.node_slice(&node)?.to_string(); + let text = node.node_as_str(&context.document.contents)?; - completions.append(&mut completions_from_extractor_object(text.as_str(), fun)?); + completions.append(&mut completions_from_extractor_object(text, fun)?); Ok(Some(completions)) } @@ -208,7 +208,7 @@ mod tests { use crate::fixtures::point_from_cursor; use crate::lsp::completions::sources::unique::extractor::completions_from_dollar; use crate::lsp::document_context::DocumentContext; - use crate::lsp::documents::Document; + use crate::lsp::document::Document; use crate::r_task; #[test] diff --git a/crates/ark/src/lsp/completions/sources/unique/file_path.rs b/crates/ark/src/lsp/completions/sources/unique/file_path.rs index 63aaed7a8..a785a3bf3 100644 --- a/crates/ark/src/lsp/completions/sources/unique/file_path.rs +++ b/crates/ark/src/lsp/completions/sources/unique/file_path.rs @@ -17,7 +17,7 @@ use tree_sitter::Node; use crate::lsp::completions::completion_item::completion_item_from_direntry; use crate::lsp::completions::sources::utils::set_sort_text_by_words_first; use crate::lsp::document_context::DocumentContext; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::traits::node::NodeExt; pub(super) fn completions_from_string_file_path( node: &Node, @@ -32,12 +32,12 @@ pub(super) fn completions_from_string_file_path( // NOTE: This includes the quotation characters on the string, and so // also includes any internal escapes! We need to decode the R string // by parsing it before searching the path entries. - let token = context.document.contents.node_slice(&node)?.to_string(); + let token = node.node_as_str(&context.document.contents)?; // It's entirely possible that we can fail to parse the string, `R_ParseVector()` // can fail in various ways. We silently swallow these because they are unlikely // to report to real file paths and just bail (posit-dev/positron#6584). - let Ok(contents) = harp::parse_expr(&token) else { + let Ok(contents) = harp::parse_expr(token) else { return Ok(completions); }; @@ -97,7 +97,7 @@ mod tests { use crate::fixtures::point_from_cursor; use crate::lsp::completions::sources::unique::file_path::completions_from_string_file_path; use crate::lsp::document_context::DocumentContext; - use crate::lsp::documents::Document; + use crate::lsp::document::Document; use crate::r_task; use crate::treesitter::node_find_string; diff --git a/crates/ark/src/lsp/completions/sources/unique/namespace.rs b/crates/ark/src/lsp/completions/sources/unique/namespace.rs index 1d1c24710..b8be0aa3c 100644 --- a/crates/ark/src/lsp/completions/sources/unique/namespace.rs +++ b/crates/ark/src/lsp/completions/sources/unique/namespace.rs @@ -23,7 +23,7 @@ use crate::lsp::completions::completion_item::completion_item_from_lazydata; use crate::lsp::completions::completion_item::completion_item_from_namespace; use crate::lsp::completions::sources::utils::set_sort_text_by_words_first; use crate::lsp::completions::sources::CompletionSource; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::traits::node::NodeExt; use crate::treesitter::NamespaceOperatorType; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; @@ -76,8 +76,7 @@ fn completions_from_namespace( return Ok(Some(completions)); }; - let package = context.document.contents.node_slice(&package)?.to_string(); - let package = package.as_str(); + let package = package.node_as_str(&context.document.contents)?; // Get the package namespace let Ok(namespace) = RFunction::new("base", "getNamespace").add(package).call() else { @@ -103,7 +102,7 @@ fn completions_from_namespace( string, *namespace, package, - completion_context.function_context(), + completion_context.function_context()?, ) }; match item { @@ -247,8 +246,8 @@ mod tests { use crate::lsp::completions::completion_context::CompletionContext; use crate::lsp::completions::sources::unique::namespace::completions_from_namespace; use crate::lsp::completions::tests::utils::find_completion_by_label; + use crate::lsp::document::Document; use crate::lsp::document_context::DocumentContext; - use crate::lsp::documents::Document; use crate::lsp::state::WorldState; use crate::r_task; diff --git a/crates/ark/src/lsp/completions/sources/unique/string.rs b/crates/ark/src/lsp/completions/sources/unique/string.rs index ff1370723..414c70a5f 100644 --- a/crates/ark/src/lsp/completions/sources/unique/string.rs +++ b/crates/ark/src/lsp/completions/sources/unique/string.rs @@ -78,7 +78,7 @@ mod tests { use crate::lsp::completions::sources::unique; use crate::lsp::completions::sources::unique::string::completions_from_string; use crate::lsp::document_context::DocumentContext; - use crate::lsp::documents::Document; + use crate::lsp::document::Document; use crate::lsp::state::WorldState; use crate::r_task; use crate::treesitter::node_find_string; diff --git a/crates/ark/src/lsp/completions/sources/unique/subset.rs b/crates/ark/src/lsp/completions/sources/unique/subset.rs index 545e4f875..ebb60f32f 100644 --- a/crates/ark/src/lsp/completions/sources/unique/subset.rs +++ b/crates/ark/src/lsp/completions/sources/unique/subset.rs @@ -5,14 +5,13 @@ // // -use ropey::Rope; use tower_lsp::lsp_types::CompletionItem; use tree_sitter::Node; use crate::lsp::completions::sources::common::subset::is_within_subset_delimiters; use crate::lsp::completions::sources::utils::completions_from_evaluated_object_names; use crate::lsp::document_context::DocumentContext; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::traits::node::NodeExt; use crate::treesitter::node_find_parent_call; use crate::treesitter::NodeTypeExt; @@ -47,10 +46,10 @@ pub(super) fn completions_from_string_subset( // completion sources from running. let mut completions: Vec = vec![]; - let text = context.document.contents.node_slice(&node)?.to_string(); + let text = node.node_as_str(&context.document.contents)?; if let Some(mut candidates) = - completions_from_evaluated_object_names(&text, ENQUOTE, node.node_type())? + completions_from_evaluated_object_names(text, ENQUOTE, node.node_type())? { completions.append(&mut candidates); } @@ -106,7 +105,7 @@ fn node_find_object_for_string_subset<'tree>( return Some(node); } -fn node_is_c_call(x: &Node, contents: &Rope) -> bool { +fn node_is_c_call(x: &Node, contents: &str) -> bool { if !x.is_call() { return false; } @@ -119,7 +118,7 @@ fn node_is_c_call(x: &Node, contents: &Rope) -> bool { return false; } - let Ok(text) = contents.node_slice(&x) else { + let Ok(text) = x.node_as_str(&contents) else { log::error!("Can't slice `contents`."); return false; }; @@ -135,7 +134,7 @@ mod tests { use crate::fixtures::point_from_cursor; use crate::lsp::completions::sources::unique::subset::completions_from_string_subset; use crate::lsp::document_context::DocumentContext; - use crate::lsp::documents::Document; + use crate::lsp::document::Document; use crate::r_task; use crate::treesitter::node_find_string; diff --git a/crates/ark/src/lsp/completions/sources/utils.rs b/crates/ark/src/lsp/completions/sources/utils.rs index ae367e4bb..17ef90b90 100644 --- a/crates/ark/src/lsp/completions/sources/utils.rs +++ b/crates/ark/src/lsp/completions/sources/utils.rs @@ -20,7 +20,6 @@ use crate::lsp::completions::completion_item::completion_item_from_data_variable use crate::lsp::document_context::DocumentContext; use crate::lsp::traits::node::NodeExt; use crate::lsp::traits::point::PointExt; -use crate::lsp::traits::rope::RopeExt; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; @@ -99,10 +98,9 @@ pub(super) fn filter_out_dot_prefixes( ) { // Remove completions that start with `.` unless the user explicitly requested them let user_requested_dot = context - .document - .contents - .node_slice(&context.node) - .and_then(|x| Ok(x.to_string().starts_with("."))) + .node + .node_as_str(&context.document.contents) + .map(|x| x.starts_with(".")) .unwrap_or(false); if !user_requested_dot { @@ -295,7 +293,7 @@ mod tests { use crate::lsp::completions::sources::utils::completions_from_evaluated_object_names; use crate::lsp::completions::sources::utils::CallNodePositionType; use crate::lsp::document_context::DocumentContext; - use crate::lsp::documents::Document; + use crate::lsp::document::Document; use crate::r_task; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; diff --git a/crates/ark/src/lsp/completions/tests/utils.rs b/crates/ark/src/lsp/completions/tests/utils.rs index af4f390b0..16697a65d 100644 --- a/crates/ark/src/lsp/completions/tests/utils.rs +++ b/crates/ark/src/lsp/completions/tests/utils.rs @@ -4,6 +4,7 @@ // Copyright (C) 2025 Posit Software, PBC. All rights reserved. // +use tower_lsp::lsp_types; use tower_lsp::lsp_types::CompletionItem; use tower_lsp::lsp_types::CompletionTextEdit; @@ -11,7 +12,7 @@ use crate::fixtures::utils::point_from_cursor; use crate::lsp::completions::provide_completions; use crate::lsp::completions::sources::utils::has_priority_prefix; use crate::lsp::document_context::DocumentContext; -use crate::lsp::documents::Document; +use crate::lsp::document::Document; use crate::lsp::state::WorldState; pub(crate) fn get_completions_at_cursor(cursor_text: &str) -> anyhow::Result> { @@ -27,13 +28,13 @@ pub(crate) fn get_completions_at_cursor(cursor_text: &str) -> anyhow::Result( - completions: &'a [tower_lsp::lsp_types::CompletionItem], + completions: &'a [lsp_types::CompletionItem], label: &str, -) -> Option<&'a tower_lsp::lsp_types::CompletionItem> { +) -> Option<&'a lsp_types::CompletionItem> { completions.iter().find(|c| c.label == label) } -pub(crate) fn assert_text_edit(item: &tower_lsp::lsp_types::CompletionItem, expected_text: &str) { +pub(crate) fn assert_text_edit(item: &lsp_types::CompletionItem, expected_text: &str) { assert!(item.text_edit.is_some()); assert!(item.insert_text.is_none()); @@ -48,21 +49,21 @@ pub(crate) fn assert_text_edit(item: &tower_lsp::lsp_types::CompletionItem, expe } } -pub(crate) fn assert_has_parameter_hints(item: &tower_lsp::lsp_types::CompletionItem) { +pub(crate) fn assert_has_parameter_hints(item: &lsp_types::CompletionItem) { match &item.command { Some(command) => assert_eq!(command.command, "editor.action.triggerParameterHints"), None => panic!("CompletionItem is missing parameter hints command"), } } -pub(crate) fn assert_no_command(item: &tower_lsp::lsp_types::CompletionItem) { +pub(crate) fn assert_no_command(item: &lsp_types::CompletionItem) { assert!( item.command.is_none(), "CompletionItem should not have an associated command" ); } -pub(crate) fn assert_sort_text_has_priority_prefix(item: &tower_lsp::lsp_types::CompletionItem) { +pub(crate) fn assert_sort_text_has_priority_prefix(item: &lsp_types::CompletionItem) { assert!(item.sort_text.is_some()); let sort_text = item.sort_text.as_ref().unwrap(); assert!(has_priority_prefix(sort_text)); diff --git a/crates/ark/src/lsp/declarations.rs b/crates/ark/src/lsp/declarations.rs index cf57425ac..dc7d42e4c 100644 --- a/crates/ark/src/lsp/declarations.rs +++ b/crates/ark/src/lsp/declarations.rs @@ -1,10 +1,10 @@ use tree_sitter::Node; use crate::lsp; +use crate::lsp::traits::node::NodeExt; use crate::treesitter::args_find_call_args; use crate::treesitter::node_arg_value; use crate::treesitter::node_is_call; -use crate::treesitter::node_text; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; use crate::treesitter::UnaryOperatorType; @@ -19,10 +19,7 @@ impl Default for TopLevelDeclare { } } -pub(crate) fn top_level_declare( - ast: &tree_sitter::Tree, - contents: &ropey::Rope, -) -> TopLevelDeclare { +pub(crate) fn top_level_declare(ast: &tree_sitter::Tree, contents: &str) -> TopLevelDeclare { let mut decls = TopLevelDeclare::default(); let Some(declare_args) = top_level_declare_args(ast, contents) else { @@ -41,7 +38,7 @@ pub(crate) fn top_level_declare( let Some(enable) = iter.find_map(|n| node_arg_value(&n, "enable", contents)) else { return decls; }; - let Some(enable_text) = node_text(&enable, contents) else { + let Ok(enable_text) = enable.node_as_str(contents) else { return decls; }; @@ -56,7 +53,7 @@ pub(crate) fn top_level_declare( fn top_level_declare_args<'tree>( ast: &'tree tree_sitter::Tree, - contents: &ropey::Rope, + contents: &str, ) -> Option> { let root = ast.root_node(); let mut cursor = root.walk(); @@ -81,17 +78,11 @@ fn top_level_declare_args<'tree>( first.child_by_field_name("arguments") } -fn declare_ark_args<'tree>( - declare_args: Node<'tree>, - contents: &ropey::Rope, -) -> Option> { +fn declare_ark_args<'tree>(declare_args: Node<'tree>, contents: &str) -> Option> { args_find_call_args(declare_args, "ark", contents) } -fn ark_diagnostics_args<'tree>( - ark_args: Node<'tree>, - contents: &ropey::Rope, -) -> Option> { +fn ark_diagnostics_args<'tree>(ark_args: Node<'tree>, contents: &str) -> Option> { args_find_call_args(ark_args, "diagnostics", contents) } @@ -102,7 +93,7 @@ mod test { use crate::lsp::declarations::declare_ark_args; use crate::lsp::declarations::top_level_declare; use crate::lsp::declarations::top_level_declare_args; - use crate::lsp::documents::Document; + use crate::lsp::document::Document; #[test] fn test_declare_args() { diff --git a/crates/ark/src/lsp/definitions.rs b/crates/ark/src/lsp/definitions.rs index 8af8f1626..8304303fd 100644 --- a/crates/ark/src/lsp/definitions.rs +++ b/crates/ark/src/lsp/definitions.rs @@ -11,12 +11,9 @@ use tower_lsp::lsp_types::GotoDefinitionResponse; use tower_lsp::lsp_types::LocationLink; use tower_lsp::lsp_types::Range; -use crate::lsp::documents::Document; -use crate::lsp::encoding::convert_point_to_position; -use crate::lsp::encoding::convert_position_to_point; +use crate::lsp::document::Document; use crate::lsp::indexer; use crate::lsp::traits::node::NodeExt; -use crate::lsp::traits::rope::RopeExt; use crate::treesitter::NodeTypeExt; pub fn goto_definition<'a>( @@ -26,29 +23,26 @@ pub fn goto_definition<'a>( // get reference to AST let ast = &document.ast; - let contents = &document.contents; - // try to find node at position let position = params.text_document_position_params.position; - let point = convert_position_to_point(contents, position); + let point = document.tree_sitter_point_from_lsp_position(position)?; let Some(node) = ast.root_node().find_closest_node_to_point(point) else { log::warn!("Failed to find the closest node to point {point}."); return Ok(None); }; - let start = convert_point_to_position(contents, node.start_position()); - let end = convert_point_to_position(contents, node.end_position()); + let start = document.lsp_position_from_tree_sitter_point(node.start_position())?; + let end = document.lsp_position_from_tree_sitter_point(node.end_position())?; let range = Range { start, end }; // Search for a reference in the document index if node.is_identifier() { - let symbol = document.contents.node_slice(&node)?.to_string(); + let symbol = node.node_as_str(&document.contents)?; // First search in current file, then in all files let uri = ¶ms.text_document_position_params.text_document.uri; - let info = - indexer::find_in_file(symbol.as_str(), uri).or_else(|| indexer::find(symbol.as_str())); + let info = indexer::find_in_file(symbol, uri).or_else(|| indexer::find(symbol)); if let Some((file_id, entry)) = info { let target_uri = file_id.as_uri().clone(); @@ -88,7 +82,7 @@ mod tests { use tower_lsp::lsp_types; use super::*; - use crate::lsp::documents::Document; + use crate::lsp::document::Document; use crate::lsp::indexer; use crate::lsp::util::test_path; diff --git a/crates/ark/src/lsp/diagnostics.rs b/crates/ark/src/lsp/diagnostics.rs index 3d4776fa7..b7e8586fd 100644 --- a/crates/ark/src/lsp/diagnostics.rs +++ b/crates/ark/src/lsp/diagnostics.rs @@ -14,7 +14,6 @@ use anyhow::bail; use anyhow::Result; use harp::utils::is_symbol_valid; use harp::utils::sym_quote_invalid; -use ropey::Rope; use stdext::*; use tower_lsp::lsp_types::Diagnostic; use tower_lsp::lsp_types::DiagnosticSeverity; @@ -25,15 +24,13 @@ use tree_sitter::Range; use crate::lsp; use crate::lsp::declarations::top_level_declare; use crate::lsp::diagnostics_syntax::syntax_diagnostics; -use crate::lsp::documents::Document; -use crate::lsp::encoding::convert_tree_sitter_range_to_lsp_range; +use crate::lsp::document::Document; use crate::lsp::indexer; use crate::lsp::inputs::library::Library; use crate::lsp::inputs::package::Package; use crate::lsp::inputs::source_root::SourceRoot; use crate::lsp::state::WorldState; use crate::lsp::traits::node::NodeExt; -use crate::lsp::traits::rope::RopeExt; use crate::treesitter::node_has_error_or_missing; use crate::treesitter::BinaryOperatorType; use crate::treesitter::NodeType; @@ -47,8 +44,8 @@ pub struct DiagnosticsConfig { #[derive(Clone)] pub struct DiagnosticContext<'a> { - /// The contents of the source document. - pub contents: &'a Rope, + /// The document under analysis + pub doc: &'a Document, /// The symbols currently defined and available in the session. pub session_symbols: HashSet, @@ -87,9 +84,9 @@ impl Default for DiagnosticsConfig { } impl<'a> DiagnosticContext<'a> { - pub fn new(contents: &'a Rope, root: &'a Option, library: &'a Library) -> Self { + pub fn new(doc: &'a Document, root: &'a Option, library: &'a Library) -> Self { Self { - contents, + doc, document_symbols: Vec::new(), session_symbols: HashSet::new(), workspace_symbols: HashSet::new(), @@ -153,7 +150,7 @@ pub(crate) fn generate_diagnostics( return diagnostics; } - let mut context = DiagnosticContext::new(&doc.contents, &state.root, &state.library); + let mut context = DiagnosticContext::new(&doc, &state.root, &state.library); // Add a 'root' context for the document. context.document_symbols.push(HashMap::new()); @@ -369,9 +366,9 @@ fn recurse_for( }); if variable.is_identifier() { - let name = context.contents.node_slice(&variable)?.to_string(); + let name = variable.node_as_str(&context.doc.contents)?; let range = variable.range(); - context.add_defined_variable(name.as_str(), range); + context.add_defined_variable(name, range); } // Now, scan the body, if it exists @@ -556,9 +553,9 @@ fn handle_assignment_variable( return Ok(()); } - let name = context.contents.node_slice(&identifier)?.to_string(); + let name = identifier.node_as_str(&context.doc.contents)?; let range = identifier.range(); - context.add_defined_variable(name.as_str(), range); + context.add_defined_variable(name, range); Ok(()) } @@ -588,7 +585,7 @@ fn handle_assignment_dotty( return Ok(()); }; - let dot = context.contents.node_slice(&dot)?; + let dot = dot.node_as_str(&context.doc.contents)?; if dot != "." { return Ok(()); }; @@ -612,8 +609,8 @@ fn handle_assignment_dotty( // so we don't want to define a variable for `x` there. if let Some(name) = child.child_by_field_name("name") { let range = name.range(); - let name = context.contents.node_slice(&name)?.to_string(); - context.add_defined_variable(name.as_str(), range); + let name = name.node_as_str(&context.doc.contents)?; + context.add_defined_variable(name, range); continue; }; @@ -624,8 +621,8 @@ fn handle_assignment_dotty( // i.e. `.[x, y]` where `value` is just a name that dotty assigns to if value.is_identifier() { let range = value.range(); - let name = context.contents.node_slice(&value)?.to_string(); - context.add_defined_variable(name.as_str(), range); + let name = value.node_as_str(&context.doc.contents)?; + context.add_defined_variable(name, range); continue; } @@ -655,7 +652,7 @@ fn node_find_magrittr_pipe<'tree>( node: &Node<'tree>, context: &DiagnosticContext, ) -> anyhow::Result>> { - if node.is_magrittr_pipe_operator(&context.contents)? { + if node.is_magrittr_pipe_operator(&context.doc.contents)? { // Found one! return Ok(Some(*node)); } @@ -692,10 +689,10 @@ fn recurse_namespace( }); // Check for a valid package name. - let package = context.contents.node_slice(&lhs)?.to_string(); - if !context.installed_packages.contains(package.as_str()) { + let package = lhs.node_as_str(&context.doc.contents)?; + if !context.installed_packages.contains(package) { let range = lhs.range(); - let range = convert_tree_sitter_range_to_lsp_range(context.contents, range); + let range = context.doc.lsp_range_from_tree_sitter_range(range)?; let message = format!("Package '{}' is not installed.", package); let diagnostic = Diagnostic::new_simple(range, message); diagnostics.push(diagnostic); @@ -728,14 +725,10 @@ fn recurse_parameters( bail!("Missing a `name` field in a `parameter` node."); }); - let symbol = unwrap!(context.contents.node_slice(&name), Err(error) => { - bail!("Failed to convert `name` node to a string due to: {error}"); - }); - let symbol = symbol.to_string(); - + let symbol = name.node_as_str(&context.doc.contents)?; let location = name.range(); - context.add_defined_variable(symbol.as_str(), location); + context.add_defined_variable(symbol, location); } ().ok() @@ -847,8 +840,7 @@ fn recurse_call( // // TODO: Handle certain 'scope-generating' function calls, e.g. // things like 'local({ ... })'. - let fun = context.contents.node_slice(&callee)?.to_string(); - let fun = fun.as_str(); + let fun = callee.node_as_str(&context.doc.contents)?; match fun { "library" | "require" => { @@ -877,14 +869,14 @@ fn handle_package_attach_call(node: Node, context: &mut DiagnosticContext) -> an // We'll do better when we have a more capable argument inspection // infrastructure. if let Some(_) = node - .arguments_names_as_string(context.contents) + .arguments_names_as_string(&context.doc.contents) .flatten() .find(|n| n == "character.only") { return Ok(()); } - let package_name = package_node.get_identifier_or_string_text(context.contents)?; + let package_name = package_node.get_identifier_or_string_text(&context.doc.contents)?; let attach_pos = node.end_position(); let package = insert_package_exports(&package_name, attach_pos, context)?; @@ -1033,8 +1025,7 @@ fn check_invalid_na_comparison( let mut cursor = node.walk(); for child in node.children(&mut cursor) { - let contents = context.contents.node_slice(&child)?.to_string(); - let contents = contents.as_str(); + let contents = child.node_as_str(&context.doc.contents)?; if matches!(contents, "NA" | "NaN" | "NULL") { let message = match contents { @@ -1044,7 +1035,7 @@ fn check_invalid_na_comparison( _ => continue, }; let range = child.range(); - let range = convert_tree_sitter_range_to_lsp_range(context.contents, range); + let range = context.doc.lsp_range_from_tree_sitter_range(range)?; let mut diagnostic = Diagnostic::new_simple(range, message.into()); diagnostic.severity = Some(DiagnosticSeverity::INFORMATION); diagnostics.push(diagnostic); @@ -1078,7 +1069,7 @@ fn check_unexpected_assignment_in_if_conditional( } let range = condition.range(); - let range = convert_tree_sitter_range_to_lsp_range(context.contents, range); + let range = context.doc.lsp_range_from_tree_sitter_range(range)?; let message = "Unexpected '='; use '==' to compare values for equality."; let diagnostic = Diagnostic::new_simple(range, message.into()); diagnostics.push(diagnostic); @@ -1119,15 +1110,15 @@ fn check_symbol_in_scope( } // Skip if a symbol with this name is in scope. - let name = context.contents.node_slice(&node)?.to_string(); - if context.has_definition(name.as_str(), node.start_position()) { + let name = node.node_as_str(&context.doc.contents)?; + if context.has_definition(name, node.start_position()) { return false.ok(); } // No symbol in scope; provide a diagnostic. let range = node.range(); - let range = convert_tree_sitter_range_to_lsp_range(context.contents, range); - let identifier = context.contents.node_slice(&node)?.to_string(); + let range = context.doc.lsp_range_from_tree_sitter_range(range)?; + let identifier = node.node_as_str(&context.doc.contents)?; let message = format!("No symbol named '{}' in scope.", identifier); let mut diagnostic = Diagnostic::new_simple(range, message); diagnostic.severity = Some(DiagnosticSeverity::WARNING); @@ -1146,7 +1137,7 @@ mod tests { use tower_lsp::lsp_types::Position; use crate::interface::console_inputs; - use crate::lsp::documents::Document; + use crate::lsp::document::Document; use crate::lsp::inputs::library::Library; use crate::lsp::inputs::package::Package; use crate::lsp::inputs::package_description::Dcf; diff --git a/crates/ark/src/lsp/diagnostics_syntax.rs b/crates/ark/src/lsp/diagnostics_syntax.rs index 277aff831..19c7bd065 100644 --- a/crates/ark/src/lsp/diagnostics_syntax.rs +++ b/crates/ark/src/lsp/diagnostics_syntax.rs @@ -10,8 +10,7 @@ use tree_sitter::Node; use tree_sitter::Range; use crate::lsp::diagnostics::DiagnosticContext; -use crate::lsp::encoding::convert_tree_sitter_range_to_lsp_range; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::traits::node::NodeExt; use crate::treesitter::node_has_error_or_missing; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; @@ -81,10 +80,13 @@ fn syntax_diagnostic(node: Node, context: &DiagnosticContext) -> anyhow::Result< // would be able to report precise error locations/messages, because it "knows" why a // parse error occurred. - Ok(syntax_diagnostic_default(node, context)) + syntax_diagnostic_default(node, context) } -fn syntax_diagnostic_default(node: Node, context: &DiagnosticContext) -> Diagnostic { +fn syntax_diagnostic_default( + node: Node, + context: &DiagnosticContext, +) -> anyhow::Result { let range = node.range(); let row_span = range.end_point.row - range.start_point.row; @@ -99,7 +101,10 @@ fn syntax_diagnostic_default(node: Node, context: &DiagnosticContext) -> Diagnos // If the syntax error spans more than 20 rows, just target the starting position // to avoid overwhelming the user. -fn syntax_diagnostic_truncated_default(range: Range, context: &DiagnosticContext) -> Diagnostic { +fn syntax_diagnostic_truncated_default( + range: Range, + context: &DiagnosticContext, +) -> anyhow::Result { // In theory this is an empty range, as they are constructed like `[ )`, but it // seems to work for the purpose of diagnostics, and getting the correct // coordinates exactly right seems challenging. @@ -219,10 +224,10 @@ fn diagnose_missing_binary_operator( let range = operator.range(); - let text = context.contents.node_slice(&operator)?; + let text = operator.node_as_str(&context.doc.contents)?; let message = format!("Invalid binary operator '{text}'. Missing a right hand side."); - diagnostics.push(new_syntax_diagnostic(message, range, context)); + diagnostics.push(new_syntax_diagnostic(message, range, context)?); Ok(()) } @@ -250,10 +255,10 @@ pub(crate) fn diagnose_missing_namespace_operator( let range = operator.range(); - let text = context.contents.node_slice(&operator)?; + let text = operator.node_as_str(&context.doc.contents)?; let message = format!("Invalid namespace operator '{text}'. Missing a right hand side."); - diagnostics.push(new_syntax_diagnostic(message, range, context)); + diagnostics.push(new_syntax_diagnostic(message, range, context)?); Ok(()) } @@ -282,7 +287,7 @@ fn diagnose_missing_close( close_token, open.range(), context, - )); + )?); Ok(()) } @@ -291,14 +296,18 @@ fn new_missing_close_diagnostic( close_token: &str, range: Range, context: &DiagnosticContext, -) -> Diagnostic { +) -> anyhow::Result { let message = format!("Unmatched opening delimiter. Missing a closing '{close_token}'."); new_syntax_diagnostic(message, range, context) } -fn new_syntax_diagnostic(message: String, range: Range, context: &DiagnosticContext) -> Diagnostic { - let range = convert_tree_sitter_range_to_lsp_range(context.contents, range); - Diagnostic::new_simple(range, message) +fn new_syntax_diagnostic( + message: String, + range: Range, + context: &DiagnosticContext, +) -> anyhow::Result { + let range = context.doc.lsp_range_from_tree_sitter_range(range)?; + Ok(Diagnostic::new_simple(range, message)) } #[cfg(test)] @@ -308,13 +317,13 @@ mod tests { use crate::lsp::diagnostics::DiagnosticContext; use crate::lsp::diagnostics_syntax::syntax_diagnostics; - use crate::lsp::documents::Document; + use crate::lsp::document::Document; use crate::lsp::inputs::library::Library; fn text_diagnostics(text: &str) -> Vec { let document = Document::new(text, None); let library = Library::default(); - let context = DiagnosticContext::new(&document.contents, &None, &library); + let context = DiagnosticContext::new(&document, &None, &library); let diagnostics = syntax_diagnostics(document.ast.root_node(), &context).unwrap(); diagnostics } diff --git a/crates/ark/src/lsp/document.rs b/crates/ark/src/lsp/document.rs new file mode 100644 index 000000000..79f2a30fa --- /dev/null +++ b/crates/ark/src/lsp/document.rs @@ -0,0 +1,458 @@ +// +// document.rs +// +// Copyright (C) 2022-2025 Posit Software, PBC. All rights reserved. +// +// + +use aether_lsp_utils::proto::from_proto; +use aether_lsp_utils::proto::PositionEncoding; +use anyhow::Result; +use tower_lsp::lsp_types; +use tree_sitter::InputEdit; +use tree_sitter::Parser; +use tree_sitter::Point; +use tree_sitter::Tree; + +use crate::lsp::config::DocumentConfig; + +fn compute_point(point: Point, text: &str) -> Point { + // figure out where the newlines in this edit are + let newline_indices: Vec<_> = text.match_indices('\n').collect(); + let num_newlines = newline_indices.len(); + let num_bytes = text.as_bytes().len(); + + if newline_indices.len() == 0 { + return Point::new(point.row, point.column + num_bytes); + } else { + let last_newline_index = newline_indices.last().unwrap(); + return Point::new( + point.row + num_newlines, + num_bytes - last_newline_index.0 - 1, + ); + } +} + +#[derive(Clone)] +pub struct Document { + /// The document's textual contents. + pub contents: String, + + /// The document's AST. + pub ast: Tree, + + /// The Rowan R syntax tree. + pub parse: aether_parser::Parse, + + /// Index of new lines and non-UTF-8 characters in `contents`. Used for converting + /// between line/col [tower_lsp::Position]s with a specified [PositionEncoding] to + /// [biome_text_size::TextSize] offsets. + pub line_index: biome_line_index::LineIndex, + + /// The version of the document we last synchronized with. + /// None if the document hasn't been synchronized yet. + pub version: Option, + + /// Position encoding used for LSP position conversions. + pub position_encoding: PositionEncoding, + + /// Configuration of the document, such as indentation settings. + pub config: DocumentConfig, +} + +impl std::fmt::Debug for Document { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Document") + .field("contents", &self.contents) + .field("ast", &self.ast) + .field("parse", &self.parse) + .finish() + } +} + +impl Document { + pub fn new(contents: &str, version: Option) -> Self { + // A one-shot parser, assumes the `Document` won't be incrementally reparsed. + // Useful for testing, `with_document()`, and `index_file()`. + let mut parser = Parser::new(); + parser + .set_language(&tree_sitter_r::LANGUAGE.into()) + .unwrap(); + + Self::new_with_parser(contents, &mut parser, version) + } + + pub fn new_with_parser(contents: &str, parser: &mut Parser, version: Option) -> Self { + let contents = String::from(contents); + let ast = parser.parse(contents.as_str(), None).unwrap(); + let parse = aether_parser::parse(&contents, Default::default()); + let line_index = biome_line_index::LineIndex::new(&contents); + + Self { + contents, + version, + ast, + parse, + line_index, + // Currently hard-coded to UTF-16, but we might want to allow UTF-8 frontends + // once/if Ark becomes an independent LSP + position_encoding: PositionEncoding::Wide(biome_line_index::WideEncoding::Utf16), + config: Default::default(), + } + } + + pub fn on_did_change( + &mut self, + parser: &mut Parser, + params: &lsp_types::DidChangeTextDocumentParams, + ) { + let new_version = params.text_document.version; + + // Check for out-of-order change notifications + if let Some(old_version) = self.version { + // According to the spec, versions might not be consecutive but they must be monotonically + // increasing. If that's not the case this is a hard nope as we + // can't maintain our state integrity. Currently panicking but in + // principle we should shut down the LSP in an orderly fashion. + if new_version < old_version { + panic!( + "out-of-sync change notification: currently at {old_version}, got {new_version}" + ); + } + } + + for event in ¶ms.content_changes { + if let Err(err) = self.update(parser, event) { + panic!("Failed to update document: {err:?}"); + } + } + + // Set new version + self.version = Some(new_version); + } + + fn update( + &mut self, + parser: &mut Parser, + change: &lsp_types::TextDocumentContentChangeEvent, + ) -> Result<()> { + // Extract edit range. Return without doing anything if there wasn't any actual edit. + let range = match change.range { + Some(r) => r, + None => return Ok(()), + }; + + let tree_sitter::Range { + start_byte, + end_byte: old_end_byte, + start_point, + end_point: old_end_point, + } = self.tree_sitter_range_from_lsp_range(range)?; + + let new_end_point = compute_point(start_point, &change.text); + let new_end_byte = start_byte + change.text.as_bytes().len(); + + // Confusing tree sitter names, the `start_position` is really a `Point` + let edit = InputEdit { + start_byte, + old_end_byte, + new_end_byte, + start_position: start_point, + old_end_position: old_end_point, + new_end_position: new_end_point, + }; + + // Update the AST. We do this before updating the underlying document + // contents, because edit computations need to be done using the current + // state of the document (prior to the edit being applied) so that byte + // offsets can be computed correctly. + self.ast.edit(&edit); + + // Now update the text before re-parsing so the AST reflects the new contents + self.contents + .replace_range(start_byte..old_end_byte, &change.text); + self.line_index = biome_line_index::LineIndex::new(&self.contents); + + // We can now re-parse incrementally by providing the old edited AST + let ast = parser.parse(self.contents.as_str(), Some(&self.ast)); + self.ast = ast.unwrap(); + + // Update the Rowan tree. This currently reparses with TS. We could pass + // down the TS tree if that turns out too expensive, but the long term + // plan is to remove any TS usage so we prefer not introduce TS trees in + // the public APIs. + self.parse = aether_parser::parse(&self.contents, Default::default()); + + Ok(()) + } + + pub fn get_line(&self, line: usize) -> Option<&str> { + let Some(line_start) = self.line_index.newlines.get(line) else { + // Forcing a full capture so we can learn the situations in which this occurs + log::error!( + "Requesting line {line} but only {n} lines exist.\n\nDocument:\n{contents}\n\nBacktrace:\n{trace}", + n = self.line_index.len(), + line = line + 1, + contents = &self.contents, + trace = std::backtrace::Backtrace::force_capture(), + ); + return None; + }; + + let line_end = self + .line_index + .newlines + .get(line + 1) + .copied() + // if `line` is last, extract text until end of buffer + .unwrap_or_else(|| (self.contents.len() as u32).into()); + + let line_start_byte: usize = line_start.to_owned().into(); + let line_end_byte: usize = line_end.into(); + + self.contents.get(line_start_byte..line_end_byte) + } + + /// Accessor that returns an annotated `RSyntaxNode` type. + /// More convenient than the generic `biome_rowan::SyntaxNode` type. + pub fn syntax(&self) -> aether_syntax::RSyntaxNode { + self.parse.syntax() + } + + pub fn tree_sitter_point_from_lsp_position( + &self, + position: lsp_types::Position, + ) -> anyhow::Result { + let offset = from_proto::offset(position, &self.line_index, self.position_encoding)?; + let line_col = self.line_index.line_col(offset).ok_or_else(|| { + anyhow::anyhow!("Failed to convert LSP position {position:?} to LineCol offset") + })?; + Ok(tree_sitter::Point::new( + line_col.line as usize, + line_col.col as usize, + )) + } + + pub fn lsp_position_from_tree_sitter_point( + &self, + point: tree_sitter::Point, + ) -> anyhow::Result { + let line_col = biome_line_index::LineCol { + line: point.row as u32, + col: point.column as u32, + }; + + match self.position_encoding { + PositionEncoding::Utf8 => Ok(lsp_types::Position::new(line_col.line, line_col.col)), + PositionEncoding::Wide(wide_encoding) => { + let wide_line_col = self + .line_index + .to_wide(wide_encoding, line_col) + .ok_or_else(|| { + anyhow::anyhow!("Failed to convert Tree-Sitter point {point:?} to wide line column for document") + })?; + Ok(lsp_types::Position::new( + wide_line_col.line as u32, + wide_line_col.col as u32, + )) + }, + } + } + + pub fn lsp_range_from_tree_sitter_range( + &self, + range: tree_sitter::Range, + ) -> anyhow::Result { + let start = self.lsp_position_from_tree_sitter_point(range.start_point)?; + let end = self.lsp_position_from_tree_sitter_point(range.end_point)?; + Ok(lsp_types::Range::new(start, end)) + } + + pub fn tree_sitter_range_from_lsp_range( + &self, + range: lsp_types::Range, + ) -> anyhow::Result { + let start_point = self.tree_sitter_point_from_lsp_position(range.start)?; + let end_point = self.tree_sitter_point_from_lsp_position(range.end)?; + + let start_offset = + from_proto::offset(range.start, &self.line_index, self.position_encoding)?; + let end_offset = from_proto::offset(range.end, &self.line_index, self.position_encoding)?; + + Ok(tree_sitter::Range { + start_byte: start_offset.into(), + end_byte: end_offset.into(), + start_point, + end_point, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_point_computation() { + // empty strings shouldn't do anything + let point = compute_point(Point::new(0, 0), ""); + assert_eq!(point, Point::new(0, 0)); + + let point = compute_point(Point::new(42, 42), ""); + assert_eq!(point, Point::new(42, 42)); + + // text insertion without newlines should just extend the column position + let point = compute_point(Point::new(0, 0), "abcdef"); + assert_eq!(point, Point::new(0, 6)); + + // text insertion with newlines should change the row + let point = compute_point(Point::new(0, 0), "abc\ndef\nghi"); + assert_eq!(point, Point::new(2, 3)); + + let point = compute_point(Point::new(0, 0), "abcdefghi\n"); + assert_eq!(point, Point::new(1, 0)); + } + + #[test] + fn test_document_starts_at_0_0_with_leading_whitespace() { + let document = Document::new("\n\n# hi there", None); + let root = document.ast.root_node(); + assert_eq!(root.start_position(), Point::new(0, 0)); + } + + #[test] + fn test_aether_syntax_integration() { + let document = Document::new("foo <- 1 + 2", None); + + let syntax = document.parse.syntax(); + let len: u32 = syntax.text_range_with_trivia().len().into(); + assert!(len > 0); + + let syntax2 = document.syntax(); + assert_eq!( + syntax.text_range_with_trivia(), + syntax2.text_range_with_trivia() + ); + + assert!(!document.parse.has_error()); + } + + #[test] + fn test_tree_sitter_point_from_lsp_position_wide_encoding() { + // The emoji is 4 UTF-8 bytes and 2 UTF-16 bytes + let mut document = Document::new("😃a", None); + document.position_encoding = PositionEncoding::Wide(biome_line_index::WideEncoding::Utf16); + + let point = document + .tree_sitter_point_from_lsp_position(lsp_types::Position::new(0, 2)) + .unwrap(); + assert_eq!(point, Point::new(0, 4)); + + let point = document + .tree_sitter_point_from_lsp_position(lsp_types::Position::new(0, 3)) + .unwrap(); + assert_eq!(point, Point::new(0, 5)); + } + + #[test] + fn test_lsp_position_from_tree_sitter_point_wide_encoding() { + let mut document = Document::new("😃a", None); + document.position_encoding = PositionEncoding::Wide(biome_line_index::WideEncoding::Utf16); + + let position = document + .lsp_position_from_tree_sitter_point(Point::new(0, 4)) + .unwrap(); + assert_eq!(position, lsp_types::Position::new(0, 2)); + + let position = document + .lsp_position_from_tree_sitter_point(Point::new(0, 5)) + .unwrap(); + assert_eq!(position, lsp_types::Position::new(0, 3)); + } + + #[test] + fn test_utf8_position_roundtrip_multibyte() { + // `é` is 2 bytes + let mut document = Document::new("é\n", None); + document.position_encoding = PositionEncoding::Utf8; + + let lsp_position = lsp_types::Position::new(0, 2); + let point = document + .tree_sitter_point_from_lsp_position(lsp_position) + .unwrap(); + assert_eq!(point, Point::new(0, 2)); + + let roundtrip_position = document.lsp_position_from_tree_sitter_point(point).unwrap(); + assert_eq!(roundtrip_position, lsp_position); + } + + // After an incremental update, the AST reflects the new document contents, + // not the old ones + #[test] + fn test_incremental_update_keeps_ast_in_sync() { + let mut parser = Parser::new(); + parser + .set_language(&tree_sitter_r::LANGUAGE.into()) + .unwrap(); + + let mut document = Document::new_with_parser("", &mut parser, Some(1)); + assert_eq!(document.contents, ""); + assert_eq!(document.ast.root_node().end_position(), Point::new(0, 0)); + + // Simulate typing "lib" character by character + let changes = [ + ( + "l", + lsp_types::Range::new( + lsp_types::Position::new(0, 0), + lsp_types::Position::new(0, 0), + ), + ), + ( + "i", + lsp_types::Range::new( + lsp_types::Position::new(0, 1), + lsp_types::Position::new(0, 1), + ), + ), + ( + "b", + lsp_types::Range::new( + lsp_types::Position::new(0, 2), + lsp_types::Position::new(0, 2), + ), + ), + ]; + + for (i, (text, range)) in changes.iter().enumerate() { + let params = lsp_types::DidChangeTextDocumentParams { + text_document: lsp_types::VersionedTextDocumentIdentifier { + uri: lsp_types::Url::parse("file:///test.R").unwrap(), + version: (i + 2) as i32, + }, + content_changes: vec![lsp_types::TextDocumentContentChangeEvent { + range: Some(*range), + range_length: None, + text: text.to_string(), + }], + }; + document.on_did_change(&mut parser, ¶ms); + } + + // After typing "lib", document should contain "lib" + assert_eq!(document.contents, "lib"); + + // The AST should reflect the current contents, not be one edit behind. + // The root node should span the entire "lib" identifier. + let root = document.ast.root_node(); + assert_eq!(root.end_position(), Point::new(0, 3)); + + // Verify we can find a node at position (0, 3) which is at the end of "lib" + use crate::lsp::traits::node::NodeExt; + let node = root.find_smallest_spanning_node(Point::new(0, 3)); + assert!(node.is_some(), "Should find spanning node at end of 'lib'"); + + // The Rowan tree contains the updated document + assert_eq!(document.syntax().text_with_trivia(), "lib"); + } +} diff --git a/crates/ark/src/lsp/document_context.rs b/crates/ark/src/lsp/document_context.rs index 78e83e336..3e622cd91 100644 --- a/crates/ark/src/lsp/document_context.rs +++ b/crates/ark/src/lsp/document_context.rs @@ -8,7 +8,7 @@ use tree_sitter::Node; use tree_sitter::Point; -use crate::lsp::documents::Document; +use crate::lsp::document::Document; use crate::lsp::traits::node::NodeExt; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; @@ -98,7 +98,6 @@ impl<'a> DocumentContext<'a> { mod tests { use super::*; use crate::fixtures::point_from_cursor; - use crate::treesitter::node_text; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; @@ -109,7 +108,10 @@ mod tests { let document = Document::new(text.as_str(), None); let context = DocumentContext::new(&document, point, None); assert_eq!( - node_text(&context.node, &context.document.contents).unwrap(), + context + .node + .node_as_str(&context.document.contents) + .unwrap(), "" ); @@ -118,11 +120,31 @@ mod tests { let document = Document::new(text.as_str(), None); let context = DocumentContext::new(&document, point, None); assert_eq!( - node_text(&context.node, &context.document.contents).unwrap(), + context + .node + .node_as_str(&context.document.contents) + .unwrap(), "1" ); } + #[test] + fn test_document_context_end_of_identifier() { + // Cursor at end of identifier "lib" at position (0, 3) + // This reproduced a panic where find_smallest_spanning_node returned None + let (text, point) = point_from_cursor("lib@"); + let document = Document::new(text.as_str(), None); + let context = DocumentContext::new(&document, point, None); + // The node should be the identifier "lib" + assert_eq!( + context + .node + .node_as_str(&context.document.contents) + .unwrap(), + "lib" + ); + } + #[test] fn test_document_context_cursor_on_empty_line() { // as if we're about to type on the second line @@ -132,7 +154,10 @@ mod tests { assert_eq!(context.node.node_type(), NodeType::Program); assert_eq!( - node_text(&context.node, &context.document.contents).unwrap(), + context + .node + .node_as_str(&context.document.contents) + .unwrap(), "toupper(letters)\n" ); @@ -141,7 +166,10 @@ mod tests { NodeType::Anonymous(String::from(")")) ); assert_eq!( - node_text(&context.closest_node, &context.document.contents).unwrap(), + context + .closest_node + .node_as_str(&context.document.contents) + .unwrap(), ")" ); } diff --git a/crates/ark/src/lsp/documents.rs b/crates/ark/src/lsp/documents.rs deleted file mode 100644 index 2c838811a..000000000 --- a/crates/ark/src/lsp/documents.rs +++ /dev/null @@ -1,235 +0,0 @@ -// -// document.rs -// -// Copyright (C) 2022-2024 Posit Software, PBC. All rights reserved. -// -// - -use anyhow::*; -use ropey::Rope; -use tower_lsp::lsp_types::DidChangeTextDocumentParams; -use tower_lsp::lsp_types::TextDocumentContentChangeEvent; -use tree_sitter::InputEdit; -use tree_sitter::Parser; -use tree_sitter::Point; -use tree_sitter::Tree; - -use crate::lsp::config::DocumentConfig; -use crate::lsp::encoding::convert_lsp_range_to_tree_sitter_range; - -fn compute_point(point: Point, text: &str) -> Point { - // figure out where the newlines in this edit are - let newline_indices: Vec<_> = text.match_indices('\n').collect(); - let num_newlines = newline_indices.len(); - let num_bytes = text.as_bytes().len(); - - if newline_indices.len() == 0 { - return Point::new(point.row, point.column + num_bytes); - } else { - let last_newline_index = newline_indices.last().unwrap(); - return Point::new( - point.row + num_newlines, - num_bytes - last_newline_index.0 - 1, - ); - } -} - -#[derive(Clone)] -pub struct Document { - // The document's textual contents. - pub contents: Rope, - - // The document's AST. - pub ast: Tree, - - // The version of the document we last synchronized with. - // None if the document hasn't been synchronized yet. - pub version: Option, - - // Configuration of the document, such as indentation settings. - pub config: DocumentConfig, -} - -impl std::fmt::Debug for Document { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Document") - .field("contents", &self.contents) - .field("ast", &self.ast) - .finish() - } -} - -impl Document { - pub fn new(contents: &str, version: Option) -> Self { - // A one-shot parser, assumes the `Document` won't be incrementally reparsed. - // Useful for testing, `with_document()`, and `index_file()`. - let mut parser = Parser::new(); - parser - .set_language(&tree_sitter_r::LANGUAGE.into()) - .unwrap(); - - Self::new_with_parser(contents, &mut parser, version) - } - - pub fn new_with_parser(contents: &str, parser: &mut Parser, version: Option) -> Self { - let document = Rope::from(contents); - let ast = parser.parse(contents, None).unwrap(); - - Self { - contents: document, - version, - ast, - config: Default::default(), - } - } - - pub fn on_did_change(&mut self, parser: &mut Parser, params: &DidChangeTextDocumentParams) { - let new_version = params.text_document.version; - - // Check for out-of-order change notifications - if let Some(old_version) = self.version { - // According to the spec, versions might not be consecutive but they must be monotonically - // increasing. If that's not the case this is a hard nope as we - // can't maintain our state integrity. Currently panicking but in - // principle we should shut down the LSP in an orderly fashion. - if new_version < old_version { - panic!( - "out-of-sync change notification: currently at {old_version}, got {new_version}" - ); - } - } - - for event in ¶ms.content_changes { - if let Err(err) = self.update(parser, event) { - panic!("Failed to update document: {err:?}"); - } - } - - // Set new version - self.version = Some(new_version); - } - - fn update( - &mut self, - parser: &mut Parser, - change: &TextDocumentContentChangeEvent, - ) -> Result<()> { - // Extract edit range. Nothing to do if there wasn't an edit. - let range = match change.range { - Some(r) => r, - None => return Ok(()), - }; - - // Update the AST. We do this before updating the underlying document - // contents, because edit computations need to be done using the current - // state of the document (prior to the edit being applied) so that byte - // offsets can be computed correctly. - let ast = &mut self.ast; - - let tree_sitter::Range { - start_byte, - end_byte: old_end_byte, - start_point, - end_point: old_end_point, - } = convert_lsp_range_to_tree_sitter_range(&self.contents, range); - - let new_end_point = compute_point(start_point, &change.text); - let new_end_byte = start_byte + change.text.as_bytes().len(); - - // Confusing tree sitter names, the `start_position` is really a `Point` - let edit = InputEdit { - start_byte, - old_end_byte, - new_end_byte, - start_position: start_point, - old_end_position: old_end_point, - new_end_position: new_end_point, - }; - - ast.edit(&edit); - - // Now, apply edits to the underlying document. - // Convert from byte offsets to character offsets. - let start_character = self.contents.byte_to_char(start_byte); - let old_end_character = self.contents.byte_to_char(old_end_byte); - - // Remove the old slice of text, and insert the new slice of text. - self.contents.remove(start_character..old_end_character); - self.contents.insert(start_character, change.text.as_str()); - - // We've edited the AST, and updated the document. We can now re-parse. - let contents = &self.contents; - let callback = &mut |byte, point| Self::parse_callback(contents, byte, point); - - let ast = parser.parse_with(callback, Some(&self.ast)); - self.ast = ast.unwrap(); - - Ok(()) - } - - /// A tree-sitter `parse_with()` callback to efficiently return a slice of the - /// document in the `Rope` that tree-sitter can reparse with. - /// - /// According to the tree-sitter docs: - /// * `callback` A function that takes a byte offset and position and - /// returns a slice of UTF8-encoded text starting at that byte offset - /// and position. The slices can be of any length. If the given position - /// is at the end of the text, the callback should return an empty slice. - /// - /// We expect that tree-sitter will call the callback again with an updated `byte` - /// if the chunk doesn't contain enough text to fully reparse. - fn parse_callback(contents: &Rope, byte: usize, point: Point) -> &[u8] { - // Get Rope "chunk" that lines up with this `byte` - let Some((chunk, chunk_byte_idx, _chunk_char_idx, _chunk_line_idx)) = - contents.get_chunk_at_byte(byte) - else { - let contents = contents.to_string(); - log::error!( - "Failed to get Rope chunk at byte {byte}, point {point}. Text '{contents}'.", - ); - return "\n".as_bytes(); - }; - - // How far into this chunk are we? - let byte = byte - chunk_byte_idx; - - // Now return the slice from that `byte` to the end of the chunk. - // SAFETY: This should never panic, since `get_chunk_at_byte()` worked. - let slice = &chunk[byte..]; - - slice.as_bytes() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_point_computation() { - // empty strings shouldn't do anything - let point = compute_point(Point::new(0, 0), ""); - assert_eq!(point, Point::new(0, 0)); - - let point = compute_point(Point::new(42, 42), ""); - assert_eq!(point, Point::new(42, 42)); - - // text insertion without newlines should just extend the column position - let point = compute_point(Point::new(0, 0), "abcdef"); - assert_eq!(point, Point::new(0, 6)); - - // text insertion with newlines should change the row - let point = compute_point(Point::new(0, 0), "abc\ndef\nghi"); - assert_eq!(point, Point::new(2, 3)); - - let point = compute_point(Point::new(0, 0), "abcdefghi\n"); - assert_eq!(point, Point::new(1, 0)); - } - - #[test] - fn test_document_starts_at_0_0_with_leading_whitespace() { - let document = Document::new("\n\n# hi there", None); - let root = document.ast.root_node(); - assert_eq!(root.start_position(), Point::new(0, 0)); - } -} diff --git a/crates/ark/src/lsp/encoding.rs b/crates/ark/src/lsp/encoding.rs deleted file mode 100644 index 5a09b245a..000000000 --- a/crates/ark/src/lsp/encoding.rs +++ /dev/null @@ -1,178 +0,0 @@ -// -// encoding.rs -// -// Copyright (C) 2024 Posit Software, PBC. All rights reserved. -// -// - -use ropey::Rope; -use tower_lsp::lsp_types::Position; -use tree_sitter::Point; - -use crate::lsp::traits::rope::RopeExt; - -/// `PositionEncodingKind` describes the encoding used for the `Position` `character` -/// column offset field. The `Position` `line` field is encoding agnostic, but the -/// `character` field specifies the number of characters offset from the beginning of -/// the line, and the "character" size is dependent on the encoding. The LSP specification -/// states: -/// -/// - UTF8: Character offsets count UTF-8 code units (e.g. bytes). -/// - UTF16: Character offsets count UTF-16 code units (default). -/// - UTF32: Character offsets count UTF-32 code units (these are the same as Unicode -/// codepoints, so this `PositionEncodingKind` may also be used for an encoding-agnostic -/// representation of character offsets.) -/// -/// The `vscode-languageclient` library that Positron uses on the frontend to create the -/// `Client` side of the LSP currently ONLY supports `UTF16`, and will error on anything -/// else. Their reasoning is that it is easier for the server (ark) to do the re-encoding, -/// since we are tracking the full document state. Track support for UTF-8 here: -/// https://github.com/microsoft/vscode-languageserver-node/issues/1224 -/// -/// The other interesting part of this is that `TextDocumentContentChangeEvent`s that -/// come through the `did_change()` event and the `TextDocumentItem` that comes through -/// the `did_open()` event encode the `text` of the change/document in UTF-8, even though -/// the `Range` (in the case of `did_change()`) that tells you where to apply the change -/// uses UTF-16, so that's cool. UTF-8 `text` is forced to come through due to how the -/// LSP specification uses jsonrpc, where the content fields must be 'utf-8' encoded: -/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#contentPart -/// This at least means we have a guarantee that the document itself and any updates to -/// it will be encoded in UTF-8, even if the Positions are UTF-16. -/// -/// So we need a way to convert the UTF-16 `Position`s to UTF-8 `tree_sitter::Point`s and -/// back. This requires the document itself, and is what the helpers in this file implement. -pub fn get_position_encoding_kind() -> tower_lsp::lsp_types::PositionEncodingKind { - tower_lsp::lsp_types::PositionEncodingKind::UTF16 -} - -pub fn convert_tree_sitter_range_to_lsp_range( - x: &Rope, - range: tree_sitter::Range, -) -> tower_lsp::lsp_types::Range { - let start = convert_point_to_position(x, range.start_point); - let end = convert_point_to_position(x, range.end_point); - tower_lsp::lsp_types::Range::new(start, end) -} - -pub fn convert_lsp_range_to_tree_sitter_range( - x: &Rope, - range: tower_lsp::lsp_types::Range, -) -> tree_sitter::Range { - let start_point = convert_position_to_point(x, range.start); - let start_byte = x.point_to_byte(start_point); - - let end_point = convert_position_to_point(x, range.end); - let end_byte = x.point_to_byte(end_point); - - tree_sitter::Range { - start_byte, - end_byte, - start_point, - end_point, - } -} - -pub fn convert_position_to_point(x: &Rope, position: Position) -> Point { - let line = position.line as usize; - let character = position.character as usize; - - let character = with_line(x, line, character, convert_character_from_utf16_to_utf8); - - Point::new(line, character) -} - -pub fn convert_point_to_position(x: &Rope, point: Point) -> Position { - let line = point.row; - let character = point.column; - - let character = with_line(x, line, character, convert_character_from_utf8_to_utf16); - - let line = line as u32; - let character = character as u32; - - Position::new(line, character) -} - -fn with_line(x: &Rope, line: usize, character: usize, f: F) -> usize -where - F: FnOnce(&str, usize) -> usize, -{ - let Some(x) = x.get_line(line) else { - let n = x.len_lines(); - let x = x.to_string(); - let line = line + 1; - // Forcing a full capture so we can learn the situations in which this occurs - let trace = std::backtrace::Backtrace::force_capture(); - log::error!( - "Requesting line {line} but only {n} lines exist.\n\nDocument:\n{x}\n\nBacktrace:\n{trace}" - ); - return 0; - }; - - // If the line is fully contained in a single chunk (likely is), use free conversion to `&str` - if let Some(x) = x.as_str() { - return f(x, character); - } - - // Otherwise, use ever so slightly more expensive String materialization of the - // line spread across chunks - let x = x.to_string(); - let x = x.as_str(); - - f(x, character) -} - -/// Converts a character offset into a particular line from UTF-16 to UTF-8 -fn convert_character_from_utf16_to_utf8(x: &str, character: usize) -> usize { - if x.is_ascii() { - // Fast pass - return character; - } - - // Initial check, since loop would skip this case - if character == 0 { - return character; - } - - let mut n = 0; - - // For each `u32` sized `char`, figure out the equivalent size in UTF-16 - // world of that `char`. Once we hit the requested number of `character`s, - // that means we have indexed into `x` to the correct position, at which - // point we can take the current bytes based `pos` that marks the start of - // this `char`, and add on its UTF-8 based size to return an adjusted column - // offset. We use `==` because I'm fairly certain they should always align - // exactly, and it would be good to log if that isn't the case. - for (pos, char) in x.char_indices() { - n += char.len_utf16(); - - if n == character { - return pos + char.len_utf8(); - } - } - - log::error!("Failed to locate UTF-16 offset of {character}. Line: '{x}'."); - return 0; -} - -/// Converts a character offset into a particular line from UTF-8 to UTF-16 -fn convert_character_from_utf8_to_utf16(x: &str, character: usize) -> usize { - if x.is_ascii() { - // Fast pass - return character; - } - - // The UTF-8 -> UTF-16 case is slightly simpler. We just slice into `x` - // using our existing UTF-8 offset, reencode the slice as a UTF-16 based - // iterator, and count up the pieces. - match x.get(..character) { - Some(x) => x.encode_utf16().count(), - None => { - let n = x.len(); - log::error!( - "Tried to take UTF-8 character {character}, but only {n} characters exist. Line: '{x}'." - ); - 0 - }, - } -} diff --git a/crates/ark/src/lsp/folding_range.rs b/crates/ark/src/lsp/folding_range.rs index b58e869d6..c732be40d 100644 --- a/crates/ark/src/lsp/folding_range.rs +++ b/crates/ark/src/lsp/folding_range.rs @@ -5,7 +5,6 @@ // // -use std::borrow::Cow; use std::cmp::Ordering; use std::sync::LazyLock; @@ -15,7 +14,7 @@ use tower_lsp::lsp_types::FoldingRangeKind; use super::symbols::parse_comment_as_section; use crate::lsp; -use crate::lsp::documents::Document; +use crate::lsp::document::Document; pub fn folding_range(document: &Document) -> anyhow::Result> { let mut folding_ranges: Vec = Vec::new(); @@ -82,10 +81,7 @@ fn parse_ts_node( } // Nested comment section handling - if let Some(comment_line) = document.contents.get_line(start.row) { - // O(n) if comment overlaps rope chunks, O(1) otherwise - let comment_line: Cow<'_, str> = comment_line.into(); - + if let Some(comment_line) = document.get_line(start.row) { if let Err(err) = nested_processor(comment_stack, folding_ranges, start.row, &comment_line) { @@ -173,7 +169,7 @@ fn comment_range(start_line: usize, end_line: usize) -> FoldingRange { } fn count_leading_whitespaces(document: &Document, line_num: usize) -> usize { - let Some(line) = document.contents.get_line(line_num) else { + let Some(line) = document.get_line(line_num) else { return 0; }; @@ -365,7 +361,7 @@ fn end_node_handler( #[cfg(test)] mod tests { use super::*; - use crate::lsp::documents::Document; + use crate::lsp::document::Document; fn test_folding_range(code: &str) -> Vec { let doc = Document::new(code, None); diff --git a/crates/ark/src/lsp/handlers.rs b/crates/ark/src/lsp/handlers.rs index 9afbf7d8a..31e115ec7 100644 --- a/crates/ark/src/lsp/handlers.rs +++ b/crates/ark/src/lsp/handlers.rs @@ -7,6 +7,7 @@ use anyhow::anyhow; use serde_json::Value; +use stdext::result::ResultExt; use stdext::unwrap; use stdext::unwrap::IntoResult; use tower_lsp::lsp_types::CodeActionParams; @@ -47,8 +48,6 @@ use crate::lsp::completions::provide_completions; use crate::lsp::completions::resolve_completion; use crate::lsp::definitions::goto_definition; use crate::lsp::document_context::DocumentContext; -use crate::lsp::encoding::convert_lsp_range_to_tree_sitter_range; -use crate::lsp::encoding::convert_position_to_point; use crate::lsp::folding_range::folding_range; use crate::lsp::help_topic::help_topic; use crate::lsp::help_topic::HelpTopicParams; @@ -58,7 +57,6 @@ use crate::lsp::indent::indent_edit; use crate::lsp::input_boundaries::InputBoundariesParams; use crate::lsp::input_boundaries::InputBoundariesResponse; use crate::lsp::main_loop::LspState; -use crate::lsp::offset::IntoLspOffset; use crate::lsp::references::find_references; use crate::lsp::selection_range::convert_selection_range_from_tree_sitter_to_lsp; use crate::lsp::selection_range::selection_range; @@ -189,7 +187,7 @@ pub(crate) fn handle_completion( let document = state.get_document(&uri)?; let position = params.text_document_position.position; - let point = convert_position_to_point(&document.contents, position); + let point = document.tree_sitter_point_from_lsp_position(position)?; let trigger = params.context.and_then(|ctxt| ctxt.trigger_character); @@ -223,7 +221,7 @@ pub(crate) fn handle_hover( let document = state.get_document(&uri)?; let position = params.text_document_position_params.position; - let point = convert_position_to_point(&document.contents, position); + let point = document.tree_sitter_point_from_lsp_position(position)?; // build document context let context = DocumentContext::new(&document, point, None); @@ -258,7 +256,7 @@ pub(crate) fn handle_signature_help( let document = state.get_document(&uri)?; let position = params.text_document_position_params.position; - let point = convert_position_to_point(&document.contents, position); + let point = document.tree_sitter_point_from_lsp_position(position)?; let context = DocumentContext::new(&document, point, None); @@ -284,17 +282,9 @@ pub(crate) fn handle_goto_definition( params: GotoDefinitionParams, state: &WorldState, ) -> anyhow::Result> { - // get reference to document let uri = ¶ms.text_document_position_params.text_document.uri; let document = state.get_document(uri)?; - - // build goto definition context - let result = unwrap!(goto_definition(&document, params), Err(err) => { - lsp::log_error!("{err:?}"); - return Ok(None); - }); - - Ok(result) + Ok(goto_definition(&document, params).log_err().flatten()) } #[tracing::instrument(level = "info", skip_all)] @@ -302,20 +292,17 @@ pub(crate) fn handle_selection_range( params: SelectionRangeParams, state: &WorldState, ) -> anyhow::Result>> { - // Get reference to document - let uri = params.text_document.uri; - let document = state.get_document(&uri)?; - - let tree = &document.ast; + let document = state.get_document(¶ms.text_document.uri)?; // Get tree-sitter points to return selection ranges for - let points: Vec = params + let points: anyhow::Result> = params .positions .into_iter() - .map(|position| convert_position_to_point(&document.contents, position)) + .map(|position| document.tree_sitter_point_from_lsp_position(position)) .collect(); + let points = points?; - let Some(selections) = selection_range(tree, points) else { + let Some(selections) = selection_range(&document.ast, points) else { return Ok(None); }; @@ -323,7 +310,7 @@ pub(crate) fn handle_selection_range( let selections = selections .into_iter() .map(|selection| convert_selection_range_from_tree_sitter_to_lsp(selection, &document)) - .collect(); + .collect::>>()?; Ok(Some(selections)) } @@ -352,16 +339,9 @@ pub(crate) fn handle_statement_range( params: StatementRangeParams, state: &WorldState, ) -> anyhow::Result> { - let uri = ¶ms.text_document.uri; - let document = state.get_document(uri)?; - - let root = document.ast.root_node(); - let contents = &document.contents; - - let position = params.position; - let point = convert_position_to_point(contents, position); - - statement_range(root, contents, point) + let document = state.get_document(¶ms.text_document.uri)?; + let point = document.tree_sitter_point_from_lsp_position(params.position)?; + statement_range(document, point) } #[tracing::instrument(level = "info", skip_all)] @@ -369,13 +349,8 @@ pub(crate) fn handle_help_topic( params: HelpTopicParams, state: &WorldState, ) -> anyhow::Result> { - let uri = ¶ms.text_document.uri; - let document = state.get_document(uri)?; - let contents = &document.contents; - - let position = params.position; - let point = convert_position_to_point(contents, position); - + let document = state.get_document(¶ms.text_document.uri)?; + let point = document.tree_sitter_point_from_lsp_position(params.position)?; help_topic(point, &document) } @@ -385,17 +360,10 @@ pub(crate) fn handle_indent( state: &WorldState, ) -> anyhow::Result>> { let ctxt = params.text_document_position; - let uri = ctxt.text_document.uri; - - let doc = state.get_document(&uri)?; - let pos = ctxt.position; - let point = convert_position_to_point(&doc.contents, pos); - - let res = indent_edit(doc, point.row); + let doc = state.get_document(&ctxt.text_document.uri)?; + let point = doc.tree_sitter_point_from_lsp_position(ctxt.position)?; - Result::map(res, |opt| { - Option::map(opt, |edits| edits.into_lsp_offset(&doc.contents)) - }) + indent_edit(doc, point.row) } #[tracing::instrument(level = "info", skip_all)] @@ -406,7 +374,7 @@ pub(crate) fn handle_code_action( ) -> anyhow::Result> { let uri = params.text_document.uri; let doc = state.get_document(&uri)?; - let range = convert_lsp_range_to_tree_sitter_range(&doc.contents, params.range); + let range = doc.tree_sitter_range_from_lsp_range(params.range)?; let code_actions = code_actions(&uri, doc, range, &lsp_state.capabilities); diff --git a/crates/ark/src/lsp/help_topic.rs b/crates/ark/src/lsp/help_topic.rs index 024091257..c628ad0be 100644 --- a/crates/ark/src/lsp/help_topic.rs +++ b/crates/ark/src/lsp/help_topic.rs @@ -14,9 +14,8 @@ use tree_sitter::Point; use tree_sitter::Tree; use crate::lsp; -use crate::lsp::documents::Document; +use crate::lsp::document::Document; use crate::lsp::traits::node::NodeExt; -use crate::lsp::traits::rope::RopeExt; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; @@ -49,10 +48,7 @@ pub(crate) fn help_topic( return Ok(None); }; - // Get the text of the node - let text = document.contents.node_slice(&node)?.to_string(); - - // Form the response + let text = node.node_to_string(&document.contents)?; let response = HelpTopicResponse { topic: text }; lsp::log_info!( @@ -103,6 +99,7 @@ mod tests { use crate::fixtures::point_from_cursor; use crate::lsp::help_topic::locate_help_node; + use crate::lsp::traits::node::NodeExt; #[test] fn test_locate_help_node() { @@ -133,7 +130,7 @@ mod tests { let (text, point) = point_from_cursor(code); let tree = parser.parse(text.as_str(), None).unwrap(); let node = locate_help_node(&tree, point).unwrap(); - let text = node.utf8_text(text.as_bytes()).unwrap(); + let text = node.node_as_str(&text).unwrap(); assert_eq!(text, expected); } } diff --git a/crates/ark/src/lsp/hover.rs b/crates/ark/src/lsp/hover.rs index 59811dc2e..5dadb1f29 100644 --- a/crates/ark/src/lsp/hover.rs +++ b/crates/ark/src/lsp/hover.rs @@ -14,7 +14,7 @@ use tree_sitter::Node; use crate::lsp::document_context::DocumentContext; use crate::lsp::help::RHtmlHelp; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::traits::node::NodeExt; use crate::treesitter::NodeTypeExt; enum HoverContext { @@ -43,8 +43,8 @@ fn hover_context(node: Node, context: &DocumentContext) -> Result Result anyhow::Result>> { +pub fn indent_edit(doc: &Document, line: usize) -> anyhow::Result>> { let text = &doc.contents; let ast = &doc.ast; let config = &doc.config.indent; - // Rope counts from 1, we count from 0 - if line >= text.len_lines() { + let line_count = if text.is_empty() { + 1 + } else { + text.chars().filter(|c| *c == '\n').count() + 1 + }; + if line >= line_count { return Err(anyhow!("`line` is OOB")); } @@ -65,8 +67,11 @@ pub fn indent_edit(doc: &Document, line: usize) -> anyhow::Result anyhow::Result tree_sitter::Node { } /// Returns indent as a pair of space size and byte size -pub fn line_indent(text: &ropey::Rope, line: usize, config: &IndentationConfig) -> (usize, usize) { +pub fn line_indent(text: &str, line: usize, config: &IndentationConfig) -> (usize, usize) { let mut byte_indent = 0; let mut indent = 0; - let mut iter = text.chars_at(text.line_to_char(line)); - while let Some(next_char) = iter.next() { + let Some(line_text) = text.lines().nth(line) else { + return (0, 0); + }; + + for next_char in line_text.chars() { if next_char == ' ' { indent = indent + 1; byte_indent = byte_indent + 1; @@ -248,14 +261,25 @@ pub fn find_enclosing_brace(node: tree_sitter::Node) -> Option, doc: &mut Document) { + from_proto::apply_text_edits( + &mut doc.contents, + edits, + &mut doc.line_index, + doc.position_encoding, + ); + *doc = test_doc(&doc.contents); + } // NOTE: If we keep adding tests we might want to switch to snapshot tests @@ -298,36 +322,28 @@ mod tests { #[test] fn test_line_indent_chains() { - let mut text = String::from("foo +\n bar +\n baz + qux |>\nfoofy()"); - let doc = test_doc(&text); + let mut doc = test_doc("foo +\n bar +\n baz + qux |>\nfoofy()"); // Indenting the first two lines doesn't change the text assert_match!(indent_edit(&doc, 0), Ok(None)); assert_match!(indent_edit(&doc, 1), Ok(None)); let edit = indent_edit(&doc, 2).unwrap().unwrap(); - apply_text_edits(edit, &mut text).unwrap(); - assert_eq!( - text, - String::from("foo +\n bar +\n baz + qux |>\nfoofy()") - ); + apply_text_edits(edit, &mut doc); + assert_eq!(doc.contents, "foo +\n bar +\n baz + qux |>\nfoofy()"); let edit = indent_edit(&doc, 3).unwrap().unwrap(); - apply_text_edits(edit, &mut text).unwrap(); - assert_eq!( - text, - String::from("foo +\n bar +\n baz + qux |>\n foofy()") - ); + apply_text_edits(edit, &mut doc); + assert_eq!(doc.contents, "foo +\n bar +\n baz + qux |>\n foofy()"); } #[test] fn test_line_indent_chains_trailing_space() { - let mut text = String::from("foo +\n bar(\n x\n ) +\n baz\n "); - let doc = test_doc(&text); + let mut doc = test_doc("foo +\n bar(\n x\n ) +\n baz\n "); let edit = indent_edit(&doc, 4).unwrap().unwrap(); - apply_text_edits(edit, &mut text).unwrap(); - assert_eq!(text, String::from("foo +\n bar(\n x\n ) +\n baz\n ")); + apply_text_edits(edit, &mut doc); + assert_eq!(doc.contents, "foo +\n bar(\n x\n ) +\n baz\n "); } #[test] @@ -340,43 +356,36 @@ mod tests { #[test] fn test_line_indent_chains_deep() { - let mut text = String::from("deep()()[] +\n deep()()[]"); - let expected = String::from("deep()()[] +\n deep()()[]"); - let doc = test_doc(&text); + let mut doc = test_doc("deep()()[] +\n deep()()[]"); let edit = indent_edit(&doc, 0).unwrap(); assert!(edit.is_none()); let edit = indent_edit(&doc, 1).unwrap().unwrap(); - apply_text_edits(edit, &mut text).unwrap(); - assert_eq!(text, expected); + apply_text_edits(edit, &mut doc); + assert_eq!(doc.contents, "deep()()[] +\n deep()()[]"); } #[test] fn test_line_indent_chains_deep_newlines() { // With newlines in the way - let mut text = String::from("deep(\n)()[] +\ndeep(\n)()[]"); - let expected = String::from("deep(\n)()[] +\n deep(\n)()[]"); - let doc = test_doc(&text); + let mut doc = test_doc("deep(\n)()[] +\ndeep(\n)()[]"); let edit = indent_edit(&doc, 0).unwrap(); assert!(edit.is_none()); let edit = indent_edit(&doc, 2).unwrap().unwrap(); - apply_text_edits(edit, &mut text).unwrap(); - assert_eq!(text, expected); + apply_text_edits(edit, &mut doc); + assert_eq!(doc.contents, "deep(\n)()[] +\n deep(\n)()[]"); } #[test] fn test_line_indent_chains_calls() { - let mut text = String::from("foo() +\n bar() +\nbaz()"); - let expected = String::from("foo() +\n bar() +\n baz()"); - - let doc = test_doc(&text); + let mut doc = test_doc("foo() +\n bar() +\nbaz()"); let edit = indent_edit(&doc, 2).unwrap().unwrap(); - apply_text_edits(edit, &mut text).unwrap(); - assert_eq!(text, expected); + apply_text_edits(edit, &mut doc); + assert_eq!(doc.contents, "foo() +\n bar() +\n baz()"); // Indenting the first two lines doesn't change the text let edit = indent_edit(&doc, 0).unwrap(); @@ -392,82 +401,74 @@ mod tests { #[test] fn test_line_indent_braced_expression() { - let mut text = String::from("{\nbar\n}"); - let doc = test_doc(&text); + let mut doc = test_doc("{\nbar\n}"); let edit = indent_edit(&doc, 1).unwrap().unwrap(); - apply_text_edits(edit, &mut text).unwrap(); - assert_eq!(text, String::from("{\n bar\n}")); + apply_text_edits(edit, &mut doc); + assert_eq!(doc.contents, "{\n bar\n}"); - let mut text = String::from("function() {\nbar\n}"); - let doc = test_doc(&text); + let mut doc = test_doc("function() {\nbar\n}"); let edit = indent_edit(&doc, 1).unwrap().unwrap(); - apply_text_edits(edit, &mut text).unwrap(); - assert_eq!(text, String::from("function() {\n bar\n}")); + apply_text_edits(edit, &mut doc); + assert_eq!(doc.contents, "function() {\n bar\n}"); } #[test] fn test_line_indent_braced_expression_closing() { - let mut text = String::from("{\n }"); - let doc = test_doc(&text); + let mut doc = test_doc("{\n }"); let edit = indent_edit(&doc, 1).unwrap().unwrap(); - apply_text_edits(edit, &mut text).unwrap(); - assert_eq!(text, String::from("{\n}")); + apply_text_edits(edit, &mut doc); + assert_eq!(doc.contents, "{\n}"); } #[test] fn test_line_indent_braced_expression_closing_multiline() { // https://github.com/posit-dev/positron/issues/3484 - let mut text = String::from("{\n\n }"); - let doc = test_doc(&text); + let mut doc = test_doc("{\n\n }"); let edit = indent_edit(&doc, 1).unwrap().unwrap(); - apply_text_edits(edit, &mut text).unwrap(); - assert_eq!(text, String::from("{\n \n}")); + apply_text_edits(edit, &mut doc); + assert_eq!(doc.contents, "{\n \n}"); } #[test] fn test_line_indent_braced_expression_multiline() { - let mut text = String::from("function(\n ) {\nfoo\n}"); - let doc = test_doc(&text); + let mut doc = test_doc("function(\n ) {\nfoo\n}"); let edit = indent_edit(&doc, 2).unwrap().unwrap(); - apply_text_edits(edit, &mut text).unwrap(); - assert_eq!(text, String::from("function(\n ) {\n foo\n}")); + apply_text_edits(edit, &mut doc); + assert_eq!(doc.contents, "function(\n ) {\n foo\n}"); } #[test] fn test_line_indent_braced_expression_multiline_empty() { - let mut text = String::from("function(\n ) {\n\n}"); - let doc = test_doc(&text); + let mut doc = test_doc("function(\n ) {\n\n}"); let edit = indent_edit(&doc, 2).unwrap().unwrap(); - apply_text_edits(edit, &mut text).unwrap(); - assert_eq!(text, String::from("function(\n ) {\n \n}")); + apply_text_edits(edit, &mut doc); + assert_eq!(doc.contents, "function(\n ) {\n \n}"); } #[test] fn test_line_indent_minimum() { // https://github.com/posit-dev/positron/issues/1683 - let mut text = String::from("function() {\n ({\n }\n)\n}"); - let doc = test_doc(&text); + let mut doc = test_doc("function() {\n ({\n }\n)\n}"); let edit = indent_edit(&doc, 3).unwrap().unwrap(); - apply_text_edits(edit, &mut text).unwrap(); - assert_eq!(text, String::from("function() {\n ({\n }\n )\n}")); + apply_text_edits(edit, &mut doc); + assert_eq!(doc.contents, "function() {\n ({\n }\n )\n}"); } #[test] fn test_line_indent_minimum_nested() { // Nested R function test with multiple levels of nesting - let mut text = String::from("{\n {\n ({\n }\n )\n }\n}"); - let doc = test_doc(&text); + let mut doc = test_doc("{\n {\n ({\n }\n )\n }\n}"); let edit = indent_edit(&doc, 4).unwrap().unwrap(); - apply_text_edits(edit, &mut text).unwrap(); - assert_eq!(text, String::from("{\n {\n ({\n }\n )\n }\n}")); + apply_text_edits(edit, &mut doc); + assert_eq!(doc.contents, "{\n {\n ({\n }\n )\n }\n}"); } #[test] @@ -526,20 +527,19 @@ mod tests { fn test_indent_snapshot() { let orig = read_text_asset("lsp/snapshots/indent.R"); - let doc = test_doc(&orig); + let mut doc = test_doc(&orig); - let mut text = orig.clone(); - let n_lines = text.matches('\n').count(); + let n_lines = doc.contents.matches('\n').count(); for i in 0..n_lines { if let Some(edit) = indent_edit(&doc, i).unwrap() { - apply_text_edits(edit, &mut text).unwrap(); + apply_text_edits(edit, &mut doc); } } - write_asset("lsp/snapshots/indent.R", &text); + write_asset("lsp/snapshots/indent.R", &doc.contents); - if orig != text { + if orig != doc.contents { panic!("Indentation snapshots have changed.\nPlease see git diff."); } } diff --git a/crates/ark/src/lsp/indexer.rs b/crates/ark/src/lsp/indexer.rs index b2d0580be..adeb2120e 100644 --- a/crates/ark/src/lsp/indexer.rs +++ b/crates/ark/src/lsp/indexer.rs @@ -12,7 +12,6 @@ use std::sync::LazyLock; use std::sync::Mutex; use regex::Regex; -use ropey::Rope; use stdext::unwrap; use stdext::unwrap::IntoResult; use tower_lsp::lsp_types::Range; @@ -23,9 +22,8 @@ use walkdir::DirEntry; use walkdir::WalkDir; use crate::lsp; -use crate::lsp::documents::Document; -use crate::lsp::encoding::convert_point_to_position; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::document::Document; +use crate::lsp::traits::node::NodeExt; use crate::treesitter::BinaryOperatorType; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; @@ -284,15 +282,14 @@ pub(crate) fn create(uri: &Url) -> anyhow::Result<()> { Ok(()) } -fn index_document(document: &Document, uri: &Url) { - let ast = &document.ast; - let contents = &document.contents; +fn index_document(doc: &Document, uri: &Url) { + let ast = &doc.ast; let root = ast.root_node(); let mut cursor = root.walk(); let mut entries = Vec::new(); for node in root.children(&mut cursor) { - if let Err(err) = index_node(uri, contents, &node, &mut entries) { + if let Err(err) = index_node(doc, &node, &mut entries) { lsp::log_error!("Can't index document: {err:?}"); } } @@ -304,20 +301,14 @@ fn index_document(document: &Document, uri: &Url) { } } -fn index_node( - uri: &Url, - contents: &Rope, - node: &Node, - entries: &mut Vec, -) -> anyhow::Result<()> { - index_assignment(uri, contents, node, entries)?; - index_comment(uri, contents, node, entries)?; +fn index_node(doc: &Document, node: &Node, entries: &mut Vec) -> anyhow::Result<()> { + index_assignment(doc, node, entries)?; + index_comment(doc, node, entries)?; Ok(()) } fn index_assignment( - uri: &Url, - contents: &Rope, + doc: &Document, node: &Node, entries: &mut Vec, ) -> anyhow::Result<()> { @@ -338,14 +329,14 @@ fn index_assignment( return Ok(()); }; - if crate::treesitter::node_is_call(&rhs, "R6Class", contents) || - crate::treesitter::node_is_namespaced_call(&rhs, "R6", "R6Class", contents) + if crate::treesitter::node_is_call(&rhs, "R6Class", &doc.contents) || + crate::treesitter::node_is_namespaced_call(&rhs, "R6", "R6Class", &doc.contents) { - index_r6_class_methods(uri, contents, &rhs, entries)?; + index_r6_class_methods(doc, &rhs, entries)?; // Fallthrough to index the variable to which the R6 class is assigned } - let lhs_text = contents.node_slice(&lhs)?.to_string(); + let lhs_text = lhs.node_to_string(&doc.contents)?; // The method matching is super hacky but let's wait until the typed API to // do better @@ -365,7 +356,7 @@ fn index_assignment( for child in parameters.children(&mut cursor) { let name = unwrap!(child.child_by_field_name("name"), None => continue); if name.is_identifier() { - let name = contents.node_slice(&name)?.to_string(); + let name = name.node_to_string(&doc.contents)?; arguments.push(name); } } @@ -373,8 +364,8 @@ fn index_assignment( // Note that unlike document symbols whose ranges cover the whole entity // they represent, the range of workspace symbols only cover the identifers - let start = convert_point_to_position(contents, lhs.start_position()); - let end = convert_point_to_position(contents, lhs.end_position()); + let start = doc.lsp_position_from_tree_sitter_point(lhs.start_position())?; + let end = doc.lsp_position_from_tree_sitter_point(lhs.end_position())?; entries.push(IndexEntry { key: lhs_text.clone(), @@ -386,8 +377,8 @@ fn index_assignment( }); } else { // Otherwise, emit variable - let start = convert_point_to_position(contents, lhs.start_position()); - let end = convert_point_to_position(contents, lhs.end_position()); + let start = doc.lsp_position_from_tree_sitter_point(lhs.start_position())?; + let end = doc.lsp_position_from_tree_sitter_point(lhs.end_position())?; entries.push(IndexEntry { key: lhs_text.clone(), range: Range { start, end }, @@ -399,8 +390,7 @@ fn index_assignment( } fn index_r6_class_methods( - _uri: &Url, - contents: &Rope, + doc: &Document, node: &Node, entries: &mut Vec, ) -> anyhow::Result<()> { @@ -427,14 +417,10 @@ fn index_r6_class_methods( }); let mut ts_query = TsQuery::from_query(&*R6_METHODS_QUERY); - // We'll switch from Rope to String in the near future so let's not - // worry about this conversion now - let contents_str = contents.to_string(); - - for method_node in ts_query.captures_for(*node, "method_name", contents_str.as_bytes()) { - let name = contents.node_slice(&method_node)?.to_string(); - let start = convert_point_to_position(contents, method_node.start_position()); - let end = convert_point_to_position(contents, method_node.end_position()); + for method_node in ts_query.captures_for(*node, "method_name", doc.contents.as_bytes()) { + let name = method_node.node_to_string(&doc.contents)?; + let start = doc.lsp_position_from_tree_sitter_point(method_node.start_position())?; + let end = doc.lsp_position_from_tree_sitter_point(method_node.end_position())?; entries.push(IndexEntry { key: name.clone(), @@ -446,20 +432,15 @@ fn index_r6_class_methods( Ok(()) } -fn index_comment( - _uri: &Url, - contents: &Rope, - node: &Node, - entries: &mut Vec, -) -> anyhow::Result<()> { +fn index_comment(doc: &Document, node: &Node, entries: &mut Vec) -> anyhow::Result<()> { // check for comment if !node.is_comment() { return Ok(()); } // see if it looks like a section - let comment = contents.node_slice(node)?.to_string(); - let matches = match RE_COMMENT_SECTION.captures(comment.as_str()) { + let comment = node.node_as_str(&doc.contents)?; + let matches = match RE_COMMENT_SECTION.captures(comment) { Some(m) => m, None => return Ok(()), }; @@ -475,8 +456,8 @@ fn index_comment( return Ok(()); } - let start = convert_point_to_position(contents, node.start_position()); - let end = convert_point_to_position(contents, node.end_position()); + let start = doc.lsp_position_from_tree_sitter_point(node.start_position())?; + let end = doc.lsp_position_from_tree_sitter_point(node.end_position())?; entries.push(IndexEntry { key: title.clone(), @@ -495,19 +476,17 @@ mod tests { use tower_lsp::lsp_types; use super::*; - use crate::lsp::documents::Document; - use crate::lsp::util::test_path; + use crate::lsp::document::Document; macro_rules! test_index { ($code:expr) => { let doc = Document::new($code, None); - let uri = test_path("/path/to/file.R"); let root = doc.ast.root_node(); let mut cursor = root.walk(); let mut entries = vec![]; for node in root.children(&mut cursor) { - let _ = index_node(&uri, &doc.contents, &node, &mut entries); + let _ = index_node(&doc, &node, &mut entries); } assert_debug_snapshot!(entries); }; diff --git a/crates/ark/src/lsp/inputs/package_namespace.rs b/crates/ark/src/lsp/inputs/package_namespace.rs index d40d36d9c..b2a8c7999 100644 --- a/crates/ark/src/lsp/inputs/package_namespace.rs +++ b/crates/ark/src/lsp/inputs/package_namespace.rs @@ -9,6 +9,7 @@ use std::sync::LazyLock; use tree_sitter::Parser; use tree_sitter::Query; +use crate::lsp::traits::node::NodeExt; use crate::treesitter::TsQuery; /// Parsed NAMESPACE file @@ -72,11 +73,7 @@ impl Namespace { let as_strings = |nodes: &Vec| { nodes .iter() - .map(|node| { - node.utf8_text(contents.as_bytes()) - .unwrap_or("") - .to_string() - }) + .map(|node| node.node_as_str(&contents).unwrap_or("").to_string()) .collect::>() }; diff --git a/crates/ark/src/lsp/main_loop.rs b/crates/ark/src/lsp/main_loop.rs index 2e6014b0c..4347c1013 100644 --- a/crates/ark/src/lsp/main_loop.rs +++ b/crates/ark/src/lsp/main_loop.rs @@ -36,7 +36,7 @@ use crate::lsp::backend::LspRequest; use crate::lsp::backend::LspResponse; use crate::lsp::capabilities::Capabilities; use crate::lsp::diagnostics::generate_diagnostics; -use crate::lsp::documents::Document; +use crate::lsp::document::Document; use crate::lsp::handlers; use crate::lsp::indexer; use crate::lsp::inputs::library::Library; diff --git a/crates/ark/src/lsp/mod.rs b/crates/ark/src/lsp/mod.rs index 4e6f1f9e9..27ebc4d3c 100644 --- a/crates/ark/src/lsp/mod.rs +++ b/crates/ark/src/lsp/mod.rs @@ -16,8 +16,7 @@ pub mod definitions; pub mod diagnostics; pub mod diagnostics_syntax; pub mod document_context; -pub mod documents; -pub mod encoding; +pub mod document; pub mod events; pub mod folding_range; pub mod handler; @@ -31,7 +30,7 @@ pub mod input_boundaries; pub mod inputs; pub mod main_loop; pub mod markdown; -pub mod offset; + pub mod references; pub mod selection_range; pub mod signature_help; diff --git a/crates/ark/src/lsp/offset.rs b/crates/ark/src/lsp/offset.rs deleted file mode 100644 index 6d8777820..000000000 --- a/crates/ark/src/lsp/offset.rs +++ /dev/null @@ -1,195 +0,0 @@ -// -// offset.rs -// -// Copyright (C) 2024 Posit Software, PBC. All rights reserved. -// -// - -// UTF-8-based types for internal usage Currently uses TS types -// TODO: Consider using https://github.com/rust-analyzer/text-size/ - -use anyhow::anyhow; -use tower_lsp::lsp_types; -pub use tree_sitter::Point as ArkPoint; - -use crate::lsp::encoding::convert_point_to_position; - -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct ArkRange { - pub start: ArkPoint, - pub end: ArkPoint, -} - -impl From for ArkRange { - fn from(value: harp::srcref::SrcRef) -> Self { - ArkRange { - start: ArkPoint { - row: value.line.start as usize, - column: value.column.start as usize, - }, - end: ArkPoint { - row: value.line.end as usize, - column: value.column.end as usize, - }, - } - } -} - -/// Like `TextEdit` from the lsp_types crate, but doen't expect positions to be -/// encoded in UTF-16. -#[derive(Clone, Debug)] -pub struct ArkTextEdit { - pub range: ArkRange, - pub new_text: String, -} - -pub trait FromArkOffset: Sized { - fn from_ark_offset(text: &ropey::Rope, value: T) -> Self; -} - -pub trait IntoLspOffset: Sized { - fn into_lsp_offset(self, text: &ropey::Rope) -> T; -} - -impl IntoLspOffset for T -where - U: FromArkOffset, -{ - fn into_lsp_offset(self, text: &ropey::Rope) -> U { - U::from_ark_offset(text, self) - } -} - -impl FromArkOffset for lsp_types::Position { - fn from_ark_offset(text: &ropey::Rope, value: ArkPoint) -> lsp_types::Position { - let point = tree_sitter::Point { - row: value.row, - column: value.column, - }; - convert_point_to_position(text, point) - } -} - -impl FromArkOffset for lsp_types::Range { - fn from_ark_offset(text: &ropey::Rope, value: ArkRange) -> lsp_types::Range { - lsp_types::Range { - start: value.start.into_lsp_offset(text), - end: value.end.into_lsp_offset(text), - } - } -} - -impl FromArkOffset for lsp_types::TextEdit { - fn from_ark_offset(text: &ropey::Rope, value: ArkTextEdit) -> lsp_types::TextEdit { - lsp_types::TextEdit { - range: value.range.into_lsp_offset(text), - new_text: value.new_text, - } - } -} - -impl FromArkOffset> for Vec { - fn from_ark_offset(text: &ropey::Rope, value: Vec) -> Vec { - value - .into_iter() - .map(|edit| edit.into_lsp_offset(text)) - .collect() - } -} - -/// Apply text edits to a string -/// -/// The edits are applied in order as documented in the LSP protocol. -/// A good strategy is to sort them from bottom to top. -pub fn apply_text_edits(edits: Vec, text: &mut String) -> anyhow::Result<()> { - for edit in edits { - let Some(start) = point_as_offset(text, edit.range.start) else { - return Err(anyhow!("Can't apply edit {edit:?} because start is OOB")); - }; - let Some(end) = point_as_offset(text, edit.range.end) else { - return Err(anyhow!("Can't apply edit {edit:?} because end is OOB")); - }; - - text.replace_range(start..end, &edit.new_text) - } - Ok(()) -} - -fn point_as_offset(text: &str, point: ArkPoint) -> Option { - line_offset(text, point.row).map(|offset| offset + point.column) -} - -fn line_offset(text: &str, line: usize) -> Option { - if line == 0 { - return Some(0); - } - - text.chars() - .enumerate() - .filter(|(_, c)| *c == '\n') - .skip(line - 1) - .next() - .map(|res| res.0 + 1) -} - -#[cfg(test)] -mod tests { - use stdext::assert_match; - - use crate::lsp::offset::apply_text_edits; - use crate::lsp::offset::line_offset; - use crate::lsp::offset::ArkPoint; - use crate::lsp::offset::ArkRange; - use crate::lsp::offset::ArkTextEdit; - - #[test] - fn test_apply_edit() { - let edits = vec![ - ArkTextEdit { - range: ArkRange { - start: ArkPoint { row: 1, column: 0 }, - end: ArkPoint { row: 1, column: 3 }, - }, - new_text: String::from("qux"), - }, - ArkTextEdit { - range: ArkRange { - start: ArkPoint { row: 0, column: 3 }, - end: ArkPoint { row: 0, column: 4 }, - }, - new_text: String::from(""), - }, - ]; - - let mut text = String::from("foo bar\nbaz"); - assert!(apply_text_edits(edits, &mut text).is_ok()); - assert_eq!(text, String::from("foobar\nqux")); - - let edits = vec![ArkTextEdit { - range: ArkRange { - start: ArkPoint { row: 1, column: 0 }, - end: ArkPoint { row: 1, column: 3 }, - }, - new_text: String::from("qux"), - }]; - - let mut text = String::from("foo"); - assert_match!(apply_text_edits(edits, &mut text), Err(_)); - } - - #[test] - fn test_line_pos() { - assert_eq!(line_offset("", 0), Some(0)); - assert_eq!(line_offset("", 1), None); - - assert_eq!(line_offset("\n", 0), Some(0)); - assert_eq!(line_offset("\n", 1), Some(1)); - assert_eq!(line_offset("\n", 2), None); - - let text = "foo\nquux\nbaz"; - assert_eq!(line_offset(text, 0), Some(0)); - assert_eq!(line_offset(text, 1), Some(4)); - assert_eq!(line_offset(text, 2), Some(9)); - assert_eq!(line_offset(text, 3), None); - } -} diff --git a/crates/ark/src/lsp/references.rs b/crates/ark/src/lsp/references.rs index 603d46cfc..dcdaf4fd6 100644 --- a/crates/ark/src/lsp/references.rs +++ b/crates/ark/src/lsp/references.rs @@ -8,7 +8,7 @@ use std::path::Path; use anyhow::anyhow; -use ropey::Rope; +use stdext::result::ResultExt; use stdext::unwrap::IntoResult; use stdext::*; use tower_lsp::lsp_types::Location; @@ -21,14 +21,12 @@ use tree_sitter::Point; use walkdir::WalkDir; use crate::lsp; -use crate::lsp::documents::Document; -use crate::lsp::encoding::convert_point_to_position; -use crate::lsp::encoding::convert_position_to_point; +use crate::lsp::document::Document; use crate::lsp::indexer::filter_entry; use crate::lsp::state::with_document; use crate::lsp::state::WorldState; use crate::lsp::traits::cursor::TreeCursorExt; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::traits::node::NodeExt; use crate::lsp::traits::url::UrlExt; use crate::treesitter::ExtractOperatorType; use crate::treesitter::NodeType; @@ -75,23 +73,29 @@ struct Context { symbol: String, } -fn add_reference(node: &Node, contents: &Rope, path: &Path, locations: &mut Vec) { - let start = convert_point_to_position(contents, node.start_position()); - let end = convert_point_to_position(contents, node.end_position()); +fn add_reference( + node: &Node, + document: &Document, + path: &Path, + locations: &mut Vec, +) -> anyhow::Result<()> { + let start = document.lsp_position_from_tree_sitter_point(node.start_position())?; + let end = document.lsp_position_from_tree_sitter_point(node.end_position())?; let location = Location::new( Url::from_file_path(path).expect("valid path"), Range::new(start, end), ); locations.push(location); + Ok(()) } -fn found_match(node: &Node, contents: &Rope, context: &Context) -> bool { +fn found_match(node: &Node, contents: &str, context: &Context) -> bool { if !node.is_identifier() { return false; } - let symbol = contents.node_slice(node).unwrap().to_string(); + let symbol = NodeExt::node_to_string(node, contents).unwrap(); if symbol != context.symbol { return false; } @@ -106,8 +110,8 @@ fn build_context(uri: &Url, position: Position, state: &WorldState) -> anyhow::R // Figure out the identifier we're looking for. let context = with_document(path.as_path(), state, |document| { let ast = &document.ast; - let contents = &document.contents; - let point = convert_position_to_point(contents, position); + let contents = document.contents.as_str(); + let point = document.tree_sitter_point_from_lsp_position(position)?; let mut node = ast .root_node() @@ -140,7 +144,7 @@ fn build_context(uri: &Url, position: Position, state: &WorldState) -> anyhow::R let kind = node_reference_kind(&node); // return identifier text contents - let symbol = document.contents.node_slice(&node)?.to_string(); + let symbol = node.node_to_string(contents)?; Ok(Context { kind, symbol }) }); @@ -165,8 +169,7 @@ fn find_references_in_folder( lsp::log_info!("found R file {}", path.display()); let result = with_document(path, state, |document| { - find_references_in_document(context, path, document, locations); - return Ok(()); + find_references_in_document(context, path, document, locations) }); match result { @@ -184,18 +187,19 @@ fn find_references_in_document( path: &Path, document: &Document, locations: &mut Vec, -) { +) -> anyhow::Result<()> { let ast = &document.ast; - let contents = &document.contents; + let contents = document.contents.as_str(); let mut cursor = ast.walk(); cursor.recurse(|node| { if found_match(&node, contents, &context) { - add_reference(&node, contents, path, locations); + add_reference(&node, document, path, locations).log_err(); } return true; }); + Ok(()) } pub(crate) fn find_references( diff --git a/crates/ark/src/lsp/selection_range.rs b/crates/ark/src/lsp/selection_range.rs index 26371996b..31f49ada5 100644 --- a/crates/ark/src/lsp/selection_range.rs +++ b/crates/ark/src/lsp/selection_range.rs @@ -5,12 +5,12 @@ // // +use tower_lsp::lsp_types; use tree_sitter::Node; use tree_sitter::Point; use tree_sitter::Range; use tree_sitter::Tree; -use crate::lsp::encoding::convert_tree_sitter_range_to_lsp_range; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; @@ -105,17 +105,20 @@ fn range_default(node: Node) -> Range { pub fn convert_selection_range_from_tree_sitter_to_lsp( selection: SelectionRange, - document: &crate::lsp::documents::Document, -) -> tower_lsp::lsp_types::SelectionRange { - let range = convert_tree_sitter_range_to_lsp_range(&document.contents, selection.range); + document: &crate::lsp::document::Document, +) -> anyhow::Result { + let range = document.lsp_range_from_tree_sitter_range(selection.range)?; // If there is a parent, convert it and box it - let parent = selection.parent.and_then(|selection| { - let selection = convert_selection_range_from_tree_sitter_to_lsp(*selection, document); - Some(Box::new(selection)) - }); + let parent = match selection.parent { + Some(selection) => { + let selection = convert_selection_range_from_tree_sitter_to_lsp(*selection, document)?; + Some(Box::new(selection)) + }, + None => None, + }; - tower_lsp::lsp_types::SelectionRange { range, parent } + Ok(lsp_types::SelectionRange { range, parent }) } #[cfg(test)] diff --git a/crates/ark/src/lsp/signature_help.rs b/crates/ark/src/lsp/signature_help.rs index 2e9960a5c..111baabd4 100644 --- a/crates/ark/src/lsp/signature_help.rs +++ b/crates/ark/src/lsp/signature_help.rs @@ -31,7 +31,6 @@ use crate::lsp::document_context::DocumentContext; use crate::lsp::help::RHtmlHelp; use crate::lsp::traits::node::NodeExt; use crate::lsp::traits::point::PointExt; -use crate::lsp::traits::rope::RopeExt; use crate::treesitter::NodeType; use crate::treesitter::NodeTypeExt; @@ -107,7 +106,7 @@ pub(crate) fn r_signature_help(context: &DocumentContext) -> anyhow::Result anyhow::Result anyhow::Result anyhow::Result = Lazy::new(|| Regex::new(r"^#+'").unwrap()); pub(crate) fn statement_range( - root: tree_sitter::Node, - contents: &ropey::Rope, + document: &Document, point: Point, ) -> anyhow::Result> { + let root = document.ast.root_node(); + let contents = &document.contents; + // Initial check to see if we are in a roxygen2 comment, in which case we parse a // subdocument containing the `@examples` or `@examplesIf` section and locate a // statement range within that to execute. The returned `code` represents the // statement range's code stripped of `#'` tokens so it is runnable. if let Some((range, code)) = find_roxygen_statement_range(&root, contents, point) { - return Ok(Some(new_statement_range_response(range, contents, code))); + return Ok(Some(new_statement_range_response(range, document, code)?)); } if let Some(range) = find_statement_range(&root, point.row) { - return Ok(Some(new_statement_range_response(range, contents, None))); + return Ok(Some(new_statement_range_response(range, document, None)?)); }; Ok(None) @@ -74,25 +74,20 @@ pub(crate) fn statement_range( fn new_statement_range_response( range: tree_sitter::Range, - contents: &Rope, + document: &Document, code: Option, -) -> StatementRangeResponse { - // Tree-sitter `Point`s - let start = range.start_point; - let end = range.end_point; - - // To LSP `Position`s - let start = convert_point_to_position(contents, start); - let end = convert_point_to_position(contents, end); +) -> anyhow::Result { + // Tree-sitter `Point`s to LSP `Position`s + let start = document.lsp_position_from_tree_sitter_point(range.start_point)?; + let end = document.lsp_position_from_tree_sitter_point(range.end_point)?; let range = lsp_types::Range { start, end }; - - StatementRangeResponse { range, code } + Ok(StatementRangeResponse { range, code }) } fn find_roxygen_statement_range( root: &Node, - contents: &Rope, + contents: &str, point: Point, ) -> Option<(tree_sitter::Range, Option)> { // Refuse to look for roxygen comments in the face of parse errors @@ -129,14 +124,14 @@ fn find_roxygen_statement_range( None } -fn as_roxygen_comment_text(node: &Node, contents: &Rope) -> Option { +fn as_roxygen_comment_text(node: &Node, contents: &str) -> Option { // Tree sitter doesn't know about the special `#'` marker, // but does tell us if we are in a `#` comment if !node.is_comment() { return None; } - let text = contents.node_slice(node).unwrap().to_string(); + let text = NodeExt::node_to_string(node, contents).ok()?; // Does the roxygen2 prefix exist? if !RE_ROXYGEN2_COMMENT.is_match(&text) { @@ -146,7 +141,7 @@ fn as_roxygen_comment_text(node: &Node, contents: &Rope) -> Option { Some(text) } -fn find_roxygen_examples_section(node: Node, contents: &Rope) -> Option { +fn find_roxygen_examples_section(node: Node, contents: &str) -> Option { // Check that the `node` we start on is a valid roxygen comment line. // We check this `node` specially because the loops below start on the previous/next // sibling, and this one would go unchecked. @@ -176,7 +171,7 @@ fn find_roxygen_examples_section(node: Node, contents: &Rope) -> Option Option Option<(tree_sitter::Range, String)> { // Anchor row that we adjust relative to let row_adjustment = range.start_point.row; // Slice out the `@examples` or `@examplesIf` code block (with leading roxygen comments) - let Some(slice) = contents.get_byte_slice(range.start_byte..range.end_byte) else { + let Some(slice) = contents.get(range.start_byte..range.end_byte) else { return None; }; @@ -293,7 +288,7 @@ fn find_roxygen_examples_range( // Slice out code to execute from the subdocument let Some(slice) = subdocument .contents - .get_byte_slice(subdocument_range.start_byte..subdocument_range.end_byte) + .get(subdocument_range.start_byte..subdocument_range.end_byte) else { return None; }; @@ -703,12 +698,11 @@ fn contains_row_at_different_start_position(node: Node, row: usize) -> Option String { + fn get_text(range: tree_sitter::Range, contents: &str) -> String { contents - .byte_slice(range.start_byte..range.end_byte) + .get(range.start_byte..range.end_byte) + .unwrap() .to_string() } diff --git a/crates/ark/src/lsp/symbols.rs b/crates/ark/src/lsp/symbols.rs index 822a4491c..8799d352e 100644 --- a/crates/ark/src/lsp/symbols.rs +++ b/crates/ark/src/lsp/symbols.rs @@ -9,7 +9,6 @@ use std::result::Result::Ok; -use ropey::Rope; use stdext::unwrap::IntoResult; use tower_lsp::lsp_types::DocumentSymbol; use tower_lsp::lsp_types::DocumentSymbolParams; @@ -20,11 +19,11 @@ use tower_lsp::lsp_types::SymbolKind; use tower_lsp::lsp_types::WorkspaceSymbolParams; use tree_sitter::Node; -use crate::lsp::encoding::convert_point_to_position; +use crate::lsp::document::Document; use crate::lsp::indexer; use crate::lsp::indexer::IndexEntryData; use crate::lsp::state::WorldState; -use crate::lsp::traits::rope::RopeExt; +use crate::lsp::traits::node::NodeExt; use crate::lsp::traits::string::StringExt; use crate::treesitter::point_end_of_previous_row; use crate::treesitter::BinaryOperatorType; @@ -160,9 +159,8 @@ pub(crate) fn document_symbols( params: &DocumentSymbolParams, ) -> anyhow::Result> { let uri = ¶ms.text_document.uri; - let document = state.documents.get(uri).into_result()?; - let ast = &document.ast; - let contents = &document.contents; + let doc = state.documents.get(uri).into_result()?; + let ast = &doc.ast; // Start walking from the root node let root_node = ast.root_node(); @@ -172,7 +170,7 @@ pub(crate) fn document_symbols( ctx.include_assignments_in_blocks = state.config.symbols.include_assignments_in_blocks; // Extract and process all symbols from the AST - if let Err(err) = collect_symbols(&mut ctx, &root_node, contents, &mut result) { + if let Err(err) = collect_symbols(&mut ctx, &root_node, doc, &mut result) { log::error!("Failed to collect symbols: {err:?}"); return Ok(Vec::new()); } @@ -184,16 +182,16 @@ pub(crate) fn document_symbols( fn collect_symbols( ctx: &mut CollectContext, node: &Node, - contents: &Rope, + doc: &Document, symbols: &mut Vec, ) -> anyhow::Result<()> { match node.node_type() { NodeType::Program => { - collect_list_sections(ctx, node, contents, symbols)?; + collect_list_sections(ctx, node, doc, symbols)?; }, NodeType::BracedExpression => { - collect_list_sections(ctx, node, contents, symbols)?; + collect_list_sections(ctx, node, doc, symbols)?; }, NodeType::IfStatement => { @@ -203,40 +201,39 @@ fn collect_symbols( // } else { // x <- top_level_assignment // } - collect_if_statement(ctx, node, contents, symbols)?; + collect_if_statement(ctx, node, doc, symbols)?; }, NodeType::BinaryOperator(BinaryOperatorType::LeftAssignment) | NodeType::BinaryOperator(BinaryOperatorType::EqualsAssignment) => { - collect_assignment(ctx, node, contents, symbols)?; + collect_assignment(ctx, node, doc, symbols)?; }, NodeType::ForStatement => { - collect_for_statement(ctx, node, contents, symbols)?; + collect_for_statement(ctx, node, doc, symbols)?; }, NodeType::WhileStatement => { - collect_while_statement(ctx, node, contents, symbols)?; + collect_while_statement(ctx, node, doc, symbols)?; }, NodeType::RepeatStatement => { - collect_repeat_statement(ctx, node, contents, symbols)?; + collect_repeat_statement(ctx, node, doc, symbols)?; }, NodeType::Call => { let old = ctx.top_level; ctx.top_level = false; - collect_call(ctx, node, contents, symbols)?; + collect_call(ctx, node, doc, symbols)?; ctx.top_level = old; }, NodeType::FunctionDefinition => { let old = ctx.top_level; ctx.top_level = false; - collect_function(ctx, node, contents, symbols)?; + collect_function(ctx, node, doc, symbols)?; ctx.top_level = old; }, - // For all other node types, no symbols need to be added _ => {}, } @@ -247,17 +244,17 @@ fn collect_symbols( fn collect_if_statement( ctx: &mut CollectContext, node: &Node, - contents: &Rope, + doc: &Document, symbols: &mut Vec, ) -> anyhow::Result<()> { if let Some(condition) = node.child_by_field_name("condition") { - collect_symbols(ctx, &condition, contents, symbols)?; + collect_symbols(ctx, &condition, doc, symbols)?; } if let Some(consequent) = node.child_by_field_name("consequence") { - collect_symbols(ctx, &consequent, contents, symbols)?; + collect_symbols(ctx, &consequent, doc, symbols)?; } if let Some(alternative) = node.child_by_field_name("alternative") { - collect_symbols(ctx, &alternative, contents, symbols)?; + collect_symbols(ctx, &alternative, doc, symbols)?; } Ok(()) @@ -266,17 +263,17 @@ fn collect_if_statement( fn collect_for_statement( ctx: &mut CollectContext, node: &Node, - contents: &Rope, + doc: &Document, symbols: &mut Vec, ) -> anyhow::Result<()> { if let Some(variable) = node.child_by_field_name("variable") { - collect_symbols(ctx, &variable, contents, symbols)?; + collect_symbols(ctx, &variable, doc, symbols)?; } if let Some(iterator) = node.child_by_field_name("iterator") { - collect_symbols(ctx, &iterator, contents, symbols)?; + collect_symbols(ctx, &iterator, doc, symbols)?; } if let Some(body) = node.child_by_field_name("body") { - collect_symbols(ctx, &body, contents, symbols)?; + collect_symbols(ctx, &body, doc, symbols)?; } Ok(()) @@ -285,14 +282,14 @@ fn collect_for_statement( fn collect_while_statement( ctx: &mut CollectContext, node: &Node, - contents: &Rope, + doc: &Document, symbols: &mut Vec, ) -> anyhow::Result<()> { if let Some(condition) = node.child_by_field_name("condition") { - collect_symbols(ctx, &condition, contents, symbols)?; + collect_symbols(ctx, &condition, doc, symbols)?; } if let Some(body) = node.child_by_field_name("body") { - collect_symbols(ctx, &body, contents, symbols)?; + collect_symbols(ctx, &body, doc, symbols)?; } Ok(()) @@ -301,11 +298,11 @@ fn collect_while_statement( fn collect_repeat_statement( ctx: &mut CollectContext, node: &Node, - contents: &Rope, + doc: &Document, symbols: &mut Vec, ) -> anyhow::Result<()> { if let Some(body) = node.child_by_field_name("body") { - collect_symbols(ctx, &body, contents, symbols)?; + collect_symbols(ctx, &body, doc, symbols)?; } Ok(()) @@ -314,14 +311,14 @@ fn collect_repeat_statement( fn collect_function( ctx: &mut CollectContext, node: &Node, - contents: &Rope, + doc: &Document, symbols: &mut Vec, ) -> anyhow::Result<()> { if let Some(parameters) = node.child_by_field_name("parameters") { - collect_function_parameters(ctx, ¶meters, contents, symbols)?; + collect_function_parameters(ctx, ¶meters, doc, symbols)?; } if let Some(body) = node.child_by_field_name("body") { - collect_symbols(ctx, &body, contents, symbols)?; + collect_symbols(ctx, &body, doc, symbols)?; } Ok(()) @@ -375,12 +372,12 @@ fn collect_function( fn collect_sections( ctx: &mut CollectContext, node: &Node, - contents: &Rope, + doc: &Document, symbols: &mut Vec, mut handle_child: F, ) -> anyhow::Result<()> where - F: FnMut(&mut CollectContext, &Node, &Rope, &mut Vec) -> anyhow::Result<()>, + F: FnMut(&mut CollectContext, &Node, &Document, &mut Vec) -> anyhow::Result<()>, { // In lists of expressions we track and collect section comments, then // collect symbols from children nodes @@ -392,7 +389,7 @@ where for child in node.children(&mut cursor) { if let NodeType::Comment = child.node_type() { - let comment_text = contents.node_slice(&child)?.to_string(); + let comment_text = child.node_to_string(&doc.contents)?; // If we have a section comment, add it to our stack and close any sections if needed if let Some((level, title)) = parse_comment_as_section(&comment_text) { @@ -401,10 +398,10 @@ where { // Set end position for the section being closed if let Some(section) = active_sections.last_mut() { - let pos = point_end_of_previous_row(child.start_position(), contents); + let pos = point_end_of_previous_row(child.start_position(), &doc.contents); section.end_position = Some(pos); } - finalize_section(&mut active_sections, symbols, contents)?; + finalize_section(&mut active_sections, symbols, doc)?; } let section = Section { @@ -425,11 +422,11 @@ where if active_sections.is_empty() { // If no active section, extend current vector of symbols - handle_child(ctx, &child, contents, symbols)?; + handle_child(ctx, &child, doc, symbols)?; } else { // Otherwise create new store of symbols for the current section let mut child_symbols = Vec::new(); - handle_child(ctx, &child, contents, &mut child_symbols)?; + handle_child(ctx, &child, doc, &mut child_symbols)?; // Nest them inside last section if !child_symbols.is_empty() { @@ -448,11 +445,11 @@ where if let Some(section) = active_sections.last_mut() { let mut pos = node.end_position(); if pos.row > section.start_position.row { - pos = point_end_of_previous_row(pos, contents); + pos = point_end_of_previous_row(pos, &doc.contents); } section.end_position = Some(pos); } - finalize_section(&mut active_sections, symbols, contents)?; + finalize_section(&mut active_sections, symbols, doc)?; } Ok(()) @@ -461,22 +458,18 @@ where fn collect_list_sections( ctx: &mut CollectContext, node: &Node, - contents: &Rope, + doc: &Document, symbols: &mut Vec, ) -> anyhow::Result<()> { - collect_sections( - ctx, - node, - contents, - symbols, - |ctx, child, contents, symbols| collect_symbols(ctx, child, contents, symbols), - ) + collect_sections(ctx, node, doc, symbols, |ctx, child, doc, symbols| { + collect_symbols(ctx, child, doc, symbols) + }) } fn collect_call( ctx: &mut CollectContext, node: &Node, - contents: &Rope, + doc: &Document, symbols: &mut Vec, ) -> anyhow::Result<()> { let Some(callee) = node.child_by_field_name("function") else { @@ -484,14 +477,14 @@ fn collect_call( }; if callee.is_identifier() { - let fun_symbol = contents.node_slice(&callee)?.to_string(); + let fun_symbol = callee.node_to_string(&doc.contents)?; match fun_symbol.as_str() { - "test_that" => return collect_call_test_that(ctx, node, contents, symbols), + "test_that" => return collect_call_test_that(ctx, node, doc, symbols), _ => {}, // fallthrough } } - collect_call_arguments(ctx, node, contents, symbols)?; + collect_call_arguments(ctx, node, doc, symbols)?; Ok(()) } @@ -499,81 +492,64 @@ fn collect_call( fn collect_call_arguments( ctx: &mut CollectContext, node: &Node, - contents: &Rope, + doc: &Document, symbols: &mut Vec, ) -> anyhow::Result<()> { let Some(arguments) = node.child_by_field_name("arguments") else { return Ok(()); }; - collect_sections( - ctx, - &arguments, - contents, - symbols, - |ctx, child, contents, symbols| { - let Some(arg_value) = child.child_by_field_name("value") else { + collect_sections(ctx, &arguments, doc, symbols, |ctx, child, doc, symbols| { + let Some(arg_value) = child.child_by_field_name("value") else { + return Ok(()); + }; + + // If this is a named function, collect it as a method (new node in the tree) + if arg_value.kind() == "function_definition" { + if let Some(arg_fun) = child.child_by_field_name("name") { + collect_method(ctx, &arg_fun, &arg_value, doc, symbols)?; return Ok(()); }; + // else fallthrough + } - // If this is a named function, collect it as a method (new node in the tree) - if arg_value.kind() == "function_definition" { - if let Some(arg_fun) = child.child_by_field_name("name") { - collect_method(ctx, &arg_fun, &arg_value, contents, symbols)?; - return Ok(()); - }; - // else fallthrough - } - - collect_symbols(ctx, &arg_value, contents, symbols)?; + collect_symbols(ctx, &arg_value, doc, symbols)?; - Ok(()) - }, - ) + Ok(()) + }) } fn collect_function_parameters( ctx: &mut CollectContext, node: &Node, - contents: &Rope, + doc: &Document, symbols: &mut Vec, ) -> anyhow::Result<()> { - collect_sections( - ctx, - &node, - contents, - symbols, - |_ctx, _child, _contents, _symbols| { - // We only collect sections and don't recurse inside parameters - return Ok(()); - }, - ) + collect_sections(ctx, &node, doc, symbols, |_ctx, _child, _doc, _symbols| { + // We only collect sections and don't recurse inside parameters + return Ok(()); + }) } fn collect_method( ctx: &mut CollectContext, arg_fun: &Node, arg_value: &Node, - contents: &Rope, + doc: &Document, symbols: &mut Vec, ) -> anyhow::Result<()> { if !arg_fun.is_identifier_or_string() { return Ok(()); } - let arg_name_str = contents.node_slice(&arg_fun)?.to_string(); + let arg_name = arg_fun.node_to_string(&doc.contents)?; - let start = convert_point_to_position(contents, arg_value.start_position()); - let end = convert_point_to_position(contents, arg_value.end_position()); + let start = doc.lsp_position_from_tree_sitter_point(arg_value.start_position())?; + let end = doc.lsp_position_from_tree_sitter_point(arg_value.end_position())?; let mut children = vec![]; - collect_symbols(ctx, arg_value, contents, &mut children)?; + collect_symbols(ctx, arg_value, doc, &mut children)?; - let mut symbol = new_symbol_node( - arg_name_str, - SymbolKind::METHOD, - Range { start, end }, - children, - ); + let mut symbol = new_symbol_node(arg_name, SymbolKind::METHOD, Range { start, end }, children); // Don't include whole function as detail as the body often doesn't // provide useful information and only make the outline more busy (with @@ -589,7 +565,7 @@ fn collect_method( fn collect_call_test_that( ctx: &mut CollectContext, node: &Node, - contents: &Rope, + doc: &Document, symbols: &mut Vec, ) -> anyhow::Result<()> { let Some(arguments) = node.child_by_field_name("arguments") else { @@ -614,15 +590,15 @@ fn collect_call_test_that( let mut cursor = arguments.walk(); for child in arguments.children_by_field_name("argument", &mut cursor) { if let Some(value) = child.child_by_field_name("value") { - collect_symbols(ctx, &value, contents, &mut children)?; + collect_symbols(ctx, &value, doc, &mut children)?; } } - let name = contents.node_slice(&string)?.to_string(); + let name = string.node_to_string(&doc.contents)?; let name = format!("Test: {name}"); - let start = convert_point_to_position(contents, node.start_position()); - let end = convert_point_to_position(contents, node.end_position()); + let start = doc.lsp_position_from_tree_sitter_point(node.start_position())?; + let end = doc.lsp_position_from_tree_sitter_point(node.end_position())?; let symbol = new_symbol_node(name, SymbolKind::FUNCTION, Range { start, end }, children); symbols.push(symbol); @@ -633,7 +609,7 @@ fn collect_call_test_that( fn collect_assignment( ctx: &mut CollectContext, node: &Node, - contents: &Rope, + doc: &Document, symbols: &mut Vec, ) -> anyhow::Result<()> { let (NodeType::BinaryOperator(BinaryOperatorType::LeftAssignment) | @@ -652,27 +628,27 @@ fn collect_assignment( // If a function, collect symbol as function let function = lhs.is_identifier_or_string() && rhs.is_function_definition(); if function { - return collect_assignment_with_function(ctx, node, contents, symbols); + return collect_assignment_with_function(ctx, node, doc, symbols); } if ctx.top_level || ctx.include_assignments_in_blocks { // Collect as generic object, but typically only if we're at top-level. Assigned // objects in nested functions and blocks cause the outline to become // too busy. - let name = contents.node_slice(&lhs)?.to_string(); + let name = lhs.node_to_string(&doc.contents)?; - let start = convert_point_to_position(contents, node.start_position()); - let end = convert_point_to_position(contents, node.end_position()); + let start = doc.lsp_position_from_tree_sitter_point(node.start_position())?; + let end = doc.lsp_position_from_tree_sitter_point(node.end_position())?; // Now recurse into RHS let mut children = Vec::new(); - collect_symbols(ctx, &rhs, contents, &mut children)?; + collect_symbols(ctx, &rhs, doc, &mut children)?; let symbol = new_symbol_node(name, SymbolKind::VARIABLE, Range { start, end }, children); symbols.push(symbol); } else { // Recurse into RHS - collect_symbols(ctx, &rhs, contents, symbols)?; + collect_symbols(ctx, &rhs, doc, symbols)?; } Ok(()) @@ -681,7 +657,7 @@ fn collect_assignment( fn collect_assignment_with_function( ctx: &mut CollectContext, node: &Node, - contents: &Rope, + doc: &Document, symbols: &mut Vec, ) -> anyhow::Result<()> { // check for lhs, rhs @@ -695,21 +671,21 @@ fn collect_assignment_with_function( let mut cursor = parameters.walk(); for parameter in parameters.children_by_field_name("parameter", &mut cursor) { let name = parameter.child_by_field_name("name").into_result()?; - let name = contents.node_slice(&name)?.to_string(); + let name = name.node_to_string(&doc.contents)?; arguments.push(name); } - let name = contents.node_slice(&lhs)?.to_string(); + let name = lhs.node_to_string(&doc.contents)?; let detail = format!("function({})", arguments.join(", ")); let range = Range { - start: convert_point_to_position(contents, lhs.start_position()), - end: convert_point_to_position(contents, rhs.end_position()), + start: doc.lsp_position_from_tree_sitter_point(lhs.start_position())?, + end: doc.lsp_position_from_tree_sitter_point(rhs.end_position())?, }; // Process the function body to extract child symbols let mut children = Vec::new(); - collect_symbols(ctx, &rhs, contents, &mut children)?; + collect_symbols(ctx, &rhs, doc, &mut children)?; let mut symbol = new_symbol_node(name, SymbolKind::FUNCTION, range, children); symbol.detail = Some(detail); @@ -722,15 +698,15 @@ fn collect_assignment_with_function( fn finalize_section( active_sections: &mut Vec
, symbols: &mut Vec, - contents: &Rope, + doc: &Document, ) -> anyhow::Result<()> { if let Some(section) = active_sections.pop() { let start_pos = section.start_position; let end_pos = section.end_position.unwrap_or(section.start_position); let range = Range { - start: convert_point_to_position(contents, start_pos), - end: convert_point_to_position(contents, end_pos), + start: doc.lsp_position_from_tree_sitter_point(start_pos)?, + end: doc.lsp_position_from_tree_sitter_point(end_pos)?, }; let symbol = new_symbol(section.title, SymbolKind::STRING, range); @@ -771,7 +747,7 @@ mod tests { use super::*; use crate::lsp::config::LspConfig; use crate::lsp::config::WorkspaceSymbolsConfig; - use crate::lsp::documents::Document; + use crate::lsp::document::Document; use crate::lsp::indexer::ResetIndexerGuard; use crate::lsp::util::test_path; @@ -780,13 +756,7 @@ mod tests { let node = doc.ast.root_node(); let mut symbols = Vec::new(); - collect_symbols( - &mut CollectContext::new(), - &node, - &doc.contents, - &mut symbols, - ) - .unwrap(); + collect_symbols(&mut CollectContext::new(), &node, &doc, &mut symbols).unwrap(); symbols } @@ -1154,7 +1124,7 @@ outer <- 4 ctx.include_assignments_in_blocks = true; let mut symbols = Vec::new(); - collect_symbols(ctx, &node, &doc.contents, &mut symbols).unwrap(); + collect_symbols(ctx, &node, &doc, &mut symbols).unwrap(); insta::assert_debug_snapshot!(symbols); } diff --git a/crates/ark/src/lsp/traits/mod.rs b/crates/ark/src/lsp/traits/mod.rs index 6d53997cd..74f8679ed 100644 --- a/crates/ark/src/lsp/traits/mod.rs +++ b/crates/ark/src/lsp/traits/mod.rs @@ -8,6 +8,5 @@ pub mod cursor; pub mod node; pub mod point; -pub mod rope; pub mod string; pub mod url; diff --git a/crates/ark/src/lsp/traits/node.rs b/crates/ark/src/lsp/traits/node.rs index 97d5d1dfb..544b76cf8 100644 --- a/crates/ark/src/lsp/traits/node.rs +++ b/crates/ark/src/lsp/traits/node.rs @@ -4,14 +4,15 @@ // // +use anyhow::anyhow; use stdext::all; +use stdext::result::ResultExt; use tree_sitter::Node; use tree_sitter::Point; use tree_sitter::Range; use tree_sitter::TreeCursor; use crate::lsp::traits::point::PointExt; -use crate::lsp::traits::rope::RopeExt; fn _dump_impl(cursor: &mut TreeCursor, source: &str, indent: &str, output: &mut String) { let node = cursor.node(); @@ -22,7 +23,7 @@ fn _dump_impl(cursor: &mut TreeCursor, source: &str, indent: &str, output: &mut format!( "{} - {} - {} ({} -- {})\n", indent, - node.utf8_text(source.as_bytes()).unwrap(), + node.node_as_str(&source).unwrap(), node.kind(), node.start_position(), node.end_position(), @@ -96,10 +97,14 @@ pub trait NodeExt: Sized { fn arguments(&self) -> impl Iterator, Option)>; fn arguments_values(&self) -> impl Iterator>; fn arguments_names(&self) -> impl Iterator>; - fn arguments_names_as_string( - &self, - contents: &ropey::Rope, - ) -> impl Iterator>; + fn arguments_names_as_string(&self, contents: &str) -> impl Iterator>; + + /// Return the node's text as a `&str` slice into `source`. + /// This is a thin wrapper around `Node::utf8_text(&node, source.as_bytes())`. + fn node_as_str<'a>(&self, source: &'a str) -> anyhow::Result<&'a str>; + + /// Convenience method returning an owned `String` for this node's text. + fn node_to_string(&self, source: &str) -> anyhow::Result; } impl<'tree> NodeExt for Node<'tree> { @@ -268,12 +273,11 @@ impl<'tree> NodeExt for Node<'tree> { self.arguments().map(|(name, _value)| name) } - fn arguments_names_as_string( - &self, - contents: &ropey::Rope, - ) -> impl Iterator> { - self.arguments_names().map(|maybe_node| { - maybe_node.and_then(|node| match contents.node_slice(&node) { + fn arguments_names_as_string(&self, contents: &str) -> impl Iterator> { + // Note: capture `contents` by reference into the closure so the iterator + // can outlive the stack frame where this method is called. + self.arguments_names().map(move |maybe_node| { + maybe_node.and_then(|node| match node.node_as_str(&contents) { Err(err) => { tracing::error!("Can't convert argument name to text: {err:?}"); None @@ -283,6 +287,17 @@ impl<'tree> NodeExt for Node<'tree> { }) } + fn node_as_str<'a>(&self, source: &'a str) -> anyhow::Result<&'a str> { + self.utf8_text(source.as_bytes()).anyhow() + } + + fn node_to_string(&self, source: &str) -> anyhow::Result { + Ok(self + .node_as_str(source) + .map(|s| s.to_string()) + .map_err(|e| anyhow!(e))?) + } + fn arguments_values(&self) -> impl Iterator>> { self.arguments().map(|(_name, value)| value) } diff --git a/crates/ark/src/lsp/traits/rope.rs b/crates/ark/src/lsp/traits/rope.rs deleted file mode 100644 index 1e7f3653b..000000000 --- a/crates/ark/src/lsp/traits/rope.rs +++ /dev/null @@ -1,44 +0,0 @@ -// -// rope.rs -// -// Copyright (C) 2022 Posit Software, PBC. All rights reserved. -// -// - -use ropey::Rope; -use ropey::RopeSlice; -use tree_sitter::Node; -use tree_sitter::Point; - -pub trait RopeExt<'a> { - fn point_to_byte(&self, point: Point) -> usize; - fn node_slice(&'a self, node: &Node) -> std::result::Result, anyhow::Error>; -} - -impl<'a> RopeExt<'a> for Rope { - /// Converts a tree-sitter [Point] into a byte offset - /// - /// Useful when constructing [tree_sitter::Range] or [tree_sitter::InputEdit] - fn point_to_byte(&self, point: Point) -> usize { - self.line_to_byte(point.row) + point.column - } - - fn node_slice(&'a self, node: &Node) -> std::result::Result, anyhow::Error> { - // For some reason Ropey returns an Option and hides the Result which includes - // the actual Error reason. We convert `None` back to an error so we can propagate it. - let range = node.start_byte()..node.end_byte(); - - if let Some(slice) = self.get_byte_slice(range) { - return Ok(slice); - } - - let message = anyhow::anyhow!( - "Failed to slice Rope at byte range {}-{}. Text: '{}'.", - node.start_byte(), - node.end_byte(), - self.to_string() - ); - - Err(message) - } -} diff --git a/crates/ark/src/treesitter.rs b/crates/ark/src/treesitter.rs index 282e0b15d..5e041dc61 100644 --- a/crates/ark/src/treesitter.rs +++ b/crates/ark/src/treesitter.rs @@ -1,10 +1,11 @@ use std::collections::HashMap; use anyhow::anyhow; +use stdext::result::ResultExt; +use streaming_iterator::StreamingIterator; use tree_sitter::Node; use crate::lsp::traits::node::NodeExt; -use crate::lsp::traits::rope::RopeExt; #[derive(Debug, PartialEq)] pub enum NodeType { @@ -285,7 +286,7 @@ pub trait NodeTypeExt: Sized { fn is_identifier(&self) -> bool; fn is_string(&self) -> bool; fn is_identifier_or_string(&self) -> bool; - fn get_identifier_or_string_text(&self, contents: &ropey::Rope) -> anyhow::Result; + fn get_identifier_or_string_text(&self, contents: &str) -> anyhow::Result; fn is_keyword(&self) -> bool; fn is_call(&self) -> bool; fn is_subset(&self) -> bool; @@ -302,8 +303,8 @@ pub trait NodeTypeExt: Sized { fn is_binary_operator(&self) -> bool; fn is_binary_operator_of_kind(&self, kind: BinaryOperatorType) -> bool; fn is_native_pipe_operator(&self) -> bool; - fn is_magrittr_pipe_operator(&self, contents: &ropey::Rope) -> anyhow::Result; - fn is_pipe_operator(&self, contents: &ropey::Rope) -> anyhow::Result; + fn is_magrittr_pipe_operator(&self, contents: &str) -> anyhow::Result; + fn is_pipe_operator(&self, contents: &str) -> anyhow::Result; } impl NodeTypeExt for Node<'_> { @@ -328,14 +329,14 @@ impl NodeTypeExt for Node<'_> { matches!(self.node_type(), NodeType::Identifier | NodeType::String) } - fn get_identifier_or_string_text(&self, contents: &ropey::Rope) -> anyhow::Result { + fn get_identifier_or_string_text(&self, contents: &str) -> anyhow::Result { match self.node_type() { - NodeType::Identifier => return Ok(contents.node_slice(self)?.to_string()), + NodeType::Identifier => return Ok(self.node_to_string(&contents)?), NodeType::String => { let string_content = self .child_by_field_name("content") .ok_or_else(|| anyhow::anyhow!("Can't extract string's `content` field"))?; - Ok(contents.node_slice(&string_content)?.to_string()) + return Ok(string_content.node_to_string(&contents)?); }, _ => { return Err(anyhow::anyhow!("Not an identifier or string")); @@ -418,7 +419,7 @@ impl NodeTypeExt for Node<'_> { self.node_type() == NodeType::BinaryOperator(BinaryOperatorType::Pipe) } - fn is_magrittr_pipe_operator(&self, contents: &ropey::Rope) -> anyhow::Result { + fn is_magrittr_pipe_operator(&self, contents: &str) -> anyhow::Result { if self.node_type() != NodeType::BinaryOperator(BinaryOperatorType::Special) { return Ok(false); } @@ -427,12 +428,14 @@ impl NodeTypeExt for Node<'_> { return Ok(false); }; - let text = contents.node_slice(&operator)?; + let Ok(text) = operator.node_as_str(&contents) else { + return Ok(false); + }; Ok(text == "%>%") } - fn is_pipe_operator(&self, contents: &ropey::Rope) -> anyhow::Result { + fn is_pipe_operator(&self, contents: &str) -> anyhow::Result { if self.is_native_pipe_operator() { return Ok(true); } @@ -445,10 +448,6 @@ impl NodeTypeExt for Node<'_> { } } -pub(crate) fn node_text(node: &Node, contents: &ropey::Rope) -> Option { - contents.node_slice(node).ok().map(|f| f.to_string()) -} - pub(crate) fn node_has_error_or_missing(node: &Node) -> bool { // According to the docs, `node.has_error()` should return `true` // if `node` is itself an error, or if it contains any errors, but that @@ -471,14 +470,14 @@ pub(crate) fn node_in_string(node: &Node) -> bool { node_find_string(node).is_some() } -pub(crate) fn node_is_call(node: &Node, name: &str, contents: &ropey::Rope) -> bool { +pub(crate) fn node_is_call(node: &Node, name: &str, contents: &str) -> bool { if !node.is_call() { return false; } let Some(fun) = node.child_by_field_name("function") else { return false; }; - let Some(fun) = node_text(&fun, contents) else { + let Ok(fun) = fun.node_as_str(contents) else { return false; }; fun == name @@ -488,7 +487,7 @@ pub(crate) fn node_is_namespaced_call( node: &Node, namespace: &str, name: &str, - contents: &ropey::Rope, + contents: &str, ) -> bool { if !node.is_call() { return false; @@ -506,10 +505,10 @@ pub(crate) fn node_is_namespaced_call( else { return false; }; - let Some(node_namespace) = node_text(&node_namespace, contents) else { + let Ok(node_namespace) = node_namespace.node_as_str(contents) else { return false; }; - let Some(node_name) = node_text(&node_name, contents) else { + let Some(node_name) = node_name.node_as_str(contents).log_err() else { return false; }; @@ -553,7 +552,7 @@ pub(crate) fn node_find_parent_call<'tree>(node: &Node<'tree>) -> Option( args: &Node<'tree>, name: &str, - contents: &ropey::Rope, + contents: &str, ) -> Option> { if args.node_type() != NodeType::Argument { return None; @@ -564,7 +563,7 @@ pub(crate) fn node_arg_value<'tree>( let Some(value_node) = args.child_by_field_name("value") else { return None; }; - let Some(name_text) = node_text(&name_node, contents) else { + let Some(name_text) = name_node.node_as_str(contents).log_err() else { return None; }; (name_text == name).then_some(value_node) @@ -573,7 +572,7 @@ pub(crate) fn node_arg_value<'tree>( pub(crate) fn args_find_call<'tree>( args: Node<'tree>, name: &str, - contents: &ropey::Rope, + contents: &str, ) -> Option> { let mut cursor = args.walk(); let mut iter = args.children(&mut cursor); @@ -587,7 +586,7 @@ pub(crate) fn args_find_call<'tree>( pub(crate) fn args_find_call_args<'tree>( args: Node<'tree>, name: &str, - contents: &ropey::Rope, + contents: &str, ) -> Option> { let call = args_find_call(args, name, contents)?; call.child_by_field_name("arguments") @@ -625,15 +624,19 @@ pub(crate) fn node_find_containing_call<'tree>(node: Node<'tree>) -> Option tree_sitter::Point { if point.row > 0 { let prev_row = point.row - 1; - let line = contents.line(prev_row as usize); - let line_len = line.len_chars().saturating_sub(1); // Subtract 1 for newline - tree_sitter::Point { - row: prev_row, - column: line_len, + if let Some(line) = contents.lines().nth(prev_row) { + let line_len = line.chars().count(); + tree_sitter::Point { + row: prev_row, + column: line_len, + } + } else { + point.column = 0; + point } } else { // We're at the very beginning of the document, can't go back further @@ -792,50 +795,49 @@ impl<'tree, 'query, 'contents> Iterator for AllCaptures<'tree, 'query, 'contents #[cfg(test)] mod tests { - use ropey::Rope; use tree_sitter::Point; use super::*; #[test] fn test_point_end_of_previous_row() { - let contents = Rope::from_str("hello world\nfoo bar\nbaz"); + let contents = "hello world\nfoo bar\nbaz"; let point = Point { row: 2, column: 1 }; - let result = point_end_of_previous_row(point, &contents); + let result = point_end_of_previous_row(point, contents); assert_eq!(result, Point { row: 1, column: 7 }); } #[test] fn test_point_end_of_previous_row_first_row() { - let contents = Rope::from_str("hello world\nfoo bar\nbaz"); + let contents = "hello world\nfoo bar\nbaz"; let point = Point { row: 0, column: 5 }; - let result = point_end_of_previous_row(point, &contents); + let result = point_end_of_previous_row(point, contents); assert_eq!(result, Point { row: 0, column: 0 }); } #[test] fn test_point_end_of_previous_row_empty_previous_line() { - let contents = Rope::from_str("hello\n\nworld"); + let contents = "hello\n\nworld"; let point = Point { row: 2, column: 1 }; - let result = point_end_of_previous_row(point, &contents); + let result = point_end_of_previous_row(point, contents); assert_eq!(result, Point { row: 1, column: 0 }); let point = Point { row: 1, column: 1 }; - let result = point_end_of_previous_row(point, &contents); + let result = point_end_of_previous_row(point, contents); assert_eq!(result, Point { row: 0, column: 5 }); } #[test] fn test_point_end_of_previous_row_single_line() { - let contents = Rope::from_str("hello world"); + let contents = "hello world"; let point = Point { row: 0, column: 0 }; - let result = point_end_of_previous_row(point, &contents); + let result = point_end_of_previous_row(point, contents); assert_eq!(result, Point { row: 0, column: 0 }); let point = Point { row: 0, column: 5 }; - let result = point_end_of_previous_row(point, &contents); + let result = point_end_of_previous_row(point, contents); assert_eq!(result, Point { row: 0, column: 0 }); } }