Skip to content

Commit 5107191

Browse files
authored
Merge pull request #12 from Codestz/fix/cpp-makefile-support
fix: add C/C++ Makefile project support, prefer source over header
2 parents 3c8a006 + 016cdc5 commit 5107191

4 files changed

Lines changed: 128 additions & 3 deletions

File tree

src/commands/find.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ pub async fn find_symbol(
4646
Ok(parse_symbol_results(&response, name, project_root))
4747
}
4848

49+
/// Header file extensions — used to prefer source definitions over declarations.
50+
const HEADER_EXTS: &[&str] = &["h", "hpp", "hxx", "hh"];
51+
4952
/// Resolve a symbol name to its absolute file path and 0-indexed (line, character) position.
5053
///
5154
/// Uses `workspace/symbol` to locate the symbol, then `find_name_position` to find
@@ -70,8 +73,18 @@ pub async fn resolve_symbol_location(
7073
} else {
7174
lsp_symbols
7275
};
76+
// For C/C++: clangd returns both the header declaration and the source definition.
77+
// Prefer the source file (the definition) over the header (the declaration).
7378
let symbol = symbols
74-
.first()
79+
.iter()
80+
.find(|s| {
81+
let ext = std::path::Path::new(&s.path)
82+
.extension()
83+
.and_then(|e| e.to_str())
84+
.unwrap_or("");
85+
!HEADER_EXTS.contains(&ext)
86+
})
87+
.or_else(|| symbols.first())
7588
.with_context(|| format!("symbol '{name}' not found"))?;
7689

7790
let abs_path = project_root.join(&symbol.path);

src/daemon/server.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,23 @@ impl DaemonState {
150150
}
151151
}
152152

