Skip to content

Commit 0cf54ee

Browse files
committed
refactor(backend): modularize runtime wiring and harden git change propagation
Extracted backend startup responsibilities into dedicated modules to simplify main runtime wiring and improve maintainability. - Added AppState::new(...) to centralize initialization of config, LSP manager, ACP manager, git manager, and shared maps. - Introduced background_tasks module to spawn ACP FS loop, diagnostics forwarding, and file watcher lifecycle in one place. - Moved Socket.IO connect/disconnect flow into handlers/connection_handler with a 5-second disconnect grace period before LSP/file cleanup. - Moved SPA/static asset fallback logic into handlers/static_handler. - Slimmed main.rs to orchestration only: build channels/state, create Socket.IO namespace, wire router, and keep watcher alive. Also fixed commit/change-tracking behavior around source control updates: - Emit changes:update immediately after successful git:commit so UI refresh does not depend solely on filesystem watcher side effects. - Reworked GitManager::commit to rebuild index from HEAD before applying selected files, preventing unrelated pre-staged leftovers from being included in commit. - This ensures commits match explicit file selection in Changes panel and keeps git status updates deterministic.
1 parent e8a0723 commit 0cf54ee

7 files changed

Lines changed: 364 additions & 321 deletions

File tree

anycode-backend/src/app_state.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
use crate::acp::AcpManager;
1+
use crate::acp::{AcpManager, AcpPermissionMode};
2+
use crate::acp_fs;
23
use crate::code::Code;
34
use crate::config::Config;
45
use crate::git::GitManager;
56
use crate::lsp::LspManager;
67
use crate::terminal::Terminal;
78
use anyhow::{Result, anyhow};
9+
use lsp_types::PublishDiagnosticsParams;
810
use socketioxide::extract::{SocketRef, State};
911
use std::collections::HashSet;
1012
use std::collections::hash_map::{Entry, HashMap};
1113
use std::{collections::VecDeque, sync::Arc};
12-
use tokio::sync::Mutex;
14+
use tokio::sync::{Mutex, mpsc};
1315
use tokio_util::sync::CancellationToken;
1416

17+
1518
#[derive(Clone)]
1619
pub struct AppState {
1720
pub config: Arc<Config>,
@@ -39,6 +42,30 @@ pub struct TerminalData {
3942
}
4043

4144
impl AppState {
45+
pub fn new(
46+
diagnostic_tx: mpsc::Sender<PublishDiagnosticsParams>,
47+
acp_fs_tx: mpsc::Sender<acp_fs::AcpFsCommand>,
48+
) -> Self {
49+
let config = crate::config::get();
50+
let acp_permission_mode = AcpPermissionMode::from_env();
51+
52+
let mut lsp_manager = LspManager::new(config.clone());
53+
lsp_manager.set_diagnostics_sender(diagnostic_tx);
54+
55+
let acp_manager = AcpManager::new(acp_permission_mode, acp_fs_tx);
56+
let git_manager = GitManager::new(crate::utils::current_dir());
57+
58+
Self {
59+
config: Arc::new(config),
60+
file2code: Arc::new(Mutex::new(HashMap::new())),
61+
lsp_manager: Arc::new(Mutex::new(lsp_manager)),
62+
acp_manager: Arc::new(Mutex::new(acp_manager)),
63+
git_manager: Arc::new(Mutex::new(git_manager)),
64+
socket2data: Arc::new(Mutex::new(HashMap::new())),
65+
terminals: Arc::new(Mutex::new(HashMap::new())),
66+
}
67+
}
68+
4269
pub async fn shutdown(&self) {
4370
self.lsp_manager.lock().await.stop_all().await;
4471
self.acp_manager.lock().await.stop_all().await;
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
use std::collections::HashMap;
2+
use std::sync::Arc;
3+
4+
use anyhow::Result;
5+
use lsp_types::PublishDiagnosticsParams;
6+
use notify::{Event, RecursiveMode, Watcher, recommended_watcher};
7+
use socketioxide::SocketIo;
8+
use tokio::sync::mpsc::Receiver;
9+
use tokio::sync::{Mutex, mpsc};
10+
11+
use crate::acp_fs;
12+
use crate::app_state::AppState;
13+
use crate::handlers::watch_handler::handle_watch_event;
14+
15+
/// Spawns all background tasks: ACP filesystem loop, LSP diagnostics forwarding,
16+
/// and file system watcher.
17+
///
18+
/// The returned `notify::RecommendedWatcher` must be kept alive for the duration
19+
/// of the program — dropping it stops file watching.
20+
pub fn spawn_all(
21+
state: &AppState,
22+
io: &Arc<SocketIo>,
23+
diagnostics_rx: Receiver<PublishDiagnosticsParams>,
24+
acp_fs_rx: Receiver<acp_fs::AcpFsCommand>,
25+
) -> Result<notify::RecommendedWatcher> {
26+
spawn_acp_fs(state, io, acp_fs_rx);
27+
spawn_diagnostics(io, diagnostics_rx);
28+
let watcher = spawn_file_watcher(state, io)?;
29+
Ok(watcher)
30+
}
31+
32+
fn spawn_acp_fs(
33+
state: &AppState,
34+
io: &Arc<SocketIo>,
35+
acp_fs_rx: Receiver<acp_fs::AcpFsCommand>,
36+
) {
37+
let file2code = state.file2code.clone();
38+
let lsp_manager = state.lsp_manager.clone();
39+
let config = state.config.as_ref().clone();
40+
let io = io.clone();
41+
42+
tokio::spawn(acp_fs::run_acp_fs_loop(
43+
acp_fs_rx,
44+
file2code,
45+
lsp_manager,
46+
config,
47+
io,
48+
));
49+
}
50+
51+
fn spawn_diagnostics(
52+
io: &Arc<SocketIo>,
53+
mut diagnostics_rx: Receiver<PublishDiagnosticsParams>,
54+
) {
55+
let socket = io.clone();
56+
tokio::spawn(async move {
57+
while let Some(diagnostic_message) = diagnostics_rx.recv().await {
58+
let send_result = socket.emit("lsp:diagnostics", &diagnostic_message).await;
59+
if let Err(e) = send_result {
60+
tracing::error!("error while sending lsp:diagnostics {}", e);
61+
}
62+
}
63+
});
64+
}
65+
66+
fn spawn_file_watcher(
67+
state: &AppState,
68+
io: &Arc<SocketIo>,
69+
) -> Result<notify::RecommendedWatcher> {
70+
let file2code = state.file2code.clone();
71+
let socket2data = state.socket2data.clone();
72+
let git_manager = state.git_manager.clone();
73+
let lsp_manager = state.lsp_manager.clone();
74+
75+
let (watch_tx, mut watch_rx) = mpsc::channel::<notify::Result<Event>>(32);
76+
let mut watcher = recommended_watcher(move |res| {
77+
let _ = watch_tx.blocking_send(res);
78+
})?;
79+
80+
let dir = std::path::Path::new(".");
81+
watcher.watch(dir, RecursiveMode::Recursive)?;
82+
83+
let file_states = Arc::new(Mutex::new(HashMap::new()));
84+
let socket = io.clone();
85+
tokio::spawn(async move {
86+
while let Some(res) = watch_rx.recv().await {
87+
match res {
88+
Ok(event) => {
89+
for path in &event.paths {
90+
if crate::utils::is_ignored_path(path) {
91+
continue;
92+
} else {
93+
handle_watch_event(
94+
path,
95+
&event,
96+
&socket,
97+
&file2code,
98+
&socket2data,
99+
&file_states,
100+
&git_manager,
101+
&lsp_manager,
102+
)
103+
.await
104+
}
105+
}
106+
}
107+
Err(e) => eprintln!("watch error: {:?}", e),
108+
}
109+
}
110+
});
111+
112+
Ok(watcher)
113+
}

anycode-backend/src/git.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,16 @@ impl GitManager {
386386
pub fn commit(&self, files: &[String], message: &str) -> Result<()> {
387387
let repo = self.repo()?;
388388
let mut index = repo.index()?;
389+
let head_commit = repo.head().ok().and_then(|h| h.peel_to_commit().ok());
390+
391+
// Build commit index from HEAD tree so the commit contains only explicitly
392+
// selected paths from the UI, not previously staged leftovers.
393+
if let Some(head) = &head_commit {
394+
let head_tree = head.tree()?;
395+
index.read_tree(&head_tree)?;
396+
} else {
397+
index.clear()?;
398+
}
389399

390400
let repo_root = repo.workdir().unwrap_or(Path::new("."));
391401
for file_path in files {
@@ -409,19 +419,10 @@ impl GitManager {
409419
let tree_id = index.write_tree()?;
410420
let tree = repo.find_tree(tree_id)?;
411421

412-
let sig = repo
413-
.signature()
414-
.or_else(|_| git2::Signature::now("Anycode User", "user@anycode.dev"))?;
415-
416-
let parents: Vec<git2::Commit> = repo
417-
.head()
418-
.ok()
419-
.and_then(|h| h.peel_to_commit().ok())
420-
.map(|c| vec![c])
421-
.unwrap_or_default();
422-
422+
let parents: Vec<git2::Commit> = head_commit.map(|c| vec![c]).unwrap_or_default();
423423
let parents_refs: Vec<&git2::Commit> = parents.iter().collect();
424424

425+
let sig = repo.signature()?;
425426
repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &parents_refs)
426427
.context("Failed to commit")?;
427428

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
use std::collections::HashSet;
2+
3+
use socketioxide::extract::{SocketRef, State};
4+
use tracing::{error, info};
5+
6+
use crate::app_state::{AppState, is_language_opened};
7+
use crate::handlers::{
8+
acp_handler::*,
9+
git_handler::*,
10+
io_handler::*,
11+
lsp_handler::*,
12+
search_handler::*,
13+
terminal_handler::*,
14+
theme_handler::*,
15+
};
16+
17+
pub async fn handle_connect(socket: SocketRef, _state: State<AppState>) {
18+
info!("Socket.IO connected: {:?} {:?}", socket.ns(), socket.id);
19+
20+
socket.on("file:open", handle_file_open);
21+
socket.on("dir:list", handle_dir_list);
22+
socket.on("file:change", handle_file_change);
23+
socket.on("file:save", handle_file_save);
24+
socket.on("file:create", handle_create);
25+
socket.on("file:close", handle_file_close);
26+
27+
socket.on("lsp:completion", handle_completion);
28+
socket.on("lsp:definition", handle_definition);
29+
socket.on("lsp:references", handle_references);
30+
socket.on("lsp:hover", handle_hover);
31+
32+
socket.on("search:start", handle_search);
33+
socket.on("search:cancel", handle_search_cancel);
34+
35+
socket.on("terminal:start", handle_terminal_start);
36+
socket.on("terminal:input", handle_terminal_input);
37+
socket.on("terminal:resize", handle_terminal_resize);
38+
socket.on("terminal:close", handle_terminal_close);
39+
socket.on("terminal:reconnect", handle_terminal_reconnect);
40+
41+
socket.on("acp:start", handle_acp_start);
42+
socket.on("acp:prompt", handle_acp_prompt);
43+
socket.on("acp:stop", handle_acp_stop);
44+
socket.on("acp:cancel", handle_acp_cancel);
45+
socket.on("acp:set_model", handle_acp_set_model);
46+
socket.on("acp:set_reasoning", handle_acp_set_reasoning);
47+
socket.on("acp:list", handle_acp_list);
48+
socket.on("acp:sessions_list", handle_acp_sessions_list);
49+
socket.on("acp:reconnect", handle_acp_reconnect);
50+
socket.on("acp:permission_response", handle_acp_permission_response);
51+
socket.on("acp:undo", handle_acp_undo);
52+
53+
socket.on("git:status", handle_git_status);
54+
socket.on("git:file-original", handle_git_file_original);
55+
socket.on("git:commit", handle_git_commit);
56+
socket.on("git:push", handle_git_push);
57+
socket.on("git:pull", handle_git_pull);
58+
socket.on("git:branches", handle_git_branches);
59+
socket.on("git:checkout", handle_git_checkout);
60+
socket.on("git:revert", handle_git_revert);
61+
62+
socket.on("theme:list", handle_theme_list);
63+
socket.on("theme:get", handle_theme_get);
64+
socket.on_disconnect(handle_disconnect)
65+
}
66+
67+
pub async fn handle_disconnect(socket: SocketRef, state: State<AppState>) {
68+
info!("Socket.IO disconnected: {}", socket.id);
69+
70+
let sid = socket.id.as_str().to_string();
71+
72+
// Get opened files for this socket before removing socket data
73+
let opened_files = {
74+
let sockets_data = state.socket2data.lock().await;
75+
match sockets_data.get(&sid) {
76+
Some(socket_data) => socket_data.opened_files.clone(),
77+
None => return,
78+
}
79+
};
80+
81+
// Get languages for files opened by this socket
82+
let languages = {
83+
let f2c = state.file2code.lock().await;
84+
opened_files
85+
.iter()
86+
.filter_map(|path| f2c.get(path).map(|code| code.lang.clone()))
87+
.collect::<HashSet<_>>()
88+
};
89+
90+
// Remove socket data immediately
91+
{
92+
let mut sockets_data = state.socket2data.lock().await;
93+
sockets_data.remove(&sid);
94+
}
95+
96+
// Spawn a delayed task to clean up files and LSP servers (5-second grace period)
97+
tokio::spawn(async move {
98+
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
99+
100+
// Get all opened files from remaining active sockets
101+
let all_opened_files: HashSet<String> = {
102+
let sockets_data = state.socket2data.lock().await;
103+
sockets_data
104+
.values()
105+
.flat_map(|data| data.opened_files.iter())
106+
.cloned()
107+
.collect()
108+
};
109+
110+
// Clean up files that are no longer opened by any socket
111+
let files_to_close: Vec<(String, String)> = {
112+
let f2c = state.file2code.lock().await;
113+
opened_files
114+
.iter()
115+
.filter(|path| !all_opened_files.contains(*path))
116+
.filter_map(|path| f2c.get(path).map(|code| (path.clone(), code.lang.clone())))
117+
.collect()
118+
};
119+
120+
// Close files (notify LSP didClose)
121+
if !files_to_close.is_empty() {
122+
let mut lsp_manager = state.lsp_manager.lock().await;
123+
for (file_path, lang) in &files_to_close {
124+
if let Some(lsp) = lsp_manager.get(lang).await {
125+
if let Err(e) = lsp.did_close(file_path) {
126+
error!("Failed to notify LSP didClose for {}: {:?}", file_path, e);
127+
}
128+
}
129+
}
130+
}
131+
132+
// Stop LSP servers for languages that have no files opened by other active sockets
133+
for lang in languages {
134+
if !is_language_opened(&lang, &state).await {
135+
info!("Lsp autoclose after grace period: '{}'", lang);
136+
let mut lsp_manager = state.lsp_manager.lock().await;
137+
lsp_manager.stop(&lang).await;
138+
}
139+
}
140+
});
141+
}

anycode-backend/src/handlers/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
pub mod acp_handler;
2+
pub mod connection_handler;
23
pub mod git_handler;
34
pub mod io_handler;
45
pub mod lsp_handler;
56
pub mod search_handler;
7+
pub mod theme_handler;
68
pub mod terminal_handler;
9+
pub mod static_handler;
710
pub mod watch_handler;
8-
pub mod theme_handler;

0 commit comments

Comments
 (0)