fix: align local markdown conflict and title behavior

This commit is contained in:
lix-2026
2026-05-24 18:16:05 +08:00
parent 289a44acea
commit 9c4bb58257
9 changed files with 664 additions and 17 deletions
@@ -92,11 +92,7 @@ async fn build_document_events_stream(
match subscription.receiver.recv().await {
Ok(payload) => {
if let Some(expected) = document_relative_path.as_deref() {
let relative_path = payload
.get("relativePath")
.and_then(Value::as_str)
.unwrap_or_default();
if expected != relative_path {
if !document_event_targets_relative_path(&payload, expected) {
continue;
}
}
@@ -239,6 +235,14 @@ fn local_markdown_relative_path_from_document_id(document_id: &str) -> Option<St
decode_local_id_segment(encoded).ok()
}
fn document_event_targets_relative_path(payload: &Value, expected: &str) -> bool {
let relative_path = payload
.get("relativePath")
.and_then(Value::as_str)
.unwrap_or_default();
expected == relative_path
}
fn system_time_ms(time: SystemTime) -> u128 {
time.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis())
@@ -368,6 +372,23 @@ mod tests {
);
}
#[test]
fn document_event_filter_rejects_resource_only_changes() {
let payload = json!({
"sourceKind": "local_folder",
"rootUri": "file:///test",
"relativePath": "docs/README/image.png",
"documentId": "",
"eventKind": "Create(File)",
"revision": 1,
});
assert!(
!document_event_targets_relative_path(&payload, "docs/README.md"),
"正文事件流不能把资源文件变化当作 Markdown 正文变化"
);
}
#[test]
fn build_tree_snapshot_payload_has_required_fields() {
let sidebar_projection = json!({
@@ -2780,7 +2780,10 @@ pub fn resolve_local_markdown_page_aggregate(
.and_then(page_options_hide_title_header_value)
.is_none()
{
page_options.hide_title_header = true;
// 我的空间(有 workspace.json 的托管工作区,包括用户本人的及分享的)默认显示页头标题。
// 普通本地文件夹(无托管清单)默认隐藏页头标题,避免文件名与正文 H1 重复。
let is_managed = load_local_workspace_manifest(&canonical_root).is_ok();
page_options.hide_title_header = !is_managed;
}
let character_count = parsed.body.chars().count() as u64;
let word_count = parsed
@@ -10344,6 +10347,23 @@ fn main() {}
let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:File~20Name.md")
.expect("aggregate");
assert_eq!(aggregate.title, "File Name");
// init_workspace 创建的是托管工作区(我的空间),默认显示页头标题。
assert!(!aggregate.layout.page_options.hide_title_header);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn local_markdown_non_managed_folder_default_hides_title_header() {
// 非托管本地文件夹:没有 .mnote/workspace.json,应该默认隐藏标题。
let root = temp_root("mnote-local-non-managed-title-default");
let _ = std::fs::create_dir_all(root.join(".mnote"));
std::fs::write(root.join("Page.md"), "# Page Heading\n").expect("write md");
let root_uri = format!("file://{}", root.display());
let aggregate = resolve_local_markdown_page_aggregate(&root_uri, "local-md:Page.md")
.expect("aggregate");
assert_eq!(aggregate.title, "Page");
assert!(aggregate.layout.page_options.hide_title_header);
let _ = std::fs::remove_dir_all(&root);
+15 -3
View File
@@ -1578,6 +1578,8 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
const sessionViews = (session) => Array.from(session.views.values());
const localFolderEventChannelKey = (session) => `${String(session?.rootUri || '').trim()}#${String(session?.documentId || '').trim()}`;
const detachSessionFromLocalFolderChannel = (session) => {
const channel = session.localFolderChannel;
if (!channel) return;
@@ -1588,7 +1590,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
} catch (_) {
// noop
}
localFolderEventRegistry.delete(channel.rootUri);
localFolderEventRegistry.delete(channel.key);
}
session.localFolderChannel = null;
};
@@ -2394,13 +2396,17 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
if (session.sourceKind !== 'local_folder' || !session.rootUri || typeof window.EventSource !== 'function') {
return;
}
let channel = localFolderEventRegistry.get(session.rootUri);
const channelKey = localFolderEventChannelKey(session);
let channel = localFolderEventRegistry.get(channelKey);
if (!channel) {
const url = new URL('/api/local-folder/events', window.location.origin);
url.searchParams.set('rootUri', session.rootUri);
url.searchParams.set('documentId', session.documentId);
const eventSource = new EventSource(url.toString());
channel = {
key: channelKey,
rootUri: session.rootUri,
documentId: session.documentId,
eventSource,
sessions: new Map(),
};
@@ -2410,6 +2416,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
Array.from(channel.sessions.values()).forEach((targetSession) => {
if (!targetSession || targetSession.views.size === 0) return;
const documentId = typeof payload.documentId === 'string' ? payload.documentId.trim() : '';
if (!documentId) return;
const eventKind = String(payload.eventKind || '');
const targetsCurrentDocument = Boolean(documentId && documentId === targetSession.documentId);
if (documentId && !targetsCurrentDocument) return;
@@ -2435,7 +2442,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
eventSource.onerror = () => {
console.warn('mnote local folder 外部更新事件流中断,将等待浏览器自动重连');
};
localFolderEventRegistry.set(session.rootUri, channel);
localFolderEventRegistry.set(channelKey, channel);
}
channel.sessions.set(session.key, session);
session.localFolderChannel = channel;
@@ -3103,6 +3110,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
view.disconnectSlashObserver();
view.disconnectSlashObserver = null;
}
clearSessionConflictSurface(view.session);
if (view.mountId != null && typeof view.runtime?.unmount === 'function') {
try {
view.runtime.unmount(view.mountId);
@@ -5422,6 +5430,9 @@ mod tests {
assert!(html.contains("refreshMindmapRuntimesFromTreePayload"));
assert!(html.contains("__MNOTE_LEPTOS_MINDMAP_BRIDGES__"));
assert!(html.contains("/api/local-folder/events"));
assert!(html.contains("localFolderEventChannelKey"));
assert!(html.contains("url.searchParams.set('documentId', session.documentId);"));
assert!(html.contains("if (!documentId) return;"));
assert!(html.contains("new EventSource(url.toString())"));
assert!(html.contains("localFolderEventRegistry"));
assert!(html
@@ -5432,6 +5443,7 @@ mod tests {
assert!(html.contains("session.views.size === 0"));
assert!(html.contains("targetSession.saving"));
assert!(html.contains("lastSelfSaveSignalAt"));
assert!(html.contains("clearSessionConflictSurface(view.session);"));
assert!(html.contains("command: 'replaceContent'"));
assert!(html.contains("external-change-conflict"));
assert!(html.contains("mnote-editor-conflict-panel"));