Skip to content

Commit f6e0745

Browse files
committed
fix: resolve 9 audit issues (#256#264) and bump to 0.5.14
- #256: formatCJKFile uses view.dispatch() for undo support - #257: read doc.length inside queued IME callback (stale closure fix) - #258: listContinuation bails out on multi-cursor - #259: listSmartIndent/Outdent bail out on multi-cursor - #260: hot-exit merge only resurrects timed-out windows, not closed ones - #261: delete_session also removes session.prev.json backup - #262: add Print menu item to Windows/Linux File menu - #263: OutlineView defers content processing with useDeferredValue - #264: StatusBarCounts defers stripMarkdown with useDeferredValue Closes #256, #257, #258, #259, #260, #261, #262, #263, #264
1 parent 68a3729 commit f6e0745

20 files changed

Lines changed: 404 additions & 28 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "vmark",
33
"private": true,
4-
"version": "0.5.13",
4+
"version": "0.5.14",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "vmark"
3-
version = "0.5.13"
3+
version = "0.5.14"
44
description = "AI friendly markdown editor"
55
authors = ["Xiaolai"]
66
edition = "2021"

src-tauri/src/hot_exit/commands.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use super::coordinator::{
1313
get_window_restore_state,
1414
mark_window_restore_complete,
1515
clear_pending_restore,
16+
CaptureResult,
1617
RestoreMultiWindowResult,
1718
};
1819

@@ -23,9 +24,11 @@ use super::coordinator::{
2324
/// instead of being silently dropped.
2425
#[tauri::command]
2526
pub async fn hot_exit_capture(app: AppHandle) -> Result<SessionData, String> {
26-
let mut session = capture_session(&app).await?;
27+
let CaptureResult { mut session, expected_labels } = capture_session(&app).await?;
2728

28-
// Merge partial captures: keep previous window states for missing windows
29+
// Merge partial captures: only resurrect windows that were expected (alive
30+
// at capture time) but failed to respond (timed out). Windows that were
31+
// intentionally closed are NOT in expected_labels and won't be merged.
2932
let captured_labels: std::collections::HashSet<String> = session
3033
.windows
3134
.iter()
@@ -35,9 +38,11 @@ pub async fn hot_exit_capture(app: AppHandle) -> Result<SessionData, String> {
3538
if let Ok(Some(prev_session)) = read_session(&app).await {
3639
let mut merged = false;
3740
for prev_window in prev_session.windows {
38-
if !captured_labels.contains(&prev_window.window_label) {
41+
if expected_labels.contains(&prev_window.window_label)
42+
&& !captured_labels.contains(&prev_window.window_label)
43+
{
3944
eprintln!(
40-
"[HotExit] Merging previous state for missing window '{}'",
45+
"[HotExit] Merging previous state for timed-out window '{}'",
4146
prev_window.window_label
4247
);
4348
session.windows.push(prev_window);

src-tauri/src/hot_exit/coordinator.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,15 @@ fn normalize_window_label(state: &mut WindowState, expected_label: &str) {
103103
}
104104
}
105105

106+
/// Result of capture_session, including the set of expected window labels
107+
/// so callers can distinguish "closed window" from "timed-out window".
108+
pub struct CaptureResult {
109+
pub session: SessionData,
110+
pub expected_labels: HashSet<String>,
111+
}
112+
106113
/// Capture session from all windows
107-
pub async fn capture_session(app: &AppHandle) -> Result<SessionData, String> {
114+
pub async fn capture_session(app: &AppHandle) -> Result<CaptureResult, String> {
108115
// Get all document windows (main + doc-*)
109116
let windows: Vec<String> = app
110117
.webview_windows()
@@ -251,6 +258,8 @@ pub async fn capture_session(app: &AppHandle) -> Result<SessionData, String> {
251258
}
252259
});
253260

261+
let expected_labels = final_state.expected_windows.clone();
262+
254263
let session = SessionData {
255264
version: SCHEMA_VERSION,
256265
timestamp: chrono::Utc::now().timestamp(),
@@ -259,7 +268,7 @@ pub async fn capture_session(app: &AppHandle) -> Result<SessionData, String> {
259268
workspace: None, // Workspace capture not yet implemented
260269
};
261270

262-
Ok(session)
271+
Ok(CaptureResult { session, expected_labels })
263272
}
264273

265274
async fn wait_for_all_responses(state: Arc<Mutex<CaptureState>>, expected: usize) {

src-tauri/src/hot_exit/storage.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ pub async fn read_session(
159159
}
160160
}
161161

162-
/// Delete session file after successful restore
162+
/// Delete session file (and backup) after successful restore
163163
pub async fn delete_session(app: &tauri::AppHandle) -> Result<(), String> {
164164
let session_path = get_session_path(app)?;
165165

@@ -169,6 +169,12 @@ pub async fn delete_session(app: &tauri::AppHandle) -> Result<(), String> {
169169
.map_err(|e| format!("Failed to delete session: {}", e))?;
170170
}
171171

172+
// Also clean up backup file
173+
let backup_path = get_backup_session_path(app)?;
174+
if backup_path.exists() {
175+
let _ = tokio::fs::remove_file(&backup_path).await;
176+
}
177+
172178
Ok(())
173179
}
174180

src-tauri/src/menu/custom_menu.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ pub(crate) fn create_menu_with_shortcuts(
150150
&MenuItem::with_id(app, "move-to", "Move to...", true, get_accel("move-to", ""))?,
151151
&PredefinedMenuItem::separator(app)?,
152152
&export_submenu,
153+
&MenuItem::with_id(app, "export-pdf", "Print...", true, get_accel("export-pdf", "CmdOrCtrl+P"))?,
153154
&PredefinedMenuItem::separator(app)?,
154155
&history_submenu,
155156
&PredefinedMenuItem::separator(app)?,

src-tauri/src/menu/default_menu.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ pub fn create_menu(app: &tauri::AppHandle) -> tauri::Result<Menu<tauri::Wry>> {
146146
&MenuItem::with_id(app, "move-to", "Move to...", true, None::<&str>)?,
147147
&PredefinedMenuItem::separator(app)?,
148148
&export_submenu,
149+
&MenuItem::with_id(app, "export-pdf", "Print...", true, Some("CmdOrCtrl+P"))?,
149150
&PredefinedMenuItem::separator(app)?,
150151
&history_submenu,
151152
&PredefinedMenuItem::separator(app)?,

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "VMark",
4-
"version": "0.5.13",
4+
"version": "0.5.14",
55
"identifier": "app.vmark",
66
"build": {
77
"beforeDevCommand": "pnpm dev",

src/components/Sidebar/OutlineView.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Displays document heading structure as a tree.
55
*/
66

7-
import { useState, useMemo, useRef } from "react";
7+
import { useState, useDeferredValue, useMemo, useRef } from "react";
88
import { ChevronRight, ChevronDown } from "lucide-react";
99
import { emit } from "@tauri-apps/api/event";
1010
import { useUIStore } from "@/stores/uiStore";
@@ -93,16 +93,17 @@ const MAX_OUTLINE_ITEMS = 100; // Limit total visible items
9393

9494
export function OutlineView() {
9595
const content = useDocumentContent();
96+
const deferredContent = useDeferredValue(content);
9697
const activeHeadingIndex = useUIStore((state) => state.activeHeadingLine);
9798

9899
// Check if document is too large (used after hooks)
99-
const isTooLarge = content.length > MAX_CONTENT_FOR_OUTLINE;
100+
const isTooLarge = deferredContent.length > MAX_CONTENT_FOR_OUTLINE;
100101

101102
// Create a stable key based only on heading lines.
102103
// This prevents re-extraction when typing in non-heading content.
103104
const headingLinesKey = useMemo(
104-
() => (isTooLarge ? "" : getHeadingLinesKey(content)),
105-
[content, isTooLarge]
105+
() => (isTooLarge ? "" : getHeadingLinesKey(deferredContent)),
106+
[deferredContent, isTooLarge]
106107
);
107108

108109
// Cache previous headings to maintain referential stability
@@ -116,12 +117,12 @@ export function OutlineView() {
116117
return prevHeadingsRef.current;
117118
}
118119
perfStart("OutlineView:extractHeadings");
119-
const newHeadings = extractHeadings(content);
120+
const newHeadings = extractHeadings(deferredContent);
120121
perfEnd("OutlineView:extractHeadings", { count: newHeadings.length });
121122
prevHeadingsRef.current = newHeadings;
122123
prevKeyRef.current = headingLinesKey;
123124
return newHeadings;
124-
}, [headingLinesKey, content, isTooLarge]);
125+
}, [headingLinesKey, deferredContent, isTooLarge]);
125126

126127
const tree = useMemo(() => {
127128
if (isTooLarge) return [];

0 commit comments

Comments
 (0)