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.
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }