Skip to content

Commit b29a96a

Browse files
committed
add file tree actions and connection status
1 parent e03d28b commit b29a96a

15 files changed

Lines changed: 1094 additions & 107 deletions

File tree

anycode-backend/src/code.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ impl Code {
133133
saved
134134
}
135135

136+
#[allow(dead_code)]
136137
pub fn set_file_name(&mut self, file_name: String) {
137138
self.file_name = file_name;
138139
}

anycode-backend/src/handlers/connection_handler.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ pub async fn handle_connect(socket: SocketRef, _state: State<AppState>) {
1818
socket.on("file:save", handle_file_save);
1919
socket.on("file:create", handle_create);
2020
socket.on("file:close", handle_file_close);
21+
socket.on("file:delete", handle_delete);
22+
socket.on("file:rename", handle_rename);
2123

2224
socket.on("lsp:completion", handle_completion);
2325
socket.on("lsp:definition", handle_definition);

anycode-backend/src/handlers/io_handler.rs

Lines changed: 238 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use lsp_types::{Position, Range, TextDocumentContentChangeEvent};
1111
use serde::{Deserialize, Serialize};
1212
use serde_json::{self, json};
1313
use socketioxide::extract::{AckSender, Data, SocketRef, State};
14-
use std::path::PathBuf;
14+
use std::path::{Component, Path, PathBuf};
1515
use tracing::{error, info, warn};
1616

1717
/// Apply edits to a Code instance and return LSP change events.
@@ -448,6 +448,14 @@ pub async fn handle_create(
448448
let name = &request.name;
449449
let is_file = request.is_file;
450450

451+
let mut name_components = Path::new(name).components();
452+
if name.is_empty()
453+
|| !matches!(name_components.next(), Some(Component::Normal(_)))
454+
|| name_components.next().is_some()
455+
{
456+
error_ack!(ack, &request.name, "Name must be a single path component");
457+
}
458+
451459
// Build path using PathBuf for cross-platform compatibility
452460
let full_path = if parent_path.is_empty() || parent_path == "." || parent_path == "./" {
453461
PathBuf::from(name)
@@ -465,6 +473,9 @@ pub async fn handle_create(
465473
};
466474

467475
let full_path_str = full_path.to_string_lossy().to_string();
476+
if full_path.exists() {
477+
error_ack!(ack, &request.name, "Destination path already exists");
478+
}
468479

469480
// Create parent directories if they don't exist
470481
if let Some(parent) = full_path.parent() {
@@ -484,10 +495,8 @@ pub async fn handle_create(
484495
Ok(_) => {
485496
info!("File created successfully: {}", full_path_str);
486497
let mut f2c = state.file2code.lock().await;
487-
let code = f2c
488-
.entry(full_path_str.clone())
489-
.or_insert_with(|| Code::new());
490-
code.set_file_name(full_path_str.clone());
498+
f2c.entry(full_path_str.clone())
499+
.or_insert_with(|| Code::new_empty(&full_path_str, &state.config));
491500

492501
socket
493502
.broadcast()
@@ -520,3 +529,227 @@ pub async fn handle_create(
520529
}
521530
}
522531
}
532+
533+
#[derive(Debug, Serialize, Deserialize, Clone)]
534+
pub struct DeleteRequest {
535+
pub path: String,
536+
}
537+
538+
pub async fn handle_delete(
539+
socket: SocketRef,
540+
Data(request): Data<DeleteRequest>,
541+
state: State<AppState>,
542+
ack: AckSender,
543+
) {
544+
info!("Received delete request: {:?}", request);
545+
546+
let abs_path = match abs_file(&request.path) {
547+
Ok(p) => p,
548+
Err(e) => error_ack!(ack, &request.path, "Failed to resolve path: {:?}", e),
549+
};
550+
551+
let path = std::path::Path::new(&abs_path);
552+
if !path.exists() {
553+
error_ack!(ack, &request.path, "Path does not exist");
554+
}
555+
556+
let result = if path.is_file() {
557+
std::fs::remove_file(path)
558+
} else {
559+
std::fs::remove_dir_all(path)
560+
};
561+
562+
match result {
563+
Ok(_) => {
564+
info!("Deleted successfully: {}", abs_path);
565+
566+
let prefix = format!("{}/", abs_path);
567+
568+
let files_to_close: Vec<(String, String)> = {
569+
let f2c = state.file2code.lock().await;
570+
f2c.iter()
571+
.filter(|(k, _)| **k == abs_path || k.starts_with(&prefix))
572+
.map(|(k, code)| (k.clone(), code.lang.clone()))
573+
.collect()
574+
};
575+
576+
if !files_to_close.is_empty() {
577+
let mut lsp_manager = state.lsp_manager.lock().await;
578+
for (file_path, lang) in &files_to_close {
579+
if let Some(lsp) = lsp_manager.get(lang).await {
580+
if let Err(e) = lsp.did_close(file_path) {
581+
error!(
582+
"Failed to notify LSP didClose for deleted file {}: {:?}",
583+
file_path, e
584+
);
585+
}
586+
}
587+
}
588+
}
589+
590+
{
591+
let mut f2c = state.file2code.lock().await;
592+
f2c.retain(|k, _| k != &abs_path && !k.starts_with(&prefix));
593+
}
594+
595+
{
596+
let mut sockets_data = state.socket2data.lock().await;
597+
for data in sockets_data.values_mut() {
598+
data.opened_files
599+
.retain(|k| k != &abs_path && !k.starts_with(&prefix));
600+
}
601+
}
602+
603+
socket.broadcast().emit("file:deleted", &abs_path).await.ok();
604+
ack.send(&json!({ "success": true, "path": abs_path })).ok();
605+
}
606+
Err(e) => {
607+
error_ack!(ack, &request.path, "Failed to delete: {:?}", e);
608+
}
609+
}
610+
}
611+
612+
#[derive(Debug, Serialize, Deserialize, Clone)]
613+
pub struct RenameRequest {
614+
pub old_path: String,
615+
pub new_path: String,
616+
}
617+
618+
pub async fn handle_rename(
619+
socket: SocketRef,
620+
Data(request): Data<RenameRequest>,
621+
state: State<AppState>,
622+
ack: AckSender,
623+
) {
624+
info!("Received rename request: {:?}", request);
625+
626+
let old_abs_path = match abs_file(&request.old_path) {
627+
Ok(p) => p,
628+
Err(e) => error_ack!(
629+
ack,
630+
&request.old_path,
631+
"Failed to resolve old path: {:?}",
632+
e
633+
),
634+
};
635+
636+
let new_abs_path = match abs_file(&request.new_path) {
637+
Ok(p) => p,
638+
Err(e) => error_ack!(
639+
ack,
640+
&request.new_path,
641+
"Failed to resolve new path: {:?}",
642+
e
643+
),
644+
};
645+
646+
let old_path = std::path::Path::new(&old_abs_path);
647+
if !old_path.exists() {
648+
error_ack!(ack, &request.old_path, "Old path does not exist");
649+
}
650+
if old_abs_path == new_abs_path {
651+
ack.send(&json!({ "success": true, "old": old_abs_path, "new": new_abs_path }))
652+
.ok();
653+
return;
654+
}
655+
if std::path::Path::new(&new_abs_path).exists() {
656+
error_ack!(ack, &request.new_path, "Destination path already exists");
657+
}
658+
659+
let result = std::fs::rename(&old_abs_path, &new_abs_path);
660+
661+
match result {
662+
Ok(_) => {
663+
info!("Renamed successfully: {} -> {}", old_abs_path, new_abs_path);
664+
665+
let old_prefix = format!("{}/", old_abs_path);
666+
let new_prefix = format!("{}/", new_abs_path);
667+
668+
let mut files_to_rename = Vec::new();
669+
{
670+
let f2c = state.file2code.lock().await;
671+
for (k, code) in f2c.iter() {
672+
if *k == old_abs_path {
673+
let new_lang = Code::new_empty(&new_abs_path, &state.config).lang;
674+
files_to_rename.push((
675+
k.clone(),
676+
new_abs_path.clone(),
677+
code.lang.clone(),
678+
new_lang,
679+
code.get_content(),
680+
));
681+
} else if k.starts_with(&old_prefix) {
682+
let sub_path = k.strip_prefix(&old_prefix).unwrap();
683+
let new_sub_path = format!("{}{}", new_prefix, sub_path);
684+
let new_lang = Code::new_empty(&new_sub_path, &state.config).lang;
685+
files_to_rename.push((
686+
k.clone(),
687+
new_sub_path,
688+
code.lang.clone(),
689+
new_lang,
690+
code.get_content(),
691+
));
692+
}
693+
}
694+
}
695+
696+
if !files_to_rename.is_empty() {
697+
let mut lsp_manager = state.lsp_manager.lock().await;
698+
699+
for (old_path, _, old_lang, _, _) in &files_to_rename {
700+
if let Some(lsp) = lsp_manager.get(old_lang).await {
701+
let _ = lsp.did_close(old_path);
702+
}
703+
}
704+
705+
{
706+
let mut f2c = state.file2code.lock().await;
707+
for (old_path, new_path, _, new_lang, _) in &files_to_rename {
708+
if let Some(mut code) = f2c.remove(old_path) {
709+
code.abs_path = new_path.clone();
710+
code.file_name = crate::utils::file_name(new_path);
711+
code.lang = new_lang.clone();
712+
f2c.insert(new_path.clone(), code);
713+
}
714+
}
715+
}
716+
717+
{
718+
let mut sockets_data = state.socket2data.lock().await;
719+
for data in sockets_data.values_mut() {
720+
for (old_path, new_path, _, _, _) in &files_to_rename {
721+
if data.opened_files.remove(old_path) {
722+
data.opened_files.insert(new_path.clone());
723+
}
724+
}
725+
}
726+
}
727+
728+
for (_, new_path, _, new_lang, content) in &files_to_rename {
729+
if let Some(lsp) = lsp_manager.get(new_lang).await {
730+
let _ = lsp.did_open(new_lang, new_path, content);
731+
}
732+
}
733+
}
734+
735+
let _ = socket.emit(
736+
"file:renamed",
737+
&json!({ "old": old_abs_path, "new": new_abs_path }),
738+
);
739+
740+
socket
741+
.broadcast()
742+
.emit(
743+
"file:renamed",
744+
&json!({ "old": old_abs_path, "new": new_abs_path }),
745+
)
746+
.await
747+
.ok();
748+
ack.send(&json!({ "success": true, "old": old_abs_path, "new": new_abs_path }))
749+
.ok();
750+
}
751+
Err(e) => {
752+
error_ack!(ack, &request.old_path, "Failed to rename: {:?}", e);
753+
}
754+
}
755+
}

anycode/App.tsx

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ import { type DiffMode } from './types/diffMode';
3636
import { normalizePath } from './utils';
3737

3838
const App: React.FC = () => {
39-
const { wsRef, isConnected } = useSocket({});
39+
const { wsRef, isConnected, connectionStatus } = useSocket({});
40+
const [showConnectionBanner, setShowConnectionBanner] = React.useState(false);
4041

4142
const [fileIconsStyle, setFileIconsStyle] = React.useState<'colored' | 'monochrome' | 'disabled'>(() => {
4243
if (typeof window === 'undefined') return 'colored';
@@ -64,6 +65,30 @@ const App: React.FC = () => {
6465
const { currentThemeId, handleThemeChange } = useTheme({ wsRef, isConnected });
6566
const layoutActionsRef = useRef<LayoutActions | null>(null);
6667

68+
useEffect(() => {
69+
if (connectionStatus === 'connected') {
70+
setShowConnectionBanner(false);
71+
return;
72+
}
73+
74+
const timeout = window.setTimeout(() => {
75+
setShowConnectionBanner(true);
76+
}, 750);
77+
78+
return () => window.clearTimeout(timeout);
79+
}, [connectionStatus]);
80+
81+
const handleWatcherRemove = useEvent((data: { path: string; isFile: boolean }) => {
82+
fileTree.handleWatcherRemove(data);
83+
if (data.isFile) editors.closeFile(data.path);
84+
else editors.closeFilesUnderPath(data.path);
85+
});
86+
87+
const handleFileRenamed = useEvent((data: { old: string; new: string }) => {
88+
fileTree.handleFileRenamed(data);
89+
editors.renameFilesUnderPath(data.old, data.new);
90+
});
91+
6792
useEffect(() => {
6893
const ws = wsRef.current;
6994
if (!ws || !isConnected) return;
@@ -72,7 +97,8 @@ const App: React.FC = () => {
7297
['lsp:diagnostics', editors.handleDiagnostics],
7398
['watcher:edits', editors.handleWatcherEdits],
7499
['watcher:create', fileTree.handleWatcherCreate],
75-
['watcher:remove', fileTree.handleWatcherRemove],
100+
['watcher:remove', handleWatcherRemove],
101+
['file:renamed', handleFileRenamed],
76102
['git:update', git.handleGitStatusUpdate],
77103
['git:update', editors.handleGitUpdate],
78104
['acp:message', agents.handleAcpMessage],
@@ -92,7 +118,8 @@ const App: React.FC = () => {
92118
editors.handleWatcherEdits,
93119
editors.handleGitUpdate,
94120
fileTree.handleWatcherCreate,
95-
fileTree.handleWatcherRemove,
121+
handleWatcherRemove,
122+
handleFileRenamed,
96123
git.handleGitStatusUpdate,
97124
agents.handleAcpMessage,
98125
agents.handleAcpHistory,
@@ -346,6 +373,9 @@ const App: React.FC = () => {
346373
onFocusEditor={() => editors.focusEditorInPane(editors.activeEditorPaneId)}
347374
onNavigateByKey={fileTree.navigateByKey}
348375
fileIconsStyle={fileIconsStyle}
376+
onDeleteNode={fileTree.deleteNode}
377+
onRenameNode={fileTree.renameNodeOnDisk}
378+
onCreateNode={fileTree.createNodeOnDisk}
349379
/>
350380
);
351381
case 'search':
@@ -440,6 +470,8 @@ const App: React.FC = () => {
440470
onSelectAgent={agentPanes.selectFromToolbar}
441471
onCloseAgent={agents.closeAgent}
442472
fileIconsStyle={fileIconsStyle}
473+
showConnectionStatus={showConnectionBanner}
474+
connectionStatus={connectionStatus}
443475
/>
444476
);
445477
case 'settings':

anycode/components/Icons.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,4 +203,12 @@ export const Icons = {
203203
<path d="M4 4L12 12M12 4L4 12" />
204204
</svg>
205205
),
206+
Trash: ({ size = 14 }: { size?: number }) => (
207+
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
208+
<polyline points="3 6 5 6 21 6" />
209+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
210+
<line x1="10" y1="11" x2="10" y2="17" />
211+
<line x1="14" y1="11" x2="14" y2="17" />
212+
</svg>
213+
),
206214
};

0 commit comments

Comments
 (0)