Skip to content

Commit d839e62

Browse files
committed
Add theming system and stabilize editor workflows
Introduce backend theme/config socket handlers and embed the bundled theme catalog so the frontend can list, load, and apply themes dynamically. Add settings and theme hooks, wire a Settings panel into the layout, and migrate the main UI/editor styling to theme-aware CSS variables across panels, toolbar, search, changes, browser, agent UI, and editor chrome. Fix References Peek preview behavior by making the selected reference item the source of truth for highlight ranges, reusing local editor/cache content when available, and updating preview selection immediately while navigating references. Improve editor initialization stability by deduplicating concurrent Tree-sitter language loads and concurrent AnycodeEditor creation for the same file. Fix git diff handling for new files whose empty original content is represented as a single blank line, with coverage for blank-line additions. Reduce noisy git/watch updates by batching debounced filesystem change notifications, expanding ignored paths, and avoiding unchanged git state updates on the frontend. Improve keyboard/focus handling in the file tree and global shortcuts so tree navigation does not leak into app-level handlers or cause scroll jumps. Optimize Changes panel and file tree rendering with memoized item components, stable callbacks, and structural equality checks to avoid unnecessary rerenders.
1 parent 9a7187b commit d839e62

67 files changed

Lines changed: 7534 additions & 451 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

anycode-backend/src/app_state.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use tokio_util::sync::CancellationToken;
1414

1515
#[derive(Clone)]
1616
pub struct AppState {
17-
pub config: Config,
17+
pub config: Arc<Mutex<Config>>,
1818
pub file2code: Arc<Mutex<HashMap<String, Code>>>,
1919
pub lsp_manager: Arc<Mutex<LspManager>>,
2020
pub acp_manager: Arc<Mutex<AcpManager>>,

anycode-backend/src/config.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@ use rust_embed::Embed;
77
#[derive(Embed, Debug)]
88
#[folder = ""]
99
#[include = "config.toml"]
10-
1110
pub struct Assets;
1211

1312
#[derive(Embed, Debug)]
1413
#[folder = "dist"]
1514
pub struct Dist;
1615

16+
#[derive(Embed, Debug)]
17+
#[folder = "../themes"]
18+
pub struct Themes;
19+
1720
#[derive(Debug, Deserialize, Clone)]
1821
pub struct Config {
1922
pub theme: String,
@@ -267,4 +270,12 @@ mod congif_tests {
267270
println!("{}", file.as_ref());
268271
}
269272
}
273+
274+
#[test]
275+
fn test_themes() {
276+
assert!(Themes::iter().count() > 0);
277+
let default_theme = Themes::get("default-theme.json").expect("default-theme.json should be embedded");
278+
let content = std::str::from_utf8(default_theme.data.as_ref()).unwrap();
279+
assert!(content.contains("themes"));
280+
}
270281
}

anycode-backend/src/handlers/io_handler.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,8 @@ pub async fn handle_file_open(
9696
};
9797

