Skip to content

Commit 38880f1

Browse files
committed
chore: Fixes LSP configu to be workspace specific.
1 parent 1c3cf78 commit 38880f1

File tree

9 files changed

+193
-94
lines changed

9 files changed

+193
-94
lines changed

crates/chat-cli/src/cli/chat/tools/code.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,11 +309,18 @@ impl Code {
309309

310310
match client.goto_definition(request).await {
311311
Ok(Some(definition)) => {
312-
// Always show location with context in single line
312+
// Show location with context (max 3 lines, then show count of remaining)
313313
let context = if let Some(source) = &definition.source_line {
314-
let trimmed = source.trim();
315-
if !trimmed.is_empty() {
316-
format!(": {}", trimmed)
314+
let lines: Vec<&str> = source.lines().collect();
315+
if !lines.is_empty() {
316+
let display_lines: Vec<String> = lines.iter().take(3).map(|line| line.trim().to_string()).collect();
317+
let remaining = lines.len().saturating_sub(3);
318+
319+
let mut context_str = format!(": {}", display_lines.join(" | "));
320+
if remaining > 0 {
321+
context_str.push_str(&format!(" ... ({} more lines)", remaining));
322+
}
323+
context_str
317324
} else {
318325
String::new()
319326
}
Lines changed: 93 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,142 @@
11
use super::json_config::LanguagesConfig;
22
use crate::model::types::LanguageServerConfig;
3-
use std::sync::OnceLock;
3+
use std::path::PathBuf;
4+
use std::sync::{Arc, Mutex};
5+
use std::time::{Duration, Instant};
46

5-
static LANGUAGES_CONFIG: OnceLock<LanguagesConfig> = OnceLock::new();
7+
const CONFIG_TTL: Duration = Duration::from_secs(60); // 1 minute TTL
68

7-
pub struct ConfigManager;
9+
#[derive(Debug)]
10+
struct CachedConfig {
11+
config: LanguagesConfig,
12+
loaded_at: Instant,
13+
}
14+
15+
#[derive(Debug)]
16+
pub struct ConfigManager {
17+
config_root: PathBuf,
18+
cached_config: Arc<Mutex<Option<CachedConfig>>>,
19+
}
820

921
impl ConfigManager {
10-
/// Get the global languages configuration
11-
fn get_languages_config() -> &'static LanguagesConfig {
12-
LANGUAGES_CONFIG.get_or_init(|| {
13-
LanguagesConfig::load().unwrap_or_else(|e| {
14-
eprintln!("Failed to load languages config: {}, using defaults", e);
15-
LanguagesConfig::default_config()
16-
})
17-
})
22+
/// Create a new ConfigManager with the specified config root
23+
pub fn new(config_root: PathBuf) -> Self {
24+
Self {
25+
config_root,
26+
cached_config: Arc::new(Mutex::new(None)),
27+
}
28+
}
29+
30+
/// Get the languages configuration (with TTL caching)
31+
pub fn get_config(&self) -> anyhow::Result<LanguagesConfig> {
32+
let mut cache = self.cached_config.lock().unwrap();
33+
34+
// Check if we need to reload
35+
let needs_reload = cache.as_ref()
36+
.map(|c| c.loaded_at.elapsed() > CONFIG_TTL)
37+
.unwrap_or(true);
38+
39+
if needs_reload {
40+
let config = LanguagesConfig::get_or_create(&self.config_root)?;
41+
*cache = Some(CachedConfig {
42+
config: config.clone(),
43+
loaded_at: Instant::now(),
44+
});
45+
Ok(config)
46+
} else {
47+
Ok(cache.as_ref().unwrap().config.clone())
48+
}
1849
}
1950

2051
/// Get project patterns for a specific language
21-
pub fn get_project_patterns_for_language(language: &str) -> Vec<String> {
22-
Self::get_languages_config().get_project_patterns_for_language(language)
52+
pub fn get_project_patterns_for_language(&self, language: &str) -> Vec<String> {
53+
self.get_config()
54+
.map(|c| c.get_project_patterns_for_language(language))
55+
.unwrap_or_default()
2356
}
2457

2558
/// Get language for file extension
26-
pub fn get_language_for_extension(extension: &str) -> Option<String> {
27-
Self::get_languages_config().get_language_for_extension(extension)
59+
pub fn get_language_for_extension(&self, extension: &str) -> Option<String> {
60+
self.get_config()
61+
.ok()
62+
.and_then(|c| c.get_language_for_extension(extension))
2863
}
2964

3065
/// Get all language server configurations
31-
pub fn all_configs() -> Vec<LanguageServerConfig> {
32-
Self::get_languages_config().all_configs()
66+
pub fn all_configs(&self) -> Vec<LanguageServerConfig> {
67+
self.get_config()
68+
.map(|c| c.all_configs())
69+
.unwrap_or_default()
3370
}
3471

35-
/// Get language server config by language name
36-
pub fn get_config_by_language(language: &str) -> Result<LanguageServerConfig, String> {
37-
Self::get_languages_config().get_config_by_language(language)
72+
/// Get configuration for a specific language
73+
pub fn get_config_by_language(&self, language: &str) -> Result<LanguageServerConfig, String> {
74+
self.get_config()
75+
.map_err(|e| e.to_string())
76+
.and_then(|c| c.get_config_by_language(language))
3877
}
3978

40-
/// Get server name for language (for workspace manager mapping)
41-
pub fn get_server_name_for_language(language: &str) -> Option<String> {
42-
Self::get_languages_config().get_server_name_for_language(language)
79+
/// Get server name for a language
80+
pub fn get_server_name_for_language(&self, language: &str) -> Option<String> {
81+
self.get_config()
82+
.ok()
83+
.and_then(|c| c.get_server_name_for_language(language))
4384
}
4485
}
4586

4687
#[cfg(test)]
4788
mod tests {
4889
use super::*;
90+
use tempfile::TempDir;
91+
92+
#[test]
93+
fn test_config_manager_new() {
94+
let temp_dir = TempDir::new().unwrap();
95+
let config_manager = ConfigManager::new(temp_dir.path().to_path_buf());
96+
assert_eq!(config_manager.config_root, temp_dir.path());
97+
}
4998

5099
#[test]
51100
fn test_get_project_patterns_for_language() {
52-
let patterns = ConfigManager::get_project_patterns_for_language("typescript");
53-
assert!(!patterns.is_empty());
101+
let temp_dir = TempDir::new().unwrap();
102+
let config_manager = ConfigManager::new(temp_dir.path().to_path_buf());
103+
let patterns = config_manager.get_project_patterns_for_language("typescript");
104+
assert!(patterns.contains(&"package.json".to_string()));
54105
}
55106

56107
#[test]
57108
fn test_get_language_for_extension() {
58-
assert_eq!(ConfigManager::get_language_for_extension("ts"), Some("typescript".to_string()));
59-
assert_eq!(ConfigManager::get_language_for_extension("rs"), Some("rust".to_string()));
60-
assert_eq!(ConfigManager::get_language_for_extension("unknown"), None);
109+
let temp_dir = TempDir::new().unwrap();
110+
let config_manager = ConfigManager::new(temp_dir.path().to_path_buf());
111+
assert_eq!(config_manager.get_language_for_extension("ts"), Some("typescript".to_string()));
112+
assert_eq!(config_manager.get_language_for_extension("rs"), Some("rust".to_string()));
113+
assert_eq!(config_manager.get_language_for_extension("unknown"), None);
61114
}
62115

63116
#[test]
64117
fn test_all_configs() {
65-
let configs = ConfigManager::all_configs();
66-
assert!(!configs.is_empty());
118+
let temp_dir = TempDir::new().unwrap();
119+
let config_manager = ConfigManager::new(temp_dir.path().to_path_buf());
120+
let configs = config_manager.all_configs();
121+
assert_eq!(configs.len(), 3); // typescript, rust, python
67122
}
68123

69124
#[test]
70125
fn test_get_config_by_language() {
71-
let config = ConfigManager::get_config_by_language("typescript");
126+
let temp_dir = TempDir::new().unwrap();
127+
let config_manager = ConfigManager::new(temp_dir.path().to_path_buf());
128+
let config = config_manager.get_config_by_language("typescript");
72129
assert!(config.is_ok());
73130

74-
let invalid = ConfigManager::get_config_by_language("nonexistent");
131+
let invalid = config_manager.get_config_by_language("nonexistent");
75132
assert!(invalid.is_err());
76133
}
77134

78135
#[test]
79136
fn test_get_server_name_for_language() {
80-
assert!(ConfigManager::get_server_name_for_language("typescript").is_some());
81-
assert_eq!(ConfigManager::get_server_name_for_language("unknown"), None);
82-
}
83-
84-
#[test]
85-
fn test_config_fallback_on_error() {
86-
// Verify default config works (covers error fallback path)
87-
let default_config = crate::config::json_config::LanguagesConfig::default_config();
88-
assert!(!default_config.languages.is_empty());
137+
let temp_dir = TempDir::new().unwrap();
138+
let config_manager = ConfigManager::new(temp_dir.path().to_path_buf());
139+
assert!(config_manager.get_server_name_for_language("typescript").is_some());
140+
assert_eq!(config_manager.get_server_name_for_language("unknown"), None);
89141
}
90142
}

crates/code-agent-sdk/src/config/json_config.rs

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
44
use serde_json::Value;
55
use std::collections::HashMap;
66

7-
#[derive(Debug, Deserialize, Serialize)]
7+
#[derive(Debug, Deserialize, Serialize, Clone)]
88
pub struct LanguageConfig {
99
pub name: String,
1010
pub command: String,
@@ -15,13 +15,33 @@ pub struct LanguageConfig {
1515
pub initialization_options: Option<Value>,
1616
}
1717

18-
#[derive(Debug, Deserialize, Serialize)]
18+
#[derive(Debug, Deserialize, Serialize, Clone)]
1919
pub struct LanguagesConfig {
20-
pub project_patterns: Vec<String>,
2120
pub languages: HashMap<String, LanguageConfig>,
2221
}
2322

2423
impl LanguagesConfig {
24+
/// Get or create configuration in config root folder
25+
pub fn get_or_create(config_root: &std::path::Path) -> Result<Self> {
26+
let config_path = config_root.join("languages.json");
27+
28+
// Create config directory if it doesn't exist
29+
if !config_root.exists() {
30+
std::fs::create_dir_all(config_root)?;
31+
}
32+
33+
// If config file exists, load it, otherwise create default
34+
if config_path.exists() {
35+
let content = std::fs::read_to_string(&config_path)?;
36+
Ok(serde_json::from_str(&content)?)
37+
} else {
38+
let default_config = Self::default_config();
39+
let config_json = serde_json::to_string_pretty(&default_config)?;
40+
std::fs::write(&config_path, config_json)?;
41+
Ok(default_config)
42+
}
43+
}
44+
2545
/// Load configuration from JSON file
2646
pub fn load() -> Result<Self> {
2747
let config_path = std::path::Path::new("config/languages.json");
@@ -97,13 +117,6 @@ impl LanguagesConfig {
97117
/// Default embedded configuration
98118
pub fn default_config() -> Self {
99119
let json = r#"{
100-
"project_patterns": [
101-
"Cargo.toml",
102-
"package.json",
103-
"tsconfig.json",
104-
"pyproject.toml",
105-
"setup.py"
106-
],
107120
"languages": {
108121
"typescript": {
109122
"name": "typescript-language-server",

crates/code-agent-sdk/src/mcp/server.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ impl CodeIntelligenceServer {
444444
let mut client_guard = self.client.lock().await;
445445
let client = client_guard.as_mut().unwrap();
446446

447-
let symbols = client.find_symbols(request).await.map_err(|e| {
447+
let symbols = client.find_symbols(request.clone()).await.map_err(|e| {
448448
ErrorData::new(
449449
ErrorCode::INTERNAL_ERROR,
450450
format!("Find symbols failed: {}", e),
@@ -462,7 +462,17 @@ impl CodeIntelligenceServer {
462462
"end_row": s.end_row,
463463
"end_column": s.end_column,
464464
"detail": s.detail
465-
})).collect::<Vec<_>>()
465+
})).collect::<Vec<_>>(),
466+
"search_context": {
467+
"symbol_name": request.symbol_name,
468+
"total_found": symbols.len(),
469+
"limit_applied": request.limit,
470+
"scope": if request.file_path.is_some() {
471+
format!("file: {}", request.file_path.as_ref().unwrap().display())
472+
} else {
473+
"workspace".to_string()
474+
}
475+
}
466476
});
467477

468478
Ok(CallToolResult::success(vec![Content::text(

crates/code-agent-sdk/src/sdk/client.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ pub struct CodeIntelligence {
5050
symbol_service: Box<dyn SymbolService>,
5151
coding_service: Box<dyn CodingService>,
5252
workspace_service: Box<dyn WorkspaceService>,
53-
workspace_manager: WorkspaceManager,
53+
pub workspace_manager: WorkspaceManager,
5454
}
5555

5656
impl std::fmt::Debug for CodeIntelligence {

crates/code-agent-sdk/src/sdk/mod.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ pub mod file_watcher;
99
pub mod services;
1010
pub mod workspace_manager;
1111

12-
use crate::config::ConfigManager;
1312
use crate::sdk::client::CodeIntelligence;
1413
use std::path::PathBuf;
1514
pub use workspace_manager::WorkspaceManager;
@@ -88,13 +87,13 @@ impl CodeIntelligenceBuilder {
8887
.map_err(|e| format!("Failed to detect workspace: {}", e))?;
8988

9089
for language in workspace_info.detected_languages {
91-
if let Ok(config) = ConfigManager::get_config_by_language(&language) {
90+
if let Ok(config) = client.workspace_manager.config_manager.get_config_by_language(&language) {
9291
client.add_language_server(config);
9392
}
9493
}
9594
} else {
9695
for language in self.languages {
97-
let config = ConfigManager::get_config_by_language(&language)?;
96+
let config = client.workspace_manager.config_manager.get_config_by_language(&language)?;
9897
client.add_language_server(config);
9998
}
10099
}

crates/code-agent-sdk/src/sdk/services/symbol_service.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,8 @@ impl LspSymbolService {
170170
}
171171

172172
// Apply limit
173-
if request.limit.is_some() {
174-
all_symbols.truncate(request.limit.unwrap() as usize);
173+
if let Some(limit) = request.limit {
174+
all_symbols.truncate(limit as usize);
175175
}
176176
Ok(all_symbols)
177177
}

crates/code-agent-sdk/src/sdk/services/workspace_service.rs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
use crate::config::ConfigManager;
21
use crate::sdk::workspace_manager::WorkspaceManager;
32
use anyhow::Result;
43
use lsp_types::*;
@@ -50,6 +49,14 @@ impl WorkspaceService for LspWorkspaceService {
5049
return Ok(()); // File already opened, no need to wait
5150
}
5251

52+
// Determine language ID from file extension using ConfigManager
53+
let language_id = if let Some(ext) = file_path.extension().and_then(|ext| ext.to_str()) {
54+
workspace_manager.config_manager.get_language_for_extension(ext)
55+
.unwrap_or_else(|| "plaintext".to_string())
56+
} else {
57+
"plaintext".to_string()
58+
};
59+
5360
let client = workspace_manager
5461
.get_client_for_file(file_path)
5562
.await?
@@ -58,14 +65,6 @@ impl WorkspaceService for LspWorkspaceService {
5865
let uri =
5966
Url::from_file_path(file_path).map_err(|_| anyhow::anyhow!("Invalid file path"))?;
6067

61-
// Determine language ID from file extension using ConfigManager
62-
let language_id = if let Some(ext) = file_path.extension().and_then(|ext| ext.to_str()) {
63-
ConfigManager::get_language_for_extension(ext)
64-
.unwrap_or_else(|| "plaintext".to_string())
65-
} else {
66-
"plaintext".to_string()
67-
};
68-
6968
let params = DidOpenTextDocumentParams {
7069
text_document: TextDocumentItem {
7170
uri,

0 commit comments

Comments
 (0)