Skip to content

Commit b5c52ea

Browse files
wtfbbqhaxbug-ops
andauthored
Fix lsp_servers[].file_patterns (#72)
* Fix lsp_servers[].file_patterns Problem While using `mcpls` in a C++ project I noticed that it always return the following error when calling get references from a '.h' header file: Error: tool call error: tool call failed for `mcpls/get_references` Caused by: tools/call failed: Mcp error: -32603: no LSP server configured for language: c After reviewing the documentation and creating an mcpls.toml with the following, it continued to not work [[lsp_servers]] language_id = "cpp" command = "clangd" args = ["--background-index", "--clang-tidy"] file_patterns = ["**/*.cpp", "**/*.cc", "**/*.cxx", "**/*.hpp", "**/*.c", "**/*.h"] # and/or with [[lsp_servers]] language_id = "c" command = "clangd" args = ["--background-index", "--clang-tidy"] file_patterns = ["**/*.c", "**/*.h"] With the above changing resulting in the language detection for matching file patterns to "plaintext" Error: tool call error: tool call failed for `mcpls/get_references` Caused by: tools/call failed: Mcp error: -32603: no LSP server configured for language: plaintext What changed - serve() now initializes the translator with an effective extension map built from both workspace mappings and LSP server file patterns: - crates/mcpls-core/src/lib.rs:114:114 - Added ServerConfig::build_effective_extension_map() to overlay extensions inferred from file_patterns: - crates/mcpls-core/src/config/mod.rs:287:287 - Added a small parser for simple glob extensions (e.g. **/*.h, *.c): - crates/mcpls-core/src/config/mod.rs:123:123 Tests added - Pattern-derived mapping overrides default extension mapping (.c/.h -> cpp): - crates/mcpls-core/src/config/mod.rs:700:700 - Complex non-simple patterns are ignored safely: - crates/mcpls-core/src/config/mod.rs:721:721 Verification - New tests passed. - Full mcpls-core unit/integration tests passed. - Existing unrelated doctest failure remains in lsp/types.rs (pre-existing visibility issue). Fix was implemented by Codex * fix(config): polish file pattern extension mapping Add direct unit coverage for extract_extension_from_pattern edge cases, including empty input, no-dot patterns, dotfiles, and multi-dot filenames. Tighten the parser to reject dotfile basenames so hidden files like .gitignore do not get treated as language extensions. Update the translator initialization test to assert against build_effective_extension_map(), which matches the runtime code path introduced by the earlier file_patterns fix. Document the resulting C/C++ language detection behavior change in CHANGELOG.md. * Update crates/mcpls-core/src/config/mod.rs Co-authored-by: Andrei G <k05h31@gmail.com> --------- Co-authored-by: Andrei G <k05h31@gmail.com>
1 parent 01b8317 commit b5c52ea

4 files changed

Lines changed: 115 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
9+
### Changed
10+
11+
- **C/C++ file pattern language detection** - When `lsp_servers[].file_patterns` include simple extensions such as `**/*.c` or `**/*.h`, mcpls now derives extension-to-language mappings from those patterns and overlays them onto the workspace extension map. This changes the default behavior for matching C/C++ files to prefer the configured LSP server language instead of falling back to built-in mappings or `plaintext`.
912

1013
## [0.3.5] - 2026-03-17
1114

crates/mcpls-core/src/bridge/translator.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2776,7 +2776,7 @@ mod tests {
27762776
lsp_servers: vec![],
27772777
};
27782778

2779-
let extension_map = config.workspace.build_extension_map();
2779+
let extension_map = config.build_effective_extension_map();
27802780
assert_eq!(extension_map.get("nu"), Some(&"nushell".to_string()));
27812781
assert_eq!(extension_map.get("rs"), Some(&"rust".to_string()));
27822782

crates/mcpls-core/src/config/mod.rs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,32 @@ impl WorkspaceConfig {
116116
}
117117
}
118118

119+
/// Extract a file extension from a glob-like file pattern.
120+
///
121+
/// Supports common patterns such as `**/*.rs` and `*.h`.
122+
/// Returns `None` for patterns without a simple trailing extension.
123+
fn extract_extension_from_pattern(pattern: &str) -> Option<String> {
124+
let basename = pattern.rsplit('/').next().unwrap_or(pattern);
125+
if basename.starts_with('.') {
126+
return None;
127+
}
128+
129+
let (_, ext) = basename.rsplit_once('.')?;
130+
if ext.is_empty() {
131+
return None;
132+
}
133+
134+
// Keep this conservative: only accept plain extension-like tokens.
135+
if ext
136+
.chars()
137+
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
138+
{
139+
Some(ext.to_string())
140+
} else {
141+
None
142+
}
143+
}
144+
119145
fn default_position_encodings() -> Vec<String> {
120146
vec!["utf-8".to_string(), "utf-16".to_string()]
121147
}
@@ -258,6 +284,25 @@ fn default_language_extensions() -> Vec<LanguageExtensionMapping> {
258284
}
259285

