1use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use std::process::Command;
8
9use anyhow::{Context, Result};
10use protobuf::Message;
11
12use crate::facts::{FileFacts, ScipFacts, ScipRef};
13use crate::model::{Symbol, SymbolKind};
14
15const DEFINITION: i32 = 1;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Mode {
20 Auto,
21 Require,
22 Off,
23}
24
25impl Mode {
26 pub fn parse(s: &str) -> Option<Mode> {
27 match s {
28 "auto" => Some(Mode::Auto),
29 "require" => Some(Mode::Require),
30 "off" => Some(Mode::Off),
31 _ => None,
32 }
33 }
34}
35
36#[derive(Debug, Default)]
38pub struct Attached {
39 pub indexes: usize,
40 pub documents: usize,
41 pub definitions: usize,
42 pub references: usize,
43}
44
45pub fn ingest(
47 mode: Mode,
48 root: &Path,
49 repo: &str,
50 symbols: &[Symbol],
51 facts: &mut [FileFacts],
52) -> Result<Attached> {
53 let mut out = Attached::default();
54 if mode == Mode::Off {
55 return Ok(out);
56 }
57 let mut indexes = find_indexes(root);
58 let tmp = std::env::temp_dir().join(format!("comindly-scip-{}-{repo}", std::process::id()));
59 if indexes.is_empty() {
60 indexes = run_indexers(root, &tmp);
61 }
62 for (path, prefix) in &indexes {
63 let a = attach(path, prefix, repo, symbols, facts)
64 .with_context(|| format!("read {}", path.display()))?;
65 out.indexes += 1;
66 out.documents += a.documents;
67 out.definitions += a.definitions;
68 out.references += a.references;
69 }
70 std::fs::remove_dir_all(&tmp).ok();
71 if mode == Mode::Require && out.documents == 0 {
72 anyhow::bail!("{repo}: no SCIP index found or produced (scip = \"require\")");
73 }
74 Ok(out)
75}
76
77fn find_indexes(root: &Path) -> Vec<(PathBuf, String)> {
79 ignore::WalkBuilder::new(root)
80 .standard_filters(false)
81 .max_depth(Some(4))
82 .filter_entry(|e| {
83 !matches!(
84 e.file_name().to_str(),
85 Some("node_modules" | ".git" | "target" | ".venv" | "venv" | "dist")
86 )
87 })
88 .build()
89 .filter_map(|e| e.ok())
90 .filter(|e| e.file_name() == "index.scip")
91 .filter_map(|e| {
92 let dir = e.path().parent()?.strip_prefix(root).ok()?;
93 Some((
94 e.path().to_path_buf(),
95 dir.to_string_lossy().replace('\\', "/"),
96 ))
97 })
98 .collect()
99}
100
101fn run_indexers(root: &Path, tmp: &Path) -> Vec<(PathBuf, String)> {
103 let has = |f: &str| root.join(f).is_file();
104 let mut jobs: Vec<(&str, Vec<String>)> = Vec::new();
105 if has("tsconfig.json") || has("package.json") {
106 jobs.push((
107 "scip-typescript",
108 vec!["index".into(), "--infer-tsconfig".into()],
109 ));
110 }
111 if has("pyproject.toml") || has("setup.py") || has("requirements.txt") {
112 jobs.push(("scip-python", vec!["index".into(), ".".into()]));
113 }
114 if has("go.mod") {
115 jobs.push(("scip-go", Vec::new()));
116 }
117 if has("Cargo.toml") {
118 jobs.push(("rust-analyzer", vec!["scip".into(), ".".into()]));
119 }
120 if has("pom.xml") || has("build.gradle") || has("build.gradle.kts") {
121 jobs.push(("scip-java", vec!["index".into()]));
122 }
123 let mut out = Vec::new();
124 for (i, (bin, mut args)) in jobs.into_iter().enumerate() {
125 if which(bin).is_none() {
126 continue;
127 }
128 if std::fs::create_dir_all(tmp).is_err() {
129 break;
130 }
131 let file = tmp.join(format!("{i}-{bin}.scip"));
132 args.push("--output".into());
133 args.push(file.to_string_lossy().to_string());
134 let ok = Command::new(bin)
135 .args(&args)
136 .current_dir(root)
137 .output()
138 .is_ok_and(|o| o.status.success());
139 if ok && file.is_file() {
140 out.push((file, String::new()));
141 }
142 }
143 out
144}
145
146pub fn which(bin: &str) -> Option<PathBuf> {
147 std::env::split_paths(&std::env::var_os("PATH")?)
148 .map(|d| d.join(bin))
149 .find(|p| p.is_file())
150}
151
152pub fn attach(
154 index: &Path,
155 prefix: &str,
156 repo: &str,
157 symbols: &[Symbol],
158 facts: &mut [FileFacts],
159) -> Result<Attached> {
160 let bytes = std::fs::read(index)?;
161 let idx = scip::types::Index::parse_from_bytes(&bytes).context("decode SCIP index")?;
162
163 let mut by_file: HashMap<&str, Vec<&Symbol>> = HashMap::new();
164 for s in symbols
165 .iter()
166 .filter(|s| s.repo.0 == repo && !matches!(s.kind, SymbolKind::File))
167 {
168 by_file.entry(s.file_path.as_str()).or_default().push(s);
169 }
170 let mut slots: HashMap<String, usize> = HashMap::new();
171 for (i, f) in facts.iter().enumerate().filter(|(_, f)| f.repo == repo) {
172 slots.insert(f.path.clone(), i);
173 }
174
175 let mut out = Attached::default();
176 for doc in &idx.documents {
177 let path = if prefix.is_empty() {
178 doc.relative_path.clone()
179 } else {
180 format!("{prefix}/{}", doc.relative_path)
181 };
182 let Some(&slot) = slots.get(&path) else {
183 continue;
184 };
185 out.documents += 1;
186 let defs_here = by_file.get(path.as_str()).cloned().unwrap_or_default();
187 let sf = facts[slot].scip.get_or_insert_with(ScipFacts::default);
188 for occ in &doc.occurrences {
189 if occ.symbol.is_empty() || scip::symbol::is_local_symbol(&occ.symbol) {
190 continue;
191 }
192 let (Some(&line0), Some(name)) = (occ.range.first(), last_name(&occ.symbol)) else {
193 continue;
194 };
195 let line = line0 as u32 + 1;
196 if occ.symbol_roles & DEFINITION != 0 {
197 if let Some(s) = defs_here
199 .iter()
200 .filter(|s| {
201 s.name == name && s.range.start.line <= line && line <= s.range.end.line
202 })
203 .max_by_key(|s| s.range.start.line)
204 {
205 sf.defs.push((occ.symbol.clone(), s.id.render()));
206 out.definitions += 1;
207 }
208 } else {
209 sf.refs.push(ScipRef {
210 line,
211 name,
212 symbol: occ.symbol.clone(),
213 });
214 out.references += 1;
215 }
216 }
217 }
218 Ok(out)
219}
220
221fn last_name(symbol: &str) -> Option<String> {
223 let s = scip::symbol::parse_symbol(symbol).ok()?;
224 s.descriptors.last().map(|d| d.name.clone())
225}