Skip to content

Commit f64a4e3

Browse files
committed
feat: Updates models to match latest, including endpoint resolver
1 parent 38880f1 commit f64a4e3

File tree

16 files changed

+517
-24
lines changed

16 files changed

+517
-24
lines changed

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

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use clap::{Parser, Subcommand};
22
use code_agent_sdk::{
33
CodeIntelligence, FindReferencesByLocationRequest, FindReferencesByNameRequest,
4-
FindSymbolsRequest, GetDocumentSymbolsRequest, GotoDefinitionRequest,
4+
FindSymbolsRequest, GetDocumentSymbolsRequest, GetDocumentDiagnosticsRequest, GotoDefinitionRequest,
55
RenameSymbolRequest, FormatCodeRequest, OpenFileRequest,
66
};
77
use code_agent_sdk::model::types::ApiSymbolKind;
@@ -89,6 +89,19 @@ enum Commands {
8989
/// Path to the file
9090
file: PathBuf,
9191
},
92+
/// Get diagnostics for a document (pull model)
93+
GetDiagnostics {
94+
/// Path to the file
95+
file: PathBuf,
96+
/// Optional identifier for the diagnostic request
97+
#[arg(long)]
98+
identifier: Option<String>,
99+
},
100+
/// Inspect server capabilities
101+
InspectCapabilities {
102+
/// Path to a file to determine which server to inspect
103+
file: PathBuf,
104+
},
92105
}
93106

94107
#[tokio::main]
@@ -369,6 +382,100 @@ async fn main() -> anyhow::Result<()> {
369382
}
370383
}
371384
}
385+
386+
Commands::GetDiagnostics { file, identifier } => {
387+
// Open the file first
388+
let content = std::fs::read_to_string(&file)?;
389+
code_intel.open_file(OpenFileRequest {
390+
file_path: file.clone(),
391+
content,
392+
}).await?;
393+
394+
// Wait for language server to analyze the file
395+
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
396+
397+
let diagnostics = code_intel
398+
.get_document_diagnostics(GetDocumentDiagnosticsRequest {
399+
file_path: file.clone(),
400+
identifier,
401+
previous_result_id: None,
402+
})
403+
.await?;
404+
405+
if diagnostics.is_empty() {
406+
println!("✅ No diagnostics found in {}", file.display());
407+
} else {
408+
println!("🔍 Diagnostics in {}:", file.display());
409+
for diagnostic in diagnostics {
410+
let severity = match diagnostic.severity {
411+
Some(lsp_types::DiagnosticSeverity::ERROR) => "❌ Error",
412+
Some(lsp_types::DiagnosticSeverity::WARNING) => "⚠️ Warning",
413+
Some(lsp_types::DiagnosticSeverity::INFORMATION) => "ℹ️ Info",
414+
Some(lsp_types::DiagnosticSeverity::HINT) => "💡 Hint",
415+
Some(_) => "🔍 Diagnostic", // Catch-all for unknown severity levels
416+
None => "🔍 Diagnostic",
417+
};
418+
419+
println!(
420+
" {} at {}:{} - {}",
421+
severity,
422+
diagnostic.range.start.line + 1, // Convert to 1-based
423+
diagnostic.range.start.character + 1, // Convert to 1-based
424+
diagnostic.message
425+
);
426+
427+
if let Some(source) = &diagnostic.source {
428+
println!(" Source: {}", source);
429+
}
430+
431+
if let Some(code) = &diagnostic.code {
432+
match code {
433+
lsp_types::NumberOrString::Number(n) => println!(" Code: {}", n),
434+
lsp_types::NumberOrString::String(s) => println!(" Code: {}", s),
435+
}
436+
}
437+
}
438+
}
439+
}
440+
441+
Commands::InspectCapabilities { file } => {
442+
let workspace_root = std::env::current_dir()?;
443+
let mut workspace_manager = code_intel.workspace_manager;
444+
445+
let client = workspace_manager.get_client_for_file(&file).await?
446+
.ok_or_else(|| anyhow::anyhow!("No LSP client available for file: {:?}", file))?;
447+
448+
println!("🔍 Inspecting server capabilities for: {}", file.display());
449+
450+
if let Some(capabilities) = client.get_server_capabilities().await {
451+
println!("📋 Server Capabilities:");
452+
453+
// Check diagnostic capabilities
454+
match &capabilities.diagnostic_provider {
455+
Some(lsp_types::DiagnosticServerCapabilities::Options(opts)) => {
456+
println!(" ✅ Diagnostic Provider: Supported");
457+
println!(" - Inter-file dependencies: {:?}", opts.inter_file_dependencies);
458+
println!(" - Workspace diagnostics: {:?}", opts.workspace_diagnostics);
459+
if let Some(id) = &opts.identifier {
460+
println!(" - Identifier: {}", id);
461+
}
462+
}
463+
Some(lsp_types::DiagnosticServerCapabilities::RegistrationOptions(_)) => {
464+
println!(" ✅ Diagnostic Provider: Registration Options");
465+
}
466+
None => {
467+
println!(" ❌ Diagnostic Provider: Not supported");
468+
}
469+
}
470+
471+
// Check other relevant capabilities
472+
println!(" 📄 Text Document Sync: {:?}", capabilities.text_document_sync);
473+
println!(" 🔍 Definition Provider: {:?}", capabilities.definition_provider.is_some());
474+
println!(" 📚 References Provider: {:?}", capabilities.references_provider.is_some());
475+
} else {
476+
println!("❌ No server capabilities available");
477+
}
478+
}
372479
}
373480

