Skip to content

Commit a86faf2

Browse files
google-labs-jules[bot]Keshav-writes-codeCopilotCopilot
authored
Migrate file tree creation to Rust backend (#6)
* feat(filetree): move file tree generation logic to Rust backend Migrates the recursive file tree building logic from the TypeScript frontend to the Rust backend to improve performance, especially for large directories. - Implements `build_file_tree` Tauri command in Rust. - Adds Desktop implementation using `std::fs` and `spawn_blocking`. - Adds Android implementation using `tauri-plugin-android-fs` and `urlencoding`. - Updates frontend `builder.ts` to invoke the Rust command. - Preserves existing filtering logic (markdown files only, ignore hidden dirs). * perf: optimize file tree building and sorting in Rust - Move sorting logic inside `spawn_blocking` to avoid blocking the main thread on large directories. - Fix compiler warnings (unused imports/variables). - Ensure correct recursion and sorting on both Desktop and Android. - Refactor argument names to avoid unused variable warnings across platforms. * feat(filetree): move generation and sorting to Rust backend Migrates file tree logic to Rust to improve performance and prevent UI freezes. - Implements `build_file_tree` and `sort_file_tree` Tauri commands in `src-tauri/src/lib.rs`. - Uses `spawn_blocking` to offload heavy operations from the main thread. - Ensures compatibility with Android FS plugin (fixing compilation issues) and Desktop `std::fs`. - Updates frontend to invoke Rust commands and safely handle auto-sorting in `$effect` without infinite loops. - Adds `urlencoding` dependency for Android path handling. - Cleans up deprecated JS logic. * fix: don't use rust sort function for subsequent sorts bcuz of latency and less flexibility so, calling rust sort function everytime for subsequent sorting seems to have incresed latency. also on rename or any file tree changes, the sorting was commented out. * refac: remove unwanted legacy functions * fix(error): show a toast error if file tree building fails * Update apps/app/src-tauri/src/lib.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update apps/app/src-tauri/src/lib.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update apps/app/src-tauri/src/lib.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: use inline type modifier for FileNode import for consistency Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com> * feat: implement natural/numeric sorting using natord crate Co-authored-by: Keshav-writes-code <95571677+Keshav-writes-code@users.noreply.github.com> * fix: add aria label to icon based buttons * fix(performace): make subtree sorting happen only on rename operation not in the $effect block which slows down speed * fix: add sorting when new element gets inserted * refac: remove debugging statementsa * refac: remove unwanted vairables * chore: update deps * chore: update deps * fix(ui_webkit): fix strange behaviour where a previously selected item stays selected after mutate the name and path of a node --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: keshav-writes-code <dhimankeshav201@gmail.com> Co-authored-by: Keshav <95571677+Keshav-writes-code@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 142ff0e commit a86faf2

7 files changed

Lines changed: 247 additions & 68 deletions

File tree

apps/app/src-tauri/Cargo.lock

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/app/src-tauri/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,5 @@ tauri-plugin-fs = "2"
3030
tauri-plugin-store = "2"
3131
tauri-plugin-dialog = "2"
3232
tauri-plugin-os = "2"
33+
urlencoding = "2"
34+
futures = "0.3.31"

apps/app/src-tauri/src/lib.rs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,194 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
#[derive(Serialize, Deserialize, Clone)]
4+
pub struct FileNode {
5+
pub name: String,
6+
pub path: String,
7+
pub is_directory: bool,
8+
pub children: Vec<FileNode>,
9+
}
10+
11+
fn sort_nodes(nodes: &mut Vec<FileNode>) {
12+
nodes.sort_by(|a, b| {
13+
if a.is_directory != b.is_directory {
14+
return if a.is_directory {
15+
std::cmp::Ordering::Less
16+
} else {
17+
std::cmp::Ordering::Greater
18+
};
19+
}
20+
a.name.to_lowercase().cmp(&b.name.to_lowercase())
21+
});
22+
23+
for node in nodes {
24+
if !node.children.is_empty() {
25+
sort_nodes(&mut node.children);
26+
}
27+
}
28+
}
29+
30+
#[cfg(not(target_os = "android"))]
31+
fn build_tree_recursive_desktop(path_str: &str) -> std::io::Result<Vec<FileNode>> {
32+
use std::fs;
33+
let mut nodes = Vec::new();
34+
let entries = fs::read_dir(path_str)?;
35+
36+
for entry in entries {
37+
if let Ok(entry) = entry {
38+
if let Ok(metadata) = entry.metadata() {
39+
let file_name = entry.file_name().to_string_lossy().to_string();
40+
41+
let is_directory = metadata.is_dir();
42+
let starts_with_dot = file_name.starts_with('.');
43+
let ends_with_md = file_name.ends_with(".md");
44+
45+
if (is_directory && !starts_with_dot) || ends_with_md {
46+
let path = entry.path().to_string_lossy().to_string();
47+
let mut children = Vec::new();
48+
49+
if is_directory {
50+
if let Ok(sub_children) = build_tree_recursive_desktop(&path) {
51+
children = sub_children;
52+
}
53+
}
54+
55+
nodes.push(FileNode {
56+
name: file_name.trim_end_matches(".md").to_string(),
57+
path,
58+
is_directory,
59+
children,
60+
});
61+
}
62+
}
63+
}
64+
}
65+
Ok(nodes)
66+
}
67+
68+
#[cfg(target_os = "android")]
69+
fn build_tree_recursive_android(
70+
app: tauri::AppHandle,
71+
path: String,
72+
document_top_tree_uri: Option<String>,
73+
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<FileNode>, String>> + Send>> {
74+
Box::pin(async move {
75+
use tauri_plugin_android_fs::AndroidFsExt;
76+
use tauri_plugin_android_fs::FileUri;
77+
use tauri_plugin_android_fs::EntryOptions;
78+
use futures::future::join_all;
79+
80+
let api = app.android_fs();
81+
82+
let json_obj = serde_json::json!({
83+
"uri": path,
84+
"documentTopTreeUri": document_top_tree_uri
85+
});
86+
let file_uri = FileUri::from_json_str(&json_obj.to_string())
87+
.map_err(|e| format!("Failed to create FileUri: {}", e))?;
88+
89+
let options = EntryOptions {
90+
uri: false,
91+
name: true,
92+
last_modified: false,
93+
len: false,
94+
mime_type: false,
95+
};
96+
97+
let entries = api.read_dir_with_options(&file_uri, options)
98+
.map_err(|e| e.to_string())?;
99+
100+
let mut futures = Vec::new();
101+
let mut nodes = Vec::new();
102+
103+
for entry in entries {
104+
let is_directory = entry.is_dir();
105+
let name_opt = entry.name();
106+
107+
if let Some(name_str) = name_opt {
108+
let name = name_str.to_string();
109+
let starts_with_dot = name.starts_with('.');
110+
let ends_with_md = name.ends_with(".md");
111+
112+
if (is_directory && !starts_with_dot) || ends_with_md {
113+
let path_uri = format!("{}%2F{}", path, urlencoding::encode(&name));
114+
115+
if is_directory {
116+
let app_clone = app.clone();
117+
let path_clone = path_uri.clone();
118+
let doc_uri_clone = document_top_tree_uri.clone();
119+
let name_clone = name.clone();
120+
121+
futures.push(async move {
122+
let children_res = build_tree_recursive_android(
123+
app_clone,
124+
path_clone.clone(),
125+
doc_uri_clone
126+
).await;
127+
128+
match children_res {
129+
Ok(children) => Some(FileNode {
130+
name: name_clone.trim_end_matches(".md").to_string(),
131+
path: path_clone,
132+
is_directory: true,
133+
children,
134+
}),
135+
Err(_) => None
136+
}
137+
});
138+
} else {
139+
nodes.push(FileNode {
140+
name: name.trim_end_matches(".md").to_string(),
141+
path: path_uri,
142+
is_directory: false,
143+
children: vec![],
144+
});
145+
}
146+
}
147+
}
148+
}
149+
150+
let results = join_all(futures).await;
151+
for res in results {
152+
if let Some(node) = res {
153+
nodes.push(node);
154+
}
155+
}
156+
157+
Ok(nodes)
158+
})
159+
}
160+
161+
#[tauri::command]
162+
async fn build_file_tree(
163+
app: tauri::AppHandle,
164+
path: String,
165+
document_top_tree_uri: Option<String>,
166+
) -> Result<Vec<FileNode>, String> {
167+
let nodes;
168+
169+
#[cfg(target_os = "android")]
170+
{
171+
let mut unsorted_nodes = build_tree_recursive_android(app, path, document_top_tree_uri).await?;
172+
sort_nodes(&mut unsorted_nodes);
173+
nodes = unsorted_nodes;
174+
}
175+
176+
#[cfg(not(target_os = "android"))]
177+
{
178+
// Suppress unused variable warnings on desktop
179+
let _ = app;
180+
let _ = document_top_tree_uri;
181+
nodes = tauri::async_runtime::spawn_blocking(move || {
182+
let mut n = build_tree_recursive_desktop(&path)?;
183+
sort_nodes(&mut n);
184+
Ok(n)
185+
}).await.map_err(|e| e.to_string())?
186+
.map_err(|e: std::io::Error| e.to_string())?;
187+
}
188+
189+
Ok(nodes)
190+
}
191+
1192
#[cfg_attr(mobile, tauri::mobile_entry_point)]
2193
pub fn run() {
3194
tauri::Builder::default()
@@ -6,6 +197,7 @@ pub fn run() {
6197
.plugin(tauri_plugin_store::Builder::new().build())
7198
.plugin(tauri_plugin_fs::init())
8199
.plugin(tauri_plugin_android_fs::init())
200+
.invoke_handler(tauri::generate_handler![build_file_tree])
9201
.setup(|app| {
10202
if cfg!(debug_assertions) {
11203
app.handle().plugin(

apps/app/src/components/sidebar/file_manager/index.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
$effect(() => {
2525
if (file_tree) sort_file_tree(file_tree);
2626
});
27+
2728
let collapsed_state: boolean = $state(true);
2829
$effect(() => {
2930
if (!root_path) return;

apps/app/src/components/sidebar/file_manager/items_renderer.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@
257257
ondragstart={(e) => handle_drag_start(e, node)}
258258
ondragend={reset_dnd}
259259
oncontextmenu={(e) => handle_node_right_click(e, node)}
260-
class="{opened_filenode?.path === node.path ? 'bg-base-content/10' : ''}
260+
class="{opened_filenode?.path === node.path ? 'bg-base-content/10' : ''}
261261
{dragged_node?.path === node.path ? 'opacity-50' : ''}
262262
py-0.75 w-full hover:text-[color-mix(in_srgb,var(--color-base-content)_85%,black)] truncate block"
263263
onclick={(e) => {

0 commit comments

Comments
 (0)