pi-lens 3.6.6 → 3.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.md +39 -2
- package/clients/dispatch/runners/similarity.ts +100 -1
- package/clients/installer/index.ts +26 -20
- package/clients/lsp/client.ts +249 -110
- package/clients/native-rust-client.ts +531 -0
- package/commands/booboo.ts +2 -2
- package/package.json +14 -2
- package/rust/Cargo.toml +34 -0
- package/rust/src/cache.rs +127 -0
- package/rust/src/index.rs +407 -0
- package/rust/src/lib.rs +209 -0
- package/rust/src/main.rs +24 -0
- package/rust/src/scan.rs +116 -0
- package/rust/src/similarity.rs +387 -0
- package/skills/ast-grep/SKILL.md +16 -4
package/rust/src/lib.rs
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
//! pi-lens-core: High-performance analysis engine
|
|
2
|
+
//!
|
|
3
|
+
//! Provides:
|
|
4
|
+
//! - Fast file system scanning with gitignore support
|
|
5
|
+
//! - State matrix similarity detection
|
|
6
|
+
//! - Parallel project indexing
|
|
7
|
+
//! - Tree-sitter query execution
|
|
8
|
+
|
|
9
|
+
#![allow(missing_docs)] // Temporarily allow during development
|
|
10
|
+
|
|
11
|
+
pub mod cache;
|
|
12
|
+
pub mod index;
|
|
13
|
+
pub mod scan;
|
|
14
|
+
pub mod similarity;
|
|
15
|
+
|
|
16
|
+
use serde::{Deserialize, Serialize};
|
|
17
|
+
|
|
18
|
+
/// Main analysis request from TypeScript
|
|
19
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
20
|
+
pub struct AnalyzeRequest {
|
|
21
|
+
pub command: Command,
|
|
22
|
+
pub project_root: String,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
26
|
+
#[serde(rename_all = "snake_case")]
|
|
27
|
+
pub enum Command {
|
|
28
|
+
Scan {
|
|
29
|
+
extensions: Vec<String>,
|
|
30
|
+
},
|
|
31
|
+
BuildIndex {
|
|
32
|
+
files: Vec<String>,
|
|
33
|
+
},
|
|
34
|
+
Similarity {
|
|
35
|
+
file_path: String,
|
|
36
|
+
threshold: f32,
|
|
37
|
+
},
|
|
38
|
+
Query {
|
|
39
|
+
language: String,
|
|
40
|
+
query: String,
|
|
41
|
+
file_path: String,
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Analysis response to TypeScript
|
|
46
|
+
#[derive(Debug, Clone, Serialize)]
|
|
47
|
+
pub struct AnalyzeResponse {
|
|
48
|
+
pub success: bool,
|
|
49
|
+
pub data: ResponseData,
|
|
50
|
+
pub error: Option<String>,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#[derive(Debug, Clone, Serialize)]
|
|
54
|
+
#[serde(rename_all = "snake_case")]
|
|
55
|
+
pub enum ResponseData {
|
|
56
|
+
Files(Vec<FileEntry>),
|
|
57
|
+
Index(IndexData),
|
|
58
|
+
Similarities(Vec<SimilarityMatch>),
|
|
59
|
+
QueryResults(Vec<QueryMatch>),
|
|
60
|
+
Empty,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[derive(Debug, Clone, Serialize)]
|
|
64
|
+
pub struct FileEntry {
|
|
65
|
+
pub path: String,
|
|
66
|
+
pub size: u64,
|
|
67
|
+
pub modified: u64,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[derive(Debug, Clone, Serialize)]
|
|
71
|
+
pub struct IndexData {
|
|
72
|
+
pub entry_count: usize,
|
|
73
|
+
pub functions: Vec<FunctionEntry>,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
77
|
+
pub struct FunctionEntry {
|
|
78
|
+
pub id: String,
|
|
79
|
+
pub file_path: String,
|
|
80
|
+
pub name: String,
|
|
81
|
+
pub line: usize,
|
|
82
|
+
pub signature: String,
|
|
83
|
+
pub matrix_hash: String,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[derive(Debug, Clone, Serialize)]
|
|
87
|
+
pub struct SimilarityMatch {
|
|
88
|
+
pub source_id: String,
|
|
89
|
+
pub target_id: String,
|
|
90
|
+
pub similarity: f32,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#[derive(Debug, Clone, Serialize)]
|
|
94
|
+
pub struct QueryMatch {
|
|
95
|
+
pub line: usize,
|
|
96
|
+
pub column: usize,
|
|
97
|
+
pub text: String,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Analyze a request and return structured response
|
|
101
|
+
pub fn analyze(request: &AnalyzeRequest) -> AnalyzeResponse {
|
|
102
|
+
match &request.command {
|
|
103
|
+
Command::Scan { extensions } => {
|
|
104
|
+
match scan::scan_project(&request.project_root, extensions) {
|
|
105
|
+
Ok(files) => AnalyzeResponse {
|
|
106
|
+
success: true,
|
|
107
|
+
data: ResponseData::Files(files),
|
|
108
|
+
error: None,
|
|
109
|
+
},
|
|
110
|
+
Err(e) => AnalyzeResponse {
|
|
111
|
+
success: false,
|
|
112
|
+
data: ResponseData::Empty,
|
|
113
|
+
error: Some(format!("{}", e)),
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
Command::BuildIndex { files } => {
|
|
118
|
+
match index::build_project_index(&request.project_root, files) {
|
|
119
|
+
Ok(index_data) => AnalyzeResponse {
|
|
120
|
+
success: true,
|
|
121
|
+
data: ResponseData::Index(index_data),
|
|
122
|
+
error: None,
|
|
123
|
+
},
|
|
124
|
+
Err(e) => AnalyzeResponse {
|
|
125
|
+
success: false,
|
|
126
|
+
data: ResponseData::Empty,
|
|
127
|
+
error: Some(format!("{}", e)),
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
Command::Similarity { file_path, threshold } => {
|
|
132
|
+
let matches = index::find_similar_to(&request.project_root, file_path, *threshold);
|
|
133
|
+
if matches.is_empty() {
|
|
134
|
+
AnalyzeResponse {
|
|
135
|
+
success: true,
|
|
136
|
+
data: ResponseData::Similarities(vec![]),
|
|
137
|
+
error: None,
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
AnalyzeResponse {
|
|
141
|
+
success: true,
|
|
142
|
+
data: ResponseData::Similarities(matches),
|
|
143
|
+
error: None,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
Command::Query { language, query, file_path } => {
|
|
148
|
+
// Tree-sitter query execution
|
|
149
|
+
match run_query(language, query, file_path) {
|
|
150
|
+
Ok(results) => AnalyzeResponse {
|
|
151
|
+
success: true,
|
|
152
|
+
data: ResponseData::QueryResults(results),
|
|
153
|
+
error: None,
|
|
154
|
+
},
|
|
155
|
+
Err(e) => AnalyzeResponse {
|
|
156
|
+
success: false,
|
|
157
|
+
data: ResponseData::Empty,
|
|
158
|
+
error: Some(format!("{}", e)),
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Run a tree-sitter query on a file
|
|
166
|
+
fn run_query(
|
|
167
|
+
language: &str,
|
|
168
|
+
query_str: &str,
|
|
169
|
+
file_path: &str,
|
|
170
|
+
) -> anyhow::Result<Vec<QueryMatch>> {
|
|
171
|
+
use tree_sitter::{Parser, Query, QueryCursor};
|
|
172
|
+
|
|
173
|
+
// Read file content
|
|
174
|
+
let content = std::fs::read_to_string(file_path)?;
|
|
175
|
+
|
|
176
|
+
// Create parser and set language
|
|
177
|
+
let mut parser = Parser::new();
|
|
178
|
+
let language = match language {
|
|
179
|
+
"typescript" => tree_sitter_typescript::language_typescript(),
|
|
180
|
+
"rust" => tree_sitter_rust::language(),
|
|
181
|
+
_ => return Err(anyhow::anyhow!("Unsupported language: {}", language)),
|
|
182
|
+
};
|
|
183
|
+
parser.set_language(&language)?;
|
|
184
|
+
|
|
185
|
+
// Parse the file
|
|
186
|
+
let tree = parser.parse(&content, None)
|
|
187
|
+
.ok_or_else(|| anyhow::anyhow!("Failed to parse file"))?;
|
|
188
|
+
|
|
189
|
+
// Create and execute query
|
|
190
|
+
let query = Query::new(&language, query_str)?;
|
|
191
|
+
let root = tree.root_node();
|
|
192
|
+
let mut cursor = QueryCursor::new();
|
|
193
|
+
let matches = cursor.matches(&query, root, content.as_bytes());
|
|
194
|
+
|
|
195
|
+
// Collect results
|
|
196
|
+
let mut results = Vec::new();
|
|
197
|
+
for m in matches {
|
|
198
|
+
for capture in m.captures {
|
|
199
|
+
let node = capture.node;
|
|
200
|
+
results.push(QueryMatch {
|
|
201
|
+
line: node.start_position().row + 1,
|
|
202
|
+
column: node.start_position().column + 1,
|
|
203
|
+
text: node.utf8_text(content.as_bytes())?.to_string(),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
Ok(results)
|
|
209
|
+
}
|
package/rust/src/main.rs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//! CLI entrypoint for pi-lens-core
|
|
2
|
+
|
|
3
|
+
use std::io::{self, Read};
|
|
4
|
+
|
|
5
|
+
use pi_lens_core::{AnalyzeRequest, analyze};
|
|
6
|
+
|
|
7
|
+
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
8
|
+
let mut input = String::new();
|
|
9
|
+
io::stdin().read_to_string(&mut input)?;
|
|
10
|
+
|
|
11
|
+
let request: AnalyzeRequest = match serde_json::from_str(&input) {
|
|
12
|
+
Ok(r) => r,
|
|
13
|
+
Err(e) => {
|
|
14
|
+
eprintln!("Failed to parse request: {}", e);
|
|
15
|
+
std::process::exit(1);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
let response = analyze(&request);
|
|
20
|
+
let json = serde_json::to_string_pretty(&response)?;
|
|
21
|
+
|
|
22
|
+
println!("{}", json);
|
|
23
|
+
Ok(())
|
|
24
|
+
}
|
package/rust/src/scan.rs
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//! Fast file system scanning with gitignore support
|
|
2
|
+
|
|
3
|
+
use crate::FileEntry;
|
|
4
|
+
use ignore::DirEntry;
|
|
5
|
+
use ignore::WalkBuilder;
|
|
6
|
+
use std::path::Path;
|
|
7
|
+
|
|
8
|
+
/// Scan project for files matching extensions
|
|
9
|
+
/// Uses ripgrep's `ignore` crate for .gitignore support
|
|
10
|
+
pub fn scan_project(root: &str, extensions: &[String]) -> anyhow::Result<Vec<FileEntry>> {
|
|
11
|
+
let root_path = Path::new(root);
|
|
12
|
+
|
|
13
|
+
let walker = WalkBuilder::new(root_path)
|
|
14
|
+
.hidden(true)
|
|
15
|
+
.git_ignore(true)
|
|
16
|
+
.git_global(true)
|
|
17
|
+
.git_exclude(true)
|
|
18
|
+
.ignore(true)
|
|
19
|
+
.build();
|
|
20
|
+
|
|
21
|
+
let mut files: Vec<FileEntry> = Vec::new();
|
|
22
|
+
|
|
23
|
+
for entry in walker {
|
|
24
|
+
if let Ok(entry) = entry {
|
|
25
|
+
if let Some(file_entry) = process_entry(entry, extensions) {
|
|
26
|
+
files.push(file_entry);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Sort for deterministic output
|
|
32
|
+
files.sort_by(|a, b| a.path.cmp(&b.path));
|
|
33
|
+
Ok(files)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn process_entry(entry: DirEntry, extensions: &[String]) -> Option<FileEntry> {
|
|
37
|
+
let path = entry.path();
|
|
38
|
+
|
|
39
|
+
// Check if it's a file
|
|
40
|
+
let file_type = entry.file_type()?;
|
|
41
|
+
if !file_type.is_file() {
|
|
42
|
+
return None;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let ext = path.extension()?.to_str()?;
|
|
46
|
+
let ext_with_dot = format!(".{}", ext);
|
|
47
|
+
|
|
48
|
+
if !extensions.contains(&ext.to_string()) && !extensions.contains(&ext_with_dot) {
|
|
49
|
+
return None;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let metadata = entry.metadata().ok()?;
|
|
53
|
+
|
|
54
|
+
Some(FileEntry {
|
|
55
|
+
path: path.to_string_lossy().to_string(),
|
|
56
|
+
size: metadata.len(),
|
|
57
|
+
modified: metadata
|
|
58
|
+
.modified()
|
|
59
|
+
.ok()?
|
|
60
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
61
|
+
.ok()?
|
|
62
|
+
.as_secs(),
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Fast parallel file scanner for bulk operations
|
|
67
|
+
pub fn scan_parallel(root: &str, extensions: &[String]) -> anyhow::Result<Vec<FileEntry>> {
|
|
68
|
+
// Currently same as scan_project - parallel processing handled by ignore crate
|
|
69
|
+
scan_project(root, extensions)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[cfg(test)]
|
|
73
|
+
mod tests {
|
|
74
|
+
use super::*;
|
|
75
|
+
use std::fs;
|
|
76
|
+
use tempfile::TempDir;
|
|
77
|
+
|
|
78
|
+
#[test]
|
|
79
|
+
fn test_scan_finds_typescript_files() {
|
|
80
|
+
let temp = TempDir::new().unwrap();
|
|
81
|
+
let root = temp.path();
|
|
82
|
+
|
|
83
|
+
// Create test files
|
|
84
|
+
fs::write(root.join("test.ts"), "// typescript").unwrap();
|
|
85
|
+
fs::write(root.join("test.js"), "// javascript").unwrap();
|
|
86
|
+
fs::write(root.join("readme.md"), "# readme").unwrap();
|
|
87
|
+
|
|
88
|
+
// Create subdirectory
|
|
89
|
+
let sub = root.join("src");
|
|
90
|
+
fs::create_dir(&sub).unwrap();
|
|
91
|
+
fs::write(sub.join("main.ts"), "// main").unwrap();
|
|
92
|
+
|
|
93
|
+
let files = scan_project(root.to_str().unwrap(), &[".ts".to_string()]).unwrap();
|
|
94
|
+
|
|
95
|
+
assert_eq!(files.len(), 2);
|
|
96
|
+
assert!(files.iter().any(|f| f.path.ends_with("test.ts")));
|
|
97
|
+
assert!(files.iter().any(|f| f.path.ends_with("main.ts")));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[test]
|
|
101
|
+
fn test_scan_respects_hidden_dirs() {
|
|
102
|
+
let temp = TempDir::new().unwrap();
|
|
103
|
+
let root = temp.path();
|
|
104
|
+
|
|
105
|
+
// Create files
|
|
106
|
+
fs::write(root.join("visible.ts"), "").unwrap();
|
|
107
|
+
fs::create_dir(root.join(".hidden")).unwrap();
|
|
108
|
+
fs::write(root.join(".hidden/secret.ts"), "").unwrap();
|
|
109
|
+
|
|
110
|
+
let files = scan_project(root.to_str().unwrap(), &[".ts".to_string()]).unwrap();
|
|
111
|
+
|
|
112
|
+
// Should find visible.ts but not .hidden/secret.ts
|
|
113
|
+
assert!(files.iter().any(|f| f.path.ends_with("visible.ts")));
|
|
114
|
+
assert!(!files.iter().any(|f| f.path.contains(".hidden")));
|
|
115
|
+
}
|
|
116
|
+
}
|