153+
/// Ensure every detected language has at least one workspace root.
154+
///
155+
/// Languages found via file-extension scan (e.g. Makefile-based C projects) won't
156+
/// appear in `roots` from `find_package_roots()` — fall back to the project root
157+
/// so the LSP pool has a slot for them.
158+
fn add_missing_language_roots(
159+
languages: &[Language],
160+
roots: &mut Vec<(Language, PathBuf)>,
161+
project_root: &Path,
162+
) {
163+
for &lang in languages {
164+
if !roots.iter().any(|(l, _)| *l == lang) {
165+
roots.push((lang, project_root.to_path_buf()));
166+
}
167+
}
168+
}
169+
153170
/// Run the daemon's accept loop until shutdown is requested or idle timeout fires.
154171
///
155172
/// # Errors
@@ -169,7 +186,7 @@ pub async fn run_server(
169186
// Load config: krait.toml → .krait/config.toml → auto-detection
170187
let loaded = config::load(project_root);
171188
let config_source = loaded.source.clone();
172-
let package_roots = if let Some(ref cfg) = loaded.config {
189+
let mut package_roots = if let Some(ref cfg) = loaded.config {
173190
let roots = config::config_to_package_roots(cfg, project_root);
174191
info!(
175192
"config: {} ({} workspaces)",
@@ -181,6 +198,8 @@ pub async fn run_server(
181198
detect::find_package_roots(project_root)
182199
};
183200

201+
add_missing_language_roots(&languages, &mut package_roots, project_root);
202+
184203
if package_roots.len() > 1 {
185204
if loaded.config.is_none() {
186205
info!("monorepo detected: {} workspace roots", package_roots.len());

src/detect/language.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ pub fn detect_languages(root: &Path) -> Vec<Language> {
122122
.workspace_markers()
123123
.iter()
124124
.any(|m| root.join(m).exists())
125+
|| has_c_files(root)
125126
{
126127
languages.push(Language::Cpp);
127128
}
@@ -178,6 +179,35 @@ fn has_ts_files(root: &Path) -> bool {
178179
false
179180
}
180181

182+
/// Returns true if the project root (or its `src/` subdirectory) contains C/C++ source files.
183+
/// Handles Makefile-based and legacy C/C++ projects that lack `CMakeLists.txt` or
184+
/// `compile_commands.json`.
185+
fn has_c_files(root: &Path) -> bool {
186+
// C source extensions (not headers — headers alone don't indicate a buildable project)
187+
const C_SRC_EXTS: &[&str] = &["c", "cpp", "cc", "cxx"];
188+
189+
let mut dirs = vec![root.to_path_buf()];
190+
let src = root.join("src");
191+
if src.is_dir() {
192+
dirs.push(src);
193+
}
194+
195+
for dir in &dirs {
196+
let Ok(entries) = std::fs::read_dir(dir) else {
197+
continue;
198+
};
199+
if entries.filter_map(Result::ok).any(|e| {
200+
e.path()
201+
.extension()
202+
.and_then(|x| x.to_str())
203+
.is_some_and(|x| C_SRC_EXTS.contains(&x))
204+
}) {
205+
return true;
206+
}
207+
}
208+
false
209+
}
210+
181211
#[cfg(test)]
182212
mod tests {
183213
use super::*;
@@ -269,4 +299,52 @@ mod tests {
269299
let langs = detect_languages(dir.path());
270300
assert!(langs.is_empty());
271301
}
302+
303+
#[test]
304+
fn detects_cpp_from_cmake() {
305+
let dir = tempfile::tempdir().unwrap();
306+
std::fs::write(dir.path().join("CMakeLists.txt"), "").unwrap();
307+
308+
let langs = detect_languages(dir.path());
309+
assert_eq!(langs, vec![Language::Cpp]);
310+
}
311+
312+
#[test]
313+
fn detects_c_project_from_root_c_files() {
314+
let dir = tempfile::tempdir().unwrap();
315+
std::fs::write(dir.path().join("main.c"), "int main() {}").unwrap();
316+
317+
let langs = detect_languages(dir.path());
318+
assert_eq!(langs, vec![Language::Cpp]);
319+
}
320+
321+
#[test]
322+
fn detects_c_project_from_src_c_files() {
323+
let dir = tempfile::tempdir().unwrap();
324+
std::fs::create_dir(dir.path().join("src")).unwrap();
325+
std::fs::write(dir.path().join("src/app.c"), "").unwrap();
326+
327+
let langs = detect_languages(dir.path());
328+
assert_eq!(langs, vec![Language::Cpp]);
329+
}
330+
331+
#[test]
332+
fn detects_cpp_project_from_src_cpp_files() {
333+
let dir = tempfile::tempdir().unwrap();
334+
std::fs::create_dir(dir.path().join("src")).unwrap();
335+
std::fs::write(dir.path().join("src/main.cpp"), "").unwrap();
336+
337+
let langs = detect_languages(dir.path());
338+
assert_eq!(langs, vec![Language::Cpp]);
339+
}
340+
341+
#[test]
342+
fn headers_only_not_detected_as_c() {
343+
// .h files alone shouldn't trigger C detection (could be headers for another language)
344+
let dir = tempfile::tempdir().unwrap();
345+
std::fs::write(dir.path().join("config.h"), "").unwrap();
346+
347+
let langs = detect_languages(dir.path());
348+
assert!(langs.is_empty());
349+
}
272350
}

src/index/cache_query.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,22 @@ pub fn cached_read_symbol(
197197
.into_iter()
198198
.find(|s| s.name == child && s.parent_name.as_deref() == Some(search_name))?
199199
} else {
200-
symbols.into_iter().next()?
200+
// For C/C++: the index may return the header declaration before the source
201+
// definition (alphabetical path order: include/ before src/). Prefer the
202+
// source file when both exist.
203+
const HEADER_EXTS: &[&str] = &["h", "hpp", "hxx", "hh"];
204+
let preferred = symbols
205+
.iter()
206+
.find(|s| {
207+
let ext = std::path::Path::new(&s.path)
208+
.extension()
209+
.and_then(|e| e.to_str())
210+
.unwrap_or("");
211+
!HEADER_EXTS.contains(&ext)
212+
})
213+
.or_else(|| symbols.first())
214+
.cloned();
215+
preferred?
201216
};
202217

203218
// Check file freshness

0 commit comments

Comments
 (0)