260286
impl ServerConfig {
287+
/// Build the effective extension map used for language detection.
288+
///
289+
/// Starts with workspace mappings and overlays mappings inferred from
290+
/// configured LSP server `file_patterns`.
291+
#[must_use]
292+
pub fn build_effective_extension_map(&self) -> HashMap<String, String> {
293+
let mut map = self.workspace.build_extension_map();
294+
295+
for server in &self.lsp_servers {
296+
for pattern in &server.file_patterns {
297+
if let Some(ext) = extract_extension_from_pattern(pattern) {
298+
map.insert(ext, server.language_id.clone());
299+
}
300+
}
301+
}
302+
303+
map
304+
}
305+
261306
/// Load configuration from the default path.
262307
///
263308
/// Default paths checked in order:
@@ -656,6 +701,71 @@ mod tests {
656701
assert_eq!(map.get("unknown"), None);
657702
}
658703

704+
#[test]
705+
fn test_extract_extension_from_pattern_empty_string() {
706+
assert_eq!(extract_extension_from_pattern(""), None);
707+
}
708+
709+
#[test]
710+
fn test_extract_extension_from_pattern_without_dot() {
711+
assert_eq!(extract_extension_from_pattern("**/*"), None);
712+
}
713+
714+
#[test]
715+
fn test_extract_extension_from_pattern_dotfile() {
716+
assert_eq!(extract_extension_from_pattern(".gitignore"), None);
717+
}
718+
719+
#[test]
720+
fn test_extract_extension_from_pattern_multi_dot_extension() {
721+
assert_eq!(
722+
extract_extension_from_pattern("foo.tar.gz"),
723+
Some("gz".to_string())
724+
);
725+
}
726+
727+
#[test]
728+
fn test_build_effective_extension_map_overrides_with_file_patterns() {
729+
let config = ServerConfig {
730+
workspace: WorkspaceConfig::default(),
731+
lsp_servers: vec![LspServerConfig {
732+
language_id: "cpp".to_string(),
733+
command: "clangd".to_string(),
734+
args: vec![],
735+
env: HashMap::new(),
736+
file_patterns: vec!["**/*.c".to_string(), "**/*.h".to_string()],
737+
initialization_options: None,
738+
timeout_seconds: 30,
739+
heuristics: None,
740+
}],
741+
};
742+
743+
let map = config.build_effective_extension_map();
744+
assert_eq!(map.get("c"), Some(&"cpp".to_string()));
745+
assert_eq!(map.get("h"), Some(&"cpp".to_string()));
746+
}
747+
748+
#[test]
749+
fn test_build_effective_extension_map_ignores_complex_patterns_without_extension() {
750+
let config = ServerConfig {
751+
workspace: WorkspaceConfig::default(),
752+
lsp_servers: vec![LspServerConfig {
753+
language_id: "cpp".to_string(),
754+
command: "clangd".to_string(),
755+
args: vec![],
756+
env: HashMap::new(),
757+
file_patterns: vec!["**/*".to_string(), "**/*.{h,hpp}".to_string()],
758+
initialization_options: None,
759+
timeout_seconds: 30,
760+
heuristics: None,
761+
}],
762+
};
763+
764+
let map = config.build_effective_extension_map();
765+
// Default C/C++ mappings remain unchanged when patterns cannot be parsed.
766+
assert_eq!(map.get("h"), Some(&"c".to_string()));
767+
}
768+
659769
#[test]
660770
fn test_get_language_for_extension() {
661771
let workspace = WorkspaceConfig {

crates/mcpls-core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ pub async fn serve(config: ServerConfig) -> Result<(), Error> {
111111
info!("Starting MCPLS server...");
112112

113113
let workspace_roots = resolve_workspace_roots(&config.workspace.roots);
114-
let extension_map = config.workspace.build_extension_map();
114+
let extension_map = config.build_effective_extension_map();
115115
let max_depth = Some(config.workspace.heuristics_max_depth);
116116

117117
let mut translator = Translator::new().with_extensions(extension_map);

0 commit comments

Comments
 (0)