diff --git a/Cargo.lock b/Cargo.lock
index 938c437..3bdc5f8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -246,15 +246,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
-version = "1.11.0"
+version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cc"
-version = "1.2.53"
+version = "1.2.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932"
+checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -284,9 +284,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.54"
+version = "4.5.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
+checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a"
dependencies = [
"clap_builder",
"clap_derive",
@@ -294,9 +294,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.5.54"
+version = "4.5.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
+checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238"
dependencies = [
"anstream",
"anstyle",
@@ -306,9 +306,9 @@ dependencies = [
[[package]]
name = "clap_derive"
-version = "4.5.49"
+version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
+checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
dependencies = [
"heck",
"proc-macro2",
@@ -601,6 +601,12 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
[[package]]
name = "fd-lock"
version = "4.0.4"
@@ -609,14 +615,14 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78"
dependencies = [
"cfg-if",
"rustix",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
]
[[package]]
name = "find-msvc-tools"
-version = "0.1.8"
+version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fixedbitset"
@@ -889,12 +895,11 @@ dependencies = [
[[package]]
name = "hyper-util"
-version = "0.1.19"
+version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
dependencies = [
"bytes",
- "futures-core",
"http",
"http-body",
"hyper",
@@ -905,9 +910,9 @@ dependencies = [
[[package]]
name = "iana-time-zone"
-version = "0.1.64"
+version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -1167,6 +1172,16 @@ version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
+[[package]]
+name = "libmimalloc-sys"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870"
+dependencies = [
+ "cc",
+ "libc",
+]
+
[[package]]
name = "libredox"
version = "0.1.12"
@@ -1244,6 +1259,15 @@ version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
+[[package]]
+name = "mimalloc"
+version = "0.1.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8"
+dependencies = [
+ "libmimalloc-sys",
+]
+
[[package]]
name = "mime"
version = "0.3.17"
@@ -1264,23 +1288,24 @@ dependencies = [
[[package]]
name = "naviscope-api"
-version = "0.5.0"
+version = "0.5.5"
dependencies = [
"async-trait",
"lasso",
"schemars",
"serde",
"serde_json",
- "thiserror 2.0.17",
+ "thiserror 2.0.18",
]
[[package]]
name = "naviscope-cli"
-version = "0.5.0"
+version = "0.5.5"
dependencies = [
"clap",
"dirs",
"indexmap",
+ "mimalloc",
"naviscope-api",
"naviscope-lsp",
"naviscope-mcp",
@@ -1299,14 +1324,16 @@ dependencies = [
[[package]]
name = "naviscope-core"
-version = "0.5.0"
+version = "0.5.5"
dependencies = [
"async-trait",
+ "dashmap 6.1.0",
"ignore",
"lasso",
"log",
"lsp-types",
"naviscope-api",
+ "naviscope-plugin",
"notify",
"once_cell",
"petgraph",
@@ -1316,7 +1343,8 @@ dependencies = [
"schemars",
"serde",
"serde_json",
- "thiserror 2.0.17",
+ "tempfile",
+ "thiserror 2.0.18",
"tokio",
"tokio-util",
"tracing",
@@ -1332,41 +1360,43 @@ dependencies = [
[[package]]
name = "naviscope-gradle"
-version = "0.5.0"
+version = "0.5.5"
dependencies = [
"lasso",
"naviscope-api",
"naviscope-core",
+ "naviscope-plugin",
"once_cell",
"petgraph",
"rmp-serde",
"serde",
"serde_json",
- "thiserror 2.0.17",
+ "thiserror 2.0.18",
"tree-sitter",
"tree-sitter-groovy",
]
[[package]]
name = "naviscope-java"
-version = "0.5.0"
+version = "0.5.5"
dependencies = [
"lasso",
"lsp-types",
"naviscope-api",
"naviscope-core",
+ "naviscope-plugin",
"petgraph",
"rmp-serde",
"serde",
"serde_json",
- "thiserror 2.0.17",
+ "thiserror 2.0.18",
"tree-sitter",
"tree-sitter-java",
]
[[package]]
name = "naviscope-lsp"
-version = "0.5.0"
+version = "0.5.5"
dependencies = [
"dashmap 6.1.0",
"naviscope-api",
@@ -1383,7 +1413,7 @@ dependencies = [
[[package]]
name = "naviscope-mcp"
-version = "0.5.0"
+version = "0.5.5"
dependencies = [
"anyhow",
"axum",
@@ -1401,9 +1431,22 @@ dependencies = [
"xxhash-rust",
]
+[[package]]
+name = "naviscope-plugin"
+version = "0.5.5"
+dependencies = [
+ "async-trait",
+ "lsp-types",
+ "naviscope-api",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "tree-sitter",
+]
+
[[package]]
name = "naviscope-runtime"
-version = "0.5.0"
+version = "0.5.5"
dependencies = [
"naviscope-api",
"naviscope-core",
@@ -1432,9 +1475,12 @@ dependencies = [
[[package]]
name = "notify-types"
-version = "2.0.0"
+version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
+checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a"
+dependencies = [
+ "bitflags 2.10.0",
+]
[[package]]
name = "nu-ansi-term"
@@ -1447,9 +1493,9 @@ dependencies = [
[[package]]
name = "num-conv"
-version = "0.1.0"
+version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
[[package]]
name = "num-traits"
@@ -1623,18 +1669,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.105"
+version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
-version = "1.0.43"
+version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
+checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
dependencies = [
"proc-macro2",
]
@@ -1741,7 +1787,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.17",
"libredox",
- "thiserror 2.0.17",
+ "thiserror 2.0.18",
]
[[package]]
@@ -1759,7 +1805,7 @@ dependencies = [
"strip-ansi-escapes",
"strum",
"strum_macros",
- "thiserror 2.0.17",
+ "thiserror 2.0.18",
"unicase",
"unicode-segmentation",
"unicode-width",
@@ -1787,9 +1833,9 @@ dependencies = [
[[package]]
name = "regex"
-version = "1.12.2"
+version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
@@ -1799,9 +1845,9 @@ dependencies = [
[[package]]
name = "regex-automata"
-version = "0.4.13"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
@@ -1810,9 +1856,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
-version = "0.8.8"
+version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
+checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
[[package]]
name = "rmcp"
@@ -1830,7 +1876,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
- "thiserror 2.0.17",
+ "thiserror 2.0.18",
"tokio",
"tokio-util",
"tracing",
@@ -1913,9 +1959,9 @@ dependencies = [
[[package]]
name = "schemars"
-version = "1.2.0"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2"
+checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc"
dependencies = [
"chrono",
"dyn-clone",
@@ -1927,9 +1973,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
-version = "1.2.0"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4908ad288c5035a8eb12cfdf0d49270def0a268ee162b75eeee0f85d155a7c45"
+checksum = "7d115b50f4aaeea07e79c1912f645c7513d81715d0420f8bc77a18c6260b307f"
dependencies = [
"proc-macro2",
"quote",
@@ -2097,9 +2143,9 @@ dependencies = [
[[package]]
name = "slab"
-version = "0.4.11"
+version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
@@ -2109,9 +2155,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
-version = "0.6.1"
+version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
+checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0"
dependencies = [
"libc",
"windows-sys 0.60.2",
@@ -2215,6 +2261,19 @@ dependencies = [
"syn",
]
+[[package]]
+name = "tempfile"
+version = "3.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.4",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "testing_table"
version = "0.3.0"
@@ -2235,11 +2294,11 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "2.0.17"
+version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
- "thiserror-impl 2.0.17",
+ "thiserror-impl 2.0.18",
]
[[package]]
@@ -2255,9 +2314,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
-version = "2.0.17"
+version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
@@ -2275,9 +2334,9 @@ dependencies = [
[[package]]
name = "time"
-version = "0.3.45"
+version = "0.3.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
+checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5"
dependencies = [
"deranged",
"itoa",
@@ -2290,15 +2349,15 @@ dependencies = [
[[package]]
name = "time-core"
-version = "0.1.7"
+version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "time-macros"
-version = "0.2.25"
+version = "0.2.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
+checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4"
dependencies = [
"num-conv",
"time-core",
@@ -2473,7 +2532,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
dependencies = [
"crossbeam-channel",
- "thiserror 2.0.17",
+ "thiserror 2.0.18",
"time",
"tracing-subscriber",
]
@@ -2530,9 +2589,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
-version = "0.26.3"
+version = "0.26.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "974d205cc395652cfa8b37daa053fe56eebd429acf8dc055503fee648dae981e"
+checksum = "12987371f54efc9b9306a20dc87ed5aaee9f320c8a8b115e28515c412b2efe39"
dependencies = [
"cc",
"regex",
@@ -2564,9 +2623,9 @@ dependencies = [
[[package]]
name = "tree-sitter-language"
-version = "0.1.6"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ae62f7eae5eb549c71b76658648b72cc6111f2d87d24a1e31fa907f4943e3ce"
+checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782"
[[package]]
name = "tungstenite"
@@ -2599,7 +2658,7 @@ dependencies = [
"log",
"rand 0.9.2",
"sha1",
- "thiserror 2.0.17",
+ "thiserror 2.0.18",
"utf-8",
]
@@ -2847,9 +2906,9 @@ dependencies = [
[[package]]
name = "windows-sys"
-version = "0.52.0"
+version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
@@ -3044,18 +3103,18 @@ dependencies = [
[[package]]
name = "zerocopy"
-version = "0.8.33"
+version = "0.8.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
+checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.33"
+version = "0.8.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
+checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0"
dependencies = [
"proc-macro2",
"quote",
@@ -3118,9 +3177,9 @@ dependencies = [
[[package]]
name = "zmij"
-version = "1.0.15"
+version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94f63c051f4fe3c1509da62131a678643c5b6fbdc9273b2b79d4378ebda003d2"
+checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
[[package]]
name = "zstd"
diff --git a/Cargo.toml b/Cargo.toml
index ca4c637..84526ff 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,6 +9,7 @@ members = [
"crates/mcp",
"crates/api",
"crates/runtime",
+ "crates/plugin",
]
[workspace.dependencies]
@@ -19,6 +20,7 @@ naviscope-lsp = { path = "crates/lsp" }
naviscope-mcp = { path = "crates/mcp" }
naviscope-api = { path = "crates/api" }
naviscope-runtime = { path = "crates/runtime" }
+naviscope-plugin = { path = "crates/plugin" }
petgraph = { version = "0.8", features = ["serde-1"] }
tree-sitter = "0.26"
@@ -62,4 +64,5 @@ async-trait = "0.1"
url = "2.5.8"
tree-sitter-java = "0.23.5"
tree-sitter-groovy = "0.1.2"
-
+mimalloc = "0.1"
+tempfile = "3.10"
diff --git a/README.md b/README.md
index a120b88..43ccf7d 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,7 @@ graph TD
classDef interface fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,rx:5,ry:5
classDef runtime fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,rx:5,ry:5
classDef language fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,rx:5,ry:5
+ classDef plugin fill:#fff9c4,stroke:#fbc02d,stroke-width:2px,rx:5,ry:5
classDef core fill:#fff3e0,stroke:#ef6c00,stroke-width:2px,rx:5,ry:5
classDef api fill:#fafafa,stroke:#616161,stroke-width:2px,rx:5,ry:5
@@ -60,6 +61,10 @@ graph TD
Gradle["naviscope-gradle
(Gradle Analysis)"]:::language
end
+ subgraph Abstraction [Plugin Layer]
+ Plugin["naviscope-plugin
(Traits & Contracts)"]:::plugin
+ end
+
subgraph Engine [Core Layer]
Core["naviscope-core
(Graph, Index & IO)"]:::core
end
@@ -84,21 +89,25 @@ graph TD
Runtime --> Core
Runtime --> API
- Java --> Core
+ Java --> Plugin
Java --> API
- Gradle --> Core
+ Gradle --> Plugin
Gradle --> API
+ Core --> Plugin
Core --> API
+
+ Plugin --> API
```
Naviscope is built on a **layered crate architecture** that separates concerns across multiple Rust crates:
- **Interface Layer** (`naviscope-cli`, `naviscope-lsp`, `naviscope-mcp`): Entry points for different use cases (CLI shell, LSP for editors, MCP for AI agents).
- **Runtime Layer** (`naviscope-runtime`): Orchestrates the engine assembly, registering language plugins and providing a unified factory.
-- **Language Layer** (`naviscope-java`, `naviscope-gradle`): Language-specific analysis plugins that parse and resolve symbols.
-- **Core Layer** (`naviscope-core`): The heart of the system - graph storage, indexing, file scanning, and persistence.
+- **Language Layer** (`naviscope-java`, `naviscope-gradle`): Language-specific implementations that implement the standard plugin contracts.
+- **Plugin Layer** (`naviscope-plugin`): Defines the standard contracts and traits (`LanguagePlugin`, `BuildToolPlugin`, `Resolver`) effectively decoupling the core engine from specific language logic.
+- **Core Layer** (`naviscope-core`): The heart of the system - graph storage, indexing, file scanning, and persistence. It consumes the plugin traits to process files.
- **API Layer** (`naviscope-api`): Common traits and models shared across all crates, ensuring a consistent interface.
The core is a language-agnostic graph structure populated by language-specific strategies (currently Java/Gradle via Tree-sitter), exposing a unified query engine to both AI agents and developer tools.
@@ -117,15 +126,13 @@ This hybrid approach combines the speed of inverted indexing with the precision
### Prerequisites
- Rust (2024 edition)
-- C Compiler (required for compiling Tree-sitter grammars)
### Installation from source code
```bash
-# 1. Clone & Update Submodules (Required for tree-sitter grammars)
+# 1. Clone
git clone https://github.com/biuld/naviscope.git
cd naviscope
-git submodule update --init --recursive
# 2. Install the Naviscope CLI
cargo install --path crates/cli
diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml
index 097d610..b5df7de 100644
--- a/crates/api/Cargo.toml
+++ b/crates/api/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "naviscope-api"
-version = "0.5.0"
+version = "0.5.5"
edition = "2024"
[dependencies]
diff --git a/crates/api/src/models/graph.rs b/crates/api/src/models/graph.rs
index bf926fa..792c1e7 100644
--- a/crates/api/src/models/graph.rs
+++ b/crates/api/src/models/graph.rs
@@ -95,7 +95,7 @@ impl GraphEdge {
}
}
-use super::symbol::Symbol;
+use super::symbol::{FqnId, Symbol};
use lasso::Reader;
use std::any::Any;
use std::fmt::Debug;
@@ -118,8 +118,8 @@ impl NodeMetadata for EmptyMetadata {
#[derive(Debug, Clone)]
pub struct GraphNode {
- /// Unique Identifier (Symbol)
- pub id: Symbol,
+ /// Unique Identifier (Structured FQN)
+ pub id: FqnId,
/// Short display name (Symbol)
pub name: Symbol,
/// Abstract categorization
@@ -135,7 +135,7 @@ pub struct GraphNode {
impl Default for GraphNode {
fn default() -> Self {
Self {
- id: Symbol(lasso::Spur::default()),
+ id: FqnId(0),
name: Symbol(lasso::Spur::default()),
kind: NodeKind::Custom("unknown".to_string()),
lang: Symbol(lasso::Spur::default()),
@@ -150,10 +150,6 @@ impl GraphNode {
Language::new(rodeo.resolve(&self.lang.0).to_string())
}
- pub fn fqn<'a>(&self, rodeo: &'a dyn Reader) -> &'a str {
- rodeo.resolve(&self.id.0)
- }
-
pub fn name<'a>(&self, rodeo: &'a dyn Reader) -> &'a str {
rodeo.resolve(&self.name.0)
}
@@ -185,16 +181,6 @@ pub struct DisplaySymbolLocation {
pub selection_range: Option,
}
-impl DisplaySymbolLocation {
- pub fn to_internal(&self, rodeo: &mut lasso::Rodeo) -> super::symbol::InternedLocation {
- super::symbol::InternedLocation {
- path: Symbol(rodeo.get_or_intern(&self.path)),
- range: self.range,
- selection_range: self.selection_range,
- }
- }
-}
-
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
pub struct DisplayGraphNode {
pub id: String,
@@ -213,19 +199,6 @@ pub struct DisplayGraphNode {
pub children: Option>,
}
-impl DisplayGraphNode {
- pub fn to_internal(&self, rodeo: &mut lasso::Rodeo) -> GraphNode {
- GraphNode {
- id: Symbol(rodeo.get_or_intern(&self.id)),
- name: Symbol(rodeo.get_or_intern(&self.name)),
- kind: self.kind.clone(),
- lang: Symbol(rodeo.get_or_intern(&self.lang)),
- location: self.location.as_ref().map(|l| l.to_internal(rodeo)),
- metadata: Arc::new(EmptyMetadata),
- }
- }
-}
-
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)]
#[serde(tag = "command", rename_all = "snake_case")]
pub enum GraphQuery {
diff --git a/crates/api/src/models/symbol.rs b/crates/api/src/models/symbol.rs
index fb834bd..6d64ade 100644
--- a/crates/api/src/models/symbol.rs
+++ b/crates/api/src/models/symbol.rs
@@ -17,6 +17,86 @@ impl JsonSchema for Symbol {
}
}
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, JsonSchema)]
+pub enum NodeId {
+ Flat(String),
+ Structured(Vec<(super::graph::NodeKind, String)>),
+}
+
+impl From for NodeId {
+ fn from(s: String) -> Self {
+ NodeId::Flat(s)
+ }
+}
+
+impl From<&str> for NodeId {
+ fn from(s: &str) -> Self {
+ NodeId::Flat(s.to_string())
+ }
+}
+
+impl std::fmt::Display for NodeId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ NodeId::Flat(s) => write!(f, "{}", s),
+ NodeId::Structured(parts) => {
+ for (i, (kind, name)) in parts.iter().enumerate() {
+ if i > 0 {
+ match kind {
+ super::graph::NodeKind::Method
+ | super::graph::NodeKind::Constructor
+ | super::graph::NodeKind::Field => {
+ write!(f, "#")?;
+ }
+ _ => {
+ write!(f, ".")?;
+ }
+ }
+ }
+ write!(f, "{}", name)?;
+ }
+ Ok(())
+ }
+ }
+ }
+}
+
+impl NodeId {
+ pub fn as_str(&self) -> &str {
+ match self {
+ NodeId::Flat(s) => s.as_str(),
+ NodeId::Structured(_) => "structured_id",
+ }
+ }
+}
+
+pub type SymbolAtom = Symbol;
+
+#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct FqnId(pub u32);
+
+impl JsonSchema for FqnId {
+ fn schema_name() -> std::borrow::Cow<'static, str> {
+ std::borrow::Cow::Borrowed("FqnId")
+ }
+
+ fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
+ u32::json_schema(generator)
+ }
+}
+
+#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, JsonSchema)]
+pub struct FqnNode {
+ pub parent: Option,
+ pub name: Symbol,
+ pub kind: NodeKind,
+}
+
+pub trait FqnReader {
+ fn resolve_node(&self, id: FqnId) -> Option;
+ fn resolve_atom(&self, atom: Symbol) -> &str;
+}
+
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema)]
pub struct Range {
pub start_line: usize,
@@ -25,6 +105,17 @@ pub struct Range {
pub end_col: usize,
}
+impl Default for Range {
+ fn default() -> Self {
+ Self {
+ start_line: 0,
+ start_col: 0,
+ end_line: 0,
+ end_col: 0,
+ }
+ }
+}
+
impl Range {
pub fn contains(&self, line: usize, col: usize) -> bool {
if line < self.start_line || line > self.end_line {
@@ -105,9 +196,9 @@ pub struct InternedLocation {
}
impl InternedLocation {
- pub fn to_display(&self, rodeo: &dyn lasso::Reader) -> super::graph::DisplaySymbolLocation {
+ pub fn to_display(&self, fqns: &dyn FqnReader) -> super::graph::DisplaySymbolLocation {
super::graph::DisplaySymbolLocation {
- path: rodeo.resolve(&self.path.0).to_string(),
+ path: fqns.resolve_atom(self.path).to_string(),
range: self.range,
selection_range: self.selection_range,
}
diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
index b110111..3af40a8 100644
--- a/crates/cli/Cargo.toml
+++ b/crates/cli/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "naviscope-cli"
-version = "0.5.0"
+version = "0.5.5"
edition = "2024"
[[bin]]
@@ -25,3 +25,4 @@ serde_json = { workspace = true }
indexmap = { workspace = true }
petgraph = { workspace = true }
naviscope-api = { workspace = true }
+mimalloc = { workspace = true }
diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs
index 78aec5e..d212de2 100644
--- a/crates/cli/src/lib.rs
+++ b/crates/cli/src/lib.rs
@@ -76,24 +76,26 @@ pub fn run() -> Result<(), Box> {
let cli = Cli::parse();
// Initialize logging based on command
- let component = match &cli.command {
- Commands::Lsp => "lsp",
- Commands::Mcp { .. } => "mcp",
- _ => "cli",
+ let (component, to_stderr) = match &cli.command {
+ Commands::Lsp => ("lsp", false),
+ Commands::Mcp { .. } => ("mcp", false),
+ Commands::Shell { .. } => ("cli", false),
+ _ => ("cli", true),
};
- let _guard = naviscope_runtime::init_logging(component);
+ let _guard = naviscope_runtime::init_logging(component, to_stderr);
let rt = tokio::runtime::Runtime::new()?;
match cli.command {
- Commands::Index { path } => rt.block_on(index::run(path)),
- Commands::Shell { path } => rt.block_on(shell::run(path)),
- Commands::Watch { path } => rt.block_on(watch::run(path)),
- Commands::Clear { path } => rt.block_on(clear::run(path)),
+ Commands::Index { path } => rt.block_on(index::run(path.canonicalize()?)),
+ Commands::Shell { path } => rt.block_on(shell::run(path.map(|p| p.canonicalize()).transpose()?)),
+ Commands::Watch { path } => rt.block_on(watch::run(path.canonicalize()?)),
+ Commands::Clear { path } => rt.block_on(clear::run(path.map(|p| p.canonicalize()).transpose()?)),
Commands::Mcp { path } => {
- let project_path = path
- .clone()
- .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
+ let project_path = match path {
+ Some(p) => p.canonicalize()?,
+ None => std::env::current_dir()?.canonicalize()?,
+ };
// Connect to LSP via proxy mode (waits for LSP if not started)
rt.block_on(async { naviscope_mcp::proxy::run_mcp_proxy(&project_path).await })?;
diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs
index 0dacad7..ee9589d 100644
--- a/crates/cli/src/main.rs
+++ b/crates/cli/src/main.rs
@@ -1,3 +1,6 @@
+#[global_allocator]
+static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
+
fn main() -> Result<(), Box> {
naviscope_cli::run()
}
diff --git a/crates/cli/src/shell/mod.rs b/crates/cli/src/shell/mod.rs
index 1ae18d0..bfe2789 100644
--- a/crates/cli/src/shell/mod.rs
+++ b/crates/cli/src/shell/mod.rs
@@ -222,8 +222,10 @@ impl ReplServer {
}
pub async fn run(path: Option) -> Result<(), Box> {
- let project_path =
- path.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
+ let project_path = match path {
+ Some(p) => p,
+ None => std::env::current_dir()?.canonicalize()?,
+ };
let server = ReplServer::new(project_path);
server.run().await
}
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
index b274de7..93a0b37 100644
--- a/crates/core/Cargo.toml
+++ b/crates/core/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "naviscope-core"
-version = "0.5.0"
+version = "0.5.5"
edition = "2024"
[dependencies]
@@ -8,6 +8,7 @@ petgraph = { workspace = true }
tree-sitter = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
+dashmap = { workspace = true }
thiserror = { workspace = true }
walkdir = { workspace = true }
log = { workspace = true }
@@ -28,8 +29,10 @@ lsp-types = { workspace = true }
lasso = { workspace = true }
zstd = { workspace = true }
naviscope-api = { workspace = true }
+naviscope-plugin = { workspace = true }
async-trait = { workspace = true }
url = { workspace = true }
[dev-dependencies]
tree-sitter-java = { workspace = true }
+tempfile = { workspace = true }
diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs
index 3372391..dfa9bb6 100644
--- a/crates/core/src/error.rs
+++ b/crates/core/src/error.rs
@@ -10,8 +10,16 @@ pub enum NaviscopeError {
Parsing(String),
#[error("Internal error: {0}")]
Internal(String),
+ #[error("Plugin error: {0}")]
+ Plugin(String),
#[error("Unknown error")]
Unknown,
}
+impl From> for NaviscopeError {
+ fn from(err: Box) -> Self {
+ NaviscopeError::Plugin(err.to_string())
+ }
+}
+
pub type Result = std::result::Result;
diff --git a/crates/core/src/facade/graph.rs b/crates/core/src/facade/graph.rs
index 214f0f7..69cde09 100644
--- a/crates/core/src/facade/graph.rs
+++ b/crates/core/src/facade/graph.rs
@@ -13,7 +13,8 @@ impl graph::GraphService for EngineHandle {
let result = tokio::task::spawn_blocking(
move || -> Result {
- let engine = QueryEngine::new(&graph, |lang| handle.get_node_renderer(lang));
+ let conventions = (*handle.naming_conventions()).clone();
+ let engine = QueryEngine::new(&graph, |lang| handle.get_node_adapter(lang), conventions);
engine.execute(&query_clone)
},
)
diff --git a/crates/core/src/facade/mod.rs b/crates/core/src/facade/mod.rs
index c726470..cab9b96 100644
--- a/crates/core/src/facade/mod.rs
+++ b/crates/core/src/facade/mod.rs
@@ -57,18 +57,11 @@ impl EngineHandle {
self.engine.get_resolver().get_semantic_resolver(language)
}
- pub fn get_metadata_plugin(
+ pub fn get_node_adapter(
&self,
language: crate::model::source::Language,
- ) -> Option> {
- self.engine.get_resolver().get_metadata_plugin(language)
- }
-
- pub fn get_node_renderer(
- &self,
- language: crate::model::source::Language,
- ) -> Option> {
- self.engine.get_resolver().get_node_renderer(language)
+ ) -> Option> {
+ self.engine.get_resolver().get_node_adapter(language)
}
pub fn get_language_by_extension(&self, ext: &str) -> Option {
@@ -88,6 +81,19 @@ impl EngineHandle {
Some((parser, lang))
}
+ /// Get naming convention for a specific language
+ pub fn get_naming_convention(
+ &self,
+ language: &str,
+ ) -> Option> {
+ self.engine.naming_conventions().get(language).cloned()
+ }
+
+ /// Get all naming conventions (cheap Arc clone)
+ pub(crate) fn naming_conventions(&self) -> Arc>> {
+ self.engine.naming_conventions()
+ }
+
// ---- File watching ----
/// Watch for filesystem changes
diff --git a/crates/core/src/facade/navigation.rs b/crates/core/src/facade/navigation.rs
index 1dc298c..13948db 100644
--- a/crates/core/src/facade/navigation.rs
+++ b/crates/core/src/facade/navigation.rs
@@ -7,13 +7,15 @@ use naviscope_api::navigation::{NavigationService, ResolveResult};
impl NavigationService for EngineHandle {
async fn resolve_path(&self, target: &str, current_context: Option<&str>) -> ResolveResult {
let graph = self.graph().await;
- let engine = NavigationEngine::new(&graph);
+ let conventions = (*self.naming_conventions()).clone();
+ let engine = NavigationEngine::new(&graph, conventions);
engine.resolve_path(target, current_context)
}
async fn get_completion_candidates(&self, prefix: &str) -> Vec {
let graph = self.graph().await;
- let engine = NavigationEngine::new(&graph);
+ let conventions = (*self.naming_conventions()).clone();
+ let engine = NavigationEngine::new(&graph, conventions);
engine.get_completion_candidates(prefix)
}
}
diff --git a/crates/core/src/facade/semantic.rs b/crates/core/src/facade/semantic.rs
index 65a577b..3eb2343 100644
--- a/crates/core/src/facade/semantic.rs
+++ b/crates/core/src/facade/semantic.rs
@@ -1,8 +1,10 @@
use crate::facade::EngineHandle;
+use crate::features::CodeGraphLike;
use crate::features::discovery::DiscoveryEngine;
use crate::util::utf16_col_to_byte_col;
use async_trait::async_trait;
use naviscope_api::graph::GraphService;
+
use naviscope_api::models::{
CallHierarchyIncomingCall, CallHierarchyOutgoingCall, DisplayGraphNode, Language, NodeKind,
PositionContext, Range, ReferenceQuery, SymbolLocation, SymbolQuery, SymbolResolution,
@@ -99,15 +101,17 @@ impl SymbolNavigator for EngineHandle {
let topology = graph.topology();
let mut locations = Vec::new();
- for idx in matches {
- let node = &topology[idx];
- if let Some(loc) = &node.location {
- let path_str = graph.symbols().resolve(&loc.path.0);
- locations.push(SymbolLocation {
- path: Arc::from(PathBuf::from(path_str)),
- range: loc.range,
- selection_range: loc.selection_range,
- });
+ for fqn_id in matches {
+ if let Some(&idx) = graph.fqn_map().get(&fqn_id) {
+ let node = &topology[idx];
+ if let Some(loc) = &node.location {
+ let path_str = graph.symbols().resolve(&loc.path.0);
+ locations.push(SymbolLocation {
+ path: Arc::from(PathBuf::from(path_str)),
+ range: loc.range,
+ selection_range: loc.selection_range,
+ });
+ }
}
}
Ok(locations)
@@ -129,15 +133,17 @@ impl SymbolNavigator for EngineHandle {
for res in type_resolutions {
let matches = resolver.find_matches(&graph, &res);
- for idx in matches {
- let node = &topology[idx];
- if let Some(loc) = &node.location {
- let path_str = graph.symbols().resolve(&loc.path.0);
- locations.push(SymbolLocation {
- path: Arc::from(PathBuf::from(path_str)),
- range: loc.range,
- selection_range: loc.selection_range,
- });
+ for fqn_id in matches {
+ if let Some(&idx) = graph.fqn_map().get(&fqn_id) {
+ let node = &topology[idx];
+ if let Some(loc) = &node.location {
+ let path_str = graph.symbols().resolve(&loc.path.0);
+ locations.push(SymbolLocation {
+ path: Arc::from(PathBuf::from(path_str)),
+ range: loc.range,
+ selection_range: loc.selection_range,
+ });
+ }
}
}
}
@@ -158,15 +164,17 @@ impl SymbolNavigator for EngineHandle {
let topology = graph.topology();
let mut locations = Vec::new();
- for idx in matches {
- let node = &topology[idx];
- if let Some(loc) = &node.location {
- let path_str = graph.symbols().resolve(&loc.path.0);
- locations.push(SymbolLocation {
- path: Arc::from(PathBuf::from(path_str)),
- range: loc.range,
- selection_range: loc.selection_range,
- });
+ for fqn_id in matches {
+ if let Some(&idx) = graph.fqn_map().get(&fqn_id) {
+ let node = &topology[idx];
+ if let Some(loc) = &node.location {
+ let path_str = graph.symbols().resolve(&loc.path.0);
+ locations.push(SymbolLocation {
+ path: Arc::from(PathBuf::from(path_str)),
+ range: loc.range,
+ selection_range: loc.selection_range,
+ });
+ }
}
}
Ok(locations)
@@ -183,8 +191,13 @@ impl ReferenceAnalyzer for EngineHandle {
let graph = self.graph().await;
let matches = resolver.find_matches(&graph, &query.resolution);
- let discovery = DiscoveryEngine::new(&graph);
- let candidate_paths = discovery.scout_references(&matches);
+ let match_indices: Vec<_> = matches
+ .iter()
+ .filter_map(|id| graph.fqn_map().get(id).copied())
+ .collect();
+ let conventions = (*self.naming_conventions()).clone();
+ let discovery = DiscoveryEngine::new(&graph, conventions.clone());
+ let candidate_paths = discovery.scout_references(&match_indices);
let mut tasks = tokio::task::JoinSet::new();
let shared_graph = Arc::new(graph);
@@ -193,6 +206,7 @@ impl ReferenceAnalyzer for EngineHandle {
let handle = self.clone();
let resolution = query.resolution.clone();
let graph_snap = Arc::clone(&shared_graph);
+ let conventions_clone = conventions.clone();
tasks.spawn(async move {
let (parser, file_lang) = match handle.get_parser_and_lang_for_path(&path) {
@@ -210,7 +224,7 @@ impl ReferenceAnalyzer for EngineHandle {
Err(_) => return Vec::new(),
};
- let discovery = DiscoveryEngine::new(graph_snap.as_ref());
+ let discovery = DiscoveryEngine::new(graph_snap.as_ref(), conventions_clone);
let uri_str = format!("file://{}", path.display());
let uri = match url::Url::parse(&uri_str) {
@@ -281,7 +295,8 @@ impl CallHierarchyAnalyzer for EngineHandle {
}
// 1. Meso-level scouting for candidate files
- let discovery = DiscoveryEngine::new(&graph);
+ let conventions = (*self.naming_conventions()).clone();
+ let discovery = DiscoveryEngine::new(&graph, conventions.clone());
let candidate_paths = discovery.scout_references(&target_indices);
// 2. Micro-level scanning
@@ -293,6 +308,7 @@ impl CallHierarchyAnalyzer for EngineHandle {
let handle = self.clone();
let res = resolution.clone();
let graph_snap = Arc::clone(&shared_graph);
+ let conventions_clone = conventions.clone();
tasks.spawn(async move {
let (parser, file_lang) = match handle.get_parser_and_lang_for_path(&path) {
@@ -310,7 +326,7 @@ impl CallHierarchyAnalyzer for EngineHandle {
Err(_) => return vec![],
};
- let discovery = DiscoveryEngine::new(graph_snap.as_ref());
+ let discovery = DiscoveryEngine::new(graph_snap.as_ref(), conventions_clone);
let uri_str = format!("file://{}", path.display());
let uri = match url::Url::parse(&uri_str) {
Ok(u) => u,
@@ -360,12 +376,15 @@ impl CallHierarchyAnalyzer for EngineHandle {
}
let mut results = Vec::new();
- let symbols = graph.symbols();
for (idx, ranges) in caller_map {
let node = &graph.topology()[idx];
- let fqn = node.fqn(symbols);
+ let lang_str = graph.symbols().resolve(&node.lang.0);
+ let convention = conventions
+ .get(lang_str)
+ .map(|c: &Arc| c.as_ref());
+ let fqn_str = graph.render_fqn(node, convention);
if let Some(display_node) = self
- .get_node_display(fqn)
+ .get_node_display(&fqn_str)
.await
.map_err(|e| SemanticError::Internal(e.to_string()))?
{
@@ -384,6 +403,7 @@ impl CallHierarchyAnalyzer for EngineHandle {
fqn: &str,
) -> SemanticResult> {
let graph = self.graph().await;
+ let conventions = (*self.naming_conventions()).clone();
let node_idx = match graph.find_node(fqn) {
Some(idx) => idx,
None => return Ok(vec![]),
@@ -448,15 +468,17 @@ impl CallHierarchyAnalyzer for EngineHandle {
if let Ok(Some(res)) = self.resolve_symbol_at(&pos_ctx).await {
let matches = resolver.find_matches(&graph, &res);
- for &m_idx in &matches {
- let m_node = &graph.topology()[m_idx];
- if matches!(m_node.kind(), NodeKind::Method | NodeKind::Constructor) {
- outgoing_calls.entry(m_idx).or_default().push(Range {
- start_line: n_range.start_point.row,
- start_col: n_range.start_point.column,
- end_line: n_range.end_point.row,
- end_col: n_range.end_point.column,
- });
+ for fqn_id in matches {
+ if let Some(&m_idx) = graph.fqn_map().get(&fqn_id) {
+ let m_node = &graph.topology()[m_idx];
+ if matches!(m_node.kind(), NodeKind::Method | NodeKind::Constructor) {
+ outgoing_calls.entry(m_idx).or_default().push(Range {
+ start_line: n_range.start_point.row,
+ start_col: n_range.start_point.column,
+ end_line: n_range.end_point.row,
+ end_col: n_range.end_point.column,
+ });
+ }
}
}
}
@@ -471,9 +493,13 @@ impl CallHierarchyAnalyzer for EngineHandle {
let mut results = Vec::new();
for (idx, ranges) in outgoing_calls {
let m_node = &graph.topology()[idx];
- let fqn = m_node.fqn(symbols);
+ let lang_str = graph.symbols().resolve(&m_node.lang.0);
+ let convention = conventions
+ .get(lang_str)
+ .map(|c: &Arc| c.as_ref());
+ let fqn_str = graph.render_fqn(m_node, convention);
if let Some(display_node) = self
- .get_node_display(fqn)
+ .get_node_display(&fqn_str)
.await
.map_err(|e| SemanticError::Internal(e.to_string()))?
{
diff --git a/crates/core/src/features/discovery.rs b/crates/core/src/features/discovery.rs
index 92d4650..000aa86 100644
--- a/crates/core/src/features/discovery.rs
+++ b/crates/core/src/features/discovery.rs
@@ -1,16 +1,36 @@
use super::CodeGraphLike;
-use crate::ingest::parser::{LspParser, SymbolResolution};
+use crate::ingest::parser::LspParser;
use lsp_types::{Location, Url};
+pub use naviscope_api::models::SymbolResolution;
use std::collections::HashSet;
/// DiscoveryEngine bridges Meso-level graph knowledge with Micro-level file scanning.
pub struct DiscoveryEngine<'a> {
index: &'a dyn CodeGraphLike,
+ naming_conventions:
+ std::collections::HashMap>,
}
impl<'a> DiscoveryEngine<'a> {
- pub fn new(index: &'a dyn CodeGraphLike) -> Self {
- Self { index }
+ pub fn new(
+ index: &'a dyn CodeGraphLike,
+ naming_conventions: std::collections::HashMap<
+ String,
+ std::sync::Arc,
+ >,
+ ) -> Self {
+ Self {
+ index,
+ naming_conventions,
+ }
+ }
+
+ fn get_convention(
+ &self,
+ node: &crate::model::GraphNode,
+ ) -> Option<&dyn naviscope_plugin::NamingConvention> {
+ let lang_str = self.index.symbols().resolve(&node.lang.0);
+ self.naming_conventions.get(lang_str).map(|c| c.as_ref())
}
/// Meso-level: Scout for candidate files that likely contain references to the given nodes.
@@ -48,7 +68,7 @@ impl<'a> DiscoveryEngine<'a> {
for &node_idx in matches {
let node = &topology[node_idx];
- let (primary, context) = Self::extract_smart_tokens(node, self.index);
+ let (primary, context) = self.extract_smart_tokens(node);
// Helper to get paths for a token string
let get_paths = |token: &str| -> Option> {
@@ -93,13 +113,11 @@ impl<'a> DiscoveryEngine<'a> {
/// Smartly extract tokens for "bag of words" intersection.
/// Returns (Primary Token, Optional Context Token)
- fn extract_smart_tokens(
- node: &crate::model::GraphNode,
- index: &dyn CodeGraphLike,
- ) -> (String, Option) {
- let symbols = index.symbols();
+ fn extract_smart_tokens(&self, node: &crate::model::GraphNode) -> (String, Option) {
+ let symbols = self.index.symbols();
let name = node.name(symbols).to_string();
- let fqn = node.fqn(symbols);
+ let convention = self.get_convention(node);
+ let fqn = self.index.render_fqn(node, convention);
// Split by ANY non-alphanumeric character (except underscore)
// This is much more language-agnostic than hardcoding '.', ':', etc.
@@ -150,7 +168,7 @@ impl<'a> DiscoveryEngine<'a> {
source,
range.start_line,
range.start_col,
- self.index,
+ self.index.as_plugin_graph(),
) {
// 3. Identity Check
if &resolved_at_loc == target_resolution {
diff --git a/crates/core/src/features/mod.rs b/crates/core/src/features/mod.rs
index 753730a..bffb7fc 100644
--- a/crates/core/src/features/mod.rs
+++ b/crates/core/src/features/mod.rs
@@ -1,4 +1,5 @@
-use naviscope_api::models::symbol::Symbol;
+use crate::model::FqnManager;
+use naviscope_api::models::symbol::{FqnId, Symbol};
use std::path::Path;
pub mod discovery;
@@ -11,7 +12,7 @@ pub trait CodeGraphLike: Send + Sync {
fn topology(
&self,
) -> &petgraph::stable_graph::StableDiGraph;
- fn fqn_map(&self) -> &std::collections::HashMap;
+ fn fqn_map(&self) -> &std::collections::HashMap;
fn path_to_nodes(&self, path: &Path) -> Option<&[petgraph::stable_graph::NodeIndex]>;
fn reference_index(&self) -> &std::collections::HashMap>;
fn find_container_node_at(
@@ -20,12 +21,35 @@ pub trait CodeGraphLike: Send + Sync {
line: usize,
col: usize,
) -> Option;
- fn symbols(&self) -> &lasso::Rodeo;
+ fn fqns(&self) -> &FqnManager;
+ fn symbols(&self) -> &lasso::ThreadedRodeo;
+ fn as_plugin_graph(&self) -> &dyn naviscope_plugin::CodeGraph;
+
+ /// Helper to render a node's full FQN with optional naming convention
+ fn render_fqn(
+ &self,
+ node: &crate::model::GraphNode,
+ convention: Option<&dyn naviscope_plugin::NamingConvention>,
+ ) -> String {
+ use naviscope_plugin::NamingConvention;
+
+ if let Some(nc) = convention {
+ nc.render_fqn(node.id, self.fqns())
+ } else {
+ // Fallback to default dot convention
+ naviscope_plugin::DotPathConvention.render_fqn(node.id, self.fqns())
+ }
+ }
/// Helper to find node by string FQN
fn find_node(&self, fqn: &str) -> Option {
- let key = self.symbols().get(fqn)?;
- self.fqn_map().get(&Symbol(key)).copied()
+ let ids = self.fqns().resolve_fqn_string(fqn);
+ for id in ids {
+ if let Some(&idx) = self.fqn_map().get(&id) {
+ return Some(idx);
+ }
+ }
+ None
}
/// Find all nodes matching an FQN string (handle duplicates if any)
@@ -47,7 +71,7 @@ impl CodeGraphLike for &T {
(*self).topology()
}
- fn fqn_map(&self) -> &std::collections::HashMap {
+ fn fqn_map(&self) -> &std::collections::HashMap {
(*self).fqn_map()
}
@@ -68,45 +92,15 @@ impl CodeGraphLike for &T {
(*self).find_container_node_at(path, line, col)
}
- fn symbols(&self) -> &lasso::Rodeo {
+ fn symbols(&self) -> &lasso::ThreadedRodeo {
(*self).symbols()
}
-}
-
-// Implement for the core CodeGraph model
-impl CodeGraphLike for crate::model::CodeGraph {
- fn topology(
- &self,
- ) -> &petgraph::stable_graph::StableDiGraph
- {
- self.topology()
- }
- fn fqn_map(&self) -> &std::collections::HashMap {
- self.fqn_map()
- }
-
- fn path_to_nodes(&self, path: &Path) -> Option<&[petgraph::stable_graph::NodeIndex]> {
- let key = self.symbols().get(path.to_string_lossy())?;
- self.file_index()
- .get(&Symbol(key))
- .map(|e| e.nodes.as_slice())
- }
-
- fn reference_index(&self) -> &std::collections::HashMap> {
- self.reference_index()
- }
-
- fn find_container_node_at(
- &self,
- path: &std::path::Path,
- line: usize,
- col: usize,
- ) -> Option {
- self.find_container_node_at(path, line, col)
+ fn fqns(&self) -> &FqnManager {
+ (*self).fqns()
}
- fn symbols(&self) -> &lasso::Rodeo {
- self.symbols()
+ fn as_plugin_graph(&self) -> &dyn naviscope_plugin::CodeGraph {
+ (*self).as_plugin_graph()
}
}
diff --git a/crates/core/src/features/navigation.rs b/crates/core/src/features/navigation.rs
index cd5cda7..55f1180 100644
--- a/crates/core/src/features/navigation.rs
+++ b/crates/core/src/features/navigation.rs
@@ -5,11 +5,23 @@ use naviscope_api::navigation::ResolveResult;
/// NavigationEngine provides logic for resolving fuzzy/relative paths within a graph.
pub struct NavigationEngine<'a> {
graph: &'a dyn CodeGraphLike,
+ naming_conventions: std::collections::HashMap>,
}
impl<'a> NavigationEngine<'a> {
- pub fn new(graph: &'a dyn CodeGraphLike) -> Self {
- Self { graph }
+ pub fn new(
+ graph: &'a dyn CodeGraphLike,
+ naming_conventions: std::collections::HashMap>,
+ ) -> Self {
+ Self {
+ graph,
+ naming_conventions,
+ }
+ }
+
+ fn get_convention(&self, node: &crate::model::GraphNode) -> Option<&dyn naviscope_plugin::NamingConvention> {
+ let lang_str = self.graph.symbols().resolve(&node.lang.0);
+ self.naming_conventions.get(lang_str).map(|c| c.as_ref())
}
pub fn resolve_path(&self, target: &str, current_context: Option<&str>) -> ResolveResult {
@@ -22,7 +34,8 @@ impl<'a> NavigationEngine<'a> {
.filter_map(|idx| {
let node = &self.graph.topology()[idx];
if matches!(node.kind(), NodeKind::Project) {
- Some(node.fqn(self.graph.symbols()).to_string())
+ let convention = self.get_convention(node);
+ Some(self.graph.render_fqn(node, convention))
} else {
None
}
@@ -53,9 +66,8 @@ impl<'a> NavigationEngine<'a> {
self.graph.topology().edge_endpoints(edge_idx).unwrap();
if let Some(parent_node) = self.graph.topology().node_weight(parent_idx)
{
- return ResolveResult::Found(
- parent_node.fqn(self.graph.symbols()).to_string(),
- );
+ let convention = self.get_convention(parent_node);
+ return ResolveResult::Found(self.graph.render_fqn(parent_node, convention));
}
}
}
@@ -95,13 +107,14 @@ impl<'a> NavigationEngine<'a> {
let edge = &self.graph.topology()[edge_idx];
if edge.edge_type == EdgeType::Contains {
let node = &self.graph.topology()[child_idx];
- let fqn = node.fqn(self.graph.symbols());
+ let convention = self.get_convention(node);
+ let fqn = self.graph.render_fqn(node, convention);
// Match by simple name (last component) or display name
- let simple_name = fqn.split(&['.', ':']).last().unwrap_or(fqn);
+ let simple_name = fqn.split(&['.', ':', '#']).last().unwrap_or(&fqn);
let display_name = node.name(self.graph.symbols());
if simple_name == target || display_name == target {
- Some(fqn.to_string())
+ Some(fqn)
} else {
None
}
@@ -118,18 +131,19 @@ impl<'a> NavigationEngine<'a> {
self.graph
.fqn_map()
.keys()
- .filter_map(|sym| {
- let fqn = self.graph.symbols().resolve(&sym.0);
- let simple_name = fqn.split(&['.', ':']).last().unwrap_or(fqn);
- if simple_name == target {
- return Some(fqn.to_string());
- }
+ .filter_map(|&fid| {
+ if let Some(&idx) = self.graph.fqn_map().get(&fid) {
+ let node = &self.graph.topology()[idx];
+ let convention = self.get_convention(node);
+ let fqn = self.graph.render_fqn(node, convention);
+ let simple_name = fqn.split(&['.', ':', '#']).last().unwrap_or(&fqn);
+ if simple_name == target {
+ return Some(fqn);
+ }
- // Also check display name
- if let Some(idx) = self.graph.fqn_map().get(sym) {
- let node = &self.graph.topology()[*idx];
+ // Also check display name
if node.name(self.graph.symbols()) == target {
- return Some(fqn.to_string());
+ return Some(fqn);
}
}
@@ -149,13 +163,16 @@ impl<'a> NavigationEngine<'a> {
self.graph
.fqn_map()
.keys()
- .filter_map(|sym| {
- let fqn = self.graph.symbols().resolve(&sym.0);
- if fqn.starts_with(prefix) {
- Some(fqn.to_string())
- } else {
- None
+ .filter_map(|&fid| {
+ if let Some(&idx) = self.graph.fqn_map().get(&fid) {
+ let node = &self.graph.topology()[idx];
+ let convention = self.get_convention(node);
+ let fqn = self.graph.render_fqn(node, convention);
+ if fqn.starts_with(prefix) {
+ return Some(fqn);
+ }
}
+ None
})
.take(50) // Reasonable limit for candidates
.collect()
diff --git a/crates/core/src/features/query.rs b/crates/core/src/features/query.rs
index 5e9df7f..c05c4b0 100644
--- a/crates/core/src/features/query.rs
+++ b/crates/core/src/features/query.rs
@@ -1,7 +1,7 @@
use crate::error::{NaviscopeError, Result};
use crate::model::source::Language;
use crate::model::{DisplayGraphNode, EdgeType, NodeKind};
-use crate::runtime::plugin::NodeRenderer;
+use crate::plugin::NodeAdapter;
pub use naviscope_api::models::{GraphQuery, QueryResult, QueryResultEdge};
use petgraph::Direction as PetDirection;
use regex::RegexBuilder;
@@ -12,22 +12,31 @@ use super::CodeGraphLike;
pub struct QueryEngine {
graph: G,
lookup: L,
+ naming_conventions: std::collections::HashMap>,
}
impl QueryEngine
where
G: CodeGraphLike,
- L: Fn(Language) -> Option>,
+ L: Fn(Language) -> Option>,
{
- pub fn new(graph: G, lookup: L) -> Self {
- Self { graph, lookup }
+ pub fn new(
+ graph: G,
+ lookup: L,
+ naming_conventions: std::collections::HashMap>,
+ ) -> Self {
+ Self {
+ graph,
+ lookup,
+ naming_conventions,
+ }
}
fn render_node(&self, node: &crate::model::GraphNode) -> DisplayGraphNode {
let symbols = self.graph.symbols();
let lang = node.language(symbols);
if let Some(renderer) = (self.lookup)(lang.clone()) {
- renderer.render_display_node(node, symbols)
+ renderer.render_display_node(node, self.graph.fqns())
} else {
panic!(
"CRITICAL: No renderer found for language '{}'. This indicates a missing plugin for indexed data.",
@@ -52,7 +61,10 @@ where
let mut nodes = Vec::new();
for node in self.graph.topology().node_weights() {
- if regex.is_match(node.fqn(symbols)) || regex.is_match(node.name(symbols)) {
+ let lang_str = symbols.resolve(&node.lang.0);
+ let convention = self.naming_conventions.get(lang_str).map(|c| c.as_ref());
+ let fqn_str = self.graph.render_fqn(node, convention);
+ if regex.is_match(&fqn_str) || regex.is_match(node.name(symbols)) {
if kind.is_empty() || kind.contains(&node.kind) {
nodes.push(self.render_node(node));
}
@@ -156,7 +168,6 @@ where
let mut edges_result = Vec::new();
let topology = self.graph.topology();
let mut edges = topology.neighbors_directed(start_idx, dir).detach();
- let symbols = self.graph.symbols();
while let Some((edge_idx, neighbor_idx)) = edges.next(topology) {
let edge_data = &topology[edge_idx];
@@ -167,15 +178,21 @@ where
if kind_filter.is_empty() || kind_filter.contains(&neighbor_node.kind) {
nodes.push(self.render_node(neighbor_node));
+ let symbols = self.graph.symbols();
+ let start_lang = symbols.resolve(&start_node.lang.0);
+ let neighbor_lang = symbols.resolve(&neighbor_node.lang.0);
+ let start_convention = self.naming_conventions.get(start_lang).map(|c| c.as_ref());
+ let neighbor_convention = self.naming_conventions.get(neighbor_lang).map(|c| c.as_ref());
+
let (from, to) = if dir == PetDirection::Outgoing {
(
- Arc::from(start_node.fqn(symbols)),
- Arc::from(neighbor_node.fqn(symbols)),
+ Arc::from(self.graph.render_fqn(start_node, start_convention)),
+ Arc::from(self.graph.render_fqn(neighbor_node, neighbor_convention)),
)
} else {
(
- Arc::from(neighbor_node.fqn(symbols)),
- Arc::from(start_node.fqn(symbols)),
+ Arc::from(self.graph.render_fqn(neighbor_node, neighbor_convention)),
+ Arc::from(self.graph.render_fqn(start_node, start_convention)),
)
};
diff --git a/crates/core/src/ingest/builder.rs b/crates/core/src/ingest/builder.rs
index 733d720..ff7e853 100644
--- a/crates/core/src/ingest/builder.rs
+++ b/crates/core/src/ingest/builder.rs
@@ -10,6 +10,7 @@ use crate::model::source::SourceFile;
// StorageContext unused
use crate::model::{GraphEdge, GraphOp};
use naviscope_api::models::symbol::Symbol;
+use naviscope_plugin::{FqnInterner, ModelConverter};
use petgraph::stable_graph::{NodeIndex, StableDiGraph};
use std::collections::HashMap;
use std::path::Path;
@@ -17,22 +18,27 @@ use std::path::Path;
/// Mutable graph builder
pub struct CodeGraphBuilder {
inner: CodeGraphInner,
+ pub naming_conventions:
+ HashMap>,
}
impl CodeGraphBuilder {
/// Create a new empty builder
pub fn new() -> Self {
+ let rodeo = std::sync::Arc::new(lasso::ThreadedRodeo::new());
Self {
inner: CodeGraphInner {
instance_id: 0, // Will be updated when built
version: crate::model::graph::CURRENT_VERSION,
topology: StableDiGraph::new(),
- symbols: lasso::Rodeo::default(),
+ fqns: crate::model::FqnManager::with_rodeo(rodeo.clone()),
+ symbols: rodeo,
fqn_index: HashMap::new(),
name_index: HashMap::new(),
file_index: HashMap::new(),
reference_index: HashMap::new(),
},
+ naming_conventions: HashMap::new(),
}
}
@@ -43,32 +49,86 @@ impl CodeGraphBuilder {
/// Create builder from internal data
pub(crate) fn from_inner(inner: CodeGraphInner) -> Self {
- Self { inner }
+ Self {
+ inner,
+ naming_conventions: HashMap::new(),
+ }
}
// ---- Mutation methods ----
+ /// Resolve and intern an ID, potentially upgrading it using NamingConventions
+ fn resolve_storage_id(
+ &self,
+ id: &naviscope_api::models::symbol::NodeId,
+ kind_hint: Option,
+ ) -> naviscope_api::models::symbol::FqnId {
+ // We need to guess the language to pick the convention.
+ // But NodeId doesn't carry language info directly unless we infer it or it's passed.
+ // HOWEVER, `id` might be lang-specific.
+ // For simplicity, we iterate over available conventions or try to detect.
+ // Since we typically have one language active per builder usage OR we can't easily know,
+ // we might have to rely on the fact that `Java` is likely the one needing this.
+ // A better approach: The `GraphOp` or `IndexNode` context.
+ // But `AddEdge` has no context.
+ // Let's iterate all conventions. If any convention claims it, we use it?
+ // Or simpler: Java is special.
+
+ // BETTER: `naming_conventions` is a map. If we have keys, we try them.
+ for (_, nc) in &self.naming_conventions {
+ match id {
+ naviscope_api::models::symbol::NodeId::Flat(s) => {
+ // Try to upgrade
+ // We don't know if this ID belongs to 'lang', but we can try parsing.
+ // A cleaner way is if the ID string itself gives a hint, but it doesn't.
+ // For now, if we have a convention, we USE it.
+ // This assumes we don't mix conflicting conventions in one builder session recklessly.
+ let parts = nc.parse_fqn(s, kind_hint.clone());
+ let structured_id = naviscope_api::models::symbol::NodeId::Structured(parts);
+ return self.inner.fqns.intern_node_id(&structured_id);
+ }
+ _ => {}
+ }
+ }
+
+ self.inner.fqns.intern_node_id(id)
+ }
+
/// Add or update a node
pub fn add_node(&mut self, node_data: crate::ingest::parser::IndexNode) -> NodeIndex {
- let fqn_sym = Symbol(self.inner.symbols.get_or_intern(&node_data.id));
+ // We have language info here! Use it to select convention.
+ let lang = crate::model::Language::new(node_data.lang.clone());
+ let fqn_id = if let Some(nc) = self.naming_conventions.get(&lang) {
+ match &node_data.id {
+ naviscope_api::models::symbol::NodeId::Flat(s) => {
+ let parts = nc.parse_fqn(s, Some(node_data.kind.clone()));
+ let structured_id = naviscope_api::models::symbol::NodeId::Structured(parts);
+ self.inner.fqns.intern_node_id(&structured_id)
+ }
+ _ => self.inner.fqns.intern_node_id(&node_data.id),
+ }
+ } else {
+ // Fallback to "any convention" or generic
+ self.resolve_storage_id(&node_data.id, Some(node_data.kind.clone()))
+ };
- if let Some(&idx) = self.inner.fqn_index.get(&fqn_sym) {
+ if let Some(&idx) = self.inner.fqn_index.get(&fqn_id) {
// Node already exists
idx
} else {
- let name_sym = Symbol(self.inner.symbols.get_or_intern(&node_data.name));
- let lang_sym = Symbol(self.inner.symbols.get_or_intern(&node_data.lang));
+ let name_sym = self.inner.fqns.intern_atom(&node_data.name);
+ let lang_sym = self.inner.fqns.intern_atom(&node_data.lang);
let location = node_data
.location
.as_ref()
- .map(|l| l.to_internal(&mut self.inner.symbols));
+ .map(|l| l.to_internal(&self.inner.fqns));
let mut ctx = crate::model::storage::model::GenericStorageContext {
- rodeo: &mut self.inner.symbols,
+ rodeo: self.inner.symbols.clone(),
};
let node = crate::model::GraphNode {
- id: fqn_sym,
+ id: fqn_id,
name: name_sym,
kind: node_data.kind.clone(),
lang: lang_sym,
@@ -77,7 +137,7 @@ impl CodeGraphBuilder {
};
let idx = self.inner.topology.add_node(node);
- self.inner.fqn_index.insert(fqn_sym, idx);
+ self.inner.fqn_index.insert(fqn_id, idx);
self.inner.name_index.entry(name_sym).or_default().push(idx);
if let Some(loc) = location {
@@ -120,18 +180,11 @@ impl CodeGraphBuilder {
pub fn remove_node(&mut self, idx: NodeIndex) {
if let Some(node) = self.inner.topology.node_weight(idx) {
let fqn = node.id; // Symbol implements Copy
- let name = node.name;
+ let _name = node.name;
// Remove from indices
self.inner.fqn_index.remove(&fqn);
- if let Some(nodes) = self.inner.name_index.get_mut(&name) {
- nodes.retain(|&i| i != idx);
- if nodes.is_empty() {
- self.inner.name_index.remove(&name);
- }
- }
-
// Remove from topology
self.inner.topology.remove_node(idx);
}
@@ -186,12 +239,12 @@ impl CodeGraphBuilder {
to_id,
edge,
} => {
- let from_sym = Symbol(self.inner.symbols.get_or_intern(from_id.as_ref()));
- let to_sym = Symbol(self.inner.symbols.get_or_intern(to_id.as_ref()));
+ let from_id = self.resolve_storage_id(&from_id, None);
+ let to_id = self.resolve_storage_id(&to_id, None);
if let (Some(&from), Some(&to)) = (
- self.inner.fqn_index.get(&from_sym),
- self.inner.fqn_index.get(&to_sym),
+ self.inner.fqn_index.get(&from_id),
+ self.inner.fqn_index.get(&to_id),
) {
self.add_edge(from, to, edge);
}
@@ -251,7 +304,7 @@ mod tests {
let mut builder = CodeGraphBuilder::new();
let node = crate::ingest::parser::IndexNode {
- id: "test_project".to_string(),
+ id: "test_project".into(),
name: "test_project".to_string(),
kind: NodeKind::Project,
lang: "buildfile".to_string(),
@@ -274,7 +327,7 @@ mod tests {
let mut builder = CodeGraphBuilder::from_graph(&graph);
let node = crate::ingest::parser::IndexNode {
- id: "new_project".to_string(),
+ id: "new_project".into(),
name: "new_project".to_string(),
kind: NodeKind::Project,
lang: "buildfile".to_string(),
diff --git a/crates/core/src/ingest/mod.rs b/crates/core/src/ingest/mod.rs
index f278b9c..09d11cf 100644
--- a/crates/core/src/ingest/mod.rs
+++ b/crates/core/src/ingest/mod.rs
@@ -1,5 +1,6 @@
pub mod builder;
pub mod parser;
+pub mod pipeline;
pub mod resolver;
pub mod scanner;
diff --git a/crates/core/src/ingest/parser/mod.rs b/crates/core/src/ingest/parser/mod.rs
index 235659a..9fb6c28 100644
--- a/crates/core/src/ingest/parser/mod.rs
+++ b/crates/core/src/ingest/parser/mod.rs
@@ -1,38 +1,12 @@
use crate::error::Result;
-use crate::model::{DisplayGraphNode, NodeKind, Range};
use std::path::Path;
-use tree_sitter::Tree;
pub mod output;
pub mod utils;
pub use naviscope_api::SymbolResolution;
-pub use output::{IndexNode, IndexRelation, ParseOutput};
-
-pub trait LspParser: Send + Sync {
- fn parse(
- &self,
- source: &str,
- old_tree: Option<&tree_sitter::Tree>,
- ) -> Option;
- fn extract_symbols(&self, tree: &Tree, source: &str) -> Vec;
- /// Maps a language-specific symbol kind string to an LSP SymbolKind
- fn symbol_kind(&self, kind: &NodeKind) -> lsp_types::SymbolKind;
-
- /// Find occurrences of a symbol within a single file's AST.
- /// This is the "Micro" part of the Discovery Engine.
- fn find_occurrences(&self, source: &str, tree: &Tree, target: &SymbolResolution) -> Vec;
-}
-
-/// Result of a global file parsing for indexing.
-#[derive(Clone)]
-pub struct GlobalParseResult {
- pub package_name: Option,
- pub imports: Vec,
- pub output: ParseOutput,
- pub source: Option,
- pub tree: Option,
-}
+pub use naviscope_api::models::symbol::NodeId;
+pub use naviscope_plugin::{GlobalParseResult, IndexNode, IndexRelation, LspParser, ParseOutput};
/// Trait for parsers that provide data for the global code knowledge graph.
pub trait IndexParser: Send + Sync {
diff --git a/crates/core/src/ingest/parser/output.rs b/crates/core/src/ingest/parser/output.rs
index 9d7921a..0542b5d 100644
--- a/crates/core/src/ingest/parser/output.rs
+++ b/crates/core/src/ingest/parser/output.rs
@@ -1,34 +1,2 @@
-use crate::model::IndexMetadata;
-use crate::model::{EdgeType, NodeKind, Range};
-use naviscope_api::models::DisplaySymbolLocation;
-use std::sync::Arc;
-
-/// Node model during the parsing phase, before interning
-/// It holds raw Strings and strongly-typed Metadata
-#[derive(Debug, Clone)]
-pub struct IndexNode {
- pub id: String,
- pub name: String,
- pub kind: NodeKind,
- pub lang: String,
- pub location: Option,
- pub metadata: Arc,
-}
-
-/// Relation model during the parsing phase
-#[derive(Debug, Clone)]
-pub struct IndexRelation {
- pub source_id: String,
- pub target_id: String,
- pub edge_type: EdgeType,
- pub range: Option,
-}
-
-/// Core model produced by the parser
-#[derive(Debug, Clone, Default)]
-pub struct ParseOutput {
- pub nodes: Vec,
- pub relations: Vec,
- /// All identifiers appearing in the file (used for global search and reference indexing)
- pub identifiers: Vec,
-}
+pub use naviscope_api::models::symbol::NodeId;
+pub use naviscope_plugin::{IndexNode, IndexRelation, ParseOutput};
diff --git a/crates/core/src/ingest/pipeline.rs b/crates/core/src/ingest/pipeline.rs
new file mode 100644
index 0000000..d585fb0
--- /dev/null
+++ b/crates/core/src/ingest/pipeline.rs
@@ -0,0 +1,53 @@
+use crate::error::Result;
+use crate::model::GraphOp;
+use std::path::PathBuf;
+
+/// Ingest task context, used to share state between batches
+pub trait PipelineContext: Send + Sync + 'static {}
+impl PipelineContext for naviscope_plugin::ProjectContext {}
+
+/// A processing stage of the pipeline
+pub trait PipelineStage: Send + Sync {
+ type Output;
+
+ /// Processes a batch of paths
+ fn process(&self, context: &C, paths: Vec) -> Result>;
+}
+
+/// Ingest pipeline engine
+pub struct IngestPipeline {
+ batch_size: usize,
+}
+
+impl IngestPipeline {
+ pub fn new(batch_size: usize) -> Self {
+ Self {
+ batch_size: if batch_size == 0 { 100 } else { batch_size },
+ }
+ }
+
+ /// Executes the pipeline: processes paths in chunks and commits the products
+ pub fn execute(
+ &self,
+ context: &C,
+ paths: Vec,
+ stage: &S,
+ mut committer: F,
+ ) -> Result<()>
+ where
+ C: PipelineContext,
+ S: PipelineStage,
+ F: FnMut(Vec) -> Result<()>,
+ {
+ for chunk in paths.chunks(self.batch_size) {
+ // 1. Process the current batch
+ let outputs = stage.process(context, chunk.to_vec())?;
+
+ // 2. Commit the products
+ committer(outputs)?;
+
+ // End of batch, local variables are cleaned up
+ }
+ Ok(())
+ }
+}
diff --git a/crates/core/src/ingest/resolver/engine.rs b/crates/core/src/ingest/resolver/engine.rs
index 45c6406..7d2d02a 100644
--- a/crates/core/src/ingest/resolver/engine.rs
+++ b/crates/core/src/ingest/resolver/engine.rs
@@ -6,7 +6,7 @@ use crate::model::{GraphOp, ResolvedUnit};
use rayon::prelude::*;
use std::sync::Arc;
-use crate::runtime::plugin::{BuildToolPlugin, LanguagePlugin};
+use crate::plugin::{BuildToolPlugin, LanguagePlugin};
/// Main resolver that dispatches to specific strategies based on file type for indexing
pub struct IndexResolver {
@@ -50,42 +50,26 @@ impl IndexResolver {
pub fn get_lsp_parser(
&self,
language: Language,
- ) -> Option> {
+ ) -> Option> {
self.lang_plugins
.iter()
.find(|p| p.name() == language)
.map(|p| p.lsp_parser())
}
- pub fn get_metadata_plugin(
+ pub fn get_node_adapter(
&self,
language: Language,
- ) -> Option> {
+ ) -> Option> {
self.lang_plugins
.iter()
.find(|p| p.name() == language)
- .map(|p| p.clone() as Arc)
+ .and_then(|p| p.get_node_adapter())
.or_else(|| {
self.build_plugins
.iter()
.find(|p| p.name().as_str() == language.as_str())
- .map(|p| p.clone() as Arc)
- })
- }
-
- pub fn get_node_renderer(
- &self,
- language: Language,
- ) -> Option> {
- self.lang_plugins
- .iter()
- .find(|p| p.name() == language)
- .map(|p| p.clone() as Arc)
- .or_else(|| {
- self.build_plugins
- .iter()
- .find(|p| p.name().as_str() == language.as_str())
- .map(|p| p.clone() as Arc)
+ .and_then(|p| p.get_node_adapter())
})
}
@@ -98,11 +82,37 @@ impl IndexResolver {
Language::from_extension(ext)
}
+ pub fn get_naming_convention(
+ &self,
+ language: Language,
+ ) -> Option> {
+ self.lang_plugins
+ .iter()
+ .find(|p| p.name() == language)
+ .and_then(|p| p.get_naming_convention())
+ }
+
/// Resolve all parsed files into graph operations using a two-phase process
pub fn resolve(&self, files: Vec) -> Result> {
- let mut all_ops = Vec::new();
+ let (mut all_ops, build_files, source_files) = self.prepare_and_partition(files);
+
+ // Phase 1: Build Tools
+ let mut project_context = ProjectContext::new();
+ let build_ops = self.resolve_build_batch(&build_files, &mut project_context)?;
+ all_ops.extend(build_ops);
- // Add RemovePath operations and UpdateFile operations for each file being processed
+ // Phase 2: Source Files
+ let source_ops = self.resolve_source_batch(&source_files, &project_context)?;
+ all_ops.extend(source_ops);
+
+ Ok(all_ops)
+ }
+
+ fn prepare_and_partition(
+ &self,
+ files: Vec,
+ ) -> (Vec, Vec, Vec) {
+ let mut all_ops = Vec::new();
for file in &files {
all_ops.push(GraphOp::RemovePath {
path: Arc::from(file.file.path.as_path()),
@@ -112,16 +122,20 @@ impl IndexResolver {
});
}
- // Separate files into build and source files
let (build_files, source_files): (Vec<_>, Vec<_>) =
files.into_iter().partition(|f| f.is_build());
- // Phase 1: Resolve Build Tools (Structure)
- let mut project_context = ProjectContext::new();
+ (all_ops, build_files, source_files)
+ }
+ pub fn resolve_build_batch(
+ &self,
+ build_files: &[ParsedFile],
+ context: &mut ProjectContext,
+ ) -> Result> {
+ let mut all_ops = Vec::new();
for plugin in &self.build_plugins {
- // Find files relevant to this plugin
- let tool_files: Vec = build_files
+ let tool_files: Vec<&ParsedFile> = build_files
.iter()
.filter(|f| {
if let Some(file_name) = f.path().file_name().and_then(|n| n.to_str()) {
@@ -130,22 +144,25 @@ impl IndexResolver {
false
}
})
- .cloned()
.collect();
- let tool_files_refs: Vec<&ParsedFile> = tool_files.iter().collect();
-
if !tool_files.is_empty() {
let resolver = plugin.build_resolver();
- let (unit, context) = resolver.resolve(&tool_files_refs)?;
+ let (unit, ctx) = resolver
+ .resolve(&tool_files)
+ .map_err(crate::error::NaviscopeError::from)?;
all_ops.extend(unit.ops);
- project_context
- .path_to_module
- .extend(context.path_to_module);
+ context.path_to_module.extend(ctx.path_to_module);
}
}
+ Ok(all_ops)
+ }
- // Phase 2: Resolve Source Files (Entities) in parallel
+ pub fn resolve_source_batch(
+ &self,
+ source_files: &[ParsedFile],
+ context: &ProjectContext,
+ ) -> Result> {
let source_results: Vec> = source_files
.par_iter()
.map(|file| {
@@ -154,18 +171,42 @@ impl IndexResolver {
if let Some(p) = plugin {
let resolver = p.lang_resolver();
- resolver.resolve(file, &project_context)
+ resolver
+ .resolve(file, context)
+ .map_err(crate::error::NaviscopeError::from)
} else {
Ok(ResolvedUnit::new())
}
})
.collect();
- // Collect and merge source operations
+ let mut all_ops = Vec::new();
for result in source_results {
let unit = result?;
all_ops.extend(unit.ops);
}
+ Ok(all_ops)
+ }
+}
+
+impl crate::ingest::pipeline::PipelineStage for IndexResolver {
+ type Output = GraphOp;
+
+ fn process(
+ &self,
+ context: &ProjectContext,
+ paths: Vec,
+ ) -> Result> {
+ // In a pipeline batch, we need to scan and then resolve
+ // For simplicity in this first step, we assume paths are already filtered
+ // We need existing_metadata to avoid redundant parsing, but for a simple pipeline we can skip it or pass it in context
+ let files =
+ crate::ingest::scanner::Scanner::scan_files(paths, &std::collections::HashMap::new());
+ let (mut all_ops, _build, source) = self.prepare_and_partition(files);
+
+ // In this stage, we only care about source files in the pipeline
+ let source_ops = self.resolve_source_batch(&source, context)?;
+ all_ops.extend(source_ops);
Ok(all_ops)
}
diff --git a/crates/core/src/ingest/resolver/mod.rs b/crates/core/src/ingest/resolver/mod.rs
index 1973440..462c64d 100644
--- a/crates/core/src/ingest/resolver/mod.rs
+++ b/crates/core/src/ingest/resolver/mod.rs
@@ -1,82 +1,4 @@
-use crate::error::Result;
-use crate::ingest::scanner::ParsedFile;
-use crate::model::ResolvedUnit;
-use naviscope_api::models::SymbolResolution;
-use petgraph::stable_graph::NodeIndex;
-use std::collections::HashMap;
-use std::path::{Path, PathBuf};
-use tree_sitter::Tree;
-
pub mod engine;
pub mod scope;
-use crate::features::CodeGraphLike;
-
-/// Core interface for resolving a symbol at a specific position in a document.
-pub trait SemanticResolver: Send + Sync {
- /// Resolve a symbol at a specific position in a document (local view)
- fn resolve_at(
- &self,
- tree: &Tree,
- source: &str,
- line: usize,
- byte_col: usize,
- index: &dyn CodeGraphLike,
- ) -> Option;
-
- /// Find nodes in the global graph matching a resolution result (global view)
- fn find_matches(&self, index: &dyn CodeGraphLike, res: &SymbolResolution) -> Vec;
-
- /// Resolve the type(s) of a symbol (e.g., return type of a method, type of a field)
- fn resolve_type_of(
- &self,
- index: &dyn CodeGraphLike,
- res: &SymbolResolution,
- ) -> Vec;
-
- /// Find implementations or overrides of a symbol (global view)
- fn find_implementations(
- &self,
- index: &dyn CodeGraphLike,
- res: &SymbolResolution,
- ) -> Vec;
-}
-
-/// Project context generated by BuildResolver during the first phase
-#[derive(Debug, Clone)]
-pub struct ProjectContext {
- /// Mapping from path prefixes to module IDs (e.g., "/project/app" -> "module::app")
- pub path_to_module: HashMap,
-}
-
-impl ProjectContext {
- pub fn new() -> Self {
- Self {
- path_to_module: HashMap::new(),
- }
- }
-
- /// Finds the best matching module ID for a given file path
- pub fn find_module_for_path(&self, path: &Path) -> Option {
- let mut current = path.to_path_buf();
- while let Some(parent) = current.parent() {
- if let Some(id) = self.path_to_module.get(parent) {
- return Some(id.clone());
- }
- current = parent.to_path_buf();
- }
- None
- }
-}
-
-/// Responsible for resolution logic at the build tool level
-pub trait BuildResolver: Send + Sync {
- /// Resolves build files into graph operations and project context
- fn resolve(&self, files: &[&ParsedFile]) -> Result<(ResolvedUnit, ProjectContext)>;
-}
-
-/// Responsible for resolution logic at the language level
-pub trait LangResolver: Send + Sync {
- /// Resolves source files into graph operations using the provided context
- fn resolve(&self, file: &ParsedFile, context: &ProjectContext) -> Result;
-}
+pub use naviscope_plugin::{BuildResolver, LangResolver, ProjectContext, SemanticResolver};
diff --git a/crates/core/src/ingest/scanner.rs b/crates/core/src/ingest/scanner.rs
index c692cc0..79a1efb 100644
--- a/crates/core/src/ingest/scanner.rs
+++ b/crates/core/src/ingest/scanner.rs
@@ -1,6 +1,6 @@
use super::is_relevant_path;
-use crate::ingest::parser::GlobalParseResult;
-use crate::model::source::{BuildTool, Language, SourceFile};
+
+use crate::model::source::SourceFile;
use ignore::WalkBuilder;
use rayon::prelude::*;
use std::collections::HashMap;
@@ -10,80 +10,7 @@ use std::path::{Path, PathBuf};
use std::time::SystemTime;
use xxhash_rust::xxh3::Xxh3;
-#[derive(Clone)]
-pub enum ParsedContent {
- Language(GlobalParseResult),
- MetaData(serde_json::Value),
- Unparsed(String),
-}
-
-#[derive(Clone)]
-pub struct ParsedFile {
- pub file: SourceFile,
- pub content: ParsedContent,
-}
-
-impl ParsedFile {
- pub fn is_build(&self) -> bool {
- match self.content {
- ParsedContent::Unparsed(..) => {
- let name = self
- .path()
- .file_name()
- .and_then(|n| n.to_str())
- .unwrap_or("");
- name == "build.gradle"
- || name == "build.gradle.kts"
- || name == "settings.gradle"
- || name == "settings.gradle.kts"
- }
- _ => false,
- }
- }
-
- pub fn build_tool(&self) -> Option {
- match self.content {
- ParsedContent::Unparsed(..) => {
- if self.is_build() {
- Some(BuildTool::GRADLE)
- } else {
- None
- }
- }
- _ => None,
- }
- }
-
- pub fn language(&self) -> Option {
- match self.content {
- ParsedContent::Language(ref res) => {
- // Try to infer from package
- if let Some(ref pkg) = res.package_name {
- if pkg.starts_with("java.") || pkg.starts_with("javax.") {
- return Some(Language::JAVA);
- }
- }
- Some(Language::UNKNOWN)
- }
- ParsedContent::MetaData(..) => None,
- ParsedContent::Unparsed(..) => {
- if self.is_build() {
- self.build_tool()
- .map(|t| Language::new(t.as_str().to_string()))
- } else {
- self.path()
- .extension()
- .and_then(|e| e.to_str())
- .and_then(Language::from_extension)
- }
- }
- }
- }
-
- pub fn path(&self) -> &Path {
- &self.file.path
- }
-}
+pub use naviscope_plugin::{ParsedContent, ParsedFile};
pub struct Scanner;
@@ -137,12 +64,16 @@ impl Scanner {
|| file_name == "build.gradle.kts"
|| file_name == "settings.gradle"
|| file_name == "settings.gradle.kts"
- || path.extension().is_some()
{
Some(ParsedFile {
file: source_file,
content: ParsedContent::Unparsed(content_str),
})
+ } else if path.extension().is_some() {
+ Some(ParsedFile {
+ file: source_file,
+ content: ParsedContent::Lazy,
+ })
} else {
None
}
diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs
index be9d5b7..71b674a 100644
--- a/crates/core/src/lib.rs
+++ b/crates/core/src/lib.rs
@@ -6,6 +6,8 @@ pub mod facade;
pub mod features;
pub mod ingest;
pub mod model;
+pub mod plugin;
pub mod runtime;
+// FQN types are now exported from model module
pub use error::Result;
diff --git a/crates/core/src/logging.rs b/crates/core/src/logging.rs
index 427adc3..a2f14e6 100644
--- a/crates/core/src/logging.rs
+++ b/crates/core/src/logging.rs
@@ -2,7 +2,7 @@ use std::path::Path;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
-pub fn init_logging(component: &str) -> WorkerGuard {
+pub fn init_logging(component: &str, to_stderr: bool) -> WorkerGuard {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
let log_dir = Path::new(&home).join(".naviscope/logs");
let _ = std::fs::create_dir_all(&log_dir);
@@ -20,24 +20,18 @@ pub fn init_logging(component: &str) -> WorkerGuard {
.with_ansi(false)
.with_target(true);
- // Stderr layer: only enable for CLI (terminal), disable for LSP/MCP to avoid interfering with protocol
- // LSP/MCP modes should only log to file to avoid interfering with stdio protocol
- if component == "cli" {
+ let registry = tracing_subscriber::registry()
+ .with(filter)
+ .with(file_layer);
+
+ if to_stderr {
let stderr_layer = fmt::layer()
.with_writer(std::io::stderr)
.with_ansi(true)
.with_target(false);
- tracing_subscriber::registry()
- .with(filter)
- .with(file_layer)
- .with(stderr_layer)
- .init();
+ registry.with(stderr_layer).init();
} else {
- // LSP/MCP: only log to file, no stderr output
- tracing_subscriber::registry()
- .with(filter)
- .with(file_layer)
- .init();
+ registry.init();
}
guard
diff --git a/crates/core/src/model/fqn.rs b/crates/core/src/model/fqn.rs
new file mode 100644
index 0000000..6dbd935
--- /dev/null
+++ b/crates/core/src/model/fqn.rs
@@ -0,0 +1,241 @@
+use dashmap::DashMap;
+use lasso::ThreadedRodeo;
+use naviscope_api::models::graph::NodeKind;
+use naviscope_api::models::symbol::NodeId;
+pub use naviscope_api::models::symbol::{FqnId, FqnNode, FqnReader, Symbol};
+pub use naviscope_plugin::FqnInterner;
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use std::sync::Arc;
+
+/// Structured FQN Storage and Manager (Thread-safe Propeller Edition)
+#[derive(Debug, Clone)]
+pub struct FqnManager {
+ pub rodeo: Arc,
+ pub nodes: Arc>,
+ pub lookup: Arc, Symbol, NodeKind), FqnId>>,
+ pub next_id: Arc,
+}
+
+impl Default for FqnManager {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl FqnManager {
+ pub fn new() -> Self {
+ Self {
+ rodeo: Arc::new(ThreadedRodeo::new()),
+ nodes: Arc::new(DashMap::new()),
+ lookup: Arc::new(DashMap::new()),
+ next_id: Arc::new(std::sync::atomic::AtomicU32::new(1)),
+ }
+ }
+
+ pub fn with_rodeo(rodeo: Arc) -> Self {
+ Self {
+ rodeo,
+ nodes: Arc::new(DashMap::new()),
+ lookup: Arc::new(DashMap::new()),
+ next_id: Arc::new(std::sync::atomic::AtomicU32::new(1)),
+ }
+ }
+
+ pub fn get_by_id(&self, id: FqnId) -> Option {
+ self.nodes.get(&id).map(|n| n.clone())
+ }
+
+ /// Try to find a child node with the given name under the given parent.
+ /// Since we don't know the Kind, we have to try potential Kinds.
+ pub fn find_child(&self, parent: Option, name: &str) -> Vec {
+ let symbol = if let Some(s) = self.rodeo.get(name) {
+ Symbol(s)
+ } else {
+ return Vec::new();
+ };
+
+ // Heuristic: Check common kinds.
+ //Ideally `NodeKind` would provide an iterator, but we can hardcode the list here based on what we know.
+ let kinds = [
+ NodeKind::Package,
+ NodeKind::Class,
+ NodeKind::Interface,
+ NodeKind::Method,
+ NodeKind::Field,
+ NodeKind::Module,
+ NodeKind::Enum,
+ NodeKind::Annotation,
+ NodeKind::Constructor,
+ NodeKind::Project,
+ ];
+
+ let mut results = Vec::new();
+ for kind in &kinds {
+ let key = (parent, symbol, kind.clone());
+ if let Some(id) = self.lookup.get(&key) {
+ results.push(*id);
+ }
+ }
+ results
+ }
+
+ /// Resolve a dot/colon separated string to potential FqnIds.
+ /// This is an expensive operation as it involves walking the path and guessing kinds.
+ pub fn resolve_fqn_string(&self, fqn: &str) -> Vec {
+ // 1. Split the string into segments
+ // We handle "." and "#" and "$" as separators.
+ let segments: Vec<&str> = fqn
+ .split(|c| c == '.' || c == '#' || c == '$')
+ .filter(|s| !s.is_empty())
+ .collect();
+
+ if segments.is_empty() {
+ return Vec::new();
+ }
+
+ // 2. Simple Tree Search
+ // We maintain a list of valid current_ids. Initially [None] (root).
+ let mut current_ids: Vec