diff --git a/.clippy.toml b/.clippy.toml index 1f4f342..40fecfc 100644 --- a/.clippy.toml +++ b/.clippy.toml @@ -1,5 +1,12 @@ cognitive-complexity-threshold = 20 +[[disallowed-methods]] +path = "tree_sitter::Node::named_child" + +[[disallowed-methods]] +path = "tree_sitter::Node::next_named_sibling" +replacement = "odoo_lsp::utils::python_next_named_sibling" + [[await-holding-invalid-types]] path = "dashmap::mapref::one::RefMut" reason = "Would deadlock if same thread wants to acquire a reference" diff --git a/.vscode/launch.json b/.vscode/launch.json index 463cb4e..8aa2fda 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,8 +7,13 @@ "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", - "args": ["--extensionDevelopmentPath=${workspaceRoot}", "${workspaceRoot}/examples"], - "outFiles": ["${workspaceRoot}/dist/*.js"], + "args": [ + "--extensionDevelopmentPath=${workspaceRoot}", + "${workspaceRoot}/examples" + ], + "outFiles": [ + "${workspaceRoot}/dist/*.js" + ], "preLaunchTask": "watch-all", "env": { "SERVER_PATH": "${workspaceRoot}/target/debug/odoo-lsp", @@ -21,8 +26,13 @@ "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", - "args": ["--extensionDevelopmentPath=${workspaceRoot}", "${workspaceRoot}/examples"], - "outFiles": ["${workspaceRoot}/dist/*.js"], + "args": [ + "--extensionDevelopmentPath=${workspaceRoot}", + "${workspaceRoot}/examples" + ], + "outFiles": [ + "${workspaceRoot}/dist/*.js" + ], "preLaunchTask": { "type": "npm", "script": "watch" @@ -38,7 +48,9 @@ "name": "Attach", "program": "${workspaceFolder}/target/debug/odoo-lsp", // "pid": "${command:pickMyProcess}", - "sourceLanguages": ["rust"], + "sourceLanguages": [ + "rust" + ], "windows": { "program": "${workspaceFolder}/target/debug/odoo-lsp.exe" } @@ -53,13 +65,149 @@ "--extensionTestsPath=${workspaceRoot}/client/out/test/index", "${workspaceRoot}/client/testFixture" ], - "outFiles": ["${workspaceRoot}/client/out/test/**/*.js"] + "outFiles": [ + "${workspaceRoot}/client/out/test/**/*.js" + ] + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'odoo_lsp'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=odoo-lsp" + ], + "filter": { + "name": "odoo_lsp", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'odoo-lsp'", + "cargo": { + "args": [ + "build", + "--bin=odoo-lsp", + "--package=odoo-lsp" + ], + "filter": { + "name": "odoo-lsp", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'odoo-lsp'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=odoo-lsp", + "--package=odoo-lsp" + ], + "filter": { + "name": "odoo-lsp", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in library 'odoo_lsp_tests'", + "cargo": { + "args": [ + "test", + "--no-run", + "--lib", + "--package=odoo-lsp-tests" + ], + "filter": { + "name": "odoo_lsp_tests", + "kind": "lib" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug benchmark 'standard'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bench=standard", + "--package=odoo-lsp-tests" + ], + "filter": { + "name": "standard", + "kind": "bench" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'ts-indent'", + "cargo": { + "args": [ + "build", + "--bin=ts-indent", + "--package=ts-indent" + ], + "filter": { + "name": "ts-indent", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'ts-indent'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=ts-indent", + "--package=ts-indent" + ], + "filter": { + "name": "ts-indent", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" } ], "compounds": [ { "name": "Client + Server", - "configurations": ["Launch Client", "Attach"] + "configurations": [ + "Launch Client", + "Attach" + ] } ] -} +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 8670db2..f6bd9f7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,8 @@ }, // Turn off tsc task auto detection since we have the necessary tasks as npm scripts "typescript.tsc.autoDetect": "off", - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "lldb.displayFormat": "auto", + "lldb.dereferencePointers": true, + "lldb.consoleMode": "commands" } diff --git a/Cargo.lock b/Cargo.lock index b307607..92f7fa0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -31,9 +31,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -46,18 +46,18 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" dependencies = [ "backtrace", ] [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] @@ -69,7 +69,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c1c85c4bb41706ad1f8338e39fa725a24bc642be41140a38d818c93b9ae91f5" dependencies = [ "futures", - "lsp-types 0.95.1", + "lsp-types", "pin-project-lite", "rustix", "serde", @@ -100,17 +100,11 @@ dependencies = [ "topological-sort", ] -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -118,7 +112,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -144,11 +138,11 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.72.0" +version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "cexpr", "clang-sys", "itertools", @@ -170,9 +164,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "block-buffer" @@ -183,11 +177,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "serde", @@ -199,18 +199,56 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "camino" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", +] [[package]] name = "cc" -version = "1.2.30" +version = "1.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ + "find-msvc-tools", "shlex", ] @@ -225,9 +263,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -267,9 +305,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const_format" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", ] @@ -303,6 +341,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -330,9 +377,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -371,6 +418,19 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "dashmap" version = "6.1.0" @@ -397,18 +457,18 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", @@ -417,21 +477,22 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" dependencies = [ "proc-macro2", "quote", + "rustc_version", "syn", ] @@ -507,12 +568,21 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", ] [[package]] @@ -529,21 +599,27 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.25" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -551,11 +627,12 @@ dependencies = [ [[package]] name = "fluent-uri" -version = "0.1.4" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" dependencies = [ - "bitflags 1.3.2", + "borrow-or-share", + "ref-cast", ] [[package]] @@ -578,9 +655,9 @@ checksum = "3f722aa875298d34a0ebb6004699f6f4ea830d36dec8ac2effdbbc840248a096" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -699,29 +776,29 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "git-version" @@ -745,21 +822,21 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] @@ -768,16 +845,16 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "ignore", "walkdir", ] [[package]] name = "h2" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -804,13 +881,19 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" @@ -819,12 +902,11 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -859,19 +941,21 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.6.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -896,9 +980,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "base64", "bytes", @@ -912,7 +996,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2", "tokio", "tower-service", "tracing", @@ -920,9 +1004,9 @@ dependencies = [ [[package]] name = "iai-callgrind" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c358f67cf086a3a890a72c390dadc371e9151129b1e9b75acc705d30a77197f" +checksum = "0b1e4910d3a9137442723dfb772c32dc10674c4181ca078d2fd227cd5dce9db0" dependencies = [ "bincode", "bindgen", @@ -954,18 +1038,18 @@ dependencies = [ [[package]] name = "iai-callgrind-runner" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6248b44a281f0c22ba57fe41b56f718a8c2cc682a99f270f667503819d758004" +checksum = "b74c9743c00c3bca4aaffc69c87cae56837796cd362438daf354a3f785788c68" dependencies = [ "serde", ] [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -976,9 +1060,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -989,11 +1073,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1004,42 +1087,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1055,9 +1134,9 @@ checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1076,15 +1155,15 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.23" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" dependencies = [ "crossbeam-deque", "globset", "log", "memchr", - "regex-automata 0.4.9", + "regex-automata", "same-file", "walkdir", "winapi-util", @@ -1092,13 +1171,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] @@ -1116,20 +1196,9 @@ dependencies = [ [[package]] name = "intmap" -version = "3.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16dd999647b7a027fadf2b3041a4ea9c8ae21562823fe5cbdecd46537d535ae2" - -[[package]] -name = "io-uring" -version = "0.7.9" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "libc", -] +checksum = "a2e611826a1868311677fdcdfbec9e8621d104c732d080f546a854530232f0ee" [[package]] name = "ipnet" @@ -1139,9 +1208,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -1164,9 +1233,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -1178,7 +1247,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e14eda50a3494b3bf7b9ce51c52434a761e383d7238ce1dd5dcec2fbc13e9fb" dependencies = [ - "dashmap", + "dashmap 6.1.0", "hashbrown 0.14.5", ] @@ -1196,58 +1265,57 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libloading" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-targets 0.53.2", + "windows-link", ] [[package]] name = "libredox" -version = "0.1.6" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "libc", "redox_syscall", ] [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru-slab" @@ -1256,45 +1324,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] -name = "lsp-types" -version = "0.95.1" +name = "ls-types" +version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e34d33a8e9b006cd3fc4fe69a921affa097bae4bb65f76271f4644f9a334365" +checksum = "7a7deb98ef9daaa7500324351a5bab7c80c644cfb86b4be0c4433b582af93510" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.10.0", + "fluent-uri", + "percent-encoding", "serde", "serde_json", - "serde_repr", - "url", ] [[package]] name = "lsp-types" -version = "0.97.0" +version = "0.95.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071" +checksum = "8e34d33a8e9b006cd3fc4fe69a921affa097bae4bb65f76271f4644f9a334365" dependencies = [ "bitflags 1.3.2", - "fluent-uri", "serde", "serde_json", "serde_repr", + "url", ] [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mini-moka" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "c325dfab65f261f386debee8b0969da215b3fa0037e74c8a1234db7ba986d803" +dependencies = [ + "crossbeam-channel", + "crossbeam-utils", + "dashmap 5.5.3", + "skeptic", + "smallvec", + "tagptr", + "triomphe", +] [[package]] name = "minimal-lexical" @@ -1309,17 +1392,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -1340,12 +1424,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "overload", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -1356,9 +1439,9 @@ checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -1366,9 +1449,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1384,9 +1467,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] @@ -1396,9 +1479,9 @@ name = "odoo-lsp" version = "0.6.1" dependencies = [ "anyhow", - "bitflags 2.9.1", + "bitflags 2.10.0", "const_format", - "dashmap", + "dashmap 6.1.0", "derive_more", "fomat-macros", "futures", @@ -1407,6 +1490,7 @@ dependencies = [ "ignore", "intmap", "lasso", + "mini-moka", "num_enum", "pathdiff", "phf", @@ -1469,23 +1553,17 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -1496,9 +1574,9 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "phf" @@ -1573,9 +1651,9 @@ checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -1607,9 +1685,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.36" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", @@ -1617,9 +1695,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] @@ -1648,9 +1726,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -1668,6 +1746,25 @@ dependencies = [ "yansi", ] +[[package]] +name = "pulldown-cmark" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +dependencies = [ + "bitflags 2.10.0", + "memchr", + "unicase", +] + +[[package]] +name = "python-stubfinder" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "qp-trie" version = "0.8.2" @@ -1689,9 +1786,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -1700,7 +1797,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2", "thiserror", "tokio", "tracing", @@ -1709,12 +1806,12 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand", "ring", @@ -1730,23 +1827,23 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -1792,14 +1889,14 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -1807,9 +1904,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -1817,56 +1914,61 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.15" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", ] [[package]] -name = "regex" -version = "1.11.1" +name = "ref-cast" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", + "ref-cast-impl", ] [[package]] -name = "regex-automata" -version = "0.1.10" +name = "ref-cast-impl" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ - "regex-syntax 0.6.29", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "regex-automata" -version = "0.4.9" +name = "regex" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-automata", + "regex-syntax", ] [[package]] -name = "regex-syntax" -version = "0.6.29" +name = "regex-automata" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "relative-path" @@ -1876,9 +1978,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64", "bytes", @@ -1970,9 +2072,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -1991,22 +2093,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.29" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "once_cell", "ring", @@ -2018,9 +2120,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ "web-time", "zeroize", @@ -2028,9 +2130,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -2039,9 +2141,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -2101,27 +2203,38 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" dependencies = [ "serde", + "serde_core", ] [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +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.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -2130,15 +2243,16 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.141" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "indexmap", "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -2212,11 +2326,26 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "skeptic" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" +dependencies = [ + "bytecount", + "cargo_metadata", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", +] + [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -2237,22 +2366,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2276,9 +2395,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "str_indices" @@ -2321,9 +2440,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -2350,6 +2469,12 @@ dependencies = [ "syn", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tar" version = "0.4.44" @@ -2363,31 +2488,31 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.20.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -2405,9 +2530,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "num-conv", @@ -2418,15 +2543,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -2434,9 +2559,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -2449,27 +2574,24 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.46.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "pin-project-lite", - "slab", - "socket2 0.5.10", + "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -2478,9 +2600,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -2488,9 +2610,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -2502,18 +2624,31 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ "indexmap", "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ "winnow", ] @@ -2540,11 +2675,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -2564,17 +2699,16 @@ checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-lsp-server" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cd168c085174eafa7492a519715f2d59436dc28cdfd9d13a5b864246899db9" +checksum = "2f0e711655c89181a6bc6a2cc348131fcd9680085f5b06b6af13427a393a6e72" dependencies = [ "bytes", - "dashmap", + "dashmap 6.1.0", "futures", "httparse", - "lsp-types 0.97.0", + "ls-types", "memchr", - "percent-encoding", "serde", "serde_json", "tokio", @@ -2591,9 +2725,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -2602,9 +2736,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -2613,9 +2747,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -2644,14 +2778,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "serde", "serde_json", "sharded-slab", @@ -2665,13 +2799,13 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.25.8" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7b8994f367f16e6fa14b5aebbcb350de5d7cbea82dc5b00ae997dd71680dd2" +checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" dependencies = [ "cc", "regex", - "regex-syntax 0.8.5", + "regex-syntax", "serde_json", "streaming-iterator", "tree-sitter-language", @@ -2722,6 +2856,12 @@ dependencies = [ "tree-sitter-language", ] +[[package]] +name = "triomphe" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" + [[package]] name = "try-lock" version = "0.2.5" @@ -2750,21 +2890,27 @@ dependencies = [ [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicase" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -2789,9 +2935,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -2819,9 +2965,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "version-compare" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "version_check" @@ -2871,45 +3017,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt 0.39.0", + "wit-bindgen 0.46.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -2920,9 +3053,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2930,22 +3063,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -2985,17 +3118,17 @@ version = "0.227.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f51cad774fb3c9461ab9bccc9c62dfb7388397b5deda31bf40e8108ccd678b2" dependencies = [ - "bitflags 2.9.1", - "hashbrown 0.15.4", + "bitflags 2.10.0", + "hashbrown 0.15.5", "indexmap", "semver", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -3013,43 +3146,27 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" @@ -3075,7 +3192,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", ] [[package]] @@ -3096,18 +3222,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "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]] @@ -3118,9 +3245,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -3130,9 +3257,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -3142,9 +3269,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -3154,9 +3281,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -3166,9 +3293,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -3178,9 +3305,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -3190,9 +3317,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -3202,15 +3329,15 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -3221,10 +3348,16 @@ version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" dependencies = [ - "wit-bindgen-rt 0.41.0", + "wit-bindgen-rt", "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "wit-bindgen-core" version = "0.41.0" @@ -3236,22 +3369,13 @@ dependencies = [ "wit-parser", ] -[[package]] -name = "wit-bindgen-rt" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.1", -] - [[package]] name = "wit-bindgen-rt" version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "futures", "once_cell", ] @@ -3294,7 +3418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "635c3adc595422cbf2341a17fb73a319669cc8d33deed3a48368a841df86b676" dependencies = [ "anyhow", - "bitflags 2.9.1", + "bitflags 2.10.0", "indexmap", "log", "serde", @@ -3326,15 +3450,15 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xattr" -version = "1.5.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af3a19837351dc82ba89f8a125e22a3c475f05aba604acc023d62b2739ae2909" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", "rustix", @@ -3354,11 +3478,10 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -3366,9 +3489,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -3384,23 +3507,23 @@ checksum = "a3ee021e3a4c69d6ae90137fcf7537d1a8a5032dc9bf180c8fa6dd1a2f7c56d7" dependencies = [ "serde", "serde_json", - "wit-bindgen", + "wit-bindgen 0.41.0", ] [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", @@ -3430,15 +3553,15 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -3447,9 +3570,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -3458,9 +3581,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", @@ -3498,9 +3621,9 @@ dependencies = [ [[package]] name = "zopfli" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", diff --git a/Cargo.toml b/Cargo.toml index 24a4999..7ec6118 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,17 +58,17 @@ tree-sitter = "0.25" ts-macros = { version = "0.5.0", path = "crates/ts-macros" } pretty_assertions = "1.4.0" tokio = { version = "1.46.0", default-features = false, features = ["macros", "rt-multi-thread", "fs", "io-std", "io-util", "net"] } -tower-lsp-server = { version = "0.22.0", features = ["proposed"] } +tower-lsp-server = { version = "0.23.0", features = ["proposed"] } tower = { version = "0.5", features = ["timeout"] } futures = "0.3.31" tree-sitter-python = "0.23.6" globwalk = "0.9.1" tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" [dependencies] ropey = { version = "2.0.0-beta.1", features = ["metric_chars"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" dashmap = { version = "6.1.0", features = ["raw-api"] } xmlparser = "0.13.5" pin-project-lite = "0.2.16" @@ -91,6 +91,8 @@ tree-sitter-javascript = "0.23.1" self_update = { version = "0.42.0", optional = true, default-features = false, features = ["archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate", "rustls"] } anyhow = { version = "1.0.97", features = ["backtrace"] } +serde.workspace = true +serde_json.workspace = true tree-sitter.workspace = true tree-sitter-python.workspace = true tokio.workspace = true @@ -100,6 +102,7 @@ futures.workspace = true globwalk.workspace = true ts-macros.workspace = true tracing-subscriber.workspace = true +mini-moka = "0.10.3" [dev-dependencies] pretty_assertions.workspace = true diff --git a/crates/extension-zed/src/lib.rs b/crates/extension-zed/src/lib.rs index f6d7ae2..271d864 100644 --- a/crates/extension-zed/src/lib.rs +++ b/crates/extension-zed/src/lib.rs @@ -31,17 +31,15 @@ impl zed::Extension for Extension { fn language_server_command(&mut self, lsid: &zed::LanguageServerId, dir: &zed::Worktree) -> Result { let env = self.prepare_env(dir); - if let Some(binary) = LspSettings::for_worktree("odoo-lsp", dir) - .ok() - .and_then(|settings| settings.binary) + if let Ok(settings) = LspSettings::for_worktree("odoo-lsp", dir) + && let Some(binary) = settings.binary + && let Some(command) = binary.path { - if let Some(command) = binary.path.clone() { - return Ok(zed::Command { - command, - args: binary.arguments.unwrap_or_default(), - env, - }); - } + return Ok(zed::Command { + command, + args: binary.arguments.unwrap_or_default(), + env, + }); } if let Some(command) = dir.which("odoo-lsp") { diff --git a/crates/python-stubfinder/Cargo.toml b/crates/python-stubfinder/Cargo.toml new file mode 100644 index 0000000..85c4ec0 --- /dev/null +++ b/crates/python-stubfinder/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "python-stubfinder" +version = "0.1.0" +edition = "2024" + +[dependencies] +# globwalk.workspace = true +serde.workspace = true +serde_json.workspace = true diff --git a/crates/python-stubfinder/src/main.rs b/crates/python-stubfinder/src/main.rs new file mode 100644 index 0000000..ffd9820 --- /dev/null +++ b/crates/python-stubfinder/src/main.rs @@ -0,0 +1,102 @@ +use std::env::args; +use std::fs::File; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio, exit}; + +use serde::Deserialize; + +fn main() { + let Some(mut module) = args().nth(1) else { + eprintln!("At least one Python module path is required!"); + exit(1); + }; + + let mut venv = std::env::var("VIRTUAL_ENV").ok().map(PathBuf::from); + if venv.is_none() { + let path = std::env::current_dir().unwrap().join(".venv"); + if path.exists() { + venv = Some(path); + } + } + + let Some(venv) = venv else { + eprintln!("No VIRTUAL_ENV defined and no .venv found at the working directory!"); + exit(1); + }; + + let python = venv.join("bin/python"); + let (stream, mut sink) = std::io::pipe().unwrap(); + let child = Command::new(python) + .arg("-") + .stdin(stream) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + _ = sink.write_all( + br#" +import sysconfig, site, json +print(json.dumps({ +'stdlib':sysconfig.get_path('stdlib'), +'site':site.getsitepackages(), +}))"#, + ); + drop(sink); + + #[derive(Deserialize)] + struct Output { + stdlib: String, + site: Vec, + } + + let result = child.wait_with_output().unwrap(); + let result: Output = serde_json::from_slice(&result.stdout).unwrap(); + + let mut alternatives = vec![result.stdlib]; + alternatives.extend(result.site); + module = module.split('.').collect::>().join("/"); + + for alternative in alternatives { + let path = Path::new(&alternative).join(&module).with_added_extension("pyi"); + if path.exists() { + print_and_quit(&module, &path); + } + + let path = Path::new(&alternative).join(&module).join("__init__.pyi"); + if path.exists() { + print_submodules(&path); + print_and_quit(&module, &path); + } + + let path = Path::new(&alternative).join(&module).with_added_extension("py"); + if path.exists() { + print_and_quit(&module, &path); + } + + let path = Path::new(&alternative).join(&module).join("__init__.py"); + if path.exists() { + print_submodules(&path); + print_and_quit(&module, &path); + } + } + + println!("Module {module} could not be resolved"); + exit(1); +} + +fn print_submodules(path: &Path) { + println!("# Visible child modules:"); + let root = path.parent().unwrap(); + for file in root.read_dir().unwrap() { + let Ok(file) = file else { continue }; + if file.path() != path { + println!("# {}", file.path().display()); + } + } +} + +fn print_and_quit(module: &str, path: &Path) -> ! { + println!("# {module} defined in {}:", path.display()); + std::io::copy(&mut File::open(path).unwrap(), &mut std::io::stdout()).unwrap(); + exit(0); +} diff --git a/justfile b/justfile index 24872cb..198021c 100644 --- a/justfile +++ b/justfile @@ -1,4 +1,4 @@ -test *args: (ensure_cargo "cargo-nextest") +test *args="--no-fail-fast": (ensure_cargo "cargo-nextest") cargo nextest run -p odoo-lsp -p odoo-lsp-tests {{args}} bench: (ensure_cargo "iai-callgrind-runner") diff --git a/src/analyze.rs b/src/analyze.rs index b32e670..da73fbe 100644 --- a/src/analyze.rs +++ b/src/analyze.rs @@ -4,9 +4,11 @@ use std::{ fmt::{Debug, Write}, ops::ControlFlow, - sync::Arc, + sync::{Arc, OnceLock, RwLock, atomic::Ordering}, }; +use dashmap::DashMap; +use fomat_macros::fomat; use lasso::Spur; use ropey::Rope; use tracing::instrument; @@ -15,15 +17,32 @@ use tree_sitter::{Node, Parser, QueryCursor, StreamingIterator}; use crate::{ ImStr, dig, format_loc, index::{_G, _I, _R, Index, Symbol}, - model::{Method, MethodReturnType, ModelEntry, ModelName, PropertyKind}, + model::{Method, ModelName, PropertyInfo}, test_utils, - utils::{ByteRange, PreTravel, RangeExt, TryResultExt, rope_conv}, + utils::{ByteOffset, ByteRange, Defer, PreTravel, RangeExt, TryResultExt, python_next_named_sibling, rope_conv}, }; use ts_macros::query; mod scope; pub use scope::Scope; +pub fn type_cache() -> &'static TypeCache { + static CACHE: OnceLock = OnceLock::new(); + CACHE.get_or_init(TypeCache::default) +} + +macro_rules! _T { + (@ $builtin:expr) => { + $crate::analyze::type_cache().get_or_intern(Type::PyBuiltin($builtin.into())) + }; + ($model:literal) => { + $crate::analyze::type_cache().get_or_intern(Type::Model($model.into())) + }; + ($expr:expr) => { + $crate::analyze::type_cache().get_or_intern($expr) + }; +} + pub static MODEL_METHODS: phf::Set<&str> = phf::phf_set!( "create", "copy", @@ -49,7 +68,7 @@ pub static MODEL_METHODS: phf::Set<&str> = phf::phf_set!( ); /// The subset of types that may resolve to a model. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum Type { Env, /// \*.env.ref() @@ -61,12 +80,70 @@ pub enum Type { Record(ImStr), Super, Method(ModelName, ImStr), + /// To hardcode some methods, such as dict.items() + PythonMethod(TypeId, ImStr), /// `odoo.http.request` HttpRequest, + Dict(TypeId, TypeId), + /// A bag of enumerated properties and their types + DictBag(Vec<(DictKey, TypeId)>), + /// Equivalent to Value, but may have a better semantic name + PyBuiltin(ImStr), + List(ListElement), + Tuple(Vec), + Iterable(Option), /// Can never be resolved, useful for non-model bindings. Value, } +impl Type { + #[inline] + fn is_dict(&self) -> bool { + matches!(self, Type::Dict(..)) + } +} + +#[derive(Clone, PartialEq, Eq, Hash)] +pub enum ListElement { + Vacant, + Occupied(TypeId), +} + +impl Debug for ListElement { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Vacant => f.write_str("..."), + Self::Occupied(inner) => inner.fmt(f), + } + } +} + +impl From for Option { + #[inline] + fn from(value: ListElement) -> Self { + match value { + ListElement::Vacant => None, + ListElement::Occupied(inner) => Some(inner), + } + } +} + +#[derive(Clone, PartialEq, Eq, Hash)] +pub enum DictKey { + String(ImStr), + Type(TypeId), +} + +impl Debug for DictKey { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::String(key) => key.fmt(f), + Self::Type(key) => key.fmt(f), + } + } +} + #[derive(Clone, Debug)] pub enum FunctionParam { Param(ImStr), @@ -92,6 +169,54 @@ impl core::fmt::Display for FunctionParam { } } +#[derive(Default)] +pub struct TypeCache { + types: RwLock>, + ids: DashMap, +} + +impl TypeCache { + #[inline] + pub fn get_or_intern(&self, type_: Type) -> TypeId { + if let Some(id) = self.ids.get(&type_) { + return *id; + } + self.intern(type_) + } + fn intern(&self, type_: Type) -> TypeId { + let mut types = self.types.write().unwrap(); + let id = TypeId(types.len() as u32); + types.push(type_.clone()); + self.ids.insert(type_, id); + id + } + #[inline] + pub fn resolve(&self, id: TypeId) -> Type { + self.types.read().unwrap()[id.0 as usize].clone() + } + pub fn is_dictlike(&self, id: TypeId) -> bool { + let types = self.types.read().unwrap(); + matches!( + &unsafe { types.get_unchecked(id.0 as usize) }, + Type::Dict(..) | Type::DictBag(..) + ) + } + pub fn is_dict(&self, id: TypeId) -> bool { + let types = self.types.read().unwrap(); + matches!(&unsafe { types.get_unchecked(id.0 as usize) }, Type::Dict(..)) + } +} + +#[repr(transparent)] +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct TypeId(u32); + +impl Debug for TypeId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + type_cache().resolve(*self).fmt(f) + } +} + pub fn normalize<'r, 'n>(node: &'r mut Node<'n>) -> &'r mut Node<'n> { let mut cursor = node.walk(); while matches!( @@ -135,7 +260,34 @@ query! { (identifier) @_func (lambda (lambda_parameters . (identifier) @ITER)))])) (#match? @_func "^(func|key)$") - (#match? @_mapped "^(mapp|filter|sort)ed$")) + (#match? @_mapped "^(mapp|filter|sort|group)ed$")) +} + +#[rustfmt::skip] +query! { + PythonBuiltinCall(Append, AppendList, AppendMap, AppendMapKey, AppendValue, UpdateMap, UpdateArgs); +// value.append(...) OR value['foobar'].append(...) +(call + (attribute + (subscript (identifier) @APPEND_MAP (string (string_content) @APPEND_MAP_KEY)) + (identifier) @_append) + (argument_list . (_) @APPEND_VALUE) + (#eq? @_append "append")) + +(call + (attribute + (identifier) @APPEND_LIST + (identifier) @APPEND) + (argument_list . (_) @APPEND_VALUE) + (#eq? @_append "append")) + +// value.update(...) +(call + (attribute + (identifier) @UPDATE_MAP + (identifier) @_update) + (argument_list) @UPDATE_ARGS + (#eq? @_update "update")) } pub type ScopeControlFlow = ControlFlow, bool>; @@ -143,9 +295,9 @@ impl Index { #[inline] pub fn model_of_range(&self, node: Node<'_>, range: ByteRange, contents: &str) -> Option { let (type_at_cursor, scope) = self.type_of_range(node, range, contents)?; - self.try_resolve_model(&type_at_cursor, &scope) + self.try_resolve_model(&type_cache().resolve(type_at_cursor), &scope) } - pub fn type_of_range(&self, root: Node<'_>, range: ByteRange, contents: &str) -> Option<(Type, Scope)> { + pub fn type_of_range(&self, root: Node<'_>, range: ByteRange, contents: &str) -> Option<(TypeId, Scope)> { // Phase 1: Determine the scope. let (self_type, fn_scope, self_param) = determine_scope(root, contents, range.start.0)?; @@ -175,7 +327,10 @@ impl Index { // TODO: fields if node_at_cursor.kind() == "identifier" && fn_scope.child_by_field_name("name") == Some(node_at_cursor) { return Some(( - Type::Method(_I(self_type).into(), contents[node_at_cursor.byte_range()].into()), + _T!(Type::Method( + _I(self_type).into(), + contents[node_at_cursor.byte_range()].into() + )), scope, )); } @@ -199,23 +354,45 @@ impl Index { // (_ left right) let lhs = node.named_child(0).unwrap(); if lhs.kind() == "identifier" - && let rhs = lhs.next_named_sibling().expect(format_loc!("rhs")) - && let Some(type_) = self.type_of(rhs, scope, contents) + && let rhs = python_next_named_sibling(lhs).expect(format_loc!("rhs")) + && let Some(id) = self.type_of(rhs, scope, contents) { let lhs = &contents[lhs.byte_range()]; - scope.insert(lhs.to_string(), type_); + scope.insert(lhs.to_string(), type_cache().resolve(id)); + } else if lhs.kind() == "subscript" + && let Some(map) = dig!(lhs, identifier) + && let Some(key) = dig!(lhs, string(1).string_content(1)) + && let Some(rhs) = python_next_named_sibling(lhs) + && let type_ = self.type_of(rhs, scope, contents) + && let Some(Type::DictBag(properties)) = scope.variables.get_mut(&contents[map.byte_range()]) + { + let type_ = type_.unwrap_or_else(|| _T!(Type::Value)); + let key = &contents[key.byte_range()]; + if let Some(idx) = properties.iter().position(|(prop, _)| match prop { + DictKey::String(prop) => prop.as_str() == key, + DictKey::Type(_) => false, + }) { + properties[idx].1 = type_; + } else { + properties.push((DictKey::String(ImStr::from(key)), type_)); + } + } else if lhs.kind() == "pattern_list" + && let Some(rhs) = python_next_named_sibling(lhs) + && let Some(type_) = self.type_of(rhs, scope, contents) + { + self.destructure_into_patternlist_like(lhs, type_, scope, contents); } } "for_statement" => { // (for_statement left right body) scope.enter(true); let lhs = node.named_child(0).unwrap(); - if lhs.kind() == "identifier" - && let rhs = lhs.next_named_sibling().expect(format_loc!("rhs")) + + if let Some(rhs) = python_next_named_sibling(lhs) && let Some(type_) = self.type_of(rhs, scope, contents) + && let Some(inner) = self.type_of_iterable(type_cache().resolve(type_)) { - let lhs = &contents[lhs.byte_range()]; - scope.insert(lhs.to_string(), type_); + self.destructure_into_patternlist_like(lhs, inner, scope, contents); } return ControlFlow::Continue(true); } @@ -239,15 +416,14 @@ impl Index { "list_comprehension" | "set_comprehension" | "dictionary_comprehension" | "generator_expression" if node.byte_range().contains(&offset) => { - // (_ body (for_in_clause left right)) + // (_ body: _ (for_in_clause left: _ right: _)) let for_in = node.named_child(1).unwrap(); - let lhs = for_in.named_child(0).unwrap(); - if lhs.kind() == "identifier" - && let rhs = lhs.next_named_sibling().expect(format_loc!("rhs")) - && let Some(type_) = self.type_of(rhs, scope, contents) + if let Some(lhs) = for_in.child_by_field_name("left") + && let Some(rhs) = for_in.child_by_field_name("right") + && let Some(tid) = self.type_of(rhs, scope, contents) + && let Some(inner) = self.type_of_iterable(type_cache().resolve(tid)) { - let lhs = &contents[lhs.byte_range()]; - scope.insert(lhs.to_string(), type_); + self.destructure_into_patternlist_like(lhs, inner, scope, contents); } } "call" if node.byte_range().contains_end(offset) => { @@ -259,14 +435,96 @@ impl Index { && let callee = mapped_call .nodes_for_capture_index(MappedCall::Callee as _) .next() - .unwrap() && let Some(type_) = self.type_of(callee, scope, contents) + .unwrap() && let Some(tid) = self.type_of(callee, scope, contents) { let iter = mapped_call .nodes_for_capture_index(MappedCall::Iter as _) .next() .unwrap(); let iter = &contents[iter.byte_range()]; - scope.insert(iter.to_string(), type_); + scope.insert(iter.to_string(), type_cache().resolve(tid)); + } + } + "call" => { + let query = PythonBuiltinCall::query(); + let mut cursor = QueryCursor::new(); + let mut matches = cursor.matches(query, node, contents.as_bytes()); + let Some(call) = matches.next() else { + return ControlFlow::Continue(false); + }; + if let Some(value) = call.nodes_for_capture_index(PythonBuiltinCall::AppendValue as _).next() + && let Some(tid) = self.type_of(value, scope, contents) + { + if let Some(list) = call.nodes_for_capture_index(PythonBuiltinCall::AppendList as _).next() { + if let Some(Type::List(slot @ ListElement::Vacant)) = + scope.variables.get_mut(&contents[list.byte_range()]) + { + *slot = ListElement::Occupied(tid); + } + } else if let Some(map) = call.nodes_for_capture_index(PythonBuiltinCall::AppendMap as _).next() { + let Some(key) = call + .nodes_for_capture_index(PythonBuiltinCall::AppendMapKey as _) + .next() + else { + return ControlFlow::Continue(false); + }; + let key = &contents[key.byte_range()]; + + if let Some(Type::DictBag(properties)) = scope.variables.get_mut(&contents[map.byte_range()]) + && let Some((_, slot)) = properties.iter_mut().find(|(prop, id)| match prop { + DictKey::String(prop) => { + prop.as_str() == key && _T!(Type::List(ListElement::Vacant)) == *id + } + DictKey::Type(_) => false, + }) { + *slot = _T!(Type::List(ListElement::Occupied(tid))); + } + } + } else if let Some(map) = call.nodes_for_capture_index(PythonBuiltinCall::UpdateMap as _).next() { + let Some(Type::DictBag(properties)) = scope.variables.get_mut(&contents[map.byte_range()]) else { + return ControlFlow::Continue(false); + }; + let Some(args) = call.nodes_for_capture_index(PythonBuiltinCall::UpdateArgs as _).next() else { + return ControlFlow::Continue(false); + }; + + let mut properties = core::mem::take(properties); + let mut cursor = args.walk(); + let mut children = args.named_children(&mut cursor); + if let Some(first) = children.by_ref().next() + && let Some(tid) = self.type_of(first, scope, contents) + && let Type::DictBag(update_props) = type_cache().resolve(tid) + { + properties.extend(update_props); + } + + for named_arg in children { + if named_arg.kind() == "keyword_argument" + && let Some(name) = named_arg.child_by_field_name("name") + && let Some(value) = named_arg.child_by_field_name("value") + { + let key = &contents[name.byte_range()]; + let type_ = self.type_of(value, scope, contents).unwrap_or_else(|| _T!(Type::Value)); + if let Some(idx) = properties.iter().position(|(prop, _)| match prop { + DictKey::String(prop) => prop.as_str() == key, + DictKey::Type(_) => false, + }) { + properties[idx].1 = type_; + } else { + properties.push((DictKey::String(ImStr::from(key)), type_)); + } + } else if named_arg.kind() == "dictionary_splat" + && let Some(value) = named_arg.named_child(0) + && let Some(tid) = self.type_of(value, scope, contents) + && let Type::DictBag(update_props) = type_cache().resolve(tid) + { + properties.extend(update_props); + } + } + + scope + .variables + .insert(contents[map.byte_range()].to_string(), Type::DictBag(properties)); } } "with_statement" => { @@ -279,7 +537,7 @@ impl Index { // (call (identifier) ..) // (as_pattern_target (identifier)))))) if let Some(value) = dig!(node, with_clause.with_item.as_pattern.call) - && let Some(target) = value.next_named_sibling() + && let Some(target) = python_next_named_sibling(value) && target.kind() == "as_pattern_target" && let Some(alias) = dig!(target, identifier) && let Some(callee) = value.named_child(0) @@ -291,10 +549,10 @@ impl Index { && let Some(type_) = self.type_of(first_arg, scope, contents) { let alias = &contents[alias.byte_range()]; - scope.insert(alias.to_string(), type_); + scope.insert(alias.to_string(), type_cache().resolve(type_)); } else if let Some(type_) = self.type_of(value, scope, contents) { let alias = &contents[alias.byte_range()]; - scope.insert(alias.to_string(), type_); + scope.insert(alias.to_string(), type_cache().resolve(type_)); } } } @@ -304,7 +562,7 @@ impl Index { ControlFlow::Continue(false) } /// [Type::Value] is not returned by this method. - pub fn type_of(&self, mut node: Node, scope: &Scope, contents: &str) -> Option { + pub fn type_of(&self, mut node: Node, scope: &Scope, contents: &str) -> Option { // What contributes to value types? // 1. *.env['foo'] => Model('foo') // 2. *.env.ref() => Model() @@ -339,25 +597,66 @@ impl Index { let lhs = node.child_by_field_name("value")?; let rhs = node.child_by_field_name("subscript")?; let obj_ty = self.type_of(lhs, scope, contents)?; - match obj_ty { + match type_cache().resolve(obj_ty) { Type::Env if rhs.kind() == "string" => { - Some(Type::Model(contents[rhs.byte_range().shrink(1)].into())) + Some(_T!(Type::Model(contents[rhs.byte_range().shrink(1)].into()))) } + Type::Env => Some(_T!["unknown"]), Type::Model(_) | Type::Record(_) => Some(obj_ty), + Type::Dict(key, value) => { + let rhs = self.type_of(rhs, scope, contents); + // FIXME: We trust that the user makes the correct judgment here and returns the type requested. + rhs.is_none_or(|rhs| rhs == key).then_some(value) + } + Type::DictBag(properties) => { + // compare by key + if let Some(rhs) = dig!(rhs, string_content(1)) { + let rhs = &contents[rhs.byte_range()]; + for (key, value) in properties { + match key { + DictKey::String(key) if key.as_str() == rhs => { + return Some(value); + } + DictKey::String(_) | DictKey::Type(_) => {} + } + } + return None; + } + + // compare by type + let rhs = self.type_of(rhs, scope, contents)?; + for (key, value) in properties { + match key { + DictKey::Type(key) if key == rhs => return Some(value), + DictKey::Type(_) | DictKey::String(_) => {} + } + } + + None + } + // FIXME: Again, just trust that the user is doing the right thing. + Type::List(ListElement::Occupied(slot)) => Some(slot), _ => None, } } "attribute" => self.type_of_attribute_node(node, scope, contents), "identifier" => { + if let Some(parent) = node.parent() + && parent.kind() == "attribute" + && parent.named_child(0).unwrap() != node + { + return self.type_of_attribute_node(parent, scope, contents); + } + let key = &contents[node.byte_range()]; if key == "super" { - return Some(Type::Super); + return Some(_T!(Type::Super)); } if let Some(type_) = scope.get(key) { - return Some(type_.clone()); + return Some(_T!(type_.clone())); } if key == "request" { - return Some(Type::HttpRequest); + return Some(_T!(Type::HttpRequest)); } None } @@ -366,28 +665,182 @@ impl Index { self.type_of(rhs, scope, contents) } "call" => self.type_of_call_node(node, scope, contents), - "binary_operator" | "boolean_operator" => { - // (_ left right) + "binary_operator" => { + if let Some(left) = node.child_by_field_name("left") + && let Some(left) = self.type_of(left, scope, contents) + { + return Some(left); + } + + self.type_of(node.child_by_field_name("right")?, scope, contents) + } + "boolean_operator" => { + if let Some(left) = node.child_by_field_name("right") + && let Some(left) = self.type_of(left, scope, contents) + { + return Some(left); + } + self.type_of(node.child_by_field_name("left")?, scope, contents) } + "conditional_expression" => { + // a if b else c + let ty = node + .named_child(0) + .and_then(|child| self.type_of(child, scope, contents)); + ty.or_else(|| { + node.named_child(2) + .and_then(|child| self.type_of(child, scope, contents)) + }) + } + "dictionary_comprehension" => { + let pair = dig!(node, pair)?; + let mut comprehension_scope; + let mut pair_scope = scope; + if let Some(for_in_clause) = dig!(node, for_in_clause(1)) + && let Some(scrutinee) = for_in_clause.child_by_field_name("left") + && let Some(iteratee) = for_in_clause.child_by_field_name("right") + && let Some(iter_ty) = self.type_of(iteratee, scope, contents) + && let Some(iter_ty) = self.type_of_iterable(type_cache().resolve(iter_ty)) + { + // FIXME: How to prevent this clone? + comprehension_scope = Scope::new(Some(scope.clone())); + self.destructure_into_patternlist_like(scrutinee, iter_ty, &mut comprehension_scope, contents); + pair_scope = &comprehension_scope; + } + let lhs = pair + .named_child(0) + .and_then(|lhs| self.type_of(lhs, pair_scope, contents)); + let rhs = pair + .named_child(1) + .and_then(|lhs| self.type_of(lhs, pair_scope, contents)); + if lhs.is_some() || rhs.is_some() { + let value_id = _T!(Type::Value); + Some(_T!(Type::Dict(lhs.unwrap_or(value_id), rhs.unwrap_or(value_id)))) + } else { + None + } + } + "dictionary" => { + let mut properties = vec![]; + for child in node.named_children(&mut node.walk()) { + if child.kind() == "pair" + && let Some(lhs) = child.child_by_field_name("key") + && let Some(rhs) = child.child_by_field_name("value") + { + let key; + if let Some(lhs) = dig!(lhs, string_content(1)) { + key = DictKey::String(ImStr::from(&contents[lhs.byte_range()])); + } else if matches!(lhs.kind(), "true" | "false" | "string" | "none" | "float" | "integer") { + key = DictKey::Type(_T!( @contents[lhs.byte_range()])); + } else if let Some(lhs) = self.type_of(lhs, scope, contents) { + key = DictKey::Type(lhs); + } else { + continue; + } + + let value = self.type_of(rhs, scope, contents).unwrap_or_else(|| _T!(Type::Value)); + properties.push((key, value)); + } + } + Some(_T!(Type::DictBag(properties))) + } + "list" => { + let mut slot = ListElement::Vacant; + for child in node.named_children(&mut node.walk()) { + if let Some(child) = self.type_of(child, scope, contents) { + slot = ListElement::Occupied(child); + break; + } + } + Some(_T!(Type::List(slot))) + } + "expression_list" | "tuple" => { + let mut cursor = node.walk(); + let value_id = _T!(Type::Value); + let tuple = node.named_children(&mut cursor).filter_map(|child| { + if child.kind() == "comment" { + return None; + } + Some(self.type_of(child, scope, contents).unwrap_or(value_id)) + }); + Some(_T!(Type::Tuple(tuple.collect()))) + } + "string" => Some(_T!( @ "str")), + "integer" => Some(_T!( @ "int")), + "float" => Some(_T!( @ "float")), + "true" | "false" | "comparison_operator" => Some(_T!( @ "bool")), _ => None, } } - fn type_of_call_node(&self, call: Node<'_>, scope: &Scope, contents: &str) -> Option { + fn type_of_iterable(&self, type_: Type) -> Option { + match type_ { + Type::Model(_) => Some(_T!(type_)), + Type::List(inner) => inner.into(), + Type::Iterable(inner) => inner, + // TODO: tuple -> union + _ => None, + } + } + fn wrap_in_container Type>(type_: Type, producer: F) -> Type { + match type_ { + Type::Model(..) => type_, + _ => producer(type_), + } + } + fn type_of_call_node(&self, call: Node<'_>, scope: &Scope, contents: &str) -> Option { let func = call.named_child(0)?; + if func.kind() == "identifier" { + match &contents[func.byte_range()] { + "zip" => { + let args = call.named_child(1)?; + let mut cursor = args.walk(); + let value_id = _T!(Type::Value); + let children = args.named_children(&mut cursor).map(|child| { + let type_ = type_cache().resolve(self.type_of(child, scope, contents).unwrap_or(value_id)); + self.type_of_iterable(type_).unwrap_or(value_id) + }); + let tuple = _T!(Type::Tuple(children.collect())); + return Some(_T!(Type::Iterable(Some(tuple)))); + } + "enumerate" => { + let arg = call.named_child(1)?.named_child(0); + let arg = arg + .and_then(|arg| self.type_of(arg, scope, contents)) + .unwrap_or_else(|| _T!(Type::Value)); + let intid = _T!(Type::PyBuiltin("int".into())); + let tuple = _T!(Type::Tuple(vec![intid, arg])); + return Some(_T!(Type::Iterable(Some(tuple)))); + } + "tuple" => { + let args = call.named_child(1)?; + if args.kind() == "argument_list" { + let mut cursor = args.walk(); + let value_id = _T!(Type::Value); + let children = args + .named_children(&mut cursor) + .map(|child| self.type_of(child, scope, contents).unwrap_or(value_id)); + return Some(_T!(Type::Tuple(children.collect()))); + } + } + "super" => {} + _ => return None, + }; + } + let func = self.type_of(func, scope, contents)?; - match func { + match type_cache().resolve(func) { Type::RefFn => { // (call (_) @func (argument_list . (string) @xml_id)) let xml_id = call.named_child(1)?.named_child(0)?; if xml_id.kind() == "string" { - Some(Type::Record(contents[xml_id.byte_range().shrink(1)].into())) + Some(_T!(Type::Record(contents[xml_id.byte_range().shrink(1)].into()))) } else { None } } - Type::ModelFn(model) => Some(Type::Model(model)), - Type::Super => scope.get(scope.super_.as_deref()?).cloned(), + Type::ModelFn(model) => Some(_T!(Type::Model(model))), + Type::Super => Some(_T!(scope.get(scope.super_.as_deref()?).cloned()?)), Type::Method(model, mapped) if mapped == "mapped" => { // (call (_) @func (argument_list . [(string) (lambda)] @mapped)) let mapped = call.named_child(1)?.named_child(0)?; @@ -396,7 +849,9 @@ impl Index { let mut model: Spur = model.into(); let mut mapped = &contents[mapped.byte_range().shrink(1)]; self.models.resolve_mapped(&mut model, &mut mapped, None).ok()?; - self.type_of_attribute(&Type::Model(_R(model).into()), mapped, scope) + let type_ = self.type_of_attribute(&Type::Model(_R(model).into()), mapped, scope)?; + let type_ = Index::wrap_in_container(type_, |it| Type::List(ListElement::Occupied(_T!(it)))); + Some(_T!(type_)) } "lambda" => { // (lambda (lambda_parameters)? body: (_)) @@ -409,32 +864,199 @@ impl Index { } } let body = mapped.child_by_field_name(b"body")?; - self.type_of(body, &scope, contents) + let type_ = self.type_of(body, &scope, contents).unwrap_or_else(|| _T!(Type::Value)); + let type_ = Index::wrap_in_container(type_cache().resolve(type_), |it| { + Type::List(ListElement::Occupied(_T!(it))) + }); + Some(_T!(type_)) + } + _ => None, + } + } + Type::Method(model, grouped) if grouped == "grouped" => { + // (call (_) @func (argument_list . [(string) (lambda)] @mapped)) + let grouped = call.named_child(1)?.named_child(0)?; + match grouped.kind() { + "string" => { + let mut model: Spur = model.into(); + let mut grouped = &contents[grouped.byte_range().shrink(1)]; + self.models.resolve_mapped(&mut model, &mut grouped, None).ok()?; + let model = Type::Model(_R(model).into()); + let groupby = self.type_of_attribute(&model, grouped, scope)?; + Some(_T!(Type::Dict(_T!(groupby), _T!(model)))) + } + "lambda" => { + let mut scope = Scope::new(Some(scope.clone())); + if let Some(params) = grouped.child_by_field_name(b"parameters") { + let first_arg = params.named_child(0)?; + if first_arg.kind() == "identifier" { + let first_arg = &contents[first_arg.byte_range()]; + scope.insert(first_arg.to_string(), Type::Model(_R(model).into())); + } + } + let body = grouped.child_by_field_name(b"body")?; + let groupby = self.type_of(body, &scope, contents).unwrap_or_else(|| _T!(Type::Value)); + let model = Type::Model(_R(model).into()); + Some(_T!(Type::Dict(groupby, _T!(model)))) } _ => None, } } + Type::Method(model, read_group) if read_group == "_read_group" => { + let mut groupby = vec![]; + let mut aggs = vec![]; + let args = call.named_child(1)?; + + fn gather_attributes<'out>(contents: &'out str, arg: Node, out: &mut Vec<&'out str>) { + let mut cursor = arg.walk(); + for field in arg.named_children(&mut cursor) { + if let Some(field) = dig!(field, string_content(1)) { + let mut field = &contents[field.byte_range()]; + if let Some((inner, _)) = field.split_once(':') { + field = inner; + } + out.push(field); + } + } + } + + for (idx, arg) in args.named_children(&mut args.walk()).enumerate().take(3) { + if arg.kind() == "keyword_argument" { + let out = match &contents[arg.child_by_field_name("key")?.byte_range()] { + "groupby" => &mut groupby, + "aggregates" => &mut aggs, + _ => continue, + }; + let Some(arg) = arg.child_by_field_name("value") else { + continue; + }; + if arg.kind() != "list" { + continue; + } + gather_attributes(contents, arg, out); + continue; + } + + if arg.kind() != "list" || idx > 2 || idx == 0 { + continue; + } + + let out = if idx == 1 { &mut groupby } else { &mut aggs }; + gather_attributes(contents, arg, out); + } + + groupby.extend(aggs); + groupby.dedup(); + let model = Type::Model(_R(model).into()); + let value_id = _T!(Type::Value); + // FIXME: This is not quite correct as only recordset and numeric aggregations make sense. + let aggs = groupby + .into_iter() + .map(|attr| match self.type_of_attribute(&model, attr, scope) { + Some(type_) => _T!(type_), + None => value_id, + }); + let tuple = _T!(Type::Tuple(aggs.collect())); + Some(_T!(Type::List(ListElement::Occupied(tuple)))) + } Type::Method(model, method) => { let method = _G(&method)?; - let ret_model = self.resolve_method_returntype(method.into(), *model)?; - Some(Type::Model(_R(ret_model).into())) + let args = self.prepare_call_scope(model, method.into(), call, scope, contents); + Some(self.eval_method_rtype(method.into(), *model, args)?) + } + Type::PythonMethod(dict, items) if type_cache().is_dict(dict) && items == "items" => { + let Type::Dict(lhs, rhs) = type_cache().resolve(dict) else { + unreachable!() + }; + let tuple = _T!(Type::Tuple(vec![lhs, rhs])); + Some(_T!(Type::Iterable(Some(tuple)))) } - Type::Env | Type::Record(..) | Type::Model(..) | Type::HttpRequest | Type::Value => None, + Type::Env + | Type::Record(..) + | Type::Model(..) + | Type::HttpRequest + | Type::Value + | Type::PyBuiltin(..) + | Type::Dict(..) + | Type::DictBag(..) + | Type::List(..) + | Type::Iterable(..) + | Type::Tuple(..) + | Type::PythonMethod(..) => None, } } - fn type_of_attribute_node(&self, attribute: Node<'_>, scope: &Scope, contents: &str) -> Option { + + #[instrument(skip_all, fields(model, method))] + fn prepare_call_scope( + &self, + model: ModelName, + method: Symbol, + call: Node, + scope: &Scope, + contents: &str, + ) -> Option<(Vec, Scope)> { + // (call + // (arguments_list + // (_) + // (keyword_argument (identifier) (_)))) + let arguments_list = dig!(call, argument_list(1))?; + + let model = self.models.populate_properties(model, &[])?; + let method = model.methods.as_ref()?.get(&method)?; + let arguments = method.arguments.clone().unwrap_or_default(); + if arguments.is_empty() { + return None; + } + + drop(model); + let mut argtypes = Scope::new(None); + let mut args = vec![]; + for (idx, arg) in arguments_list.named_children(&mut arguments_list.walk()).enumerate() { + if arg.kind() == "keyword_argument" + && let Some(key) = arg.child_by_field_name("key") + && let Some(value) = arg.child_by_field_name("value") + { + let key = &contents[key.byte_range()]; + if !arguments.iter().any(|arg| match arg { + FunctionParam::Named(arg) => arg.as_str() == key, + _ => false, + }) { + continue; + } + let Some(tid) = self.type_of(value, scope, contents) else { + continue; + }; + args.push(key.into()); + argtypes.insert(key.to_string(), type_cache().resolve(tid)); + } else if let Some(FunctionParam::Param(argname)) = arguments.get(idx) + && let Some(tid) = self.type_of(arg, scope, contents) + { + args.push(argname.clone()); + argtypes.insert(argname.to_string(), type_cache().resolve(tid)); + } else { + continue; + } + } + + Some((args, argtypes)) + } + #[instrument(skip_all, ret)] + fn type_of_attribute_node(&self, attribute: Node<'_>, scope: &Scope, contents: &str) -> Option { let lhs = attribute.named_child(0)?; - let lhs = self.type_of(lhs, scope, contents)?; + let lhsid = self.type_of(lhs, scope, contents)?; + let lhs = type_cache().resolve(lhsid); let rhs = attribute.named_child(1)?; + let attrname = &contents[rhs.byte_range()]; match &contents[rhs.byte_range()] { "env" if matches!(lhs, Type::Model(..) | Type::Record(..) | Type::HttpRequest) => Some(Type::Env), "ref" if matches!(lhs, Type::Env) => Some(Type::RefFn), "user" if matches!(lhs, Type::Env) => Some(Type::Model("res.users".into())), "company" | "companies" if matches!(lhs, Type::Env) => Some(Type::Model("res.company".into())), - "mapped" => { + "mapped" | "grouped" | "_read_group" => { let model = self.try_resolve_model(&lhs, scope)?; - Some(Type::Method(model, "mapped".into())) + Some(Type::Method(model, attrname.into())) } + dict_method @ "items" if lhs.is_dict() => Some(Type::PythonMethod(lhsid, dict_method.into())), func if MODEL_METHODS.contains(func) => match lhs { Type::Model(model) => Some(Type::ModelFn(model)), Type::Record(xml_id) => { @@ -447,19 +1069,54 @@ impl Index { ident if rhs.kind() == "identifier" => self.type_of_attribute(&lhs, ident, scope), _ => None, } + .map(|it| _T!(it)) } + #[instrument(skip_all, fields(attr=attr), ret)] pub fn type_of_attribute(&self, type_: &Type, attr: &str, scope: &Scope) -> Option { let model = self.try_resolve_model(type_, scope)?; let model_entry = self.models.populate_properties(model, &[])?; - let attr_key = _G(attr)?; - let attr_kind = model_entry.prop_kind(attr_key)?; - match attr_kind { - PropertyKind::Field => { - drop(model_entry); - let relation = self.models.resolve_related_field(attr_key.into(), model.into())?; - Some(Type::Model(_R(relation).into())) - } - PropertyKind::Method => Some(Type::Method(model, attr.into())), + if let Some(attr_key) = _G(attr) + && let Some(attr_kind) = model_entry.prop_kind(attr_key) + { + match attr_kind { + PropertyInfo::Field(type_) => { + drop(model_entry); + if let Some(relation) = self.models.resolve_related_field(attr_key.into(), model.into()) { + return Some(Type::Model(_R(relation).into())); + } + + match _R(type_) { + "Selection" | "Char" | "Text" | "Html" => Some(Type::PyBuiltin("str".into())), + "Integer" => Some(Type::PyBuiltin("int".into())), + "Float" | "Monetary" => Some(Type::PyBuiltin("float".into())), + "Date" => Some(Type::PyBuiltin("date".into())), + "Datetime" => Some(Type::PyBuiltin("datetime".into())), + _ => None, + } + } + PropertyInfo::Method => Some(Type::Method(model, attr.into())), + } + } else { + match attr { + "id" if matches!(type_, Type::Model(..) | Type::Record(..)) => Some(Type::PyBuiltin("int".into())), + "ids" if matches!(type_, Type::Model(..) | Type::Record(..)) => { + Some(Type::List(ListElement::Occupied(_T!(Type::PyBuiltin("int".into()))))) + } + "display_name" if matches!(type_, Type::Model(..) | Type::Record(..)) => { + Some(Type::PyBuiltin("str".into())) + } + "create_date" | "write_date" if matches!(type_, Type::Model(..) | Type::Record(..)) => { + Some(Type::PyBuiltin("datetime".into())) + } + "create_uid" | "write_uid" if matches!(type_, Type::Model(..) | Type::Record(..)) => { + Some(Type::Model("res.users".into())) + } + "_fields" if matches!(type_, Type::Model(..) | Type::Record(..)) => { + Some(Type::Dict(_T!(Type::PyBuiltin("str".into())), _T!["ir.model.fields"])) + } + "env" if matches!(type_, Type::Model(..) | Type::Record(..) | Type::HttpRequest) => Some(Type::Env), + _ => None, + } } } pub fn has_attribute(&self, type_: &Type, attr: &str, scope: &Scope) -> bool { @@ -485,6 +1142,81 @@ impl Index { _ => None, } } + #[inline] + pub fn type_display(&self, type_: TypeId) -> Option { + self.type_display_indent(type_, 0) + } + fn type_display_indent(&self, type_: TypeId, indent: usize) -> Option { + match type_cache().resolve(type_) { + Type::Dict(lhs, rhs) => { + let lhs = self.type_display_indent(lhs, indent); + let lhs = lhs.as_deref().unwrap_or("..."); + let rhs = self.type_display_indent(rhs, indent); + let rhs = rhs.as_deref().unwrap_or("..."); + Some(fomat! { "dict[" (lhs) ", " (rhs) "]" }) + } + Type::DictBag(properties) => { + let preindent = " ".repeat(indent + 2); + let empty_properties = properties.is_empty(); + let properties_fragment = fomat! { + for (key, value) in properties { + (preindent) + match key { + DictKey::String(key) => { "\"" (key) "\"" } + DictKey::Type(key) if type_cache().is_dictlike(key) => { "{...}" } + DictKey::Type(key) => { (self.type_display_indent(key, indent + 2).as_deref().unwrap_or("...")) } + } ": " (self.type_display_indent(value, indent + 2).as_deref().unwrap_or("...")) + } sep { ",\n" } + }; + let unindent = " ".repeat(indent); + Some(fomat! { + if !empty_properties { + "{\n" (properties_fragment) "\n" (unindent) "}" + } else { + "{}" + } + }) + } + Type::PyBuiltin(builtin) => Some(builtin.as_str().into()), + Type::List(slot) => { + let slot = match slot { + ListElement::Vacant => None, + ListElement::Occupied(slot) => self.type_display_indent(slot, indent), + }; + Some(match slot { + Some(slot) => format!("list[{slot}]"), + None => "list".into(), + }) + } + Type::Env => Some("Environment".into()), + Type::Model(model) => Some(format!(r#"Model["{model}"]"#)), + Type::Record(xml_id) => { + let xml_id = _G(xml_id)?; + let record = self.records.get(&xml_id.into())?; + Some(_R(record.model?).into()) + } + Type::Tuple(items) => Some(fomat! { + "tuple[" + for item in items { + (self.type_display_indent(item, indent).as_deref().unwrap_or("...")) + } sep { ", " } + "]" + }), + Type::Iterable(output) => { + let output = output.and_then(|inner| self.type_display_indent(inner, indent)); + let output = output.as_deref().unwrap_or("..."); + Some(format!("Iterable[{output}]")) + } + Type::Method(..) => unreachable!("Bug: this function should not handle methods"), + Type::RefFn | Type::ModelFn(_) | Type::Super | Type::HttpRequest | Type::Value | Type::PythonMethod(..) => { + if cfg!(debug_assertions) { + Some(format!("{type_:?}")) + } else { + None + } + } + } + } /// Iterates depth-first over `node` using [`PreTravel`]. Automatically calls [`Scope::exit`] at suitable points. /// /// [`ControlFlow::Continue`] accepts a boolean to indicate whether [`Scope::enter`] was called. @@ -519,29 +1251,72 @@ impl Index { (scope, None) } /// Resolves the return type of a method as well as populating its arguments and docstring. + /// + /// `parameters` can be provided using [`Index::prepare_call_scope`]. #[instrument(level = "trace", ret, skip(self, model), fields(model = _R(model)))] - pub fn resolve_method_returntype(&self, method: Symbol, model: Spur) -> Option> { + pub fn eval_method_rtype( + &self, + method: Symbol, + model: Spur, + parameters: Option<(Vec, Scope)>, + ) -> Option { _ = self.models.populate_properties(model.into(), &[]); - let mut model_entry = self.models.get_mut(&model.into())?; + let mut model_entry = self.models.try_get_mut(&model.into()).expect(format_loc!("deadlock"))?; let method_obj = model_entry.methods.as_mut()?.get_mut(&method)?; - match method_obj.return_type { - MethodReturnType::Unprocessed => {} - MethodReturnType::Value | MethodReturnType::Processing => return None, - MethodReturnType::Relational(rel) => return Some(rel), + + if method_obj + .pending_eval + .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) + .is_err() + { + return None; + } + + let _guard = Defer(Some(|| { + if let Some(model_entry) = self.models.get_mut(&model.into()) + && let Some(methods) = model_entry.methods.as_ref() + && let Some(method) = methods.get(&method) + { + method.pending_eval.store(false, Ordering::Relaxed); + } + })); + + let (argnames, mut scope) = parameters.unwrap_or_default(); + let cache_key = argnames + .into_iter() + .map(|arg| _T!(scope.variables.get(&*arg).cloned().unwrap_or(Type::Value))) + .collect::>(); + if let Some(tid) = method_obj.eval_cache.get(&cache_key) { + drop(model_entry); + return Some(tid); } let location = method_obj.locations.first().cloned()?; - Arc::make_mut(method_obj).return_type = MethodReturnType::Processing; drop(model_entry); - let contents = test_utils::fs::read_to_string(location.path.to_path()).unwrap(); - let rope = Rope::from_str(&contents); - let rope = rope.slice(..); - - let mut parser = Parser::new(); - parser.set_language(&tree_sitter_python::LANGUAGE.into()).unwrap(); - let range: ByteRange = rope_conv(location.range, rope).ok()?; - let ast = parser.parse(contents.as_bytes(), None)?; + let ast; + let contents; + let end_offset: ByteOffset; + let path = location.path.to_path(); + if let Some(cached) = self.ast_cache.get(&path) { + end_offset = rope_conv(location.range.end, cached.rope.slice(..)); + ast = cached.tree.clone(); + contents = String::from(cached.rope.clone()); + } else { + contents = test_utils::fs::read_to_string(location.path.to_path()).unwrap(); + let rope = Rope::from_str(&contents); + end_offset = rope_conv(location.range.end, rope.slice(..)); + let mut parser = Parser::new(); + parser.set_language(&tree_sitter_python::LANGUAGE.into()).unwrap(); + ast = parser.parse(contents.as_bytes(), None)?; + self.ast_cache.insert( + path, + Arc::new(crate::index::AstCacheItem { + tree: ast.clone(), + rope, + }), + ); + } // TODO: Improve this heuristic fn is_toplevel_return(mut node: Node) -> bool { @@ -566,8 +1341,7 @@ impl Index { node.parent().is_some_and(is_block_of_class) } - let (self_type, fn_scope, self_param) = determine_scope(ast.root_node(), &contents, range.end.0)?; - let mut scope = Scope::default(); + let (self_type, fn_scope, self_param) = determine_scope(ast.root_node(), &contents, end_offset.0)?; let self_type = match self_type { Some(type_) => &contents[type_.byte_range().shrink(1)], None => "", @@ -585,10 +1359,12 @@ impl Index { let Some(type_) = self.type_of(child, scope, &contents) else { return ControlFlow::Continue(entered); }; - let Some(resolved) = self.try_resolve_model(&type_, scope) else { - return ControlFlow::Continue(entered); + + let type_ = type_cache().resolve(type_); + return match self.try_resolve_model(&type_, scope) { + Some(resolved) => ControlFlow::Break(Some(Type::Model(ImStr::from(_R(resolved))))), + None => ControlFlow::Break(Some(type_)), }; - return ControlFlow::Break(Some(resolved)); } ControlFlow::Continue(entered) @@ -596,10 +1372,6 @@ impl Index { let mut model = self.models.try_get_mut(&model.into()).expect(format_loc!("deadlock"))?; let method = Arc::make_mut(model.methods.as_mut()?.get_mut(&method)?); - match type_ { - Some(rel) => method.return_type = MethodReturnType::Relational(rel), - None => method.return_type = MethodReturnType::Value, - } let docstring = Self::parse_method_docstring(fn_scope, &contents) .map(|doc| ImStr::from(Method::postprocess_docstring(doc))); @@ -633,7 +1405,39 @@ impl Index { method.arguments = Some(args.collect()); } - type_ + method.pending_eval.store(false, Ordering::Release); + if let Some(type_) = type_ { + let tid = _T!(type_); + method.eval_cache.insert(cache_key, tid); + Some(tid) + } else { + None + } + } + /// `pattern` is `(identifier | pattern_list | tuple_pattern)`, the `a, b` in `for a, b in ...`. + fn destructure_into_patternlist_like(&self, pattern: Node, tid: TypeId, scope: &mut Scope, contents: &str) { + if pattern.kind() == "identifier" { + let name = &contents[pattern.byte_range()]; + scope.insert(name.to_string(), type_cache().resolve(tid)); + } else if matches!(pattern.kind(), "pattern_list" | "tuple_pattern") { + if let Type::Tuple(mut inner) = type_cache().resolve(tid) { + inner.reverse(); + for child in pattern.named_children(&mut pattern.walk()) { + if matches!(child.kind(), "identifier" | "tuple_pattern") + && let Some(type_) = inner.pop() + { + self.destructure_into_patternlist_like(child, type_, scope, contents); + } + } + } else if let Some(inner) = self.type_of_iterable(type_cache().resolve(tid)) { + // spread this type to all params + for child in pattern.named_children(&mut pattern.walk()) { + if matches!(child.kind(), "identifier" | "tuple_pattern") { + self.destructure_into_patternlist_like(child, inner, scope, contents); + } + } + } + } } fn parse_method_docstring<'out>(fn_scope: Node, contents: &'out str) -> Option<&'out str> { let block = fn_scope.child_by_field_name("body")?; @@ -694,11 +1498,11 @@ pub fn determine_scope<'out, 'node>( mod tests { use pretty_assertions::assert_eq; use ropey::Rope; - use tower_lsp_server::lsp_types::Position; + use tower_lsp_server::ls_types::Position; use tree_sitter::{Parser, QueryCursor, StreamingIterator, StreamingIteratorMut}; - use crate::analyze::FieldCompletion; - use crate::index::{_I, _R}; + use crate::analyze::{FieldCompletion, Type, type_cache}; + use crate::index::_I; use crate::utils::{ByteOffset, acc_vec, rope_conv}; use crate::{index::Index, test_utils::cases::foo::prepare_foo_index}; @@ -756,7 +1560,7 @@ class Foo(models.Model): "#; let ast = parser.parse(contents, None).unwrap(); let rope = Rope::from(contents); - let fn_start: ByteOffset = rope_conv(Position { line: 3, character: 1 }, rope.slice(..)).unwrap(); + let fn_start: ByteOffset = rope_conv(Position { line: 3, character: 1 }, rope.slice(..)); let fn_scope = ast .root_node() .named_descendant_for_byte_range(fn_start.0, fn_start.0) @@ -773,8 +1577,8 @@ class Foo(models.Model): }; assert_eq!( - index.resolve_method_returntype(_I("test").into(), _I("bar")).map(_R), - Some("foo") + index.eval_method_rtype(_I("test").into(), _I("bar"), None), + Some(type_cache().get_or_intern(Type::Model("foo".into()))) ) } @@ -786,8 +1590,8 @@ class Foo(models.Model): }; assert_eq!( - index.resolve_method_returntype(_I("test").into(), _I("quux")).map(_R), - Some("foo") + index.eval_method_rtype(_I("test").into(), _I("quux"), None), + Some(type_cache().get_or_intern(Type::Model("foo".into()))) ) } } diff --git a/src/backend.rs b/src/backend.rs index 9944641..065998f 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -16,9 +16,10 @@ use fomat_macros::fomat; use globwalk::FileType; use smart_default::SmartDefault; use tower_lsp_server::Client; -use tower_lsp_server::{UriExt, lsp_types::*}; +use tower_lsp_server::ls_types::*; use tree_sitter::{Parser, Tree}; +use crate::analyze::{Scope, Type, TypeId, type_cache}; use crate::prelude::*; use crate::component::{Prop, PropDescriptor}; @@ -97,6 +98,7 @@ pub struct Capabilities { pub pull_diagnostics: AtomicBool, /// Whether [`workspace/workspaceFolders`][request::WorkspaceFoldersRequest] can be called. pub workspace_folders: AtomicBool, + pub can_create_wdp: AtomicBool, } pub struct TextDocumentItem { @@ -222,7 +224,7 @@ impl Backend { } } - #[instrument(skip_all)] + #[instrument(skip_all, ret)] pub async fn on_change(&self, params: TextDocumentItem) -> anyhow::Result<()> { let split_uri = params.uri.path().as_str().rsplit_once('.'); let rope; @@ -286,6 +288,7 @@ impl Backend { } if eager_diagnostics { + let client = self.client.clone(); let diagnostics = { self.document_map .get(params.uri.path().as_str()) @@ -293,9 +296,11 @@ impl Backend { .diagnostics_cache .clone() }; - self.client - .publish_diagnostics(params.uri, diagnostics, Some(params.version)) - .await; + tokio::spawn(async move { + client + .publish_diagnostics(params.uri, diagnostics, Some(params.version)) + .await + }); } Ok(()) @@ -341,10 +346,10 @@ impl Backend { for change in delta { // TODO: Handle full text changes in delta let range = ok!(change.range, "delta without range"); - let start: ByteOffset = ok!(rope_conv(range.start, rope), "delta start"); + let start: ByteOffset = rope_conv(range.start, rope); // The old rope is used to calculate the *old* range-end, because // the diff may have caused it to fall out of the new rope's bounds. - let end: ByteOffset = ok!(rope_conv(range.end, old_rope), "delta end"); + let end: ByteOffset = rope_conv(range.end, old_rope); let len_new = change.text.len(); let start_position = tree_sitter::Point { row: range.start.line as usize, @@ -356,7 +361,7 @@ impl Backend { }; // calculate new_end_position using rope let new_end_offset = ByteOffset(start.0 + len_new); - let new_end_position: Position = ok!(rope_conv(new_end_offset, rope), "new_end_position"); + let new_end_position: Position = rope_conv(new_end_offset, rope); let new_end_position = tree_sitter::Point { row: new_end_position.line as usize, column: new_end_position.character as usize, @@ -561,7 +566,7 @@ impl Index { if !items.has_space() { return Ok(()); } - let range = ok!(rope_conv(range, rope), "(complete_xml_id) range"); + let range = rope_conv(range, rope); let Ok(by_prefix) = self.records.by_prefix.try_read() else { return Ok(()); }; @@ -644,7 +649,7 @@ impl Index { return Ok(()); } let model_key = _I(&model); - let range = ok!(rope_conv(range.clone(), rope), "range={:?}", range); + let range = rope_conv(range.clone(), rope); let Some(model_entry) = self.models.populate_properties(model_key.into(), &[]) else { return Ok(()); }; @@ -719,7 +724,7 @@ impl Index { ) -> anyhow::Result<()> { let component = ok!(_G(component), "(complete_component_prop) component"); let component = ok!(self.components.get(&component.into()), "component"); - let range = ok!(rope_conv(range, rope), "(complete_component_prop) range"); + let range = rope_conv(range, rope); let completions = component.props.iter().map(|(prop, desc)| { let prop = _R(prop); CompletionItem { @@ -773,14 +778,15 @@ impl Index { let method = _G(&completion.label)?; let model_key = _G(&model)?; let method_name = _R(method); - let rtype = self.resolve_method_returntype(method.into(), model_key).map(_R); + let rtype = self.eval_method_rtype(method.into(), model_key, None); + let rtype = rtype.and_then(|rtype| self.type_display(rtype)); let entry = self.models.get(&model_key.into())?; let methods = entry.methods.as_ref()?; let method_entry = methods.get(&method.into())?; completion.documentation = Some(Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, - value: self.method_docstring(method_name, method_entry, rtype), + value: self.method_docstring(method_name, method_entry, rtype.as_deref()), })); Some(()) @@ -813,10 +819,7 @@ impl Index { } } }; - let rtype = match rtype { - Some(type_) => format!("Model[\"{type_}\"]"), - None => "...".to_string(), - }; + let rtype = rtype.unwrap_or("..."); let params_fragment = match method.arguments.as_deref() { None => "...".to_string(), Some([]) => String::new(), @@ -831,11 +834,35 @@ impl Index { fomat! { "```python\n" "(method) def " (name) "(" (params_fragment) ") -> " (rtype) - "\n" - "```\n" + "\n```\n" (origin_fragment) } } + #[instrument(level = "trace", skip_all, fields(name, type_))] + pub fn hover_variable( + &self, + name: Option<&str>, + type_: TypeId, + range: Option, + ) -> anyhow::Result> { + let type_fragment = match type_cache().resolve(type_) { + Type::Method(model, method) => return self.hover_property_name(&method, _R(model), range), + _ => self.type_display(type_), + }; + let type_fragment = type_fragment.as_deref().unwrap_or("Unknown"); + let value = fomat! { + "```py\n" + if let Some(name) = name { "(variable) " (name) ": " } (type_fragment) + "\n```" + }; + Ok(Some(Hover { + range, + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value, + }), + })) + } pub fn completion_resolve_field(&self, completion: &mut CompletionItem) -> Option<()> { let CompletionData { model } = completion .data @@ -887,11 +914,13 @@ impl Index { if let Some(help) = &field.help { (help.to_string()) } } } + #[instrument(level = "trace", skip_all, fields(name, model))] pub fn hover_property_name(&self, name: &str, model: &str, range: Option) -> anyhow::Result> { let model_key = _I(model); let entry = some!(self.models.populate_properties(model_key.into(), &[])); - let prop = some!(_G(name)); - if let Some(ref fields) = entry.fields + let prop = _G(name); + if let Some(prop) = prop + && let Some(ref fields) = entry.fields && let Some(field) = fields.get(&prop.into()) { Ok(Some(Hover { @@ -901,22 +930,26 @@ impl Index { value: self.field_docstring(field, true), }), })) - } else if let Some(ref methods) = entry.methods + } else if let Some(prop) = prop + && let Some(ref methods) = entry.methods && methods.contains_key(&prop.into()) { drop(entry); - let rtype = self.resolve_method_returntype(prop.into(), model_key); + let rtype = self.eval_method_rtype(prop.into(), model_key, None); + let rtype = rtype.and_then(|rtype| self.type_display(rtype)); let model = self.models.get(&model_key.into()).unwrap(); let method = model.methods.as_ref().unwrap().get(&prop.into()).unwrap(); Ok(Some(Hover { range, contents: HoverContents::Markup(MarkupContent { kind: MarkupKind::Markdown, - value: self.method_docstring(name, method, rtype.map(_R)), + value: self.method_docstring(name, method, rtype.as_deref()), }), })) } else { - Ok(None) + drop(entry); + let attr_type = some!(self.type_of_attribute(&Type::Model(ImStr::from(model)), name, &Scope::new(None))); + self.hover_variable(Some(name), type_cache().get_or_intern(attr_type), range) } } /// Returns a Markdown-formatted docstring for a model. @@ -943,7 +976,7 @@ impl Index { fomat! { if let Some(name) = model_name { "```python\n" - if let Some(ident) = identifier { (ident) ": " } "Model[\"" (name) "\"]\n" + if let Some(ident) = identifier { "(variable) " (ident) ": " } "Model[\"" (name) "\"]\n" "``` \n" } if let Some(module) = module { @@ -1144,7 +1177,7 @@ impl Index { rope: RopeSlice<'_>, items: &mut MaxVec, ) -> anyhow::Result<()> { - let range = ok!(rope_conv(range, rope)); + let range = rope_conv(range, rope); let completions = self.actions.iter().flat_map(|tag| { let tag = tag.key().to_string(); Some(CompletionItem { @@ -1186,7 +1219,7 @@ impl Index { rope: RopeSlice<'_>, items: &mut MaxVec, ) -> anyhow::Result<()> { - let range = ok!(rope_conv(range, rope)); + let range = rope_conv(range, rope); let completions = self.widgets.iter().flat_map(|widget| { let widget = widget.key().to_string(); Some(CompletionItem { @@ -1219,7 +1252,7 @@ impl Text { out = NULL_RANGE; continue; }; - let range: ByteRange = rope_conv(range, rope).ok()?; + let range: ByteRange = rope_conv(range, rope); out = out.start.min(range.start.0)..out.end.max(range.end.0); } debug!("(damage_zone)\nseed={seed:?}\n out={out:?}"); diff --git a/src/component.rs b/src/component.rs index cadeb50..0484a4d 100644 --- a/src/component.rs +++ b/src/component.rs @@ -2,7 +2,7 @@ //! //! [Owl]: https://odoo.github.io/owl/ -use tower_lsp_server::lsp_types::Range; +use tower_lsp_server::ls_types::Range; use crate::index::{Symbol, SymbolMap, TemplateIndex}; use crate::template::TemplateName; diff --git a/src/index.rs b/src/index.rs index e6247ab..7e7a712 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1,5 +1,6 @@ //! The main indexer for all language items, including [`Record`]s, QWeb [`Template`]s, and Owl [`Component`]s +use mini_moka::sync::Cache; use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -13,7 +14,7 @@ use ignore::Match; use ignore::gitignore::Gitignore; use lasso::ThreadedRodeo; use smart_default::SmartDefault; -use tower_lsp_server::{Client, lsp_types::*}; +use tower_lsp_server::{Client, ls_types::*}; use xmlparser::{Token, Tokenizer}; use crate::prelude::*; @@ -78,6 +79,13 @@ pub struct Index { /// Cache for transitive dependencies to avoid recalculation #[default(_code = "DashMap::with_shard_amount(4)")] pub(crate) transitive_deps_cache: DashMap>, + #[default(_code = "Cache::new(16)")] + pub(crate) ast_cache: Cache>, +} + +pub struct AstCacheItem { + pub tree: tree_sitter::Tree, + pub rope: Rope, } pub type ModuleName = Symbol; @@ -206,7 +214,8 @@ impl Index { // Clear transitive dependencies cache since modules have been added/modified self.transitive_deps_cache.clear(); - // After adding all modules in the root, check for auto_install modules with unsatisfied dependencies // We pass an empty set because no modules are loaded yet, this will identify all auto_install + // After adding all modules in the root, check for auto_install modules with unsatisfied dependencies + // We pass an empty set because no modules are loaded yet, this will identify all auto_install // modules and track those with missing dependencies debug!("Checking for auto_install modules after adding root"); let auto_install_check = self.find_auto_install_modules(&HashSet::new()); @@ -214,6 +223,29 @@ impl Index { Ok(()) } + #[instrument(skip(self), ret)] + pub async fn add_root_for_file(&self, path: &Path) { + if self.find_module_of(path).is_some() { + return; + } + + debug!("oob: {}", path.display()); + let mut path = Some(path); + while let Some(path_) = path { + if tokio::fs::try_exists(path_.with_file_name("__manifest__.py")) + .await + .unwrap_or(false) + && let Some(file_path) = path_.parent().and_then(|p| p.parent()) + { + _ = self + .add_root(file_path, None) + .await + .inspect_err(|err| warn!("failed to add root {}:\n{err}", file_path.display())); + return; + } + path = path_.parent(); + } + } #[instrument(skip_all, ret, fields(path = path.display().to_string()))] pub(crate) async fn load_modules_for_document(&self, document_blocker: Arc, path: &Path) -> Option<()> { let _blocker = document_blocker.block(); @@ -538,17 +570,17 @@ impl Index { let mut outputs = tokio::task::JoinSet::new(); for (module_key, root) in modules.into_iter().zip(roots.into_iter()) { - info!("{} depends on {}", _R(module_name), _R(module_key)); let root_display = root.key().to_string_lossy(); let root_key = _I(&root_display); let module = root .get(&module_key) .expect(format_loc!("module must already be present by now")); - let module_dir = Path::new(&*root_display).join(&module.path); if module.loaded.compare_exchange(false, true, Relaxed, Relaxed) != Ok(false) { continue; } + info!("{} depends on {}", _R(module_name), _R(module_key)); + let module_dir = Path::new(&*root_display).join(&module.path); if let Ok(xmls) = globwalk::glob_builder(format!("{}/**/*.xml", module_dir.display())) .file_type(FileType::FILE | FileType::SYMLINK) .follow_links(true) @@ -638,7 +670,7 @@ impl Index { } } } - debug!( + trace!( "Currently loaded modules: {:?}", all_loaded_modules.iter().map(|m| _R(*m)).collect::>() ); @@ -651,7 +683,7 @@ impl Index { auto_install_candidates.len() ); for auto_module in auto_install_candidates { - info!( + trace!( "Auto-installing module {} because all dependencies are satisfied", _R(auto_module) ); @@ -1091,7 +1123,7 @@ pub fn index_models(contents: &[u8]) -> anyhow::Result> { }); match &contents[capture.byte_range()] { b"_name" => { - let Some(name_decl) = capture.next_named_sibling() else { + let Some(name_decl) = python_next_named_sibling(capture) else { continue; }; if name_decl.kind() == "string" { @@ -1103,7 +1135,7 @@ pub fn index_models(contents: &[u8]) -> anyhow::Result> { } } b"_inherit" => { - let Some(inherit_decl) = capture.next_named_sibling() else { + let Some(inherit_decl) = python_next_named_sibling(capture) else { continue; }; match inherit_decl.kind() { @@ -1129,7 +1161,7 @@ pub fn index_models(contents: &[u8]) -> anyhow::Result> { } } b"_inherits" => { - let Some(inherit_dict) = capture.next_named_sibling() else { + let Some(inherit_dict) = python_next_named_sibling(capture) else { continue; }; if inherit_dict.kind() != "dictionary" { diff --git a/src/index/js.rs b/src/index/js.rs index 18ca6e0..247b57d 100644 --- a/src/index/js.rs +++ b/src/index/js.rs @@ -1,3 +1,5 @@ +#![allow(clippy::disallowed_methods)] + use std::ops::DerefMut; use std::{collections::HashMap, path::PathBuf}; @@ -177,7 +179,7 @@ pub(super) async fn add_root_js(root: Spur, pathbuf: PathBuf) -> anyhow::Result< type_: Default::default(), location: MinLoc { path, - range: rope_conv(range.map_unit(ByteOffset), rope).unwrap(), + range: rope_conv(range.map_unit(ByteOffset), rope), }, }), }; @@ -200,7 +202,7 @@ pub(super) async fn add_root_js(root: Spur, pathbuf: PathBuf) -> anyhow::Result< Some(JsQuery::TemplateInline) => { let Some(component) = &mut component else { continue }; let range = capture.node.byte_range().shrink(1).map_unit(ByteOffset); - component.template = Some(ComponentTemplate::Inline(rope_conv(range, rope).unwrap())); + component.template = Some(ComponentTemplate::Inline(rope_conv(range, rope))); } Some(JsQuery::Subcomponent) => { let Some(component) = &mut component else { continue }; diff --git a/src/index/symbol.rs b/src/index/symbol.rs index 25eb4fe..5eafc43 100644 --- a/src/index/symbol.rs +++ b/src/index/symbol.rs @@ -22,9 +22,11 @@ pub struct Symbol { pub struct PathSymbol(Spur, Spur); impl PathSymbol { - /// Panics if `root` is not a parent of `path`. + /// Creates a path symbol from a path and its prefix. + /// + /// Panics if `root` is not a parent/prefix of `path`. pub fn strip_root(root: Spur, path: &Path) -> Self { - let path = path.strip_prefix(interner().resolve(&root)).unwrap(); + let path = path.strip_prefix(_R(root)).unwrap(); let path = _I(path.to_string_lossy()); PathSymbol(root, path) } diff --git a/src/js.rs b/src/js.rs index f65ed1d..d7e7301 100644 --- a/src/js.rs +++ b/src/js.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use std::sync::atomic::Ordering::Relaxed; -use tower_lsp_server::lsp_types::*; +use tower_lsp_server::ls_types::*; use tree_sitter::{QueryCursor, Tree}; use crate::prelude::*; @@ -52,9 +52,7 @@ impl Backend { .ast_map .get(file_path.to_str().unwrap()) .ok_or_else(|| errloc!("Did not build AST for {}", uri.path().as_str()))?; - let Ok(ByteOffset(offset)) = rope_conv(params.text_document_position_params.position, rope) else { - Err(errloc!("could not find offset for {}", uri.path().as_str()))? - }; + let ByteOffset(offset) = rope_conv(params.text_document_position_params.position, rope); let contents = Cow::from(rope); // try templates first @@ -123,9 +121,7 @@ impl Backend { .ast_map .get(file_path.to_str().unwrap()) .ok_or_else(|| errloc!("Did not build AST for {}", uri.path().as_str()))?; - let Ok(ByteOffset(offset)) = rope_conv(params.text_document_position.position, rope) else { - Err(errloc!("could not find offset for {}", uri.path().as_str()))? - }; + let ByteOffset(offset) = rope_conv(params.text_document_position.position, rope); let contents = Cow::from(rope); let query = JsQuery::query(); let mut cursor = QueryCursor::new(); @@ -157,9 +153,7 @@ impl Backend { .ast_map .get(file_path.to_str().unwrap()) .ok_or_else(|| errloc!("Did not build AST for {}", uri.path().as_str()))?; - let Ok(ByteOffset(offset)) = rope_conv(params.text_document_position_params.position, rope) else { - return Err(errloc!("could not find offset for {}", uri.path().as_str())); - }; + let ByteOffset(offset) = rope_conv(params.text_document_position_params.position, rope); let contents = Cow::from(rope); let query = JsQuery::query(); let mut cursor = QueryCursor::new(); @@ -206,9 +200,7 @@ impl Backend { { let range = range.shrink(1); let model = &contents[range.clone()]; - return self - .index - .hover_model(model, rope_conv(range.map_unit(ByteOffset), rope).ok(), false, None); + return (self.index).hover_model(model, Some(rope_conv(range.map_unit(ByteOffset), rope)), false, None); } if let Some(model_node) = model_arg_node @@ -219,9 +211,11 @@ impl Backend { let range = range.shrink(1); let model = &contents[model_node.byte_range().shrink(1)]; let method = &contents[range.clone()]; - return self - .index - .hover_property_name(method, model, rope_conv(range.map_unit(ByteOffset), rope).ok()); + return self.index.hover_property_name( + method, + model, + Some(rope_conv(range.map_unit(ByteOffset), rope)), + ); } } @@ -235,12 +229,8 @@ impl Backend { ast: Tree, rope: RopeSlice<'_>, ) -> anyhow::Result> { - let uri = ¶ms.text_document_position.text_document.uri; let position = params.text_document_position.position; - let Ok(ByteOffset(offset)) = rope_conv(position, rope) else { - return Err(errloc!("could not find offset for {}", uri.path().as_str())); - }; - + let ByteOffset(offset) = rope_conv(position, rope); let path = some!(params.text_document_position.text_document.uri.to_file_path()); let completions_limit = self .workspaces @@ -276,10 +266,7 @@ impl Backend { // Extract the current prefix (excluding quotes) let inner_range = range.shrink(1); let prefix = &contents[inner_range.start..offset]; - let lsp_range = ok!( - rope_conv(inner_range.map_unit(ByteOffset), rope), - "range conversion failed" - ); + let lsp_range = rope_conv(inner_range.map_unit(ByteOffset), rope); let mut items = MaxVec::new(completions_limit); self.index.complete_model(prefix, lsp_range, &mut items)?; diff --git a/src/main.rs b/src/main.rs index f7dc9d0..5c187c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -104,16 +104,13 @@ fn main() { } _ => {} } + let threads = threads.max(1); - let rt = if threads <= 1 { - tokio::runtime::Builder::new_current_thread().enable_all().build() - } else { - tokio::runtime::Builder::new_multi_thread() - .worker_threads(threads) - .enable_all() - .build() - } - .expect("failed to build runtime"); + let rt = tokio::runtime::Builder::new_multi_thread() + .worker_threads(threads) + .enable_all() + .build() + .expect("failed to build runtime"); rt.block_on(async move { if cli::run(args).await { return; @@ -139,7 +136,10 @@ fn main() { .layer(tower::timeout::TimeoutLayer::new(Duration::from_secs(30))) .layer_fn(CatchPanic) .service(service); - Server::new(stdin, stdout, socket).serve(service).await; + Server::new(stdin, stdout, socket) + .concurrency_level(2) + .serve(service) + .await; }) } diff --git a/src/model.rs b/src/model.rs index 030e970..7358b4e 100644 --- a/src/model.rs +++ b/src/model.rs @@ -7,15 +7,19 @@ use std::fmt::Display; use std::ops::Deref; use std::sync::Arc; use std::sync::RwLock; +use std::sync::atomic::AtomicBool; use dashmap::DashMap; use dashmap::mapref::one::RefMut; +use dashmap::try_result::TryResult; use derive_more::{Deref, DerefMut}; +use mini_moka::sync::Cache; use qp_trie::Trie; use rayon::prelude::{IntoParallelIterator, ParallelIterator}; use smart_default::SmartDefault; use ts_macros::query; +use crate::analyze::TypeId; use crate::prelude::*; use crate::analyze::FunctionParam; @@ -86,6 +90,12 @@ pub enum PropertyKind { Method, } +/// Twin of [PropertyKind] where field contains field type info +pub enum PropertyInfo { + Field(Spur), + Method, +} + #[derive(Clone, Debug)] pub struct Field { pub kind: FieldKind, @@ -94,12 +104,26 @@ pub struct Field { pub help: Option, } -#[derive(Clone, Debug)] +#[derive(Debug, SmartDefault)] pub struct Method { - pub return_type: MethodReturnType, pub locations: Vec, pub docstring: Option, pub arguments: Option>, + pub pending_eval: AtomicBool, + #[default(_code = "Cache::new(8)")] + pub eval_cache: Cache, TypeId>, +} + +impl Clone for Method { + fn clone(&self) -> Self { + Self { + locations: self.locations.clone(), + docstring: self.docstring.clone(), + arguments: self.arguments.clone(), + pending_eval: AtomicBool::new(false), + eval_cache: Cache::new(8), + } + } } #[derive(Deref, DerefMut, Clone, Debug)] @@ -117,16 +141,6 @@ impl From for TrackedMinLoc { } } -#[derive(Clone, Debug, Default)] -pub enum MethodReturnType { - #[default] - Unprocessed, - /// Set to prevent recursion - Processing, - Value, - Relational(Symbol), -} - impl Field { pub fn merge<'this>(self: &'this mut Arc, other: &Self) -> &'this mut Self { let self_ = Arc::make_mut(self); @@ -422,9 +436,17 @@ impl ModelIndex { model: ModelName, locations_filter: &[PathSymbol], ) -> Option> { - let model_name = _R(model); - let mut entry = self.try_get_mut(&model).expect(format_loc!("deadlock"))?; - if entry.fields.is_some() && entry.methods.is_some() && locations_filter.is_empty() { + let mut entry = match self.try_get_mut(&model) { + TryResult::Present(entry) => entry, + TryResult::Absent => { + return None; + } + TryResult::Locked => { + cold_path(); + panic!("{} deadlock on model {}", loc!(), _R(model)); + } + }; + if likely(entry.fields.is_some() && entry.methods.is_some() && locations_filter.is_empty()) { return Some(entry); } let t0 = std::time::Instant::now(); @@ -610,7 +632,6 @@ impl ModelIndex { { loc.active = false; method.arguments = None; - method.return_type = MethodReturnType::Unprocessed; } } } @@ -675,10 +696,8 @@ impl ModelIndex { Entry::Vacant(empty) => { empty.insert( Method { - return_type: Default::default(), locations: vec![method_location.into()], - docstring: None, - arguments: None, + ..Default::default() } .into(), ); @@ -697,11 +716,12 @@ impl ModelIndex { }); } + let model_name = _R(model); info!( "{model_name}: {} fields, {} methods, {}ms", out_fields.len(), out_methods.len(), - t0.elapsed().as_millis() + t0.elapsed().as_millis(), ); let mut entry = self.try_get_mut(&model).expect(format_loc!("deadlock")).unwrap(); entry.fields = Some(out_fields); @@ -847,24 +867,20 @@ impl ModelEntry { return Ok(()); } } - self.docstring = Some(ImStr::from_static("")); + self.docstring = Some("".into()); } Ok(()) } - pub fn prop_kind(&self, prop: Spur) -> Option { - if self - .fields - .as_ref() - .is_some_and(|fields| fields.contains_key(&prop.into())) - { - Some(PropertyKind::Field) + pub fn prop_kind(&self, prop: Spur) -> Option { + if let Some(field) = self.fields.as_ref().and_then(|fields| fields.get(&prop.into())) { + Some(PropertyInfo::Field(field.type_)) } else if self .methods .as_ref() .is_some_and(|methods| methods.contains_key(&prop.into())) { - Some(PropertyKind::Method) + Some(PropertyInfo::Method) } else { None } diff --git a/src/prelude.rs b/src/prelude.rs index 00dceed..e76fc68 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -6,9 +6,8 @@ pub use lasso::{Key, Spur}; pub use ropey::LineType::LF_CR as LINE_TYPE; pub use ropey::{Rope, RopeSlice}; pub use serde::{Deserialize, Serialize}; -pub use tower_lsp_server::UriExt; -pub use tower_lsp_server::lsp_types::Range; -pub use tower_lsp_server::lsp_types::Uri; +pub use tower_lsp_server::ls_types::Range; +pub use tower_lsp_server::ls_types::Uri; pub use tracing::{debug, error, info, instrument, trace, warn}; pub use tree_sitter::{Node, Parser, QueryCursor, StreamingIterator, StreamingIteratorMut}; pub use ts_macros::query; diff --git a/src/python.rs b/src/python.rs index f08daf1..626c3fe 100644 --- a/src/python.rs +++ b/src/python.rs @@ -3,14 +3,14 @@ use std::path::Path; use lasso::Spur; use ropey::Rope; -use tower_lsp_server::{UriExt, lsp_types::*}; +use tower_lsp_server::ls_types::*; use tracing::{debug, instrument, trace, warn}; use tree_sitter::{Node, Parser, QueryMatch}; use ts_macros::query; use crate::prelude::*; -use crate::analyze::Type; +use crate::analyze::{Type, type_cache}; use crate::index::{_G, _I, _R, PathSymbol, index_models}; use crate::model::{ModelName, ModelType}; use crate::xml::determine_csv_xmlid_subgroup; @@ -299,7 +299,7 @@ impl Backend { })) } - #[tracing::instrument(skip_all, fields(uri))] + #[tracing::instrument(skip_all, ret, fields(uri))] pub fn on_change_python( &self, text: &Text, @@ -563,9 +563,7 @@ impl Backend { .ast_map .get(file_path_str) .ok_or_else(|| errloc!("Did not build AST for {}", file_path_str))?; - let Ok(ByteOffset(offset)) = rope_conv(params.text_document_position_params.position, rope) else { - return Err(errloc!("could not find offset for {}", file_path_str)); - }; + let ByteOffset(offset) = rope_conv(params.text_document_position_params.position, rope); let contents = Cow::from(rope); let root = some!(top_level_stmt(ast.root_node(), offset)); @@ -652,7 +650,7 @@ impl Backend { } let model = _R(model); return self.index.jump_def_property_name(needle, model); - } else if let Some(cmdlist) = capture.node.next_named_sibling() + } else if let Some(cmdlist) = python_next_named_sibling(capture.node) && Backend::is_commandlist(cmdlist, offset) { let (needle, _, model) = some!(self.gather_commandlist( @@ -669,7 +667,7 @@ impl Backend { } } Some(PyCompletions::FieldDescriptor) => { - let Some(desc_value) = capture.node.next_named_sibling() else { + let Some(desc_value) = python_next_named_sibling(capture.node) else { continue; }; @@ -782,7 +780,7 @@ impl Backend { let idx = contents[..=offset].bytes().rposition(|c| c == b'.')?; let ident = contents[..=idx].bytes().rposition(|c| c.is_ascii_alphanumeric())?; lhs = root.descendant_for_byte_range(ident, ident)?; - rhs = lhs.next_named_sibling().and_then(|attr| match attr.kind() { + rhs = python_next_named_sibling(lhs).and_then(|attr| match attr.kind() { "identifier" => Some(attr), "attribute" => attr.child_by_field_name("attribute"), _ => None, @@ -837,7 +835,7 @@ impl Backend { params: ReferenceParams, rope: RopeSlice<'_>, ) -> anyhow::Result>> { - let ByteOffset(offset) = ok!(rope_conv(params.text_document_position.position, rope)); + let ByteOffset(offset) = rope_conv(params.text_document_position.position, rope); let uri = ¶ms.text_document_position.text_document.uri; let file_path = uri.to_file_path().unwrap(); let file_path_str = file_path.to_str().unwrap(); @@ -896,7 +894,7 @@ impl Backend { } } Some(PyCompletions::FieldDescriptor) => { - let Some(desc_value) = capture.node.next_named_sibling() else { + let Some(desc_value) = python_next_named_sibling(capture.node) else { continue; }; let descriptor = &contents[range]; @@ -948,9 +946,7 @@ impl Backend { .ast_map .get(file_path_str) .ok_or_else(|| errloc!("Did not build AST for {}", file_path_str))?; - let Ok(ByteOffset(offset)) = rope_conv(params.text_document_position_params.position, rope) else { - return Err(errloc!("could not find offset for {}", file_path_str)); - }; + let ByteOffset(offset) = rope_conv(params.text_document_position_params.position, rope); let contents = Cow::from(rope); let root = some!(top_level_stmt(ast.root_node(), offset)); @@ -970,7 +966,13 @@ impl Backend { let slice = ok!(rope.try_slice(range.clone())); let slice = Cow::from(slice); return self.index.hover_model(&slice, Some(lsp_range), false, None); - } else if range.end < offset { + } + if range.end < offset + && match_ + .nodes_for_capture_index(PyCompletions::Prop as _) + .next() + .is_some() + { this_model.tag_model(capture.node, match_, root.byte_range(), &contents); } } @@ -998,10 +1000,8 @@ impl Backend { ); } let model = _R(model); - return self - .index - .hover_property_name(needle, model, rope_conv(range, rope).ok()); - } else if let Some(cmdlist) = capture.node.next_named_sibling() + return (self.index).hover_property_name(needle, model, Some(rope_conv(range, rope))); + } else if let Some(cmdlist) = python_next_named_sibling(capture.node) && Backend::is_commandlist(cmdlist, offset) { let (needle, range, model) = some!(self.gather_commandlist( @@ -1014,7 +1014,7 @@ impl Backend { &contents, false, )); - let range = rope_conv(range, rope).ok(); + let range = Some(rope_conv(range, rope)); return self.index.hover_property_name(needle, _R(model), range); } } @@ -1033,9 +1033,7 @@ impl Backend { slice = needle; } } - return self - .index - .hover_record(slice, rope_conv(range.map_unit(ByteOffset), rope).ok()); + return (self.index).hover_record(slice, Some(rope_conv(range.map_unit(ByteOffset), rope))); } Some(PyCompletions::Prop) if range.contains(&offset) => { let model = some!(this_model.inner); @@ -1044,7 +1042,7 @@ impl Backend { return self.index.hover_property_name(name, model, Some(range)); } Some(PyCompletions::FieldDescriptor) => { - let Some(desc_value) = capture.node.next_named_sibling() else { + let Some(desc_value) = python_next_named_sibling(capture.node) else { continue; }; let descriptor = &contents[range]; @@ -1082,9 +1080,7 @@ impl Backend { ); } let model = _R(model); - return self - .index - .hover_property_name(needle, model, rope_conv(range, rope).ok()); + return (self.index).hover_property_name(needle, model, Some(rope_conv(range, rope))); } else if matches!(descriptor, "groups") { let range = desc_value.byte_range().shrink(1); let value = Cow::from(ok!(rope.try_slice(range.clone()))); @@ -1093,7 +1089,7 @@ impl Backend { let (needle, byte_range) = some!(ref_); return self .index - .hover_record(needle, rope_conv(byte_range.map_unit(ByteOffset), rope).ok()); + .hover_record(needle, Some(rope_conv(byte_range.map_unit(ByteOffset), rope))); } return Ok(None); @@ -1113,7 +1109,7 @@ impl Backend { } } if let Some((model, prop, range)) = self.attribute_at_offset(offset, root, &contents) { - let lsp_range = rope_conv(range.map_unit(ByteOffset), rope).ok(); + let lsp_range = Some(rope_conv(range.map_unit(ByteOffset), rope)); return self.index.hover_property_name(prop, model, lsp_range); } @@ -1123,25 +1119,24 @@ impl Backend { let lsp_range = span_conv(needle.range()); let (type_, scope) = some!((self.index).type_of_range(root, needle.byte_range().map_unit(ByteOffset), &contents)); - if let Some(model) = self.index.try_resolve_model(&type_, &scope) { + if let Some(model) = self.index.try_resolve_model(&type_cache().resolve(type_), &scope) { let model = _R(model); let identifier = (needle.kind() == "identifier").then(|| &contents[needle.byte_range()]); return self.index.hover_model(model, Some(lsp_range), true, identifier); } - // not a model! we only have so many things we can hover... - match type_ { - Type::Method(model, method) => self.index.hover_property_name(&method, _R(model), Some(lsp_range)), - _ => Ok(None), - } + self.index.hover_variable( + (needle.kind() == "identifier").then(|| &contents[needle.byte_range()]), + type_, + Some(lsp_range), + ) } pub(crate) fn python_signature_help(&self, params: SignatureHelpParams) -> anyhow::Result> { use std::fmt::Write; let uri = ¶ms.text_document_position_params.text_document.uri; - let document = - some!((self.document_map).get(uri.path().as_str())); + let document = some!((self.document_map).get(uri.path().as_str())); let file_path = uri.to_file_path().unwrap(); let ast = some!((self.ast_map).get(file_path.to_str().unwrap())); let contents = Cow::from(&document.rope); @@ -1164,9 +1159,8 @@ impl Backend { } let active_parameter = 'find_param: { - if let Ok(offset) = - rope_conv::<_, ByteOffset>(params.text_document_position_params.position, document.rope.slice(..)) - && let Some(contents) = contents.get(..=offset.0) + let ByteOffset(offset) = rope_conv(params.text_document_position_params.position, document.rope.slice(..)); + if let Some(contents) = contents.get(..=offset) && let Some(idx) = contents.bytes().rposition(|c| c == b',' || c == b'(') { if contents.as_bytes()[idx] == b'(' { @@ -1186,13 +1180,16 @@ impl Backend { }; let callee = some!(args.prev_named_sibling()); - let Some((Type::Method(model_key, method), _)) = + let Some((tid, _)) = (self.index).type_of_range(ast.root_node(), callee.byte_range().map_unit(ByteOffset), &contents) else { return Ok(None); }; + let Type::Method(model_key, method) = type_cache().resolve(tid) else { + return Ok(None); + }; let method_key = some!(_G(&method)); - let rtype = (self.index).resolve_method_returntype(method_key.into(), model_key.into()); + let rtype = (self.index).eval_method_rtype(method_key.into(), model_key.into(), None); let model = some!((self.index).models.get(&model_key)); let method_obj = some!(some!(model.methods.as_ref()).get(&method_key.into())); @@ -1215,8 +1212,9 @@ impl Backend { }); } + let rtype = rtype.and_then(|rtype| self.index.type_display(rtype)); match rtype { - Some(rtype) => drop(write!(&mut label, ") -> Model[\"{}\"]", _R(rtype))), + Some(rtype) => drop(write!(&mut label, ") -> {rtype}")), None => label.push_str(") -> ..."), }; @@ -1250,7 +1248,7 @@ impl Backend { let ast = some!(self.ast_map.get(file_path.to_str().unwrap())); let rope = &document.rope; let contents = Cow::from(rope); - let ByteOffset(offset) = some!(rope_conv(params.position, rope.slice(..)).ok()); + let ByteOffset(offset) = rope_conv(params.position, rope.slice(..)); let root = some!(top_level_stmt(ast.root_node(), offset)); let needle = some!(root.named_descendant_for_byte_range(offset, offset)); let (type_, _) = some!((self.index).type_of_range(root, needle.byte_range().map_unit(ByteOffset), &contents)); diff --git a/src/python/completions.rs b/src/python/completions.rs index 11dda06..33f83d5 100644 --- a/src/python/completions.rs +++ b/src/python/completions.rs @@ -1,15 +1,15 @@ -use tower_lsp_server::lsp_types::{CompletionList, CompletionResponse}; use tree_sitter::{Node, QueryMatch}; use std::borrow::Cow; use std::sync::atomic::Ordering::Relaxed; -use tower_lsp_server::{UriExt, lsp_types::*}; -use tracing::{debug, warn}; +use tower_lsp_server::ls_types::*; +use tracing::debug; use tree_sitter::Tree; use crate::prelude::*; +use crate::analyze::{DictKey, Type, type_cache}; use crate::backend::Backend; use crate::index::{_G, _I, _R, symbol::Symbol}; use crate::model::{FieldKind, ModelEntry, ModelName, PropertyKind}; @@ -28,10 +28,7 @@ impl Backend { ast: Tree, rope: RopeSlice<'_>, ) -> anyhow::Result> { - let Ok(ByteOffset(offset)) = rope_conv(params.text_document_position.position, rope) else { - warn!("invalid position {:?}", params.text_document_position.position); - return Ok(None); - }; + let ByteOffset(offset) = rope_conv(params.text_document_position.position, rope); let path = some!(params.text_document_position.text_document.uri.to_file_path()); let Some(current_module) = self.index.find_module_of(&path) else { debug!("no current module"); @@ -92,7 +89,7 @@ impl Backend { &needle, range.map_unit(ByteOffset), rope, - model_filter.map(|m| vec![ImStr::from_static(m)]).as_deref(), + model_filter.map(|m| vec![m.into()]).as_deref(), current_module, &mut items, )?; @@ -106,7 +103,7 @@ impl Backend { Some(PyCompletions::Model) => { if range.contains_end(offset) { let (needle, byte_range) = extract_string_needle_at_offset(rope, range, offset)?; - let range = ok!(rope_conv(byte_range, rope)); + let range = rope_conv(byte_range, rope); early_return.lift(move || async move { let mut items = MaxVec::new(completions_limit); self.index.complete_model(&needle, range, &mut items)?; @@ -272,7 +269,7 @@ impl Backend { Some(PropertyKind::Field), rope, ); - } else if let Some(cmdlist) = capture.node.next_named_sibling() + } else if let Some(cmdlist) = python_next_named_sibling(capture.node) && Backend::is_commandlist(cmdlist, offset) && let Some((needle, range, model)) = self.gather_commandlist( cmdlist, @@ -303,7 +300,7 @@ impl Backend { } } Some(PyCompletions::FieldDescriptor) => { - let Some(desc_value) = capture.node.next_named_sibling() else { + let Some(desc_value) = python_next_named_sibling(capture.node) else { continue; }; @@ -333,7 +330,7 @@ impl Backend { let range = desc_value.byte_range(); let (needle, byte_range) = extract_string_needle_at_offset(rope, range, offset)?; - let range = ok!(rope_conv(byte_range, rope)); + let range = rope_conv(byte_range, rope); early_return.lift(move || async move { let mut items = MaxVec::new(completions_limit); self.index.complete_model(&needle, range, &mut items)?; @@ -358,7 +355,7 @@ impl Backend { &needle, range.map_unit(ByteOffset), rope, - Some(&[ImStr::from_static("res.groups")]), + Some(&["res.groups".into()]), current_module, &mut items, )?; @@ -435,14 +432,12 @@ impl Backend { } } if early_return.is_none() { - // Check if we're in a broken syntax situation (string without colon in dictionary) let cursor_node = root.descendant_for_byte_range(offset, offset); if let Some(node) = cursor_node { - // Check if we're in a string that's part of an ERROR node let mut current = node; while let Some(parent) = current.parent() { + // (dictionary (ERROR ^cursor)) if parent.kind() == "ERROR" { - // Check if the ERROR's parent is a dictionary if let Some(grandparent) = parent.parent() && grandparent.kind() == "dictionary" { @@ -514,6 +509,40 @@ impl Backend { }))); } } + } else if current.kind() == "string" + && parent.kind() == "subscript" + && let Some(lhs) = parent.named_child(0) + && let Some((tid, _)) = + self.index + .type_of_range(root, dbg!(lhs).byte_range().map_unit(ByteOffset), &contents) + && let Type::DictBag(dict) = type_cache().resolve(tid) + { + let mut items = MaxVec::new(completions_limit); + let dict = dict.into_iter().flat_map(|(key, _)| match key { + DictKey::String(str) => Some(str.to_string()), + _ => None, + }); + let range = current.byte_range().shrink(1).map_unit(ByteOffset); + let range = rope_conv(range, rope); + let to_item = |label: String| { + let new_text = label.clone(); + CompletionItem { + label, + kind: Some(CompletionItemKind::CONSTANT), + text_edit: Some(CompletionTextEdit::Edit(TextEdit { range, new_text })), + ..Default::default() + } + }; + if offset <= current.start_byte() + 1 { + items.extend(dict.map(to_item)); + } else { + let needle = &contents[current.start_byte() + 1..offset]; + items.extend(dict.filter(|label| label.starts_with(needle)).map(to_item)); + } + return Ok(Some(CompletionResponse::List(CompletionList { + is_incomplete: !items.has_space(), + items: items.into_inner(), + }))); } current = parent; } diff --git a/src/python/diagnostics.rs b/src/python/diagnostics.rs index b31b45a..4181f61 100644 --- a/src/python/diagnostics.rs +++ b/src/python/diagnostics.rs @@ -1,9 +1,10 @@ use std::{borrow::Cow, cmp::Ordering, ops::ControlFlow}; -use tower_lsp_server::lsp_types::{Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, Location}; +use tower_lsp_server::ls_types::{Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, Location}; use tracing::{debug, warn}; use tree_sitter::{Node, QueryCursor, QueryMatch}; +use crate::analyze::type_cache; use crate::index::{_R, Index}; use crate::prelude::*; @@ -37,10 +38,8 @@ impl Backend { let before_count = diagnostics.len(); diagnostics.retain(|diag| { // If we couldn't get a range here, rope has changed significantly so just toss the diag. - let Ok(range) = rope_conv::<_, ByteRange>(diag.range, rope) else { - return false; - }; - !root.byte_range().contains(&range.start.0) + let ByteOffset(start) = rope_conv(diag.range.start, rope); + !root.byte_range().contains(&start) }); debug!( "Retained {}/{} diagnostics after damage zone check", @@ -107,8 +106,7 @@ impl Backend { } if !id_found { - let range = rope_conv(range.map_unit(ByteOffset), rope) - .expect(format_loc!("failed to get range for xmlid diag")); + let range = rope_conv(range.map_unit(ByteOffset), rope); diagnostics.push(Diagnostic { range, message: format!("No XML record with ID `{xmlid}` found"), @@ -128,7 +126,7 @@ impl Backend { let has_model = model_key.map(|model| self.index.models.contains_key(&model.into())); if !has_model.unwrap_or(false) { diagnostics.push(Diagnostic { - range: rope_conv(range.map_unit(ByteOffset), rope).unwrap(), + range: rope_conv(range.map_unit(ByteOffset), rope), message: format!("`{model}` is not a valid model name"), severity: Some(DiagnosticSeverity::ERROR), ..Default::default() @@ -151,7 +149,7 @@ impl Backend { let has_model = model_key.map(|model| self.index.models.contains_key(&model.into())); if !has_model.unwrap_or(false) { diagnostics.push(Diagnostic { - range: rope_conv(range.map_unit(ByteOffset), rope).unwrap(), + range: rope_conv(range.map_unit(ByteOffset), rope), message: format!("`{model}` is not a valid model name"), severity: Some(DiagnosticSeverity::ERROR), ..Default::default() @@ -179,7 +177,7 @@ impl Backend { Some(PyCompletions::FieldDescriptor) => { // fields.Many2one(field_descriptor=...) - let Some(desc_value) = capture.node.next_named_sibling() else { + let Some(desc_value) = python_next_named_sibling(capture.node) else { continue; }; @@ -239,7 +237,7 @@ impl Backend { let has_model = model_key.map(|model| self.index.models.contains_key(&model.into())); if !has_model.unwrap_or(false) { diagnostics.push(Diagnostic { - range: rope_conv(range.map_unit(ByteOffset), rope).unwrap(), + range: rope_conv(range.map_unit(ByteOffset), rope), message: format!("`{model}` is not a valid model name"), severity: Some(DiagnosticSeverity::ERROR), ..Default::default() @@ -309,11 +307,11 @@ impl Backend { }; let mut scope = Scope::default(); let self_type = match self_type { - Some(type_) => ImStr::from(&contents[type_.byte_range().shrink(1)]), - None => ImStr::from_static(""), + Some(type_) => &contents[type_.byte_range().shrink(1)], + None => "", }; scope.super_ = Some(self_param.into()); - scope.insert(self_param.to_string(), Type::Model(self_type)); + scope.insert(self_param.to_string(), Type::Model(self_type.into())); let scope_end = fn_scope.end_byte(); Index::walk_scope(fn_scope, Some(scope), |scope, node| { let entered = (self.index).build_scope(scope, node, scope_end, contents)?; @@ -324,20 +322,11 @@ impl Backend { } let attribute = attribute.unwrap(); + #[rustfmt::skip] static MODEL_BUILTINS: phf::Set<&str> = phf::phf_set!( - "env", - "id", - "ids", - "display_name", - "create_date", - "write_date", - "create_uid", - "write_uid", - "pool", - "record", - "flush_model", - "mapped", - "fields_get", + "env", "id", "ids", "display_name", "create_date", "write_date", + "create_uid", "write_uid", "pool", "record", "flush_model", "mapped", + "grouped", "_read_group", "filtered", "sorted", "_origin", "fields_get", "user_has_groups", ); let prop = &contents[attribute.byte_range()]; @@ -348,6 +337,7 @@ impl Backend { let Some(lhs_t) = (self.index).type_of(node.child_by_field_name("object").unwrap(), scope, contents) else { return ControlFlow::Continue(entered); }; + let lhs_t = type_cache().resolve(lhs_t); let Some(model_name) = (self.index).try_resolve_model(&lhs_t, scope) else { return ControlFlow::Continue(entered); @@ -484,7 +474,7 @@ impl Backend { if let Some(dot) = needle.find('.') { let message_range = range.start.0 + dot..range.end.0; diagnostics.push(Diagnostic { - range: rope_conv(message_range.map_unit(ByteOffset), rope).unwrap(), + range: rope_conv(message_range.map_unit(ByteOffset), rope), severity: Some(DiagnosticSeverity::ERROR), message: "Dotted access is not supported in this context".to_string(), ..Default::default() @@ -497,7 +487,7 @@ impl Backend { Ok(()) => {} Err(ResolveMappedError::NonRelational) => { diagnostics.push(Diagnostic { - range: rope_conv(range, rope).unwrap(), + range: rope_conv(range, rope), severity: Some(DiagnosticSeverity::ERROR), message: format!("`{needle}` is not a relational field"), ..Default::default() @@ -538,7 +528,7 @@ impl Backend { } if !has_property { diagnostics.push(Diagnostic { - range: rope_conv(range, rope).unwrap(), + range: rope_conv(range, rope), severity: Some(DiagnosticSeverity::ERROR), message: format!( "Model `{}` has no {} `{needle}`", diff --git a/src/python/tests.rs b/src/python/tests.rs index db6151b..0f97943 100644 --- a/src/python/tests.rs +++ b/src/python/tests.rs @@ -390,10 +390,10 @@ class TestModel(models.Model): }; // Check if followed by colon - if let Some(next) = string_node.next_sibling() { - if next.kind() == ":" { - has_proper_syntax = true; - } + if let Some(next) = string_node.next_sibling() + && next.kind() == ":" + { + has_proper_syntax = true; } } diff --git a/src/record.rs b/src/record.rs index f9861f9..2aa83b3 100644 --- a/src/record.rs +++ b/src/record.rs @@ -1,6 +1,6 @@ //! XML [records][Record], as well as [`template`][Record::template] and [`menuitem`][Record::menuitem] shorthands. -use tower_lsp_server::lsp_types::*; +use tower_lsp_server::ls_types::*; use xmlparser::{ElementEnd, Token, Tokenizer}; use crate::prelude::*; @@ -37,7 +37,7 @@ impl Record { // nested records are a thing apparently let mut stack = 1; let mut in_record = true; - let start: Position = ok!(rope_conv(offset, rope), "{}", path); + let start: Position = rope_conv(offset, rope); loop { match reader.next() { @@ -97,7 +97,7 @@ impl Record { line: err.pos().row - 1, character: err.pos().col - 1, }; - end = rope_conv(pos, rope).ok(); + end = Some(rope_conv(pos, rope)); break; } _ => {} @@ -105,7 +105,7 @@ impl Record { } let id = some!(id); let end = end.ok_or_else(|| errloc!("Unbound range for record"))?; - let end = ok!(rope_conv(end, rope), "{}", path); + let end = rope_conv(end, rope); let range = Range { start, end }; Ok(Some(Self { @@ -124,7 +124,7 @@ impl Record { reader: &mut Tokenizer, rope: RopeSlice<'_>, ) -> anyhow::Result> { - let start: Position = ok!(rope_conv(offset, rope), "{}", path); + let start: Position = rope_conv(offset, rope); let mut id = None; let mut inherit_id = None; let mut end = None; @@ -190,14 +190,14 @@ impl Record { line: err.pos().row - 1, character: err.pos().col - 1, }; - end = rope_conv(pos, rope).ok(); + end = Some(rope_conv(pos, rope)); break; } _ => {} } } let end = end.ok_or_else(|| errloc!("Unbound range for template"))?; - let end = ok!(rope_conv(end, rope), "{}", path); + let end = rope_conv(end, rope); let range = Range { start, end }; Ok(Some(Self { @@ -218,7 +218,7 @@ impl Record { ) -> anyhow::Result> { let mut id = None; let mut end = None; - let start = ok!(rope_conv(offset, rope), "{}", path); + let start = rope_conv(offset, rope); loop { match reader.next() { @@ -239,7 +239,7 @@ impl Record { line: err.pos().row - 1, character: err.pos().col - 1, }; - end = rope_conv(pos, rope).ok(); + end = Some(rope_conv(pos, rope)); break; } _ => {} @@ -248,7 +248,7 @@ impl Record { let id = some!(id); let end = end.ok_or_else(|| errloc!("Unbound range for menuitem"))?; - let end = ok!(rope_conv(end, rope), "{}", path); + let end = rope_conv(end, rope); let range = Range { start, end }; Ok(Some(Self { diff --git a/src/server.rs b/src/server.rs index 00f64d5..cbc1386 100644 --- a/src/server.rs +++ b/src/server.rs @@ -6,11 +6,12 @@ use ropey::Rope; use serde_json::Value; use tower_lsp_server::LanguageServer; use tower_lsp_server::jsonrpc::Result; -use tower_lsp_server::lsp_types::notification::{DidChangeConfiguration, Notification}; -use tower_lsp_server::{UriExt, lsp_types::*}; +use tower_lsp_server::ls_types::notification::{DidChangeConfiguration, Notification}; +use tower_lsp_server::ls_types::request::WorkDoneProgressCreate; +use tower_lsp_server::ls_types::*; use tracing::{debug, error, info, instrument, warn}; -use crate::{GITVER, NAME, VERSION, await_did_open_document}; +use crate::{GITVER, NAME, VERSION, await_did_open_document, format_loc}; use crate::backend::{Backend, Document, Language, Text}; use crate::index::{_G, _R}; @@ -59,6 +60,14 @@ impl LanguageServer for Backend { self.capabilities.pull_diagnostics.store(true, Relaxed); } + if let Some(WindowClientCapabilities { + work_done_progress: Some(true), + .. + }) = params.capabilities.window + { + self.capabilities.can_create_wdp.store(true, Relaxed); + } + Ok(InitializeResult { server_info: Some(ServerInfo { name: NAME.to_string(), @@ -124,7 +133,7 @@ impl LanguageServer for Backend { self.document_map.remove(path); self.record_ranges.remove(path); - + let file_path = params.text_document.uri.to_file_path().unwrap(); self.ast_map.remove(file_path.to_str().unwrap()); @@ -200,33 +209,30 @@ impl LanguageServer for Backend { } }; + let mut progress = None; + let token = ProgressToken::String(format!("odoo_lsp_open:{file_path_str}")); + if self.capabilities.can_create_wdp.load(Relaxed) + && self + .client + .send_request::(WorkDoneProgressCreateParams { token: token.clone() }) + .await + .is_ok() + { + progress = Some( + self.client + .progress(token, format!("Opening {file_path_str}")) + .begin() + .await, + ); + } + let rope = Rope::from_str(¶ms.text_document.text); self.document_map.insert( params.text_document.uri.path().as_str().to_string(), Document::new(rope.clone()), ); - if self.index.find_module_of(&file_path).is_none() { - // outside of root? - debug!("oob: {}", file_path_str); - let path = params.text_document.uri.to_file_path(); - let mut path = path.as_deref(); - while let Some(path_) = path { - if tokio::fs::try_exists(path_.with_file_name("__manifest__.py")) - .await - .unwrap_or(false) - && let Some(file_path) = path_.parent().and_then(|p| p.parent()) - { - _ = self - .index - .add_root(file_path, Some(self.client.clone())) - .await - .inspect_err(|err| warn!("failed to add root {}:\n{err}", file_path.display())); - break; - } - path = path_.parent(); - } - } + self.index.add_root_for_file(&file_path).await; _ = self .on_change(backend::TextDocumentItem { @@ -239,6 +245,10 @@ impl LanguageServer for Backend { }) .await .inspect_err(|err| warn!("{err}")); + + if let Some(progress) = progress { + tokio::spawn(progress.finish()); + } } #[instrument(skip_all, ret, fields(uri = params.text_document.uri.as_str()))] async fn did_change(&self, mut params: DidChangeTextDocumentParams) { @@ -279,8 +289,7 @@ impl LanguageServer for Backend { document.rope = ropey::Rope::from_str(&change.text); } else { let range = change.range.expect("LSP change event must have a range"); - let range: CharRange = - rope_conv(range, document.rope.slice(..)).expect("did_change applying delta: no range"); + let range: CharRange = rope_conv(range, document.rope.slice(..)); let rope = &mut document.rope; rope.remove(range.erase()); if !change.text.is_empty() { @@ -318,7 +327,7 @@ impl LanguageServer for Backend { }; await_did_open_document!(self, path); - let Some(document) = self.document_map.get(path) else { + let Some(document) = self.document_map.try_get(path).expect(format_loc!("deadlock")) else { panic!("Bug: did not build a document for {}", uri.path().as_str()); }; if document.setup.should_wait() { @@ -488,7 +497,7 @@ impl LanguageServer for Backend { } } } - #[instrument(skip_all, ret, fields(uri = params.text_document_position_params.text_document.uri.as_str()))] + #[instrument(level = "trace", skip_all, ret, fields(uri = params.text_document_position_params.text_document.uri.as_str()))] async fn hover(&self, params: HoverParams) -> Result> { self.root_setup.wait().await; @@ -535,7 +544,7 @@ impl LanguageServer for Backend { for (config, ws) in configs.into_iter().zip(workspace_paths) { match serde_json::from_value(config) { Ok(config) => self.on_change_config(config, Some(&ws)), - Err(err) => error!("Could not parse updated configuration for {}:\n{err}", ws.display()), + Err(err) => warn!("Ignoring config update for {}:\n {err}", ws.display()), } } self.ensure_nonoverlapping_roots(); @@ -560,32 +569,20 @@ impl LanguageServer for Backend { for added in workspaces.difference(&roots) { if let Err(err) = self.index.add_root(added, None).await { - error!("failed to add root {}:\n{err}", added.display()); + error!("failed to add root {}:\n {err}", added.display()); } } } #[instrument(skip(self))] async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) { - self.root_setup.wait().await; for added in params.event.added { - let Some(file_path) = added.uri.to_file_path() else { - error!("not a file path: {}", added.uri.as_str()); - continue; - }; - _ = self - .index - .add_root(&file_path, None) - .await - .inspect_err(|err| warn!("failed to add root {}:\n{err}", file_path.display())); + let Some(path) = added.uri.to_file_path() else { continue }; + self.index.add_root_for_file(&path).await; } for removed in params.event.removed { - let Some(file_path) = removed.uri.to_file_path() else { - error!("not a file path: {}", removed.uri.as_str()); - continue; - }; - self.index.remove_root(&file_path); + warn!("unimplemented removing workspace folder {}", removed.name); } - self.index.delete_marked_entries(); + // self.index.delete_marked_entries(); } /// For VSCode and capable LSP clients, these events represent changes mostly to configuration files. #[instrument(skip(self))] @@ -626,16 +623,16 @@ impl LanguageServer for Backend { }); } } - self.client.publish_diagnostics(uri, diagnostics, None).await; + if !diagnostics.is_empty() { + let client = self.client.clone(); + tokio::spawn(async move { client.publish_diagnostics(uri, diagnostics, None).await }); + } break; } } } #[instrument(skip_all, fields(query = params.query))] - async fn symbol( - &self, - params: WorkspaceSymbolParams, - ) -> Result, Vec>>> { + async fn symbol(&self, params: WorkspaceSymbolParams) -> Result> { self.root_setup.wait().await; let query = ¶ms.query; @@ -679,13 +676,17 @@ impl LanguageServer for Backend { .and_then(|record| (record.module == module).then(|| to_symbol_information(&record))) }) }); - Ok(Some(OneOf::Left(models.chain(records).take(limit).collect()))) + Ok(Some(WorkspaceSymbolResponse::Flat( + models.chain(records).take(limit).collect(), + ))) } else { let records = records_by_prefix.iter_prefix(query.as_bytes()).flat_map(|(_, keys)| { keys.iter() .flat_map(|key| self.index.records.get(key).map(|record| to_symbol_information(&record))) }); - Ok(Some(OneOf::Left(models.chain(records).take(limit).collect()))) + Ok(Some(WorkspaceSymbolResponse::Flat( + models.chain(records).take(limit).collect(), + ))) } } #[instrument(skip_all, fields(path))] diff --git a/src/str.rs b/src/str.rs index 17fe915..ac7eca1 100644 --- a/src/str.rs +++ b/src/str.rs @@ -6,7 +6,6 @@ use std::path::Path; use std::sync::Arc; use const_format::assertcp_eq; -use num_enum::IntoPrimitive; use num_enum::TryFromPrimitive; /// Immutable, [`String`]-sized clone-friendly string. @@ -18,12 +17,11 @@ const INLINE_BYTES: usize = if cfg!(target_pointer_width = "64") { 23 } else { 1 pub(crate) enum Repr { Arc(Arc), Inline(u23, [u8; INLINE_BYTES]), - Static(&'static str), } #[allow(non_camel_case_types)] #[rustfmt::skip] -#[derive(IntoPrimitive, TryFromPrimitive, Clone, Copy)] +#[derive(TryFromPrimitive, Clone, Copy)] #[repr(u8)] pub(crate) enum u23 { _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, @@ -37,7 +35,7 @@ impl Deref for ImStr { fn deref(&self) -> &Self::Target { match &self.0 { Repr::Arc(inner) => inner, - Repr::Static(inner) => inner, + // Repr::Static(inner) => inner, Repr::Inline(len, bytes) => { let slice = &bytes[..(*len as usize)]; unsafe { std::str::from_utf8_unchecked(slice) } @@ -144,10 +142,6 @@ impl ImStr { pub fn as_str(&self) -> &str { self.deref() } - #[inline] - pub const fn from_static(inner: &'static str) -> Self { - Self(Repr::Static(inner)) - } } const _: () = { diff --git a/src/template.rs b/src/template.rs index b3db882..8fefc39 100644 --- a/src/template.rs +++ b/src/template.rs @@ -1,5 +1,5 @@ use ropey::RopeSlice; -use tower_lsp_server::lsp_types::{Position, Range}; +use tower_lsp_server::ls_types::{Position, Range}; use xmlparser::{ElementEnd, Token, Tokenizer}; use crate::prelude::*; @@ -80,14 +80,8 @@ pub fn gather_templates( } }; let name = _I(name).into(); - let start = ok!( - rope_conv(ByteOffset(tag_start), document), - "qweb_templates start <- tag_start" - ); - let end = ok!( - rope_conv(ByteOffset(span.end()), document), - "qweb_templates end <- span.end()" - ); + let start = rope_conv(ByteOffset(tag_start), document); + let end = rope_conv(ByteOffset(span.end()), document); let range = Range { start, end }; templates.push(NewTemplate { base, @@ -122,10 +116,7 @@ pub fn gather_templates( let name_candidate = if base { t_name } else { t_inherit }; let Some(name) = name_candidate else { break }; let name = _I(name).into(); - let start = ok!( - rope_conv(ByteOffset(tag_start), document), - "qweb_templates start <- tag_start" - ); + let start = rope_conv(ByteOffset(tag_start), document); let end = Position { line: err.pos().row, character: err.pos().col, diff --git a/src/utils.rs b/src/utils.rs index fd98717..57c49b6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,16 +1,17 @@ +use core::fmt::{Debug, Display}; +use core::future::Future; use core::ops::{Add, Sub}; use std::borrow::Cow; use std::ffi::OsStr; -use std::fmt::Display; -use std::future::Future; use std::path::Path; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering::Relaxed; use dashmap::try_result::TryResult; use futures::future::BoxFuture; use ropey::RopeSlice; use smart_default::SmartDefault; -use tower_lsp_server::lsp_types::*; +use tower_lsp_server::ls_types::*; use tracing::warn; use xmlparser::{StrSpan, TextPos, Token}; @@ -44,24 +45,23 @@ macro_rules! some { macro_rules! dig { () => { None }; ($start:expr, $($rest:tt)+) => { - dig!(@inner Some($start), $($rest)+) + $crate::dig!(@inner Some($start), $($rest)+) }; (@inner $node:expr, $kind:ident($idx:literal).$($rest:tt)+) => { - dig!( + $crate::dig!( @inner - if let Some(node) = $node && let Some(child) = node.named_child($idx) && child.kind() == stringify!($kind) { Some(child) } else { None }, + if let Some(node) = $node && let Some(child) = $crate::utils::python_nth_named_child_matching::<$idx>(node, stringify!($kind)) { Some(child) } else { None }, $($rest)+ ) }; - (@inner $node:expr, $kind:ident.$($rest:tt)+) => { - dig!(@inner $node, $kind(0).$($rest)+) + $crate::dig!(@inner $node, $kind(0).$($rest)+) }; (@inner $node:expr, $kind:ident($idx:literal)) => { - if let Some(node) = $node && let Some(child) = node.named_child($idx) && child.kind() == stringify!($kind) { Some(child) } else { None } + if let Some(node) = $node && let Some(child) = $crate::utils::python_nth_named_child_matching::<$idx>(node, stringify!($kind)) { Some(child) } else { None } }; (@inner $node:expr, $kind:ident) => { - dig!(@inner $node, $kind(0)) + $crate::dig!(@inner $node, $kind(0)) }; } @@ -81,14 +81,19 @@ macro_rules! await_did_open_document { ($self:expr, $path:expr) => { let mut blocker = None; { - if let Some(document) = $self.document_map.get($path) + if let Some(document) = $self + .document_map + .try_get($path) + .expect($crate::format_loc!("deadlock")) && document.setup.should_wait() { - blocker = Some(document.setup.clone()); + blocker = Some(std::sync::Arc::clone(&document.setup)); } } if let Some(blocker) = blocker { + info!("[{}] waiting on {}", $crate::loc!(), $path); blocker.wait().await; + info!("[{}] done waiting on {}", $crate::loc!(), $path); } }; } @@ -168,69 +173,64 @@ where /// - [Range] -> [CharRange] /// - [Range] <-> [ByteRange] #[inline] -pub fn rope_conv(src: T, rope: RopeSlice<'_>) -> Result>>::Error> +pub fn rope_conv(src: T, rope: RopeSlice<'_>) -> U where - for<'a> U: TryFrom>, + for<'a> U: From>, { - RopeAdapter(src, rope).try_into() + RopeAdapter(src, rope).into() } -impl<'a> TryFrom> for Position { - type Error = std::convert::Infallible; - fn try_from(value: RopeAdapter<'a, ByteOffset>) -> Result { +impl<'a> From> for Position { + fn from(value: RopeAdapter<'a, ByteOffset>) -> Self { let RopeAdapter(offset, rope) = value; let line = rope.byte_to_line_idx(offset.0, LINE_TYPE); let line_start_byte = rope.line_to_byte_idx(line, LINE_TYPE); let line_start_char = rope.byte_to_char_idx(line_start_byte); let char_offset = rope.byte_to_char_idx(offset.0); let column = char_offset - line_start_char; - Ok(Position::new(line as u32, column as u32)) + Position::new(line as u32, column as u32) } } -impl<'a> TryFrom> for ByteOffset { - type Error = ropey::Error; - fn try_from(value: RopeAdapter<'a, Position>) -> Result { +impl<'a> From> for ByteOffset { + fn from(value: RopeAdapter<'a, Position>) -> Self { let RopeAdapter(position, rope) = value; - let CharOffset(char_offset) = position_to_char(position, rope)?; + let CharOffset(char_offset) = position_to_char(position, rope); let byte_offset = rope.char_to_byte_idx(char_offset); - Ok(ByteOffset(byte_offset)) + ByteOffset(byte_offset) } } -impl<'a> TryFrom> for CharRange { - type Error = ropey::Error; - fn try_from(value: RopeAdapter<'a, Range>) -> Result { +impl<'a> From> for CharRange { + fn from(value: RopeAdapter<'a, Range>) -> Self { let RopeAdapter(range, rope) = value; - let start = position_to_char(range.start, rope)?; - let end = position_to_char(range.end, rope)?; - Ok(start..end) + let start = position_to_char(range.start, rope); + let end = position_to_char(range.end, rope); + start..end } } -impl<'a> TryFrom> for ByteRange { - type Error = ropey::Error; - fn try_from(value: RopeAdapter<'a, Range>) -> Result { +impl<'a> From> for ByteRange { + fn from(value: RopeAdapter<'a, Range>) -> Self { let RopeAdapter(range, rope) = value; - let start = rope_conv(range.start, rope)?; - let end = rope_conv(range.end, rope)?; - Ok(start..end) + let start = rope_conv(range.start, rope); + let end = rope_conv(range.end, rope); + start..end } } -impl<'a> TryFrom> for Range { - type Error = std::convert::Infallible; - fn try_from(value: RopeAdapter<'a, ByteRange>) -> Result { +impl<'a> From> for Range { + fn from(value: RopeAdapter<'a, ByteRange>) -> Self { let RopeAdapter(range, rope) = value; - let start = rope_conv(range.start, rope)?; - let end = rope_conv(range.end, rope)?; - Ok(Range { start, end }) + let start = rope_conv(range.start, rope); + let end = rope_conv(range.end, rope); + Range { start, end } } } -fn position_to_char(position: Position, rope: RopeSlice<'_>) -> ropey::Result { +fn position_to_char(position: Position, rope: RopeSlice<'_>) -> CharOffset { let line_offset_in_byte = rope.line_to_byte_idx(position.line as usize, LINE_TYPE); let line_offset_in_char = rope.byte_to_char_idx(line_offset_in_byte); - Ok(CharOffset(line_offset_in_char + position.character as usize)) + CharOffset(line_offset_in_char + position.character as usize) } impl From> for Position { @@ -301,6 +301,62 @@ pub fn cow_split_once<'src>( } } +#[inline(always)] +#[cold] +pub const fn cold_path() {} + +/// Copied from https://github.com/rust-lang/hashbrown/commit/64bd7db1d1b148594edfde112cdb6d6260e2cfc3 +#[inline(always)] +pub const fn likely(cond: bool) -> bool { + if cond { + true + } else { + cold_path(); + false + } +} + +#[inline(always)] +pub const fn unlikely(cond: bool) -> bool { + if !cond { + true + } else { + cold_path(); + false + } +} + +/// Only useful for Python, since the default grammar does not mark comments as extra nodes. +#[allow(clippy::disallowed_methods)] +#[inline] +pub fn python_next_named_sibling(mut node: Node) -> Option { + loop { + node = node.next_named_sibling()?; + if likely(node.kind() != "comment") { + return Some(node); + } + } +} + +#[allow(clippy::disallowed_methods)] +#[inline] +pub fn python_nth_named_child_matching<'node, const NTH: usize>( + mut node: Node<'node>, + kind: &'static str, +) -> Option> { + let mut idx = 0; + node = node.named_child(0)?; + loop { + if idx == NTH && likely(node.kind() == kind) { + return Some(node); + } + if likely(node.kind() != "comment") { + idx += 1; + } + node = node.next_named_sibling()?; + } +} + #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] #[repr(transparent)] pub struct ByteOffset(pub usize); @@ -485,37 +541,35 @@ pub fn init_for_test() { }); } +#[derive(Default)] pub struct Semaphore { - should_wait: AtomicBool, + blocked: AtomicBool, notifier: tokio::sync::Notify, } -impl Default for Semaphore { - fn default() -> Self { - Self { - should_wait: AtomicBool::new(true), - notifier: Default::default(), - } - } -} - pub struct Blocker<'a>(&'a Semaphore); impl Semaphore { + /// Panics if lock is held by another thread. + #[must_use] + #[track_caller] pub fn block(&self) -> Blocker<'_> { - self.should_wait.store(true, Ordering::SeqCst); + if self.blocked.compare_exchange(false, true, Relaxed, Relaxed) != Ok(false) { + panic!("Misuse of Semaphore::block: lock is still being held!"); + } + info!("{:?} blocking {:p}", std::thread::current().id(), self); Blocker(self) } - pub const WAIT_LIMIT: std::time::Duration = std::time::Duration::from_secs(10); + pub const WAIT_LIMIT: std::time::Duration = std::time::Duration::from_secs(2); /// Waits for a maximum of [`WAIT_LIMIT`][Self::WAIT_LIMIT] for a notification. pub async fn wait(&self) { - if self.should_wait.load(Ordering::SeqCst) { + while self.should_wait() { tokio::select! { - _ = self.notifier.notified() => {} + _ = self.notifier.notified() => return, _ = tokio::time::sleep(Self::WAIT_LIMIT) => { - warn!("WAIT_LIMIT elapsed (thread={:?})", std::thread::current().id()); + warn!("WAIT_LIMIT elapsed (thread={:?} blocker={:p})", std::thread::current().id(), self); } } } @@ -523,13 +577,16 @@ impl Semaphore { #[inline] pub fn should_wait(&self) -> bool { - self.should_wait.load(Ordering::SeqCst) + self.blocked.load(Relaxed) } } impl Drop for Blocker<'_> { fn drop(&mut self) { - self.0.should_wait.store(false, Ordering::SeqCst); + if self.0.blocked.compare_exchange(true, false, Relaxed, Relaxed) != Ok(true) { + panic!("Failed to release Semaphore lock for {:?}", std::thread::current().id()); + } + info!("{:?} dropping {:p}", std::thread::current().id(), self.0); self.0.notifier.notify_waiters(); } } @@ -622,6 +679,20 @@ pub fn to_display_path(path: impl AsRef) -> String { path.as_ref().to_string_lossy().into_owned() } +pub struct Defer(pub Option) +where + T: FnOnce(); + +impl Drop for Defer +where + T: FnOnce(), +{ + fn drop(&mut self) { + let func = self.0.take().unwrap(); + func() + } +} + /// On Windows, rewrites the wide path prefix `\\?\C:` to `C:` /// Source: https://stackoverflow.com/a/70970317 #[inline] @@ -686,4 +757,28 @@ mod tests { let rhs = strict_canonicalize(&lhs).unwrap(); assert_eq!(lhs, rhs); } + + #[test] + fn test_python_first_nth_child_matching() { + use tree_sitter::Parser; + use tree_sitter_python::LANGUAGE; + + let contents = r#"[ + # A comment + 1, + # Another comment + 2, + 3, + } + }"#; + + let mut parser = Parser::new(); + parser.set_language(&LANGUAGE.into()).unwrap(); + let tree = parser.parse(contents, None).unwrap(); + let root = tree.root_node(); + let list_node = root.named_child(0).unwrap(); + let first_element = list_node.named_child(1).unwrap(); + let second_element = super::python_nth_named_child_matching::<0>(list_node, "integer").unwrap(); + pretty_assertions::assert_eq!(first_element, second_element); + } } diff --git a/src/utils/catch_panic.rs b/src/utils/catch_panic.rs index 84709d7..7fcf702 100644 --- a/src/utils/catch_panic.rs +++ b/src/utils/catch_panic.rs @@ -75,7 +75,7 @@ fn handle_panic_err(err: &dyn Any) { } enum ServerErrors { - PreawaitPanic, + PreawaitPanic = 0x1000, PostawaitPanic, } diff --git a/src/xml.rs b/src/xml.rs index 393b1f0..4271608 100644 --- a/src/xml.rs +++ b/src/xml.rs @@ -6,14 +6,14 @@ use std::sync::atomic::Ordering::Relaxed; use fomat_macros::fomat; use lasso::Spur; -use tower_lsp_server::{UriExt, lsp_types::*}; +use tower_lsp_server::ls_types::*; use tracing::{debug, instrument, warn}; use tree_sitter::Parser; use xmlparser::{ElementEnd, Error, StrSpan, StreamError, Token, Tokenizer}; use crate::prelude::*; -use crate::analyze::{Scope, Type, normalize}; +use crate::analyze::{Scope, Type, normalize, type_cache}; use crate::component::{ComponentTemplate, PropType}; use crate::index::Index; use crate::model::{Field, FieldKind, PropertyKind}; @@ -129,7 +129,7 @@ impl Backend { record_ranges.extend( entries .into_iter() - .map(|entry| rope_conv(entry.template.location.unwrap().range, rope).unwrap()), + .map(|entry| rope_conv(entry.template.location.unwrap().range, rope)), ); continue; } @@ -142,10 +142,7 @@ impl Backend { continue; } }; - let Ok(range) = rope_conv(record.location.range, rope) else { - debug!("no range for {}", record.id); - continue; - }; + let range = rope_conv(record.location.range, rope); record_ranges.push(range); if let Some(prefix) = record_prefix.as_mut() { self.index @@ -161,7 +158,7 @@ impl Backend { record_ranges.extend( entries .into_iter() - .map(|entry| rope_conv(entry.template.location.unwrap().range, rope).unwrap()), + .map(|entry| rope_conv(entry.template.location.unwrap().range, rope)), ); } } @@ -191,7 +188,7 @@ impl Backend { .record_ranges .get(uri.path().as_str()) .ok_or_else(|| errloc!("Did not build record ranges for {}", uri.path().as_str()))?; - let mut offset_at_cursor = ok!(rope_conv(position, rope)); + let mut offset_at_cursor = rope_conv(position, rope); let Ok(record) = ranges.value().binary_search_by(|range| { if offset_at_cursor < range.start { Ordering::Greater @@ -305,6 +302,7 @@ impl Backend { let ast = some!(parser.parse(needle, None)); let (object, field, _) = some!(Self::attribute_node_at_offset(py_offset, ast.root_node(), needle)); let model = some!(self.index.type_of(object, &scope, needle)); + let model = type_cache().resolve(model); let model = some!(self.index.try_resolve_model(&model, &Scope::default())); self.index.jump_def_property_name(field, _R(model)) } @@ -374,11 +372,10 @@ impl Backend { let (mut needle, ref_range) = some!(ref_at_cursor); - let mut lsp_range = rope_conv( + let mut lsp_range = Some(rope_conv( ref_range.clone().map_unit(|unit| ByteOffset(unit + relative_offset)), rope, - ) - .ok(); + )); let current_module = self.index.find_module_of(&path); match ref_kind { Some(RefKind::Model) => self.index.hover_model(needle, lsp_range, false, None), @@ -427,14 +424,13 @@ impl Backend { let cursor_node = some!(ast.root_node().named_descendant_for_byte_range(py_offset, py_offset)); let needle = &needle[cursor_node.byte_range()]; let scope_type = some!(scope.get(needle)); - let lsp_range = rope_conv( + let lsp_range = Some(rope_conv( cursor_node .byte_range() .clone() .map_unit(|unit| ByteOffset(unit + ref_range.start + relative_offset)), rope, - ) - .ok(); + )); if let Some(model) = (self.index).try_resolve_model(scope_type, &scope) { return self.index.hover_model(_R(model), lsp_range, true, Some(needle)); } @@ -447,12 +443,16 @@ impl Backend { })); }; let model = some!(self.index.type_of(object, &scope, needle)); + let model = type_cache().resolve(model); let model = some!(self.index.try_resolve_model(&model, &scope)); let anchor = ref_range.start + relative_offset; self.index.hover_property_name( field, _R(model), - rope_conv(range.map_unit(|rel_unit| ByteOffset(rel_unit + anchor)), rope).ok(), + Some(rope_conv( + range.map_unit(|rel_unit| ByteOffset(rel_unit + anchor)), + rope, + )), ) } Some(RefKind::Component) => Ok(self.index.hover_component(needle, lsp_range)), @@ -604,7 +604,7 @@ impl Index { )? } RefKind::Model => { - let range = rope_conv(replace_range, rope)?; + let range = rope_conv(replace_range, rope); self.complete_model(needle, range, &mut items)? } ref ref_kind @ RefKind::PropertyName(ref access) | ref ref_kind @ RefKind::MethodName(ref access) => { @@ -637,7 +637,7 @@ impl Index { )?; } RefKind::TInherit | RefKind::TCall => { - let range = rope_conv(replace_range, rope)?; + let range = rope_conv(replace_range, rope); self.complete_template_name(needle, range, &mut items)?; } RefKind::PropOf(component) => { @@ -691,6 +691,7 @@ impl Index { }); let scope = scope.unwrap_or(default_scope); let model = some!(self.type_of(object, &scope, value)); + let model = type_cache().resolve(model); let model = some!(self.try_resolve_model(&model, &scope)); let needle_end = py_offset.saturating_sub(range.start); let mut needle = field; @@ -875,11 +876,11 @@ impl Index { ref_at_cursor = Some((inner, start_offset..end_offset)); } ref_kind = Some(RefKind::Id); - model_filter = Some(ACTION_MODELS.iter().map(|&s| ImStr::from_static(s)).collect()); + model_filter = Some(ACTION_MODELS.iter().map(|&s| s.into()).collect()); } else if button_type == Some("action") { ref_at_cursor = Some((value.as_str(), value.range())); ref_kind = Some(RefKind::Id); - model_filter = Some(ACTION_MODELS.iter().map(|&s| ImStr::from_static(s)).collect()); + model_filter = Some(ACTION_MODELS.iter().map(|&s| s.into()).collect()); } else { ref_at_cursor = Some((value.as_str(), value.range())); ref_kind = Some(RefKind::MethodName(vec![])); @@ -937,12 +938,12 @@ impl Index { "action" => { ref_at_cursor = Some((value.as_str(), value.range())); ref_kind = Some(RefKind::Id); - model_filter = Some(ACTION_MODELS.iter().map(|&s| ImStr::from_static(s)).collect()); + model_filter = Some(ACTION_MODELS.iter().map(|&s| s.into()).collect()); } "groups" => { ref_kind = Some(RefKind::Id); arch_model = None; - model_filter = Some(vec![ImStr::from_static("res.groups")]); + model_filter = Some(vec!["res.groups".into()]); determine_csv_xmlid_subgroup_of_xmlspan(&mut ref_at_cursor, value, offset_at_cursor); } _ => {} @@ -1098,8 +1099,8 @@ impl Index { // // move one place back to get the attribute name expected_eq_pos.col = expected_eq_pos.col.saturating_sub(1); - let start_pos: ByteOffset = rope_conv(span_conv(start_pos), slice)?; - let expected_eq_pos: ByteOffset = rope_conv(span_conv(expected_eq_pos), slice)?; + let start_pos: ByteOffset = rope_conv(span_conv(start_pos), slice); + let expected_eq_pos: ByteOffset = rope_conv(span_conv(expected_eq_pos), slice); // may be an invalid attribute, but no point in checking let mut range = (start_pos..expected_eq_pos).erase(); @@ -1167,7 +1168,10 @@ impl Index { contents: &str, ) -> anyhow::Result<()> { normalize(&mut root); - let type_ = self.type_of(root, scope, contents).unwrap_or(Type::Value); + let type_ = match self.type_of(root, scope, contents) { + Some(tid) => type_cache().resolve(tid), + None => Type::Value, + }; let type_ = self .try_resolve_model(&type_, scope) .map(|model| Type::Model(_R(model).into())) diff --git a/src/xml/tests.rs b/src/xml/tests.rs index e980730..cafed4b 100644 --- a/src/xml/tests.rs +++ b/src/xml/tests.rs @@ -1,5 +1,4 @@ use super::*; -use crate::xml::CompletionResponse; #[test] fn test_determine_csv_xmlid_subgroup_single() { diff --git a/testing/Cargo.toml b/testing/Cargo.toml index 19eafe6..967af03 100644 --- a/testing/Cargo.toml +++ b/testing/Cargo.toml @@ -13,7 +13,7 @@ async-lsp = { version = "0.2.2", features = ["tokio", "forward"] } tokio-util = { version = "0.7.14", features = ["compat"] } tree-sitter-xml = "0.7.0" tree-sitter-javascript = "0.23.1" -rstest = "0.25.0" +rstest = { version = "0.25.0", features = ["async-timeout"] } ts-macros.workspace = true pretty_assertions.workspace = true tower.workspace = true @@ -27,10 +27,10 @@ tracing-subscriber.workspace = true odoo-lsp.path = ".." [target.'cfg(target_os = "linux")'.dependencies] -iai-callgrind = { version = "0.16.0", features = ["client_requests"] } +iai-callgrind = { version = "0.16.1", features = ["client_requests"] } [target.'cfg(not(target_os = "linux"))'.dependencies] -iai-callgrind = "0.16.0" +iai-callgrind = "0.16.1" [[bench]] name = "standard" diff --git a/testing/fixtures/python_expressions/.odoo_lsp b/testing/fixtures/python_expressions/.odoo_lsp new file mode 100644 index 0000000..6c17534 --- /dev/null +++ b/testing/fixtures/python_expressions/.odoo_lsp @@ -0,0 +1 @@ +{"module":{"roots":["."]}} \ No newline at end of file diff --git a/testing/fixtures/python_expressions/main/__manifest__.py b/testing/fixtures/python_expressions/main/__manifest__.py new file mode 100644 index 0000000..b02b1fb --- /dev/null +++ b/testing/fixtures/python_expressions/main/__manifest__.py @@ -0,0 +1 @@ +{"name": "main"} \ No newline at end of file diff --git a/testing/fixtures/python_expressions/main/models.py b/testing/fixtures/python_expressions/main/models.py new file mode 100644 index 0000000..09ba6ae --- /dev/null +++ b/testing/fixtures/python_expressions/main/models.py @@ -0,0 +1,8 @@ +class Main(models.Model): + _name = 'main' + + def test_expression(self): + foo = self if True else self + #^type Model("main") + bar = self if True else 123 # we just pick an arbitrary one + #^type Model("main") diff --git a/testing/fixtures/python_types/.odoo_lsp b/testing/fixtures/python_types/.odoo_lsp new file mode 100644 index 0000000..6c17534 --- /dev/null +++ b/testing/fixtures/python_types/.odoo_lsp @@ -0,0 +1 @@ +{"module":{"roots":["."]}} \ No newline at end of file diff --git a/testing/fixtures/python_types/bakery/__manifest__.py b/testing/fixtures/python_types/bakery/__manifest__.py new file mode 100644 index 0000000..980dd3b --- /dev/null +++ b/testing/fixtures/python_types/bakery/__manifest__.py @@ -0,0 +1 @@ +{"name": "bakery"} \ No newline at end of file diff --git a/testing/fixtures/python_types/bakery/models/models.py b/testing/fixtures/python_types/bakery/models/models.py new file mode 100644 index 0000000..d4c6efd --- /dev/null +++ b/testing/fixtures/python_types/bakery/models/models.py @@ -0,0 +1,140 @@ +from odoo import models, fields + + +class Bread(models.Model): + _name = 'bakery.bread' + + def _test(self): + items = {item: item for item in self} + #^type Dict(Model("bakery.bread"), Model("bakery.bread")) + + foobar = {'a': self, 'b': 123} + #^type DictBag([("a", Model("bakery.bread")), ("b", PyBuiltin("int"))]) + + aaaa = foobar['a'] + #^type Model("bakery.bread") + + bbbb = foobar['b'] + #^type PyBuiltin("int") + + return foobar + + def identity(self, what): + return {'c': what} + + def _test_return(self): + foobar = self._test() + aaaa = foobar['a'] + #^type Model("bakery.bread") + bbbb = foobar['b'] + #^type PyBuiltin("int") + + baz = self.identity(self) + cccc = baz['c'] + #^type Model("bakery.bread") + + def test_variable_append(self): + foo = [] + #^type List(...) + foo.append(self) + foo + #^type List(Model("bakery.bread")) + + def test_dictkey_append(self): + foo = self.identity([]) + foo['c'].append(self) + cccc = foo['c'] + #^type List(Model("bakery.bread")) + elem = cccc[12] + #^type Model("bakery.bread") + + def test_dict_set(self): + foobar = {} + foobar['a'] = self + aaaa = foobar['a'] + #^type Model("bakery.bread") + foobar['b'] = nonexistent + foobar + #^type DictBag([("a", Model("bakery.bread")), ("b", Value)]) + + def test_dict_update(self): + foobar = {} + foobar.update({'a': self}) + aaaa = foobar['a'] + #^type Model("bakery.bread") + + def test_sanity(self): + foobar = ['what'] + #^type List(PyBuiltin("str")) + self._fields + #^type Dict(PyBuiltin("str"), Model("ir.model.fields")) + + def test_builtins(self): + for aaaa, bbbb in enumerate(self): + aaaa + #^type PyBuiltin("int") + bbbb + #^type Model("bakery.bread") + + ints = [1, 2, 3] + for aaaa, bbbb in zip(self, ints): + aaaa + #^type Model("bakery.bread") + bbbb + #^type PyBuiltin("int") + + what = [ + 123 for + cccc, + #^type Model("bakery.bread") + dddd + #^type PyBuiltin("int") + in zip(self, ints) + ] + + def _identity_tuple(self, obj): + return self, obj + + def _test_tuple(self): + foo, bar = self._identity_tuple(123) + #^type Model("bakery.bread") + bar + #^type PyBuiltin("int") + + def test_subscript(self): + foobar = {'abcde': 123, 'fool': 234} + foobar[''] + # ^complete abcde fool + foobar['f'] + # ^complete fool + + +class Wine(models.Model): + _name = 'bakery.wine' + + name = fields.Char() + make = fields.Char() + value = fields.Float() + + def _test_read_group(self): + domain = [] + for name, make, value in self._read_group(domain, ['name', 'make'], ['value:sum']): + #^type PyBuiltin("str") + make + #^type PyBuiltin("str") + value + #^type PyBuiltin("float") + + def _test_mapped(self): + foo = self.mapped('make') + #^type List(PyBuiltin("str")) + + def test_grouped(self): + for name, records in self.grouped('name').items(): + #^type PyBuiltin("str") + records + #^type Model("bakery.wine") + for (some, thing), _records in self.grouped(lambda mov: (mov.name, mov.value)).items(): + #^type PyBuiltin("str") + thing + #^type PyBuiltin("float") diff --git a/testing/src/tests.rs b/testing/src/tests.rs index b21553b..fe78f0b 100644 --- a/testing/src/tests.rs +++ b/testing/src/tests.rs @@ -30,11 +30,11 @@ fn init_tracing() { } #[rstest] -#[timeout(Duration::from_secs(1))] #[tokio::test(flavor = "multi_thread")] +#[timeout(Duration::from_secs(10))] async fn fixture_test(#[files("fixtures/*")] root: PathBuf) { std::env::set_current_dir(&root).unwrap(); - let mut server = server::setup_lsp_server(None); + let mut server = server::setup_lsp_server(Some(2)); init_tracing(); _ = server @@ -93,7 +93,7 @@ async fn fixture_test(#[files("fixtures/*")] root: PathBuf) { uri: uri.clone(), language_id, version: 1, - text: text.clone(), + text, }, }); @@ -507,7 +507,7 @@ fn gather_expected(root: &Path, lang: TestLanguages) -> HashMap