Skip to content

fix: force XWayland backend on Linux to fix unresponsive window controls#182

Open
GihanRathnayake wants to merge 5 commits into
erictli:mainfrom
GihanRathnayake:main
Open

fix: force XWayland backend on Linux to fix unresponsive window controls#182
GihanRathnayake wants to merge 5 commits into
erictli:mainfrom
GihanRathnayake:main

Conversation

@GihanRathnayake

@GihanRathnayake GihanRathnayake commented Jun 26, 2026

Copy link
Copy Markdown

Problem

On Linux under native Wayland (reproduced on Fedora 44 / KDE Plasma / KWin), the window's title bar controls (close, minimize, maximize) are unresponsive on first click. The window's input region isn't committed until a resize, so the controls only start working after the window is maximized/restored.

Fix

Default GDK_BACKEND to x11 (XWayland) on Linux before GTK/WebKitGTK initializes. This sidesteps the native-Wayland input-region issue while leaving an explicit user-set GDK_BACKEND untouched.

#[cfg(target_os = "linux")]
if std::env::var_os("GDK_BACKEND").is_none() {
    std::env::set_var("GDK_BACKEND", "x11");
}
  • Linux-only (#[cfg(target_os = "linux")]); macOS/Windows unaffected.
  • Respects an explicit GDK_BACKEND override.
  • Set in main() before run(), so it applies before any GTK init — works in both tauri dev and shipped tauri build binaries.

Testing

  • cargo build passes.
  • Confirmed manually that GDK_BACKEND=x11 npm run tauri dev makes the close button respond on first click; this change applies that automatically.

Note: PR built and fixed with Claude Code.

Summary by CodeRabbit

  • New Features
    • Create and edit plain-text and code files alongside Markdown notes.
    • View supported code files with syntax highlighting and switch to source editing.
    • Create new files directly from the sidebar.
    • Added Inter as an editor font option.
  • Improvements
    • Search results, note previews, and copied file paths now support multiple file types.
    • Refined typography, heading spacing, and list layout.
    • Improved Linux compatibility for Wayland environments.

GihanRathnayake and others added 2 commits June 26, 2026 15:09
Under native Wayland (e.g. KWin) the window's input region is not
committed until a resize, leaving the title bar controls unresponsive
until the window is maximized. Default GDK_BACKEND to x11 on Linux,
unless the user has set it explicitly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix: force XWayland backend on Linux to fix unresponsive window controls
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds multi-format note support for recognized text and code files, including extension-aware IDs, indexing, file creation, search metadata, and editor rendering. It also adds Inter font support, typography adjustments, and Linux X11 backend initialization.

Changes

Multi-format note workflows

Layer / File(s) Summary
Extension contracts and service commands
src/types/note.ts, src/services/notes.ts, src-tauri/src/lib.rs
Note and search entities now include extensions, with service commands for creating files and duplicating notes.
Extension-aware identifiers and indexing
src-tauri/src/lib.rs
Recognized extensions are preserved in IDs and propagated through Tantivy schemas, indexing, previews, and search results.
Note lifecycle and file operations
src-tauri/src/lib.rs
Listing, reading, saving, creating, importing, duplicating, and watching notes now handle markdown and non-markdown extensions.
File creation and editor UI
src/context/NotesContext.tsx, src/components/layout/Sidebar.tsx, src/components/notes/*, src/components/editor/*
The UI supports new files, extension-aware filepath copying, syntax-highlighted code views, and textarea editing for plain-text notes.

Inter typography

Layer / File(s) Summary
Inter font and typography styling
package.json, src/main.tsx, src/context/ThemeContext.tsx, src/components/settings/EditorSettingsSection.tsx, src/App.css
Inter is loaded and selectable, with font-specific spacing variables and updated prose/list styling.

Linux backend initialization

Layer / File(s) Summary
GDK backend selection
src-tauri/src/main.rs
Linux startup defaults GDK_BACKEND to x11 when unset.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant Sidebar
  participant NotesContext
  participant Tauri
  participant FileSystem
  participant SearchIndex
  participant Editor

  User->>Sidebar: Select New File
  Sidebar->>NotesContext: createFile(filename)
  NotesContext->>Tauri: invoke create_file
  Tauri->>FileSystem: create file with extension
  Tauri->>SearchIndex: index note and extension
  Tauri-->>NotesContext: return Note with extension
  NotesContext-->>Editor: select created note
  Editor->>Editor: render highlighted code or textarea
Loading

Possibly related PRs

Suggested reviewers: erictli

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the Linux GDK_BACKEND/XWayland fix, which is one of the PR’s main changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Warning

⚠️ This pull request shows signs of AI-generated slop (redundant_comments). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

GihanRathnayake and others added 3 commits June 26, 2026 17:32
Bundle the Inter variable font (self-hosted via @fontsource-variable/inter,
works offline) and expose it as a fourth editor font alongside Sans, Serif,
and Mono.

- Add "inter" to the FontFamily type, font map, and settings dropdown
- Export fontFamilyMap and reuse it for the settings preview (removes a
  duplicated, now-incomplete font-stack ternary)
- Inter-scoped typographic tuning: negative letter-spacing on body and
  headings, plus font-optical-sizing; other families keep normal tracking
- List spacing polish: gap between sibling items, tighter marker-to-text
  gap, slightly deeper indent

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends note discovery, search indexing, and the editor to recognize a
built-in allowlist of code/text extensions (.go, .py, .js, etc.) in
addition to .md, so users can keep code snippets alongside their
markdown notes in the same folder. Non-markdown files get a
syntax-highlighted read view with click-to-edit, reusing the existing
raw-textarea "source mode" plumbing, and a new "New File..." action to
create them from the sidebar.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/editor/Editor.tsx (1)

1798-1807: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Prevent copy menu state leak for plain-text notes.

Pressing Cmd+Shift+C on a plain-text note sets copyMenuOpen to true. Since the dropdown is hidden for plain-text notes, the menu won't appear immediately, but the state lingers. As a result, the export dropdown will spontaneously pop open the next time the user clicks on a markdown note.

🛠️ Proposed fix to ignore the shortcut for plain text
   // Keyboard shortcut for Cmd+Shift+C to open copy menu
   useEffect(() => {
     const handleKeyDown = (e: KeyboardEvent) => {
       if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === "c") {
+        if (isPlainTextNote) return;
         e.preventDefault();
         setCopyMenuOpen(true);
       }
     };
     document.addEventListener("keydown", handleKeyDown);
     return () => document.removeEventListener("keydown", handleKeyDown);
-  }, []);
+  }, [isPlainTextNote]);
🤖 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/components/editor/Editor.tsx` around lines 1798 - 1807, Update the
keydown handler in the editor’s copy-menu useEffect to ignore Cmd/Ctrl+Shift+C
when the current note is plain text, so it never calls setCopyMenuOpen(true) in
that mode. Preserve the existing shortcut behavior for markdown notes and the
listener cleanup.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@src-tauri/src/lib.rs`:
- Around line 1466-1476: Update the ID-building logic in both create_file and
duplicate_note so extension omission occurs only for the literal "md", matching
id_from_abs_path; preserve "markdown" in generated IDs and filenames. Extract
the duplicated closure logic into a shared build_note_id helper if practical,
and use it at both call sites so .markdown files retain their requested
extension.
- Around line 738-750: Update the extension check in the ID construction flow
around is_supported_note_extension so `.md` matching is case-insensitive. Ensure
any casing of the Markdown extension uses the strip_suffix behavior and produces
an extension-less legacy ID, while preserving extension retention for other
supported note types.
- Around line 1554-1607: Update duplicate_note after extract_title_for computes
title so markdown duplicates receive a distinct title by appending “ (Copy)”.
Apply this only when is_markdown_ext(&ext) is true, while preserving the
existing extracted title for non-markdown files.

In `@src/components/editor/Editor.tsx`:
- Around line 1424-1427: Prevent debounced source-mode edits from being
discarded by tracking the latest sourceContent in a ref and adding a flusher
that persists pending changes immediately. Invoke this flusher before cancelling
sourceTimeoutRef in both plain-text and markdown note-switch paths, and during
component unmount, while preserving the existing timeout cleanup.

---

Outside diff comments:
In `@src/components/editor/Editor.tsx`:
- Around line 1798-1807: Update the keydown handler in the editor’s copy-menu
useEffect to ignore Cmd/Ctrl+Shift+C when the current note is plain text, so it
never calls setCopyMenuOpen(true) in that mode. Preserve the existing shortcut
behavior for markdown notes and the listener cleanup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 60dfcc7d-cf0d-4ea4-b59f-e111f6f385ad

📥 Commits

Reviewing files that changed from the base of the PR and between a638f02 and c57f734.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (17)
  • package.json
  • src-tauri/src/lib.rs
  • src-tauri/src/main.rs
  • src/App.css
  • src/components/editor/Editor.tsx
  • src/components/editor/codeHighlight.ts
  • src/components/editor/lowlight.ts
  • src/components/layout/Sidebar.tsx
  • src/components/notes/FolderNameDialog.tsx
  • src/components/notes/FolderTreeView.tsx
  • src/components/notes/NoteList.tsx
  • src/components/settings/EditorSettingsSection.tsx
  • src/context/NotesContext.tsx
  • src/context/ThemeContext.tsx
  • src/main.tsx
  • src/services/notes.ts
  • src/types/note.ts

Comment thread src-tauri/src/lib.rs
Comment on lines +738 to +750
let ext = file_path.extension()?.to_str()?;
let rel_str = rel.to_str()?;
let id = rel_str.strip_suffix(".md")?.replace(std::path::MAIN_SEPARATOR, "/");

let id = if ext == "md" {
// Strip .md by converting to string and trimming (avoids with_extension
// which breaks on stems containing dots like "meeting.2024-01-15.md").
rel_str.strip_suffix(".md")?.replace(std::path::MAIN_SEPARATOR, "/")
} else if is_supported_note_extension(ext) {
// Every other recognized extension (.markdown, .go, .py, ...) keeps its extension in the ID.
rel_str.replace(std::path::MAIN_SEPARATOR, "/")
} else {
return None;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Case-sensitive .md check breaks the "extension-less legacy ID" invariant for non-lowercase .md files.

ext == "md" is a case-sensitive literal comparison, but is_supported_note_extension (the fallback branch) matches case-insensitively. A file named e.g. NOTES.MD fails the first check and falls into the "keep extension in ID" branch, producing ID "NOTES.MD" instead of the intended legacy extension-less "NOTES".

Concretely, extension_from_id still lowercases this to "md", so NoteMetadata.extension reports "md" while note.id still embeds .MD. Downstream, FolderTreeView.handleCopyFilepath (note.extension.toLowerCase() === "md" ? folder+"/"+note.id+".md" : ...) then appends .md a second time, producing a broken path like .../NOTES.MD.md. This persists until the note is edited and saved (which triggers the markdown rename path).

🐛 Proposed fix
-    let id = if ext == "md" {
+    let id = if ext.eq_ignore_ascii_case("md") {
         // Strip .md by converting to string and trimming (avoids with_extension
         // which breaks on stems containing dots like "meeting.2024-01-15.md").
-        rel_str.strip_suffix(".md")?.replace(std::path::MAIN_SEPARATOR, "/")
+        rel_str.strip_suffix(&format!(".{ext}"))?.replace(std::path::MAIN_SEPARATOR, "/")
     } else if is_supported_note_extension(ext) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let ext = file_path.extension()?.to_str()?;
let rel_str = rel.to_str()?;
let id = rel_str.strip_suffix(".md")?.replace(std::path::MAIN_SEPARATOR, "/");
let id = if ext == "md" {
// Strip .md by converting to string and trimming (avoids with_extension
// which breaks on stems containing dots like "meeting.2024-01-15.md").
rel_str.strip_suffix(".md")?.replace(std::path::MAIN_SEPARATOR, "/")
} else if is_supported_note_extension(ext) {
// Every other recognized extension (.markdown, .go, .py, ...) keeps its extension in the ID.
rel_str.replace(std::path::MAIN_SEPARATOR, "/")
} else {
return None;
};
let ext = file_path.extension()?.to_str()?;
let rel_str = rel.to_str()?;
let id = if ext.eq_ignore_ascii_case("md") {
// Strip .md by converting to string and trimming (avoids with_extension
// which breaks on stems containing dots like "meeting.2024-01-15.md").
rel_str.strip_suffix(&format!(".{ext}"))?.replace(std::path::MAIN_SEPARATOR, "/")
} else if is_supported_note_extension(ext) {
// Every other recognized extension (.markdown, .go, .py, ...) keeps its extension in the ID.
rel_str.replace(std::path::MAIN_SEPARATOR, "/")
} else {
return None;
};
🤖 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 738 - 750, Update the extension check in
the ID construction flow around is_supported_note_extension so `.md` matching is
case-insensitive. Ensure any casing of the Markdown extension uses the
strip_suffix behavior and produces an extension-less legacy ID, while preserving
extension retention for other supported note types.

Comment thread src-tauri/src/lib.rs
Comment on lines +1466 to +1476
let build_id = |s: &str| -> String {
let leaf = if is_markdown_ext(&ext) {
s.to_string()
} else {
format!("{}.{}", s, ext)
};
match target_folder.as_deref() {
Some(prefix) if !prefix.is_empty() => format!("{}/{}", prefix.trim_end_matches('/'), leaf),
_ => leaf,
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

is_markdown_ext misused to decide ID-extension omission — .markdown files silently become .md.

Both create_file and duplicate_note use is_markdown_ext(&ext) (true for both "md" and "markdown") to decide whether to omit the extension when building the note ID/leaf. But the ID scheme (see id_from_abs_path) only omits the extension for literal "md".markdown files always keep their extension embedded.

Concretely, creating a file named notes.markdown via create_file: build_id omits the extension (since is_markdown_ext("markdown") is true) → the resulting ID has no extension → abs_path_from_id appends .md by default → the file is actually written as notes.md, silently changing the extension the user explicitly requested. The returned Note.extension ("markdown") then no longer matches the real file's extension. The same pattern reproduces in duplicate_note for existing .markdown notes, producing a mangled filename like readme.markdown-copy.md instead of readme-copy.markdown.

🐛 Proposed fix (both sites)
 // create_file (~1466-1476)
     let build_id = |s: &str| -> String {
-        let leaf = if is_markdown_ext(&ext) {
+        let leaf = if ext == "md" {
             s.to_string()
         } else {
             format!("{}.{}", s, ext)
         };
         match target_folder.as_deref() {
             Some(prefix) if !prefix.is_empty() => format!("{}/{}", prefix.trim_end_matches('/'), leaf),
             _ => leaf,
         }
     };
 // duplicate_note (~1563-1579)
-    let stem = if is_markdown_ext(&ext) {
+    let stem = if ext == "md" {
         leaf
     } else {
         leaf.strip_suffix(&format!(".{ext}")).unwrap_or(&leaf).to_string()
     };

     let build_id = |s: &str| -> String {
-        let leaf = if is_markdown_ext(&ext) {
+        let leaf = if ext == "md" {
             s.to_string()
         } else {
             format!("{}.{}", s, ext)
         };
         match &dir_prefix {
             Some(prefix) => format!("{}/{}", prefix, leaf),
             None => leaf,
         }
     };

Consider extracting a shared fn build_note_id(dir_or_folder: Option<&str>, stem: &str, ext: &str) -> String helper used by both commands — the duplicated logic is exactly why this bug exists in two places at once.

Also applies to: 1563-1579

🤖 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 1466 - 1476, Update the ID-building logic
in both create_file and duplicate_note so extension omission occurs only for the
literal "md", matching id_from_abs_path; preserve "markdown" in generated IDs
and filenames. Extract the duplicated closure logic into a shared build_note_id
helper if practical, and use it at both call sites so .markdown files retain
their requested extension.

Comment thread src-tauri/src/lib.rs
Comment on lines +1554 to +1607
let content = fs::read_to_string(&source_path)
.await
.map_err(|e| e.to_string())?;
let ext = extension_from_id(&id);

let (dir_prefix, leaf) = match id.rfind('/') {
Some(pos) => (Some(id[..pos].to_string()), id[pos + 1..].to_string()),
None => (None, id.clone()),
};
let stem = if is_markdown_ext(&ext) {
leaf
} else {
leaf.strip_suffix(&format!(".{ext}")).unwrap_or(&leaf).to_string()
};

let build_id = |s: &str| -> String {
let leaf = if is_markdown_ext(&ext) {
s.to_string()
} else {
format!("{}.{}", s, ext)
};
match &dir_prefix {
Some(prefix) => format!("{}/{}", prefix, leaf),
None => leaf,
}
};

let mut new_stem = format!("{}-copy", stem);
let mut new_id = build_id(&new_stem);
let mut counter = 1;
while abs_path_from_id(&folder_path, &new_id)
.map(|p| p.exists())
.unwrap_or(false)
{
new_stem = format!("{}-copy-{}", stem, counter);
new_id = build_id(&new_stem);
counter += 1;
}

let dest_path = abs_path_from_id(&folder_path, &new_id)?;
fs::write(&dest_path, &content)
.await
.map_err(|e| e.to_string())?;

let metadata = fs::metadata(&dest_path)
.await
.map_err(|e| e.to_string())?;
let modified = metadata
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
let title = extract_title_for(&content, &new_id, &ext);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant functions and surrounding code.
rg -n "duplicateNote|extract_title_for|title =|build_id|is_markdown_ext|extension_from_id|abs_path_from_id" src-tauri/src/lib.rs src-tauri -S

# Show the implementation slice around the duplication logic.
sed -n '1500,1645p' src-tauri/src/lib.rs

# Show the title extraction helper if present in this file.
sed -n '1,420p' src-tauri/src/lib.rs

Repository: erictli/scratch

Length of output: 28656


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where note titles are displayed / sourced on the frontend if needed.
rg -n "title|duplicateNote|id\.|extract_title|markdown" src-tauri/src frontend src -S

Repository: erictli/scratch

Length of output: 39012


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '497,548p' src-tauri/src/lib.rs
sed -n '108,126p' src/components/notes/NoteList.tsx
sed -n '1,40p' src/lib/utils.ts

Repository: erictli/scratch

Length of output: 3694


Duplicated markdown notes need a distinct title. duplicate_note copies the markdown content verbatim, and extract_title_for derives the new title from that same content. Since the sidebar renders title, the copy is indistinguishable from the original; append (Copy) when duplicating markdown notes.

🤖 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 1554 - 1607, Update duplicate_note after
extract_title_for computes title so markdown duplicates receive a distinct title
by appending “ (Copy)”. Apply this only when is_markdown_ext(&ext) is true,
while preserving the existing extracted title for non-markdown files.

Comment on lines +1424 to +1427
if (sourceTimeoutRef.current) {
clearTimeout(sourceTimeoutRef.current);
sourceTimeoutRef.current = null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Prevent data loss by flushing pending source mode saves.

When switching notes, clearing sourceTimeoutRef.current without saving discards any keystrokes made within the 300ms debounce window. Since plain-text notes exclusively use the textarea (sourceMode), this bug now consistently affects the primary editing path for plain-text files.

Track the latest sourceContent in a ref and create a flusher to persist pending changes before the timeout is cancelled during a note switch or component unmount.

💾 Proposed fix for source mode saves

First, track the source content and add a flush callback (e.g., around line 612 near the other refs):

   const notesCtxRef = useRef(notesCtx);
   notesCtxRef.current = notesCtx;
 
+  // Track latest source content for flushing on note switch
+  const sourceContentRef = useRef(sourceContent);
+  useEffect(() => {
+    sourceContentRef.current = sourceContent;
+  }, [sourceContent]);
+
+  const flushPendingSourceSave = useCallback(async () => {
+    if (sourceTimeoutRef.current && loadedNoteIdRef.current) {
+      clearTimeout(sourceTimeoutRef.current);
+      sourceTimeoutRef.current = null;
+      await saveImmediately(loadedNoteIdRef.current, sourceContentRef.current);
+    }
+  }, [saveImmediately]);
+
   // Keep ref in sync with current note ID
   currentNoteIdRef.current = currentNote?.id ?? null;

Then, replace the cancellation blocks in both the plain-text and markdown note-switch paths (lines ~1424 and ~1470):

-      if (sourceTimeoutRef.current) {
-        clearTimeout(sourceTimeoutRef.current);
-        sourceTimeoutRef.current = null;
-      }
+      if (sourceTimeoutRef.current) {
+        flushPendingSourceSave();
+      }

Finally, flush the save on unmount (around line 1581):

       if (needsSaveRef.current && editorRef.current) {
         needsSaveRef.current = false;
         const manager = editorRef.current.storage.markdown?.manager;
         const markdown = manager
           ? manager.serialize(editorRef.current.getJSON())
           : editorRef.current.getText();
         // Fire and forget - save will complete in background
         saveNote(markdown);
       }
+      if (sourceTimeoutRef.current && loadedNoteIdRef.current) {
+        clearTimeout(sourceTimeoutRef.current);
+        sourceTimeoutRef.current = null;
+        saveNote(sourceContentRef.current, loadedNoteIdRef.current);
+      }
       if (linkPopupRef.current) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (sourceTimeoutRef.current) {
clearTimeout(sourceTimeoutRef.current);
sourceTimeoutRef.current = null;
}
if (sourceTimeoutRef.current) {
flushPendingSourceSave();
}
🤖 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/components/editor/Editor.tsx` around lines 1424 - 1427, Prevent debounced
source-mode edits from being discarded by tracking the latest sourceContent in a
ref and adding a flusher that persists pending changes immediately. Invoke this
flusher before cancelling sourceTimeoutRef in both plain-text and markdown
note-switch paths, and during component unmount, while preserving the existing
timeout cleanup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant