Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 139 additions & 80 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
"crates/mcp",
"crates/api",
"crates/runtime",
"crates/plugin",
]

[workspace.dependencies]
Expand All @@ -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"
Expand Down Expand Up @@ -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"
21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -60,6 +61,10 @@ graph TD
Gradle["naviscope-gradle<br/>(Gradle Analysis)"]:::language
end

subgraph Abstraction [Plugin Layer]
Plugin["naviscope-plugin<br/>(Traits & Contracts)"]:::plugin
end

subgraph Engine [Core Layer]
Core["naviscope-core<br/>(Graph, Index & IO)"]:::core
end
Expand All @@ -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.
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "naviscope-api"
version = "0.5.0"
version = "0.5.5"
edition = "2024"

[dependencies]
Expand Down
35 changes: 4 additions & 31 deletions crates/api/src/models/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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()),
Expand All @@ -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)
}
Expand Down Expand Up @@ -185,16 +181,6 @@ pub struct DisplaySymbolLocation {
pub selection_range: Option<Range>,
}

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,
Expand All @@ -213,19 +199,6 @@ pub struct DisplayGraphNode {
pub children: Option<Vec<DisplayGraphNode>>,
}

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 {
Expand Down
95 changes: 93 additions & 2 deletions crates/api/src/models/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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<FqnId>,
pub name: Symbol,
pub kind: NodeKind,
}

pub trait FqnReader {
fn resolve_node(&self, id: FqnId) -> Option<FqnNode>;
fn resolve_atom(&self, atom: Symbol) -> &str;
}

#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, JsonSchema)]
pub struct Range {
pub start_line: usize,
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
}
Expand Down
3 changes: 2 additions & 1 deletion crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "naviscope-cli"
version = "0.5.0"
version = "0.5.5"
edition = "2024"

[[bin]]
Expand All @@ -25,3 +25,4 @@ serde_json = { workspace = true }
indexmap = { workspace = true }
petgraph = { workspace = true }
naviscope-api = { workspace = true }
mimalloc = { workspace = true }
26 changes: 14 additions & 12 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,24 +76,26 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
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 })?;
Expand Down
3 changes: 3 additions & 0 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;

fn main() -> Result<(), Box<dyn std::error::Error>> {
naviscope_cli::run()
}
6 changes: 4 additions & 2 deletions crates/cli/src/shell/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,10 @@ impl ReplServer {
}

pub async fn run(path: Option<PathBuf>) -> Result<(), Box<dyn std::error::Error>> {
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
}
5 changes: 4 additions & 1 deletion crates/core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
[package]
name = "naviscope-core"
version = "0.5.0"
version = "0.5.5"
edition = "2024"

[dependencies]
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 }
Expand All @@ -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 }
Loading