374481
Ok(())

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,17 @@ impl LanguagesConfig {
143143
"buildScripts": {
144144
"enable": true
145145
}
146+
},
147+
"diagnostics": {
148+
"enable": true,
149+
"enableExperimental": true
150+
},
151+
"workspace": {
152+
"symbol": {
153+
"search": {
154+
"scope": "workspace"
155+
}
156+
}
146157
}
147158
}
148159
},

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

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::lsp::protocol::*;
2+
use crate::model::entities::DiagnosticEvent;
23
use crate::types::LanguageServerConfig;
34
use anyhow::Result;
45
use lsp_types::*;
@@ -7,8 +8,9 @@ use std::collections::HashMap;
78
use std::process::Stdio;
89
use std::sync::Arc;
910
use tokio::io::BufReader;
11+
use tokio::io::AsyncBufReadExt;
1012
use tokio::process::Command;
11-
use tokio::sync::{oneshot, Mutex};
13+
use tokio::sync::{oneshot, Mutex, broadcast};
1214
use tracing::{debug, error};
1315
use url::Url;
1416

@@ -20,11 +22,14 @@ type ResponseCallback = Box<dyn FnOnce(Result<Value>) + Send>;
2022
/// - Symbol finding and navigation
2123
/// - Code formatting and refactoring
2224
/// - Document lifecycle management
25+
/// - Diagnostic notifications (push model)
2326
pub struct LspClient {
2427
stdin: Arc<Mutex<tokio::process::ChildStdin>>,
2528
pending_requests: Arc<Mutex<HashMap<u64, ResponseCallback>>>,
29+
diagnostic_sender: broadcast::Sender<DiagnosticEvent>,
2630
next_id: Arc<Mutex<u64>>,
2731
config: LanguageServerConfig,
32+
init_result: Arc<Mutex<Option<InitializeResult>>>,
2833
}
2934