9898
let mut f2c = state.file2code.lock().await;
99-
let code = match get_or_create_code(&mut f2c, &abs_path, &state.config) {
99+
let config = state.config.lock().await;
100+
let code = match get_or_create_code(&mut f2c, &abs_path, &config) {
100101
Ok(c) => c,
101102
Err(e) => error_ack!(ack, &abs_path, "{:?}", e),
102103
};
@@ -241,10 +242,10 @@ pub async fn handle_file_close(
241242
Err(e) => error_ack!(ack, &request, "Failed to resolve file: {:?}", e),
242243
};
243244

244-
// Get code and language before removing from file2code
245245
let lang = {
246246
let mut f2c = state.file2code.lock().await;
247-
let code = match get_or_create_code(&mut f2c, &abs_path, &state.config) {
247+
let config = state.config.lock().await;
248+
let code = match get_or_create_code(&mut f2c, &abs_path, &config) {
248249
Ok(c) => c,
249250
Err(e) => error_ack!(ack, &abs_path, "{:?}", e),
250251
};
@@ -317,7 +318,8 @@ pub async fn handle_file_change(
317318
};
318319

319320
let mut f2c = state.file2code.lock().await;
320-
let code = match get_or_create_code(&mut f2c, &abs_path, &state.config) {
321+
let config = state.config.lock().await;
322+
let code = match get_or_create_code(&mut f2c, &abs_path, &config) {
321323
Ok(c) => c,
322324
Err(e) => {
323325
tracing::error!("Failed to get code: {:?}", e);
@@ -350,7 +352,8 @@ pub async fn handle_file_change(
350352
}
351353
}
352354

353-
if state.config.autosave {
355+
let autosave = state.config.lock().await.autosave;
356+
if autosave {
354357
if let Err(e) = code.save_file() {
355358
error!("Autosave failed for {}: {:?}", abs_path, e);
356359
} else if let Some(lsp) = lsp_manager.get(&code.lang).await {
@@ -381,7 +384,8 @@ pub async fn handle_file_save(
381384
};
382385

383386
let mut f2c = state.file2code.lock().await;
384-
let code = match get_or_create_code(&mut f2c, &abs_path, &state.config) {
387+
let config = state.config.lock().await;
388+
let code = match get_or_create_code(&mut f2c, &abs_path, &config) {
385389
Ok(c) => c,
386390
Err(e) => error_ack!(ack, &abs_path, "{:?}", e),
387391
};

anycode-backend/src/handlers/lsp_handler.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ pub async fn handle_completion(
2828
};
2929

3030
let mut f2c = state.file2code.lock().await;
31-
let code = match get_or_create_code(&mut f2c, &abs_path, &state.config) {
31+
let config = state.config.lock().await;
32+
let code = match get_or_create_code(&mut f2c, &abs_path, &config) {
3233
Ok(c) => c,
3334
Err(e) => error_ack!(ack, &abs_path, "{:?}", e),
3435
};
@@ -68,7 +69,8 @@ pub async fn handle_hover(
6869
};
6970

7071
let mut f2c = state.file2code.lock().await;
71-
let code = match get_or_create_code(&mut f2c, &abs_path, &state.config) {
72+
let config = state.config.lock().await;
73+
let code = match get_or_create_code(&mut f2c, &abs_path, &config) {
7274
Ok(c) => c,
7375
Err(e) => error_ack!(ack, &abs_path, "{:?}", e),
7476
};
@@ -109,7 +111,8 @@ pub async fn handle_definition(
109111
};
110112

111113
let mut f2c = state.file2code.lock().await;
112-
let code = match get_or_create_code(&mut f2c, &abs_path, &state.config) {
114+
let config = state.config.lock().await;
115+
let code = match get_or_create_code(&mut f2c, &abs_path, &config) {
113116
Ok(c) => c,
114117
Err(e) => error_ack!(ack, &abs_path, "{:?}", e),
115118
};
@@ -149,7 +152,8 @@ pub async fn handle_references(
149152
};
150153

151154
let mut f2c = state.file2code.lock().await;
152-
let code = match get_or_create_code(&mut f2c, &abs_path, &state.config) {
155+
let config = state.config.lock().await;
156+
let code = match get_or_create_code(&mut f2c, &abs_path, &config) {
153157
Ok(c) => c,
154158
Err(e) => error_ack!(ack, &abs_path, "{:?}", e),
155159
};

anycode-backend/src/handlers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ pub mod lsp_handler;
55
pub mod search_handler;
66
pub mod terminal_handler;
77
pub mod watch_handler;
8+
pub mod theme_handler;
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
use crate::app_state::AppState;
2+
use serde::{Deserialize, Serialize};
3+
use serde_json::{self, json, Value};
4+
use socketioxide::extract::{AckSender, Data, SocketRef, State};
5+
use std::fs;
6+
use std::path::PathBuf;
7+
use tracing::info;
8+
9+
#[derive(Debug, Serialize, Deserialize)]
10+
pub struct ThemeListResponseItem {
11+
pub id: String,
12+
pub name: String,
13+
#[serde(rename = "fileName")]
14+
pub file_name: String,
15+
#[serde(rename = "themeName")]
16+
pub theme_name: String,
17+
}
18+
19+
#[derive(Debug, Deserialize)]
20+
pub struct ThemeGetRequest {
21+
#[serde(rename = "fileName")]
22+
pub file_name: String,
23+
#[serde(rename = "themeName")]
24+
pub theme_name: String,
25+
}
26+
27+
#[derive(Debug, Deserialize)]
28+
struct ThemeFile {
29+
themes: Vec<ThemeDefinition>,
30+
}
31+
32+
#[derive(Debug, Deserialize)]
33+
struct ThemeDefinition {
34+
name: String,
35+
mode: String,
36+
colors: Value,
37+
highlight: Value,
38+
}
39+
40+
fn get_themes_dir() -> PathBuf {
41+
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
42+
.parent()
43+
.map(|p| p.join("themes"))
44+
.unwrap_or_else(|| PathBuf::from("themes"))
45+
}
46+
47+
pub async fn handle_theme_list(
48+
_socket: SocketRef,
49+
ack: AckSender,
50+
) {
51+
info!("theme:list requested");
52+
let mut list = Vec::new();
53+
let themes_dir = get_themes_dir();
54+
let mut seen_files = std::collections::HashSet::new();
55+
56+
if let Ok(entries) = fs::read_dir(themes_dir) {
57+
for entry in entries.filter_map(Result::ok) {
58+
let path = entry.path();
59+
if path.extension().and_then(|s| s.to_str()) == Some("json") {
60+
let file_name = match path.file_name().and_then(|s| s.to_str()) {
61+
Some(name) => name.to_string(),
62+
None => continue,
63+
};
64+
if let Ok(content) = fs::read_to_string(&path) {
65+
if let Ok(theme_file) = serde_json::from_str::<ThemeFile>(&content) {
66+
seen_files.insert(file_name.clone());
67+
for t in theme_file.themes {
68+
let id = format!("{}:{}", file_name, t.name);
69+
list.push(ThemeListResponseItem {
70+
id,
71+
name: format!("{} ({})", t.name, t.mode),
72+
file_name: file_name.clone(),
73+
theme_name: t.name,
74+
});
75+
}
76+
}
77+
}
78+
}
79+
}
80+
}
81+
82+
for embedded_file in crate::config::Themes::iter() {
83+
let file_name = embedded_file.as_ref().to_string();
84+
if !seen_files.contains(&file_name) {
85+
if let Some(file_data) = crate::config::Themes::get(&file_name) {
86+
if let Ok(content) = std::str::from_utf8(file_data.data.as_ref()) {
87+
if let Ok(theme_file) = serde_json::from_str::<ThemeFile>(content) {
88+
for t in theme_file.themes {
89+
let id = format!("{}:{}", file_name, t.name);
90+
list.push(ThemeListResponseItem {
91+
id,
92+
name: format!("{} ({})", t.name, t.mode),
93+
file_name: file_name.clone(),
94+
theme_name: t.name,
95+
});
96+
}
97+
}
98+
}
99+
}
100+
}
101+
}
102+
103+
// Sort list by name for nicer presentation
104+
list.sort_by(|a, b| a.name.cmp(&b.name));
105+
106+
let _ = ack.send(&json!(list));
107+
}
108+
109+
pub async fn handle_theme_get(
110+
_socket: SocketRef,
111+
Data(request): Data<ThemeGetRequest>,
112+
ack: AckSender,
113+
) {
114+
info!("theme:get requested: {:?}", request);
115+
let themes_dir = get_themes_dir();
116+
117+
let theme_file_path = themes_dir.join(&request.file_name);
118+
// Basic security check to avoid path traversal
119+
if theme_file_path.parent() != Some(&themes_dir) {
120+
let _ = ack.send(&json!({ "success": false, "error": "Invalid theme file path" }));
121+
return;
122+
}
123+
124+
let content = match fs::read_to_string(&theme_file_path) {
125+
Ok(c) => Some(c),
126+
Err(_) => {
127+
// Fallback to embedded theme asset
128+
crate::config::Themes::get(&request.file_name)
129+
.and_then(|file_data| std::str::from_utf8(file_data.data.as_ref()).ok().map(|s| s.to_string()))
130+
}
131+
};
132+
133+
let content = match content {
134+
Some(c) => c,
135+
None => {
136+
let _ = ack.send(&json!({ "success": false, "error": format!("Theme file not found: {}", request.file_name) }));
137+
return;
138+
}
139+
};
140+
141+
let theme_file = match serde_json::from_str::<ThemeFile>(&content) {
142+
Ok(f) => f,
143+
Err(e) => {
144+
let _ = ack.send(&json!({ "success": false, "error": format!("Failed to parse theme: {}", e) }));
145+
return;
146+
}
147+
};
148+
149+
if let Some(theme_def) = theme_file.themes.into_iter().find(|t| t.name == request.theme_name) {
150+
let _ = ack.send(&json!({
151+
"success": true,
152+
"theme": {
153+
"name": theme_def.name,
154+
"mode": theme_def.mode,
155+
"colors": theme_def.colors,
156+
"highlight": theme_def.highlight,
157+
}
158+
}));
159+
} else {
160+
let _ = ack.send(&json!({ "success": false, "error": "Theme name not found in file" }));
161+
}
162+
}
163+
164+
pub async fn handle_config_get(
165+
_socket: SocketRef,
166+
ack: AckSender,
167+
state: State<AppState>,
168+
) {
169+
info!("config:get requested");
170+
let autosave = state.config.lock().await.autosave;
171+
let _ = ack.send(&json!({ "success": true, "autosave": autosave }));
172+
}

anycode-backend/src/handlers/watch_handler.rs

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::collections::HashMap;
44
use std::path::{Path, PathBuf};
55
use std::sync::Arc;
66
use std::time::Duration;
7-
use tokio::sync::{Mutex, watch};
7+
use tokio::sync::{Mutex, watch, mpsc};
88
use tokio_util::sync::CancellationToken;
99
use tracing::info;
1010

@@ -15,7 +15,6 @@ use crate::handlers::io_handler::apply_edits_to_code;
1515
use crate::lsp::LspManager;
1616
use crate::search::search_file_result;
1717
use crate::utils::normalize_watch_path;
18-
use crate::git::GitManager;
1918

2019
const DEBOUNCE: Duration = Duration::from_millis(100);
2120

@@ -93,7 +92,7 @@ pub async fn handle_watch_event(
9392
file2code: &Arc<Mutex<HashMap<String, Code>>>,
9493
socket2data: &Arc<Mutex<HashMap<String, SocketData>>>,
9594
file_states: &Arc<Mutex<HashMap<String, FileWatchState>>>,
96-
git_manager: &Arc<Mutex<GitManager>>,
95+
git_update_tx: &mpsc::Sender<PathBuf>,
9796
lsp_manager: &Arc<Mutex<LspManager>>,
9897
) {
9998
let normalized_path = normalize_watch_path(path);
@@ -128,7 +127,7 @@ pub async fn handle_watch_event(
128127
let file2code = file2code.clone();
129128
let socket2data = socket2data.clone();
130129
let file_states = file_states.clone();
131-
let git_manager = git_manager.clone();
130+
let git_update_tx = git_update_tx.clone();
132131
let lsp_manager = lsp_manager.clone();
133132
let path_str_key = path_str.clone();
134133
let event_kind = event.kind.clone();
@@ -156,7 +155,7 @@ pub async fn handle_watch_event(
156155
.await;
157156

158157
handle_search_update(&path, &socket, &socket2data).await;
159-
handle_changes_update(&path, &socket, &git_manager).await;
158+
let _ = git_update_tx.send(path.clone()).await;
160159

161160
let mut states = file_states.lock().await;
162161
if let Some(state) = states.get_mut(&path_str_key) {
@@ -272,23 +271,7 @@ async fn handle_search_update(
272271
}
273272
}
274273

275-
async fn handle_changes_update(
276-
path: &Path,
277-
socket: &Arc<socketioxide::SocketIo>,
278-
git_manager: &Arc<Mutex<crate::git::GitManager>>
279-
) {
280-
let update = {
281-
let mut git = git_manager.lock().await;
282-
if git.should_ignore(path) {
283-
return;
284-
}
285-
git.check_status_changed_for_paths(&[path.to_path_buf()])
286-
};
287274

288-
if let Some(update) = update {
289-
let _ = socket.emit("changes:update", &update.to_json()).await;
290-
}
291-
}
292275

293276
async fn handle_file_modification(
294277
path: &PathBuf,

0 commit comments

Comments
 (0)