Fix local filetree page tree open performance
This commit is contained in:
@@ -37,6 +37,9 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
};
|
||||
var fileTreeState = fileTreeViewState;
|
||||
var fileTreeExpandedRelativePaths = fileTreeViewState.expandedParents;
|
||||
var pageTreeState = {
|
||||
pageTreeRequestGeneration: 0
|
||||
};
|
||||
var fileTreeLazyCacheRootUri = '';
|
||||
var fileTreeExpansionStorageRootUri = '';
|
||||
var fileTreeExpansionRestoreTimer = 0;
|
||||
@@ -660,6 +663,29 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
fileTreeState.dirtyParents.add(key);
|
||||
}
|
||||
|
||||
function beginPageTreeRequest() {
|
||||
pageTreeState.pageTreeRequestGeneration += 1;
|
||||
return pageTreeState.pageTreeRequestGeneration;
|
||||
}
|
||||
|
||||
function isLatestPageTreeRequest(generation) {
|
||||
return generation === pageTreeState.pageTreeRequestGeneration;
|
||||
}
|
||||
|
||||
function yieldForPageTreeRender() {
|
||||
return new Promise(function(resolve) {
|
||||
if (typeof window !== 'undefined' && typeof window.requestIdleCallback === 'function') {
|
||||
window.requestIdleCallback(function() { resolve(); }, { timeout: 120 });
|
||||
return;
|
||||
}
|
||||
if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
|
||||
window.requestAnimationFrame(function() { resolve(); });
|
||||
return;
|
||||
}
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
}
|
||||
|
||||
function pageTreeChevronSvg() {
|
||||
return '<svg class="tree-toggle-icon" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><path d="M7.84 14.955c.206 0 .37-.07.505-.21l4.277-4.179a.79.79 0 0 0 .264-.574.78.78 0 0 0-.258-.574L8.35 5.24a.7.7 0 0 0-.51-.21.721.721 0 0 0-.498 1.247l3.814 3.721-3.814 3.709a.721.721 0 0 0 .498 1.248"></path></svg>';
|
||||
}
|
||||
@@ -687,15 +713,26 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderPageProjection(projection) {
|
||||
async function renderPageProjection(projection) {
|
||||
var generation = beginPageTreeRequest();
|
||||
var tree = document.getElementById('sidebar-tree-root');
|
||||
if (!tree) return false;
|
||||
var rows = projectionItems(projection).filter(function(item) {
|
||||
return String(item.rowKind || 'document') === 'document';
|
||||
});
|
||||
await yieldForPageTreeRender();
|
||||
if (!isLatestPageTreeRequest(generation)) {
|
||||
document.documentElement.setAttribute('data-mnote-page-tree-stale-projection-ignored', String(generation));
|
||||
return false;
|
||||
}
|
||||
var activeId = currentDocumentId();
|
||||
var activeRowId = currentFileTreeActiveRowId();
|
||||
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-page-renderer="initial_v1">' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId, 0) : '') + '</ul>';
|
||||
var template = document.createElement('template');
|
||||
template.innerHTML = '<ul class="tree-root" role="tree" data-rust-page-renderer="initial_v1">' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId, 0) : '') + '</ul>';
|
||||
if (!isLatestPageTreeRequest(generation)) {
|
||||
document.documentElement.setAttribute('data-mnote-page-tree-stale-projection-ignored', String(generation));
|
||||
return false;
|
||||
}
|
||||
tree.replaceChildren(template.content.cloneNode(true));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -977,14 +1014,22 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
}
|
||||
|
||||
function renderSidebarSnapshot(payload) {
|
||||
var renderedPage = hasProjectionItems(payload) ? renderPageProjection(payload) : false;
|
||||
var renderedPage = false;
|
||||
if (hasProjectionItems(payload)) {
|
||||
renderedPage = true;
|
||||
void renderPageProjection(payload);
|
||||
}
|
||||
var fileProjection = readDatasetProjection(payload, 'kernel_file_tree_projection', 'kernelFileTreeProjection');
|
||||
var renderedFile = fileProjection && hasProjectionItems(fileProjection) ? renderFileProjection(fileProjection) : false;
|
||||
return renderedPage || renderedFile;
|
||||
}
|
||||
|
||||
function renderLiveSidebarSnapshot(payload) {
|
||||
var renderedPage = hasProjectionItems(payload) ? renderPageProjection(payload) : false;
|
||||
var renderedPage = false;
|
||||
if (hasProjectionItems(payload)) {
|
||||
renderedPage = true;
|
||||
void renderPageProjection(payload);
|
||||
}
|
||||
var fileProjection = readDatasetProjection(payload, 'kernel_file_tree_projection', 'kernelFileTreeProjection');
|
||||
var hasFileProjection = fileProjection && hasProjectionItems(fileProjection);
|
||||
if (currentFileTreeScope()) {
|
||||
@@ -1109,9 +1154,11 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
|
||||
var sidebarPayload = sidebarResponse && sidebarResponse.ok ? await sidebarResponse.json().catch(function() { return null; }) : null;
|
||||
var resolvedSidebarPayload = sidebarPayload ? (sidebarPayload.result || sidebarPayload) : null;
|
||||
var renderedPage = resolvedSidebarPayload && hasProjectionItems(resolvedSidebarPayload)
|
||||
? renderPageProjection(resolvedSidebarPayload)
|
||||
: false;
|
||||
var renderedPage = false;
|
||||
if (resolvedSidebarPayload && hasProjectionItems(resolvedSidebarPayload)) {
|
||||
renderedPage = true;
|
||||
void renderPageProjection(resolvedSidebarPayload);
|
||||
}
|
||||
if (renderedFile && fileTreeScope) {
|
||||
var fileRoot = document.getElementById('sidebar-file-tree-root');
|
||||
if (fileRoot instanceof HTMLElement) fileRoot.setAttribute('data-mnote-filetree-scope', fileTreeScope);
|
||||
|
||||
@@ -735,26 +735,37 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
url.searchParams.set('sourceKind', 'local_folder');
|
||||
url.searchParams.set('rootUri', rootUri);
|
||||
url.searchParams.set('parentRelativePath', relativePath);
|
||||
var sidebarResponse = await fetch(sidebarUrl.toString(), {
|
||||
var sidebarRequest = fetch(sidebarUrl.toString(), {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: { accept: 'application/json' },
|
||||
cache: 'no-store'
|
||||
});
|
||||
var sidebarPayload = await sidebarResponse.json().catch(function() { return null; });
|
||||
if (!sidebarResponse.ok) throw new Error(sidebarPayload && (sidebarPayload.error || sidebarPayload.message) || 'local_page_tree_failed_' + sidebarResponse.status);
|
||||
var response = await fetch(url.toString(), {
|
||||
var fileRequest = fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
headers: { accept: 'application/json' },
|
||||
cache: 'no-store'
|
||||
});
|
||||
var response = await fileRequest;
|
||||
var payload = await response.json().catch(function() { return null; });
|
||||
if (!response.ok) throw new Error(payload && (payload.error || payload.message) || 'scoped_filetree_failed_' + response.status);
|
||||
var sidebarProjection = sidebarPayload && (sidebarPayload.result || sidebarPayload) || {};
|
||||
var fileProjection = payload && (payload.result || payload);
|
||||
var rendered = renderSidebarSnapshot(Object.assign({}, sidebarProjection, {
|
||||
dataset: Object.assign({}, sidebarProjection.dataset || {}, { kernel_file_tree_projection: fileProjection })
|
||||
var renderedFile = renderSidebarSnapshot({
|
||||
dataset: { kernel_file_tree_projection: fileProjection }
|
||||
});
|
||||
root = document.getElementById('sidebar-file-tree-root');
|
||||
if (root instanceof HTMLElement) {
|
||||
root.setAttribute('data-workspace-id', workspaceId);
|
||||
root.setAttribute('data-mnote-filetree-scope', relativePath);
|
||||
root.setAttribute('data-mnote-filetree-scope-title', row.textContent ? row.textContent.trim() : relativePath);
|
||||
}
|
||||
var sidebarResponse = await sidebarRequest;
|
||||
var sidebarPayload = await sidebarResponse.json().catch(function() { return null; });
|
||||
if (!sidebarResponse.ok) throw new Error(sidebarPayload && (sidebarPayload.error || sidebarPayload.message) || 'local_page_tree_failed_' + sidebarResponse.status);
|
||||
var sidebarProjection = sidebarPayload && (sidebarPayload.result || sidebarPayload) || {};
|
||||
var renderedPage = renderSidebarSnapshot(Object.assign({}, sidebarProjection, {
|
||||
dataset: Object.assign({}, sidebarProjection.dataset || {})
|
||||
}));
|
||||
root = document.getElementById('sidebar-file-tree-root');
|
||||
if (root instanceof HTMLElement) {
|
||||
@@ -762,7 +773,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
root.setAttribute('data-mnote-filetree-scope', relativePath);
|
||||
root.setAttribute('data-mnote-filetree-scope-title', row.textContent ? row.textContent.trim() : relativePath);
|
||||
}
|
||||
return rendered;
|
||||
return renderedFile || renderedPage;
|
||||
}
|
||||
|
||||
const sidebarFileTreeOpen = createSidebarFileTreeOpenRuntime({
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::routes::web_shell::{
|
||||
render_document_title_controller_script, render_editor_island_adapter_script,
|
||||
render_editor_runtime_preload_links, render_local_file_tree_html,
|
||||
render_local_file_tree_html_scoped, render_local_sidebar_tree_html,
|
||||
render_local_sidebar_tree_html_from_snapshot,
|
||||
};
|
||||
use crate::transport::convex::execute_convex_mutation_by_name;
|
||||
use crate::workspace_shell::{
|
||||
@@ -359,8 +360,10 @@ pub async fn root_entry(
|
||||
.first()
|
||||
.map(|item| item.id.as_str()),
|
||||
);
|
||||
let sidebar_tree_html =
|
||||
render_local_sidebar_tree_html(root_uri, selected_active_page_id.as_deref())?;
|
||||
let sidebar_tree_html = render_local_sidebar_tree_html_from_snapshot(
|
||||
&page_tree_snapshot,
|
||||
selected_active_page_id.as_deref(),
|
||||
);
|
||||
let restore_focus_row_id = query
|
||||
.restore_focus_row_id
|
||||
.as_deref()
|
||||
@@ -426,8 +429,10 @@ pub async fn root_entry(
|
||||
.first()
|
||||
.map(|item| item.id.as_str()),
|
||||
);
|
||||
let sidebar_tree_html =
|
||||
render_local_sidebar_tree_html(&root_uri, selected_active_page_id.as_deref())?;
|
||||
let sidebar_tree_html = render_local_sidebar_tree_html_from_snapshot(
|
||||
&snapshot,
|
||||
selected_active_page_id.as_deref(),
|
||||
);
|
||||
let file_tree_html =
|
||||
render_local_file_tree_html(&root_uri, selected_active_page_id.as_deref(), None)?;
|
||||
(
|
||||
@@ -607,12 +612,12 @@ pub async fn root_entry(
|
||||
Err(_) => ("MNOTE".to_string(), render_workspace_entry(), String::new()),
|
||||
}
|
||||
};
|
||||
let editor_runtime_preload_links =
|
||||
if body_extra.contains("document-editor-adapter-runtime.js") {
|
||||
render_editor_runtime_preload_links()
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let editor_runtime_preload_links = if body_extra.contains("document-editor-adapter-runtime.js")
|
||||
{
|
||||
render_editor_runtime_preload_links()
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let mut response = Html(format!(
|
||||
r#"<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
@@ -2817,6 +2822,44 @@ mod tests {
|
||||
assert!(!html.contains(r#"href="/tree"#));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn root_entry_local_folder_reuses_page_tree_snapshot_for_sidebar_html() {
|
||||
let root = temp_root("mnote-root-local-folder-page-tree-snapshot-once");
|
||||
std::fs::create_dir_all(root.join("docs")).expect("create local docs");
|
||||
std::fs::write(root.join("README.md"), "# Local Root\n正文\n").expect("write root md");
|
||||
std::fs::write(root.join("docs").join("child.md"), "# Child\n").expect("write child md");
|
||||
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
crate::routes::local_folder_source::initialize_local_workspace_for_actor(
|
||||
"user_real",
|
||||
&root_uri,
|
||||
)
|
||||
.expect("init local workspace");
|
||||
crate::routes::local_folder_source::reset_local_page_tree_snapshot_test_loads();
|
||||
let response = app_with_config("http://127.0.0.1:3100".into(), false)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri(format!(
|
||||
"/?sourceKind=local_folder&rootUri={root_uri}&treeView=filetree"
|
||||
))
|
||||
.header("x-mnote-actor-id", "user_real")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
crate::routes::local_folder_source::local_page_tree_snapshot_test_loads(),
|
||||
1,
|
||||
"root entry should reuse the already loaded local page tree snapshot instead of scanning it twice",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn root_entry_local_folder_without_active_page_keeps_resource_tab_host() {
|
||||
let root = temp_root("mnote-root-local-folder-no-active-page");
|
||||
|
||||
@@ -125,7 +125,9 @@ async fn project_projection(
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|error| WebError::internal(format!("本地树 projection 构建任务失败: {error}")))??;
|
||||
.map_err(|error| {
|
||||
WebError::internal(format!("本地树 projection 构建任务失败: {error}"))
|
||||
})??;
|
||||
return Ok(ok_response(&context, snapshot.projection));
|
||||
}
|
||||
|
||||
@@ -409,7 +411,13 @@ mod tests {
|
||||
.as_array()
|
||||
.expect("items")
|
||||
.iter()
|
||||
.any(|item| item["title"] == "本地页面.md"));
|
||||
.any(|item| item["title"] == "本地页面"
|
||||
&& item["rowKind"] == "folder"
|
||||
&& item["documentId"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
.starts_with("local-md:")
|
||||
&& item["resourceMeta"]["extra"]["source"]["relativePath"] == "本地页面"));
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
@@ -31,9 +31,53 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fs;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[cfg(test)]
|
||||
static LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS: std::sync::atomic::AtomicU64 =
|
||||
std::sync::atomic::AtomicU64::new(0);
|
||||
#[cfg(test)]
|
||||
static LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS: std::sync::atomic::AtomicU64 =
|
||||
std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct LocalPageTreeSnapshotCacheEntry {
|
||||
watch_revision: LocalFolderWatchRevision,
|
||||
snapshot: ProjectionSnapshot,
|
||||
}
|
||||
|
||||
static LOCAL_PAGE_TREE_SNAPSHOT_CACHE: OnceLock<
|
||||
Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>>,
|
||||
> = OnceLock::new();
|
||||
|
||||
fn local_page_tree_snapshot_cache(
|
||||
) -> &'static Mutex<BTreeMap<String, LocalPageTreeSnapshotCacheEntry>> {
|
||||
LOCAL_PAGE_TREE_SNAPSHOT_CACHE.get_or_init(|| Mutex::new(BTreeMap::new()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn reset_local_page_tree_snapshot_test_loads() {
|
||||
LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS.store(0, std::sync::atomic::Ordering::SeqCst);
|
||||
LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS.store(0, std::sync::atomic::Ordering::SeqCst);
|
||||
if let Some(cache) = LOCAL_PAGE_TREE_SNAPSHOT_CACHE.get() {
|
||||
if let Ok(mut cache) = cache.lock() {
|
||||
cache.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn local_page_tree_snapshot_test_loads() -> u64 {
|
||||
LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS.load(std::sync::atomic::Ordering::SeqCst)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn local_page_tree_snapshot_scan_test_loads() -> u64 {
|
||||
LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS.load(std::sync::atomic::Ordering::SeqCst)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct LocalFolderEntry {
|
||||
path: PathBuf,
|
||||
@@ -2724,6 +2768,9 @@ fn load_local_folder_file_tree_scope_snapshot(
|
||||
pub fn load_local_folder_page_tree_snapshot(
|
||||
root_uri: &str,
|
||||
) -> Result<ProjectionSnapshot, WebError> {
|
||||
#[cfg(test)]
|
||||
LOCAL_PAGE_TREE_SNAPSHOT_TEST_LOADS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
|
||||
let root_path = parse_file_root_uri(root_uri)?;
|
||||
let canonical_root = root_path.canonicalize().map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
@@ -2740,8 +2787,21 @@ pub fn load_local_folder_page_tree_snapshot(
|
||||
|
||||
let workspace_id = local_workspace_id(&canonical_root);
|
||||
let root_source_uri = file_uri_for_path(&canonical_root);
|
||||
let watch_revision = local_folder_watch_revision_for_root(&canonical_root, &root_source_uri)?;
|
||||
if let Ok(cache) = local_page_tree_snapshot_cache().lock() {
|
||||
if let Some(entry) = cache.get(&root_source_uri) {
|
||||
if entry.watch_revision.revision == watch_revision.revision
|
||||
&& entry.watch_revision.entry_count == watch_revision.entry_count
|
||||
&& entry.watch_revision.latest_modified_ms == watch_revision.latest_modified_ms
|
||||
{
|
||||
return Ok(entry.snapshot.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
let metadata = load_local_folder_metadata(&canonical_root)?;
|
||||
let mut rows = Vec::new();
|
||||
#[cfg(test)]
|
||||
LOCAL_PAGE_TREE_SNAPSHOT_SCAN_TEST_LOADS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
||||
scan_markdown_page_tree(
|
||||
&canonical_root,
|
||||
&canonical_root,
|
||||
@@ -2773,8 +2833,7 @@ pub fn load_local_folder_page_tree_snapshot(
|
||||
.iter()
|
||||
.map(local_folder_row_to_projection_item)
|
||||
.collect::<Vec<_>>();
|
||||
let watch_revision = local_folder_watch_revision_for_root(&canonical_root, &root_source_uri)?;
|
||||
Ok(ProjectionSnapshot {
|
||||
let snapshot = ProjectionSnapshot {
|
||||
dataset: json!({
|
||||
"workspace": {
|
||||
"id": local_workspace_id(&canonical_root),
|
||||
@@ -2803,7 +2862,20 @@ pub fn load_local_folder_page_tree_snapshot(
|
||||
"watchRevision": watch_revision,
|
||||
"items": items,
|
||||
}),
|
||||
})
|
||||
};
|
||||
if let Ok(mut cache) = local_page_tree_snapshot_cache().lock() {
|
||||
if cache.len() > 64 {
|
||||
cache.clear();
|
||||
}
|
||||
cache.insert(
|
||||
root_source_uri,
|
||||
LocalPageTreeSnapshotCacheEntry {
|
||||
watch_revision,
|
||||
snapshot: snapshot.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
pub fn local_folder_watch_revision(root_uri: &str) -> Result<LocalFolderWatchRevision, WebError> {
|
||||
@@ -9070,6 +9142,27 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_folder_page_tree_snapshot_reuses_cache_until_watch_revision_changes() {
|
||||
let root = temp_root("mnote-page-tree-snapshot-cache");
|
||||
init_workspace(&root);
|
||||
std::fs::write(root.join("page.md"), "# Page\n").expect("write md");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
super::reset_local_page_tree_snapshot_test_loads();
|
||||
|
||||
let first = load_local_folder_page_tree_snapshot(&root_uri).expect("first snapshot");
|
||||
let second = load_local_folder_page_tree_snapshot(&root_uri).expect("second snapshot");
|
||||
assert_eq!(first.projection, second.projection);
|
||||
assert_eq!(super::local_page_tree_snapshot_scan_test_loads(), 1);
|
||||
|
||||
std::fs::write(root.join("next.md"), "# Next\n").expect("write next");
|
||||
let third = load_local_folder_page_tree_snapshot(&root_uri).expect("third snapshot");
|
||||
assert_ne!(second.projection, third.projection);
|
||||
assert_eq!(super::local_page_tree_snapshot_scan_test_loads(), 2);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_markdown_identity_uses_path_even_when_frontmatter_has_mnote_id() {
|
||||
let root = temp_root("mnote-local-frontmatter-path-id");
|
||||
|
||||
@@ -1350,7 +1350,9 @@ pub(crate) async fn build_page_aggregate_snapshot(
|
||||
resolve_local_markdown_page_aggregate(&root_uri_for_build, &document_id)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| WebError::internal(format!("本地 Page Aggregate 构建任务失败: {error}")))??;
|
||||
.map_err(|error| {
|
||||
WebError::internal(format!("本地 Page Aggregate 构建任务失败: {error}"))
|
||||
})??;
|
||||
super::ui_preferences::apply_effective_page_preferences(
|
||||
state,
|
||||
context,
|
||||
@@ -1661,12 +1663,22 @@ pub(crate) fn render_local_sidebar_tree_html(
|
||||
active_document_id: Option<&str>,
|
||||
) -> Result<String, WebError> {
|
||||
let snapshot = load_local_folder_page_tree_snapshot(root_uri)?;
|
||||
Ok(render_local_sidebar_tree_html_from_snapshot(
|
||||
&snapshot,
|
||||
active_document_id,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn render_local_sidebar_tree_html_from_snapshot(
|
||||
snapshot: &crate::routes::snapshot_support::ProjectionSnapshot,
|
||||
active_document_id: Option<&str>,
|
||||
) -> String {
|
||||
let rows = collect_page_tree_render_rows(&snapshot.projection);
|
||||
Ok(render_initial_page_tree_html(&PageTreeInitialRenderInput {
|
||||
render_initial_page_tree_html(&PageTreeInitialRenderInput {
|
||||
rows,
|
||||
active_node_id: active_document_id.map(ToOwned::to_owned),
|
||||
focused_node_id: None,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn render_local_file_tree_html(
|
||||
@@ -2436,8 +2448,7 @@ mod tests {
|
||||
let aggregate_uri = format!(
|
||||
"/api/page-aggregate/local-md:Cold~20Start~2FCold~20Start.md?sourceKind=local_folder&rootUri={root_uri}"
|
||||
);
|
||||
super::LOCAL_PAGE_AGGREGATE_TEST_BLOCK_MS
|
||||
.store(400, std::sync::atomic::Ordering::SeqCst);
|
||||
super::LOCAL_PAGE_AGGREGATE_TEST_BLOCK_MS.store(400, std::sync::atomic::Ordering::SeqCst);
|
||||
struct ResetLocalAggregateBlock;
|
||||
impl Drop for ResetLocalAggregateBlock {
|
||||
fn drop(&mut self) {
|
||||
|
||||
@@ -554,8 +554,9 @@ mod tests {
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("/api/tree/projections/file"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||
.contains("var resolvedSidebarPayload = sidebarPayload ? (sidebarPayload.result || sidebarPayload) : null"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("var renderedPage = false;"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||
.contains("var renderedPage = resolvedSidebarPayload && hasProjectionItems(resolvedSidebarPayload)"));
|
||||
.contains("void renderPageProjection(resolvedSidebarPayload);"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("refreshFileTreeParent(fileTreeScope)"));
|
||||
assert!(
|
||||
SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("data-mnote-local-folder-watch-applied")
|
||||
@@ -1099,8 +1100,9 @@ mod tests {
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function patchFileTreeParentChildren"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||
.contains("return patchFileTreeParentChildren(parentRelativePath, rows);"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("var renderedPage = false;"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||
.contains("var renderedPage = resolvedSidebarPayload && hasProjectionItems(resolvedSidebarPayload)"));
|
||||
.contains("void renderPageProjection(resolvedSidebarPayload);"));
|
||||
let render_start = SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||
.find("function renderFileProjection(projection)")
|
||||
.expect("renderFileProjection");
|
||||
@@ -1150,6 +1152,17 @@ mod tests {
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("fileTreeState.requestGeneration += 1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_runtime_applies_page_projection_with_generation_and_yield() {
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("pageTreeRequestGeneration: 0"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function beginPageTreeRequest"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("function isLatestPageTreeRequest"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("async function renderPageProjection"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains("await yieldForPageTreeRender()"));
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||
.contains("if (!isLatestPageTreeRequest(generation))"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sidebar_filetree_runtime_prefers_command_affected_parents() {
|
||||
assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS
|
||||
|
||||
Reference in New Issue
Block a user