3035
impl LspClient {
@@ -52,14 +57,35 @@ impl LspClient {
5257
.stdout
5358
.take()
5459
.ok_or_else(|| anyhow::anyhow!("No stdout"))?;
60+
let stderr = child
61+
.stderr
62+
.take()
63+
.ok_or_else(|| anyhow::anyhow!("No stderr"))?;
64+
65+
// Create broadcast channel for diagnostics (capacity of 100 events)
66+
let (diagnostic_sender, _) = broadcast::channel(100);
5567

5668
let client = Self {
5769
stdin: Arc::new(Mutex::new(stdin)),
5870
pending_requests: Arc::new(Mutex::new(HashMap::new())),
71+
diagnostic_sender,
5972
next_id: Arc::new(Mutex::new(1)),
60-
config,
73+
config: config.clone(),
74+
init_result: Arc::new(Mutex::new(None)),
6175
};
6276

77+
// Start stderr monitoring
78+
let server_name = config.name.clone();
79+
tokio::spawn(async move {
80+
let mut reader = BufReader::new(stderr);
81+
let mut line = String::new();
82+
while let Ok(n) = reader.read_line(&mut line).await {
83+
if n == 0 { break; }
84+
tracing::error!("LSP {} stderr: {}", server_name, line.trim());
85+
line.clear();
86+
}
87+
});
88+
6389
client.start_message_handler(stdout).await;
6490
Ok(client)
6591
}
@@ -72,13 +98,17 @@ impl LspClient {
7298
/// # Returns
7399
/// * `Result<InitializeResult>` - Server capabilities or initialization error
74100
pub async fn initialize(&self, root_uri: Url) -> Result<InitializeResult> {
101+
tracing::debug!("Initializing LSP client for workspace: {}", root_uri);
102+
75103
let (tx, rx) = oneshot::channel();
76104

77105
let init_params = crate::lsp::LspConfig::build_initialize_params(
78-
root_uri,
106+
root_uri.clone(),
79107
self.config.initialization_options.clone(),
80108
);
81109

110+
tracing::debug!("Sending initialize request to LSP server: {}", self.config.name);
111+
82112
self.send_request("initialize", json!(init_params), move |result| {
83113
let _ = tx.send(result);
84114
})
@@ -87,10 +117,29 @@ impl LspClient {
87117
let result = rx.await??;
88118
let init_result: InitializeResult = serde_json::from_value(result)?;
89119

120+
// Store the initialization result
121+
*self.init_result.lock().await = Some(init_result.clone());
122+
123+
tracing::debug!("Sending initialized notification to LSP server: {}", self.config.name);
90124
self.send_notification("initialized", json!({})).await?;
125+
126+
tracing::debug!("LSP client initialization completed for: {}", self.config.name);
91127
Ok(init_result)
92128
}
93129

130+
/// Subscribe to diagnostic notifications from the language server
131+
///
132+
/// # Returns
133+
/// * `broadcast::Receiver<DiagnosticEvent>` - Receiver for diagnostic events
134+
pub fn subscribe_diagnostics(&self) -> broadcast::Receiver<DiagnosticEvent> {
135+
self.diagnostic_sender.subscribe()
136+
}
137+
138+
/// Get the server capabilities from initialization
139+
pub async fn get_server_capabilities(&self) -> Option<ServerCapabilities> {
140+
self.init_result.lock().await.as_ref().map(|result| result.capabilities.clone())
141+
}
142+
94143
/// Navigate to symbol definition
95144
///
96145
/// # Arguments
@@ -221,6 +270,21 @@ impl LspClient {
221270
.await
222271
}
223272

273+
/// Request diagnostics for a document (pull model)
274+
///
275+
/// # Arguments
276+
/// * `params` - Document diagnostic parameters
277+
///
278+
/// # Returns
279+
/// * `Result<Option<DocumentDiagnosticReport>>` - Diagnostic report or None
280+
pub async fn document_diagnostic(
281+
&self,
282+
params: DocumentDiagnosticParams,
283+
) -> Result<Option<DocumentDiagnosticReport>> {
284+
self.send_lsp_request("textDocument/diagnostic", params)
285+
.await
286+
}
287+
224288
/// Generic LSP request handler with automatic response parsing
225289
async fn send_lsp_request<T, R>(&self, method: &str, params: T) -> Result<Option<R>>
226290
where
@@ -254,11 +318,12 @@ impl LspClient {
254318
/// Start background task to handle LSP messages from server
255319
async fn start_message_handler(&self, stdout: tokio::process::ChildStdout) {
256320
let pending_requests = self.pending_requests.clone();
321+
let diagnostic_sender = self.diagnostic_sender.clone();
257322
tokio::spawn(async move {
258323
let mut reader = BufReader::new(stdout);
259324

260325
while let Ok(content) = read_lsp_message(&mut reader).await {
261-
if let Err(e) = Self::process_message(&content, &pending_requests).await {
326+
if let Err(e) = Self::process_message(&content, &pending_requests, &diagnostic_sender).await {
262327
error!("Failed to process LSP message: {}", e);
263328
}
264329
}
@@ -270,11 +335,51 @@ impl LspClient {
270335
async fn process_message(
271336
content: &str,
272337
pending_requests: &Arc<Mutex<HashMap<u64, ResponseCallback>>>,
338+
diagnostic_sender: &broadcast::Sender<DiagnosticEvent>,
273339
) -> Result<()> {
274340
let message = parse_lsp_message(content)?;
341+
342+
// Debug: Log all incoming messages
343+
debug!("LSP message received: method={}, has_id={}", message.method, message.id.is_some());
344+
345+
// Handle notifications (no ID)
346+
if message.id.is_none() {
347+
match message.method.as_str() {
348+
"textDocument/publishDiagnostics" => {
349+
debug!("Processing publishDiagnostics notification");
350+
if let Some(params) = message.params {
351+
match serde_json::from_value::<PublishDiagnosticsParams>(params) {
352+
Ok(diagnostic_params) => {
353+
let event = DiagnosticEvent {
354+
uri: diagnostic_params.uri.to_string(),
355+
diagnostics: diagnostic_params.diagnostics,
356+
};
357+
358+
debug!("Sending diagnostic event: uri={}, count={}", event.uri, event.diagnostics.len());
359+
360+
// Send to broadcast channel (ignore if no receivers)
361+
match diagnostic_sender.send(event) {
362+
Ok(_) => debug!("Diagnostic event sent successfully"),
363+
Err(e) => error!("Failed to send diagnostic event: {}", e),
364+
}
365+
}
366+
Err(e) => {
367+
error!("Failed to parse publishDiagnostics params: {}", e);
368+
}
369+
}
370+
}
371+
}
372+
_ => {
373+
// Other notifications - just log for now
374+
debug!("Received LSP notification: {}", message.method);
375+
}
376+
}
377+
return Ok(());
378+
}
275379

380+
// Handle responses (with ID)
276381
let Some(id) = message.id.and_then(|id| id.as_u64()) else {
277-
return Ok(()); // Notification or invalid ID
382+
return Ok(()); // Invalid ID
278383
};
279384

280385
let Some(callback) = pending_requests.lock().await.remove(&id) else {

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ impl LspConfig {
2626
..Default::default()
2727
}),
2828
text_document: Some(TextDocumentClientCapabilities {
29+
publish_diagnostics: Some(PublishDiagnosticsClientCapabilities {
30+
related_information: Some(true),
31+
version_support: Some(true),
32+
code_description_support: Some(true),
33+
data_support: Some(true),
34+
..Default::default()
35+
}),
36+
diagnostic: Some(DiagnosticClientCapabilities {
37+
dynamic_registration: Some(true),
38+
related_document_support: Some(true),
39+
}),
2940
definition: Some(GotoCapability {
3041
dynamic_registration: Some(true),
3142
link_support: Some(true),
@@ -51,10 +62,6 @@ impl LspConfig {
5162
dynamic_registration: Some(true),
5263
..Default::default()
5364
}),
54-
diagnostic: Some(DiagnosticClientCapabilities {
55-
dynamic_registration: Some(true),
56-
related_document_support: Some(true),
57-
}),
5865
..Default::default()
5966
}),
6067
workspace: Some(WorkspaceClientCapabilities {

crates/code-agent-sdk/src/model/entities.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
use lsp_types::{Location, WorkspaceSymbol};
1+
use lsp_types::{Location, WorkspaceSymbol, Diagnostic};
22
use serde::{Deserialize, Serialize};
33
use std::path::Path;
44

5+
/// Diagnostic event from LSP server
6+
#[derive(Debug, Clone)]
7+
pub struct DiagnosticEvent {
8+
pub uri: String,
9+
pub diagnostics: Vec<Diagnostic>,
10+
}
11+
512
/// Helper function to read a single source line from a file (trimmed)
613
fn read_source_line(file_path: &Path, line_number: u32) -> Option<String> {
714
let content = std::fs::read_to_string(file_path).ok()?;

0 commit comments

Comments
 (0)