Skip to content

Commit 869d05c

Browse files
committed
feat: enable autosave by default and simplify save UX
- add autosave to backend config with a default value of true - save file content automatically on file:change when autosave is enabled - notify LSP with did_save after successful autosave - remove frontend dirty-state tracking and related state updates - remove tab dirty indicator from file tabs - adjust file watch debounce flow to use tokio::sync::watch - apply small editor/UI style tweaks
1 parent a323799 commit 869d05c

8 files changed

Lines changed: 60 additions & 77 deletions

File tree

anycode-backend/config.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
theme = "themes/vesper.yml"
22
left_panel_width = 25
3+
autosave = true
34

45
terminal.command = "bash"
56

@@ -148,4 +149,4 @@ exec = "dotnet run {file}"
148149
name = "json"
149150
types = [".json"]
150151
comment = "//"
151-
indent = { width = 2, unit = " " }
152+
indent = { width = 2, unit = " " }

anycode-backend/src/config.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ pub struct Config {
1818
pub theme: String,
1919
pub language: Vec<Language>,
2020
pub terminal: Option<Terminal>,
21+
#[serde(default = "default_autosave")]
22+
pub autosave: bool,
2123
}
2224

2325
impl Config {
@@ -26,10 +28,15 @@ impl Config {
2628
theme: "default".to_string(),
2729
language: vec![],
2830
terminal: None,
31+
autosave: default_autosave(),
2932
}
3033
}
3134
}
3235

36+
fn default_autosave() -> bool {
37+
true
38+
}
39+
3340
#[derive(Debug, Deserialize, Clone)]
3441
pub struct Language {
3542
pub name: String,

anycode-backend/src/handlers/io_handler.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,14 @@ pub async fn handle_file_change(
293293
}
294294
}
295295

296+
if state.config.autosave {
297+
if let Err(e) = code.save_file() {
298+
error!("Autosave failed for {}: {:?}", abs_path, e);
299+
} else if let Some(lsp) = lsp_manager.get(&code.lang).await {
300+
lsp.did_save(&abs_path, Some(&code.text.to_string()));
301+
}
302+
}
303+
296304
// Broadcast as a single message for other clients if needed
297305
socket.broadcast().emit("file:change", &change).await.ok();
298306
}
@@ -411,4 +419,4 @@ pub async fn handle_create(
411419
}
412420
}
413421
}
414-
}
422+
}

anycode-backend/src/handlers/watch_handler.rs

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::path::PathBuf;
22
use std::sync::Arc;
33
use std::collections::HashMap;
44
use std::time::Duration;
5-
use tokio::sync::{Mutex, Notify};
5+
use tokio::sync::{Mutex, watch};
66
use serde_json::json;
77
use tracing::info;
88
use anyhow::Result;
@@ -13,15 +13,15 @@ use crate::app_state::SocketData;
1313
use crate::code::Code;
1414
use crate::diff::compute_text_edits;
1515

16-
#[derive(Clone, Debug, PartialEq)]
16+
#[derive(Clone, Debug, PartialEq)] // Added Copy/Clone/PartialEq if needed by usage, sticking to original Clone/PartialEq + Debug
1717
enum FileState {
1818
Exists,
1919
DoesNotExist,
2020
}
2121

2222
pub struct FileWatchState {
2323
pub state: FileState,
24-
pub notify: Arc<Notify>,
24+
pub sender: watch::Sender<()>,
2525
pub pending: bool,
2626
}
2727

