Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 72 additions & 9 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ pub struct Settings {
pub custom_colors_light: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "customColorsDark")]
pub custom_colors_dark: Option<std::collections::HashMap<String, String>>,
#[serde(rename = "noteOrder")]
pub note_order: Option<Vec<String>>,
#[serde(rename = "folderOrder")]
pub folder_order: Option<Vec<String>>,
}

// Search result
Expand Down Expand Up @@ -949,25 +953,41 @@ async fn list_notes(state: State<'_, AppState>) -> Result<Vec<NoteMetadata>, Str
})
.collect();

// Load pinned note IDs from settings
let pinned_ids: HashSet<String> = {
// Load pinned note IDs and note_order from settings
let (pinned_ids, note_order): (HashSet<String>, Option<Vec<String>>) = {
let settings = state.settings.read().expect("settings read lock");
settings
let pinned = settings
.pinned_note_ids
.as_ref()
.map(|ids| ids.iter().cloned().collect())
.unwrap_or_default()
.unwrap_or_default();
let order = settings.note_order.clone();
(pinned, order)
};

// Sort: pinned notes first (by date), then unpinned notes (by date)
// Sort: pinned notes first, then unpinned notes (sorted by note_order, fallback to modified date)
notes.sort_by(|a, b| {
let a_pinned = pinned_ids.contains(&a.id);
let b_pinned = pinned_ids.contains(&b.id);

match (a_pinned, b_pinned) {
(true, false) => std::cmp::Ordering::Less, // a pinned, b not -> a first
(false, true) => std::cmp::Ordering::Greater, // b pinned, a not -> b first
_ => b.modified.cmp(&a.modified), // both same status -> sort by date (newest first)
_ => {
// both same status (both pinned or both unpinned)
if let Some(ref order) = note_order {
let a_idx = order.iter().position(|id| id == &a.id);
let b_idx = order.iter().position(|id| id == &b.id);
match (a_idx, b_idx) {
(Some(i), Some(j)) => i.cmp(&j),
(Some(_), None) => std::cmp::Ordering::Less, // ordered items first
(None, Some(_)) => std::cmp::Ordering::Greater, // ordered items first
(None, None) => b.modified.cmp(&a.modified), // both unordered -> newest first
}
} else {
b.modified.cmp(&a.modified) // sort by date (newest first)
}
}
Comment on lines +968 to +990

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid repeated linear scans in the sort comparator.

order.iter().position(...) runs on every comparison, which turns sorting into a much heavier operation for large note sets. Build an index map once and use O(1) lookups in the comparator.

Proposed optimization
-    notes.sort_by(|a, b| {
+    let order_index: Option<HashMap<&str, usize>> = note_order.as_ref().map(|order| {
+        order.iter().enumerate().map(|(i, id)| (id.as_str(), i)).collect()
+    });
+
+    notes.sort_by(|a, b| {
         let a_pinned = pinned_ids.contains(&a.id);
         let b_pinned = pinned_ids.contains(&b.id);

         match (a_pinned, b_pinned) {
             (true, false) => std::cmp::Ordering::Less,
             (false, true) => std::cmp::Ordering::Greater,
             _ => {
-                if let Some(ref order) = note_order {
-                    let a_idx = order.iter().position(|id| id == &a.id);
-                    let b_idx = order.iter().position(|id| id == &b.id);
+                if let Some(ref idx) = order_index {
+                    let a_idx = idx.get(a.id.as_str()).copied();
+                    let b_idx = idx.get(b.id.as_str()).copied();
                     match (a_idx, b_idx) {
                         (Some(i), Some(j)) => i.cmp(&j),
                         (Some(_), None) => std::cmp::Ordering::Less,
                         (None, Some(_)) => std::cmp::Ordering::Greater,
                         (None, None) => b.modified.cmp(&a.modified),
                     }
                 } else {
                     b.modified.cmp(&a.modified)
                 }
             }
         }
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/src/lib.rs` around lines 968 - 990, The comparator for
notes.sort_by does repeated linear scans via order.iter().position(...) which
makes sorting O(n^2); instead build a one-time index (e.g., HashMap or
Vec<Option<usize>>) from note_order before calling notes.sort_by and replace the
position(...) calls with O(1) lookups into that index (use the index to obtain
Option<usize> for a.id and b.id), keeping the existing pinned_ids logic and
fallback to b.modified.cmp(&a.modified) when both are unordered.

}
});

Expand Down Expand Up @@ -1485,7 +1505,7 @@ async fn rename_folder(
.await
.map_err(|e| e.to_string())?;

// Update pinned note IDs in settings
// Update pinned note IDs, note_order, and folder_order in settings
{
let mut settings = state.settings.write().expect("settings write lock");
if let Some(ref mut pinned) = settings.pinned_note_ids {
Expand All @@ -1497,6 +1517,24 @@ async fn rename_folder(
}
}
}
if let Some(ref mut order) = settings.note_order {
for id in order.iter_mut() {
if id.starts_with(&old_prefix) {
*id = format!("{}{}", new_prefix, &id[old_prefix.len()..]);
} else if *id == old_path {
*id = new_path.clone();
}
}
}
if let Some(ref mut order) = settings.folder_order {
for path in order.iter_mut() {
if path.starts_with(&old_prefix) {
*path = format!("{}{}", new_prefix, &path[old_prefix.len()..]);
} else if *path == old_path {
*path = new_path.clone();
}
}
}
// Save settings
let _ = save_settings(&folder, &settings);
}
Expand Down Expand Up @@ -1586,7 +1624,7 @@ async fn move_note(
.await
.map_err(|e| e.to_string())?;

// Update pinned note IDs
// Update pinned note IDs and note_order
{
let mut settings = state.settings.write().expect("settings write lock");
if let Some(ref mut pinned) = settings.pinned_note_ids {
Expand All @@ -1596,6 +1634,13 @@ async fn move_note(
}
}
}
if let Some(ref mut order) = settings.note_order {
for ord_id in order.iter_mut() {
if *ord_id == id {
*ord_id = new_id.clone();
}
}
}
let _ = save_settings(&folder, &settings);
}

Expand Down Expand Up @@ -1691,7 +1736,7 @@ async fn move_folder(
.await
.map_err(|e| e.to_string())?;

// Update pinned note IDs
// Update pinned note IDs, note_order, and folder_order
{
let mut settings = state.settings.write().expect("settings write lock");
if let Some(ref mut pinned) = settings.pinned_note_ids {
Expand All @@ -1701,6 +1746,24 @@ async fn move_folder(
}
}
}
if let Some(ref mut order) = settings.note_order {
for id in order.iter_mut() {
if id.starts_with(&old_prefix) {
*id = format!("{}{}", new_prefix, &id[old_prefix.len()..]);
} else if *id == path {
*id = new_path.clone();
}
}
}
if let Some(ref mut order) = settings.folder_order {
for p in order.iter_mut() {
if p.starts_with(&old_prefix) {
*p = format!("{}{}", new_prefix, &p[old_prefix.len()..]);
} else if *p == path {
*p = new_path.clone();
}
}
}
let _ = save_settings(&folder, &settings);
}

Expand Down
Loading