Skip to content

Commit 4f6f1a8

Browse files
committed
Files synchronization work
1 parent 31876e8 commit 4f6f1a8

File tree

5 files changed

+158
-5
lines changed

5 files changed

+158
-5
lines changed

crates/code-agent-sdk/Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ tracing.workspace = true
1919
tracing-subscriber.workspace = true
2020
clap.workspace = true
2121
rmcp = { workspace = true, features = ["server", "macros", "transport-io"] }
22+
notify.workspace = true
23+
ignore.workspace = true
24+
globset.workspace = true
2225

2326
[dev-dependencies]
2427
tempfile.workspace = true
2528
tracing-subscriber.workspace = true
26-
ignore = "0.4"
27-
notify = "6.0"
2829

2930
[[bin]]
3031
name = "code-agent-cli"

crates/code-agent-sdk/config/languages.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"args": ["--stdio"],
2121
"file_extensions": ["ts", "js", "tsx", "jsx", "mjs", "cjs", "mts", "cts", "vue"],
2222
"project_patterns": ["package.json", "tsconfig.json"],
23+
"exclude_patterns": ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.next/**", "**/coverage/**"],
2324
"initialization_options": {
2425
"preferences": {
2526
"disableSuggestions": false
@@ -32,6 +33,7 @@
3233
"args": [],
3334
"file_extensions": ["rs"],
3435
"project_patterns": ["Cargo.toml"],
36+
"exclude_patterns": ["**/target/**", "**/Cargo.lock"],
3537
"initialization_options": {
3638
"cargo": {
3739
"buildScripts": {
@@ -46,6 +48,7 @@
4648
"args": [],
4749
"file_extensions": ["py"],
4850
"project_patterns": ["pyproject.toml", "setup.py", "requirements.txt"],
51+
"exclude_patterns": ["**/__pycache__/**", "**/venv/**", "**/env/**", "**/.venv/**", "**/build/**", "**/dist/**"],
4952
"initialization_options": {
5053
"pylsp": {
5154
"plugins": {
@@ -62,6 +65,7 @@
6265
"args": [],
6366
"file_extensions": ["go"],
6467
"project_patterns": ["go.mod"],
68+
"exclude_patterns": ["**/vendor/**", "**/bin/**"],
6569
"initialization_options": {}
6670
},
6771
"java": {
@@ -70,6 +74,7 @@
7074
"args": [],
7175
"file_extensions": ["java"],
7276
"project_patterns": ["pom.xml", "build.gradle"],
77+
"exclude_patterns": ["**/target/**", "**/build/**", "**/.gradle/**", "**/bin/**"],
7378
"initialization_options": {}
7479
},
7580
"csharp": {
@@ -78,6 +83,7 @@
7883
"args": ["--languageserver"],
7984
"file_extensions": ["cs"],
8085
"project_patterns": ["*.csproj", "*.sln"],
86+
"exclude_patterns": ["**/bin/**", "**/obj/**", "**/packages/**"],
8187
"initialization_options": {}
8288
},
8389
"php": {
@@ -86,6 +92,7 @@
8692
"args": ["--stdio"],
8793
"file_extensions": ["php"],
8894
"project_patterns": ["composer.json"],
95+
"exclude_patterns": ["**/vendor/**", "**/cache/**"],
8996
"initialization_options": {}
9097
},
9198
"ruby": {
@@ -94,6 +101,7 @@
94101
"args": ["stdio"],
95102
"file_extensions": ["rb"],
96103
"project_patterns": ["Gemfile"],
104+
"exclude_patterns": ["**/vendor/**", "**/tmp/**"],
97105
"initialization_options": {}
98106
},
99107
"kotlin": {
@@ -102,6 +110,7 @@
102110
"args": [],
103111
"file_extensions": ["kt", "kts"],
104112
"project_patterns": ["build.gradle", "pom.xml"],
113+
"exclude_patterns": ["**/build/**", "**/target/**", "**/.gradle/**"],
105114
"initialization_options": {}
106115
},
107116
"swift": {
@@ -110,6 +119,7 @@
110119
"args": [],
111120
"file_extensions": ["swift"],
112121
"project_patterns": ["Package.swift"],
122+
"exclude_patterns": ["**/.build/**", "**/build/**"],
113123
"initialization_options": {}
114124
}
115125
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,15 @@ impl LspClient {
203203
.await
204204
}
205205

206+
/// Notify server about created files (LSP 3.16+)
207+
///
208+
/// # Arguments
209+
/// * `params` - Created file parameters
210+
pub async fn did_create_files(&self, params: CreateFilesParams) -> Result<()> {
211+
self.send_notification("workspace/didCreateFiles", json!(params))
212+
.await
213+
}
214+
206215
/// Notify server about document content changes
207216
///
208217
/// # Arguments

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ impl LspConfig {
7474
dynamic_registration: Some(true),
7575
relative_pattern_support: Some(true),
7676
}),
77+
file_operations: Some(WorkspaceFileOperationsClientCapabilities {
78+
dynamic_registration: Some(true),
79+
will_rename: Some(true),
80+
will_delete: Some(true),
81+
did_create: Some(true),
82+
will_create: Some(true),
83+
did_rename: Some(true),
84+
did_delete: Some(true),
85+
}),
7786
..Default::default()
7887
}),
7988
..Default::default()

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

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ impl EventProcessor {
277277
}
278278

279279
async fn handle_file_event(&mut self, event: &FsEvent) -> Result<()> {
280-
use lsp_types::{DidChangeTextDocumentParams, VersionedTextDocumentIdentifier, TextDocumentContentChangeEvent, DidChangeWatchedFilesParams, FileEvent, FileChangeType};
280+
use lsp_types::{DidChangeTextDocumentParams, VersionedTextDocumentIdentifier, TextDocumentContentChangeEvent, DidChangeWatchedFilesParams, FileEvent, FileChangeType, DidCloseTextDocumentParams, TextDocumentIdentifier};
281281

282282
// Convert relative URI to absolute path
283283
if let Ok(relative_path) = event.uri.to_file_path() {
@@ -288,6 +288,98 @@ impl EventProcessor {
288288
};
289289

290290
match event.kind {
291+
FsEventKind::Created => {
292+
// Send workspace/didChangeWatchedFiles for new files (only if not already open)
293+
unsafe {
294+
let workspace_manager = &mut *self.workspace_manager;
295+
if !workspace_manager.is_file_opened(&absolute_path) {
296+
if let Ok(Some(client)) = workspace_manager.get_client_for_file(&absolute_path).await {
297+
// 1. Send didChangeWatchedFiles notification
298+
let params = DidChangeWatchedFilesParams {
299+
changes: vec![FileEvent {
300+
uri: absolute_uri.clone(),
301+
typ: FileChangeType::CREATED,
302+
}],
303+
};
304+
305+
tracing::info!("📄 File created, sending didChangeWatchedFiles: {:?}", absolute_path);
306+
let _ = client.did_change_watched_files(params).await;
307+
308+
// 2. Force parsing by sending didOpen (cross-server way to guarantee indexing)
309+
if let Ok(content) = std::fs::read_to_string(&absolute_path) {
310+
// Determine language ID from file extension using ConfigManager
311+
let language_id = if let Some(ext) = absolute_path.extension().and_then(|ext| ext.to_str()) {
312+
crate::config::ConfigManager::get_language_for_extension(ext)
313+
.unwrap_or_else(|| "plaintext".to_string())
314+
} else {
315+
"plaintext".to_string()
316+
};
317+
318+
let open_params = lsp_types::DidOpenTextDocumentParams {
319+
text_document: lsp_types::TextDocumentItem {
320+
uri: absolute_uri.clone(),
321+
language_id: language_id.clone(),
322+
version: 1,
323+
text: content,
324+
},
325+
};
326+
327+
tracing::info!("📂 Forcing parse with didOpen ({}): {:?}", language_id, absolute_path);
328+
let _ = client.did_open(open_params).await;
329+
330+
// Mark as opened in workspace manager
331+
workspace_manager.mark_file_opened(absolute_path.clone());
332+
}
333+
}
334+
} else {
335+
tracing::info!("📄 File created but already open, skipping notification: {:?}", absolute_path);
336+
}
337+
}
338+
}
339+
340+
FsEventKind::Deleted => {
341+
unsafe {
342+
let workspace_manager = &mut *self.workspace_manager;
343+
344+
if workspace_manager.is_file_opened(&absolute_path) {
345+
// File was opened - send didClose AND didChangeWatchedFiles
346+
if let Ok(Some(client)) = workspace_manager.get_client_for_file(&absolute_path).await {
347+
// 1. Close the opened file
348+
let close_params = DidCloseTextDocumentParams {
349+
text_document: TextDocumentIdentifier {
350+
uri: absolute_uri.clone(),
351+
},
352+
};
353+
tracing::info!("🗑️ Opened file deleted, sending didClose: {:?}", absolute_path);
354+
let _ = client.did_close(close_params).await;
355+
356+
// 2. Notify filesystem deletion
357+
let watch_params = DidChangeWatchedFilesParams {
358+
changes: vec![FileEvent {
359+
uri: absolute_uri,
360+
typ: FileChangeType::DELETED,
361+
}],
362+
};
363+
tracing::info!("🗑️ Sending didChangeWatchedFiles for deleted file: {:?}", absolute_path);
364+
let _ = client.did_change_watched_files(watch_params).await;
365+
}
366+
workspace_manager.mark_file_closed(&absolute_path);
367+
} else {
368+
// File was closed - just send didChangeWatchedFiles
369+
if let Ok(Some(client)) = workspace_manager.get_client_for_file(&absolute_path).await {
370+
let params = DidChangeWatchedFilesParams {
371+
changes: vec![FileEvent {
372+
uri: absolute_uri,
373+
typ: FileChangeType::DELETED,
374+
}],
375+
};
376+
tracing::info!("🗑️ File deleted, sending didChangeWatchedFiles: {:?}", absolute_path);
377+
let _ = client.did_change_watched_files(params).await;
378+
}
379+
}
380+
}
381+
}
382+
291383
FsEventKind::Modified => {
292384
// SAFETY: We know workspace_manager is valid during EventProcessor lifetime
293385
unsafe {
@@ -331,12 +423,44 @@ impl EventProcessor {
331423
}
332424
}
333425
}
334-
FsEventKind::Created => tracing::info!("📄 File created: {:?}", absolute_path),
335-
FsEventKind::Deleted => tracing::info!("🗑️ File deleted: {:?}", absolute_path),
426+
336427
FsEventKind::Renamed { ref from } => {
337428
if let Ok(from_path) = from.to_file_path() {
338429
let from_absolute = self.workspace_root.join(&from_path);
339430
tracing::info!("📋 File renamed: {:?} -> {:?}", from_absolute, absolute_path);
431+
432+
unsafe {
433+
let workspace_manager = &mut *self.workspace_manager;
434+
435+
// Handle as Delete(old) + Create(new)
436+
if workspace_manager.is_file_opened(&from_absolute) {
437+
// Old file was opened - send didClose
438+
if let Ok(Some(client)) = workspace_manager.get_client_for_file(&from_absolute).await {
439+
if let Ok(from_uri) = Url::from_file_path(&from_absolute) {
440+
let params = DidCloseTextDocumentParams {
441+
text_document: TextDocumentIdentifier {
442+
uri: from_uri,
443+
},
444+
};
445+
tracing::info!("📋 Renamed file was opened, sending didClose for old path: {:?}", from_absolute);
446+
let _ = client.did_close(params).await;
447+
}
448+
}
449+
workspace_manager.mark_file_closed(&from_absolute);
450+
}
451+
452+
// Send didChangeWatchedFiles for new file
453+
if let Ok(Some(client)) = workspace_manager.get_client_for_file(&absolute_path).await {
454+
let params = DidChangeWatchedFilesParams {
455+
changes: vec![FileEvent {
456+
uri: absolute_uri,
457+
typ: FileChangeType::CREATED,
458+
}],
459+
};
460+
tracing::info!("📋 Sending didChangeWatchedFiles for renamed file: {:?}", absolute_path);
461+
let _ = client.did_change_watched_files(params).await;
462+
}
463+
}
340464
}
341465
}
342466
}

0 commit comments

Comments
 (0)