@@ -109,33 +109,30 @@ pub async fn handle_watch_event(
109109
git_manager: &Arc<Mutex<crate::git::GitManager>>,
110110
) {
111111
let path_str = match path.to_str() {
112-
Some(s) => s.to_string(),
113-
None => return,
112+
Some(s) => s.to_string(), None => return,
114113
};
115114

116-
let should_spawn = {
115+
let (should_spawn, rx) = {
117116
let mut states = file_states.lock().await;
118-
let entry = states.entry(path_str.clone()).or_insert_with(|| FileWatchState {
119-
state: FileState::DoesNotExist, // never seen = didn't exist for us
120-
notify: Arc::new(Notify::new()),
121-
pending: false,
117+
let entry = states.entry(path_str.clone()).or_insert_with(|| {
118+
let (tx, _) = watch::channel(());
119+
FileWatchState { state: FileState::DoesNotExist, sender: tx, pending: false, }
122120
});
123121

124-
// Signal the existing task to reset its timer
125-
entry.notify.notify_one();
122+
let _ = entry.sender.send(());
126123

127124
if entry.pending {
128-
// A task is already waiting — it will pick up the new event
129-
false
125+
(false, None)
130126
} else {
131127
entry.pending = true;
132-
true
128+
let receiver = entry.sender.subscribe();
129+
(true, Some(receiver))
133130
}
134131
};
135132

136-
if !should_spawn {
137-
return;
138-
}
133+
if !should_spawn { return; }
134+
135+
let mut rx = rx.unwrap();
139136

140137
// Spawn a single debounce task for this file
141138
let path = path.clone();
@@ -144,33 +141,30 @@ pub async fn handle_watch_event(
144141
let socket2data = socket2data.clone();
145142
let file_states = file_states.clone();
146143
let git_manager = git_manager.clone();
147-
148-
// Get the notify handle for this file
149-
let file_notify = {
150-
let states = file_states.lock().await;
151-
states.get(&path_str).unwrap().notify.clone()
152-
};
144+
let path_str_key = path_str.clone();
153145

154146
tokio::spawn(async move {
155147
// Wait until events stop arriving (trailing-edge debounce)
156148
loop {
157-
match tokio::time::timeout(DEBOUNCE, file_notify.notified()).await {
149+
// Mark as seen so we wait for *new* changes
150+
let _ = rx.borrow_and_update();
151+
match tokio::time::timeout(DEBOUNCE, rx.changed()).await {
158152
Ok(_) => continue, // new event arrived — reset timer
159153
Err(_) => break, // timeout — silence, time to process
160154
}
161155
}
162156

163157
process_watch_event(
164-
&path, &path_str, &socket, &file2code, &socket2data, &file_states, &git_manager,
158+
&path, &path_str_key, &socket, &file2code, &socket2data, &file_states, &git_manager,
165159
).await;
166160

167161
// Mark as not pending so future events spawn a new task
168-
{
169-
let mut states = file_states.lock().await;
170-
if let Some(state) = states.get_mut(&path_str) {
171-
state.pending = false;
172-
}
162+
163+
let mut states = file_states.lock().await;
164+
if let Some(state) = states.get_mut(&path_str_key) {
165+
state.pending = false;
173166
}
167+
174168
});
175169
}
176170

anycode-base/src/editor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export class AnycodeEditor {
6868

6969
private search: Search = new Search();
7070

71-
private diffEnabled: boolean = true;
71+
private diffEnabled: boolean = false;
7272
private originalCode?: string;
7373
private diffs?: Map<number, DiffInfo>;
7474

anycode-base/src/styles.css

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,44 +83,44 @@
8383
position: relative;
8484
}
8585

86-
.gutter .ln.diff-changed::before {
86+
/* .gutter .ln.diff-changed::before {
8787
content: '';
8888
position: absolute;
8989
left: -5px;
9090
top: 0;
9191
bottom: 0;
9292
width: 3px;
9393
background-color: rgba(48, 123, 215, 0.8);
94-
}
94+
} */
9595

9696
.diff-added {
9797
background-color: rgba(76, 175, 79, 0.206);
9898
position: relative;
9999
}
100100

101-
.gutter .ln.diff-added::before {
101+
/* .gutter .ln.diff-added::before {
102102
content: '';
103103
position: absolute;
104104
left: -6px;
105105
top: 0;
106106
bottom: 0;
107107
width: 3px;
108108
background-color: rgba(76, 175, 79, 0.405);
109-
}
109+
} */
110110

111-
.gutter .ln.diff-deleted {
111+
/* .gutter .ln.diff-deleted {
112112
position: relative;
113-
}
113+
} */
114114

115-
.gutter .ln.diff-deleted::before {
115+
/* .gutter .ln.diff-deleted::before {
116116
content: '';
117117
position: absolute;
118118
left: -6px;
119119
bottom: 0;
120120
width: 2px;
121121
height: 5px;
122122
background-color: rgba(244, 67, 54, 0.5);
123-
}
123+
} */
124124

125125

126126
.completion-box {
@@ -164,10 +164,10 @@
164164
}
165165

166166
.line-deleted-ghost {
167-
color: #f44336;
167+
color: #f6564b;
168168
opacity: 0.6;
169169
background-color: rgba(244, 67, 54, 0.15);
170-
pointer-events: none;
170+
/* pointer-events: none; */
171171
user-select: none;
172172
}
173173

anycode/App.css

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@
6060
overflow: hidden;
6161
/* text-overflow: ellipsis; */
6262
/* height: 100%; */
63-
padding-left: 4px;
64-
padding-right: 4px;
63+
padding-left: 8px;
64+
/* padding-right: 4px; */
6565

6666
}
6767

@@ -121,11 +121,11 @@
121121
align-items: center;
122122
position: absolute;
123123
bottom: var(--toolbar-bottom-margin);
124-
left: 16px;
125-
right: 16px;
124+
left: 8px;
125+
right: 8px;
126126
user-select: none;
127127
z-index: 1000;
128-
border-radius: 16px;
128+
border-radius: 12px;
129129
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
130130
height: var(--toolbar-height);
131131
border: 1px solid rgb(70, 70, 70);

anycode/App.tsx

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ const App: React.FC = () => {
4040
const [files, setFiles] = useState<FileState[]>([]);
4141
const filesRef = useRef<FileState[]>([]);
4242
const savedFileContentsRef = useRef<Map<string, string>>(new Map());
43-
const dirtyFlagsRef = useRef<Map<string, boolean>>(new Map());
44-
const [dirtyFlags, setDirtyFlags] = useState<Map<string, boolean>>(new Map());
4543
const [activeFileId, setActiveFileId] = useState<string | null>(null);
4644
const [editorStates, setEditorStates] = useState<Map<string, AnycodeEditor>>(new Map());
4745
const editorRefs = useRef<Map<string, AnycodeEditor>>(new Map());
@@ -176,8 +174,7 @@ const App: React.FC = () => {
176174
if (pendingDiff !== undefined) {
177175
editor.setOriginalCode(pendingDiff);
178176
editor.setDiffEnabled(true);
179-
// Optional: clear it if we don't want to persist across re-opens without re-fetching
180-
// pendingOriginalContentRef.current.delete(file.id);
177+
pendingOriginalContentRef.current.delete(file.id);
181178
}
182179
} else {
183180
// if editor already exists, just use it
@@ -301,22 +298,7 @@ const App: React.FC = () => {
301298
let oldcontent = savedFileContentsRef.current.get(file.id);
302299
if (!oldcontent) return;
303300

304-
let newContentLength = editor.getTextLength();
305301

306-
let isDirty = false;
307-
if (newContentLength !== oldcontent.length) {
308-
isDirty = true;
309-
} else {
310-
let newContent = editor.getText();
311-
isDirty = newContent !== oldcontent;
312-
}
313-
314-
const currentDirtyFlag = dirtyFlagsRef.current.get(file.id);
315-
if (currentDirtyFlag !== isDirty) {
316-
console.log('setDirtyFlags', file.id, isDirty);
317-
dirtyFlagsRef.current.set(file.id, isDirty);
318-
setDirtyFlags(prev => new Map(prev).set(file.id, isDirty));
319-
}
320302
};
321303

322304
const handleCursorChange = (filename: string, newCursor: Position, oldCursor: Position) => {
@@ -361,12 +343,6 @@ const App: React.FC = () => {
361343
editorRefs.current.delete(fileId);
362344

363345
savedFileContentsRef.current.delete(fileId);
364-
dirtyFlagsRef.current.delete(fileId);
365-
setDirtyFlags(prev => {
366-
const newFlags = new Map(prev);
367-
newFlags.delete(fileId);
368-
return newFlags;
369-
});
370346

371347
// Unselect the closed file in the tree
372348
if (fileToClose) {
@@ -412,8 +388,6 @@ const App: React.FC = () => {
412388
if (response.success) {
413389
console.log('File saved successfully:', fileId);
414390
savedFileContentsRef.current.set(fileId, content);
415-
dirtyFlagsRef.current.set(fileId, false);
416-
setDirtyFlags(prev => new Map(prev).set(fileId, false));
417391
} else {
418392
console.error('Failed to save file:', response.error);
419393
}
@@ -2044,7 +2018,6 @@ const App: React.FC = () => {
20442018
className={`tab ${activeFileId === file.id ? 'active' : ''}`}
20452019
onClick={() => openTab(file)}
20462020
>
2047-
<span className={`tab-dirty-indicator ${dirtyFlags.get(file.id) ? 'dirty' : ''}`}></span>
20482021
<span className="tab-filename"> {file.name} </span>
20492022
<button className="tab-close-button" onClick={(e) => { e.stopPropagation(); closeTab(file); }}> × </button>
20502023
</div>

0 commit comments

Comments
 (0)