@rightkit/document-workspace 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +74 -2
- package/dist/app/App.d.ts +1 -0
- package/dist/app/App.js +6 -0
- package/dist/app/AppDialogs.d.ts +52 -0
- package/dist/app/AppDialogs.js +23 -0
- package/dist/app/AppDialogsHost.d.ts +31 -0
- package/dist/app/AppDialogsHost.js +30 -0
- package/dist/app/AppLifecycle.d.ts +19 -0
- package/dist/app/AppLifecycle.js +87 -0
- package/dist/app/AppProvider.d.ts +317 -0
- package/dist/app/AppProvider.js +217 -0
- package/dist/app/AppRoot.d.ts +8 -0
- package/dist/app/AppRoot.js +159 -0
- package/dist/app/AppShell.d.ts +2 -0
- package/dist/app/AppShell.js +81 -0
- package/dist/app/CommandPalette.d.ts +13 -0
- package/dist/app/CommandPalette.js +47 -0
- package/dist/app/DocActionBar.d.ts +36 -0
- package/dist/app/DocActionBar.js +300 -0
- package/dist/app/DocumentContextMenu.d.ts +29 -0
- package/dist/app/DocumentContextMenu.js +122 -0
- package/dist/app/DocumentPane.d.ts +52 -0
- package/dist/app/DocumentPane.js +30 -0
- package/dist/app/DocumentRouterTable.d.ts +58 -0
- package/dist/app/DocumentRouterTable.js +143 -0
- package/dist/app/ErrorBoundary.d.ts +12 -0
- package/dist/app/ErrorBoundary.js +16 -0
- package/dist/app/FindBar.d.ts +12 -0
- package/dist/app/FindBar.js +199 -0
- package/dist/app/LiveTitle.d.ts +14 -0
- package/dist/app/LiveTitle.js +14 -0
- package/dist/app/OpeningProgress.d.ts +13 -0
- package/dist/app/OpeningProgress.js +21 -0
- package/dist/app/PrintPreviewWindow.d.ts +2 -0
- package/dist/app/PrintPreviewWindow.js +57 -0
- package/dist/app/ShortcutHelp.d.ts +3 -0
- package/dist/app/ShortcutHelp.js +99 -0
- package/dist/app/Sidebar.d.ts +36 -0
- package/dist/app/Sidebar.js +121 -0
- package/dist/app/ToastViewport.d.ts +1 -0
- package/dist/app/ToastViewport.js +31 -0
- package/dist/app/Toolbar.d.ts +26 -0
- package/dist/app/Toolbar.js +63 -0
- package/dist/app/activeFormatSavers.d.ts +7 -0
- package/dist/app/activeFormatSavers.js +4 -0
- package/dist/app/clipboardPaths.d.ts +1 -0
- package/dist/app/clipboardPaths.js +31 -0
- package/dist/app/documentDrafts.d.ts +17 -0
- package/dist/app/documentDrafts.js +104 -0
- package/dist/app/draftResolution.d.ts +5 -0
- package/dist/app/draftResolution.js +25 -0
- package/dist/app/markdownPaletteActions.d.ts +8 -0
- package/dist/app/markdownPaletteActions.js +107 -0
- package/dist/app/resourceCensusDev.d.ts +41 -0
- package/dist/app/resourceCensusDev.js +518 -0
- package/dist/app/setupRecovery.d.ts +34 -0
- package/dist/app/setupRecovery.js +70 -0
- package/dist/app/shell/DocStatsPill.d.ts +18 -0
- package/dist/app/shell/DocStatsPill.js +32 -0
- package/dist/app/shell/DocumentTools.d.ts +9 -0
- package/dist/app/shell/DocumentTools.js +73 -0
- package/dist/app/shell/InfoInspector.d.ts +9 -0
- package/dist/app/shell/InfoInspector.js +67 -0
- package/dist/app/shell/StartPage.d.ts +9 -0
- package/dist/app/shell/StartPage.js +49 -0
- package/dist/app/shell/readEdit.d.ts +6 -0
- package/dist/app/shell/readEdit.js +6 -0
- package/dist/app/sidebar/SidebarDocumentMenu.d.ts +20 -0
- package/dist/app/sidebar/SidebarDocumentMenu.js +56 -0
- package/dist/app/sidebar/SidebarExpandedPanel.d.ts +9 -0
- package/dist/app/sidebar/SidebarExpandedPanel.js +78 -0
- package/dist/app/sidebar/SidebarIcons.d.ts +3 -0
- package/dist/app/sidebar/SidebarIcons.js +18 -0
- package/dist/app/sidebar/SidebarMenus.d.ts +19 -0
- package/dist/app/sidebar/SidebarMenus.js +64 -0
- package/dist/app/sidebar/SidebarPathUtils.d.ts +10 -0
- package/dist/app/sidebar/SidebarPathUtils.js +55 -0
- package/dist/app/sidebar/SidebarPrimitives.d.ts +12 -0
- package/dist/app/sidebar/SidebarPrimitives.js +45 -0
- package/dist/app/sidebar/SidebarRail.d.ts +5 -0
- package/dist/app/sidebar/SidebarRail.js +37 -0
- package/dist/app/sidebar/explorer/ExplorerRowMenu.d.ts +21 -0
- package/dist/app/sidebar/explorer/ExplorerRowMenu.js +37 -0
- package/dist/app/sidebar/explorer/ExplorerSections.d.ts +16 -0
- package/dist/app/sidebar/explorer/ExplorerSections.js +259 -0
- package/dist/app/sidebar/explorer/ExplorerTree.d.ts +46 -0
- package/dist/app/sidebar/explorer/ExplorerTree.js +387 -0
- package/dist/app/sidebar/explorer/explorerState.d.ts +37 -0
- package/dist/app/sidebar/explorer/explorerState.js +135 -0
- package/dist/app/tabMountPolicy.d.ts +45 -0
- package/dist/app/tabMountPolicy.js +104 -0
- package/dist/app/tabWarmOrder.d.ts +7 -0
- package/dist/app/tabWarmOrder.js +48 -0
- package/dist/app/tabs/TabFuseCurves.d.ts +3 -0
- package/dist/app/tabs/TabFuseCurves.js +15 -0
- package/dist/app/tabs/TabOverview.d.ts +24 -0
- package/dist/app/tabs/TabOverview.js +121 -0
- package/dist/app/tabs/TabStrip.d.ts +42 -0
- package/dist/app/tabs/TabStrip.js +380 -0
- package/dist/app/tabs/tabIds.d.ts +5 -0
- package/dist/app/tabs/tabIds.js +7 -0
- package/dist/app/tabs/useTabDirty.d.ts +3 -0
- package/dist/app/tabs/useTabDirty.js +11 -0
- package/dist/app/useAppChromeState.d.ts +21 -0
- package/dist/app/useAppChromeState.js +48 -0
- package/dist/app/useAppCommands.d.ts +66 -0
- package/dist/app/useAppCommands.js +71 -0
- package/dist/app/useAppDocumentCore.d.ts +210 -0
- package/dist/app/useAppDocumentCore.js +40 -0
- package/dist/app/useAppEffects.d.ts +18 -0
- package/dist/app/useAppEffects.js +52 -0
- package/dist/app/useAppPaneSearch.d.ts +37 -0
- package/dist/app/useAppPaneSearch.js +74 -0
- package/dist/app/useAppShellSetup.d.ts +40 -0
- package/dist/app/useAppShellSetup.js +52 -0
- package/dist/app/useAppSlots.d.ts +28 -0
- package/dist/app/useAppSlots.js +40 -0
- package/dist/app/useAppTabChrome.d.ts +51 -0
- package/dist/app/useAppTabChrome.js +66 -0
- package/dist/app/useAutoSave.d.ts +34 -0
- package/dist/app/useAutoSave.js +116 -0
- package/dist/app/useCloseAll.d.ts +52 -0
- package/dist/app/useCloseAll.js +295 -0
- package/dist/app/useClosedTabEviction.d.ts +10 -0
- package/dist/app/useClosedTabEviction.js +38 -0
- package/dist/app/useCommandPalette.d.ts +44 -0
- package/dist/app/useCommandPalette.js +254 -0
- package/dist/app/useCrossDocSearch.d.ts +7 -0
- package/dist/app/useCrossDocSearch.js +94 -0
- package/dist/app/useDistractionFree.d.ts +21 -0
- package/dist/app/useDistractionFree.js +73 -0
- package/dist/app/useDocumentCreation.d.ts +18 -0
- package/dist/app/useDocumentCreation.js +63 -0
- package/dist/app/useDocumentDirty.d.ts +31 -0
- package/dist/app/useDocumentDirty.js +46 -0
- package/dist/app/useDocumentLoader.d.ts +55 -0
- package/dist/app/useDocumentLoader.js +267 -0
- package/dist/app/useDocumentOutputActions.d.ts +6 -0
- package/dist/app/useDocumentOutputActions.js +45 -0
- package/dist/app/useDocumentRuntimeEffects.d.ts +36 -0
- package/dist/app/useDocumentRuntimeEffects.js +458 -0
- package/dist/app/useDocumentSaver.d.ts +58 -0
- package/dist/app/useDocumentSaver.js +511 -0
- package/dist/app/useDocumentTabs.d.ts +41 -0
- package/dist/app/useDocumentTabs.js +207 -0
- package/dist/app/useDocumentWatcher.d.ts +83 -0
- package/dist/app/useDocumentWatcher.js +393 -0
- package/dist/app/useDocumentZoom.d.ts +24 -0
- package/dist/app/useDocumentZoom.js +104 -0
- package/dist/app/useDocuments.d.ts +169 -0
- package/dist/app/useDocuments.js +455 -0
- package/dist/app/useDraftConfirmGate.d.ts +27 -0
- package/dist/app/useDraftConfirmGate.js +47 -0
- package/dist/app/useFileDragOverlay.d.ts +12 -0
- package/dist/app/useFileDragOverlay.js +49 -0
- package/dist/app/useFormatApis.d.ts +13 -0
- package/dist/app/useFormatApis.js +62 -0
- package/dist/app/useFormatDirtyState.d.ts +29 -0
- package/dist/app/useFormatDirtyState.js +81 -0
- package/dist/app/useGlobalShortcuts.d.ts +47 -0
- package/dist/app/useGlobalShortcuts.js +359 -0
- package/dist/app/useHtmlSession.d.ts +68 -0
- package/dist/app/useHtmlSession.js +463 -0
- package/dist/app/useHtmlSessionProps.d.ts +33 -0
- package/dist/app/useHtmlSessionProps.js +57 -0
- package/dist/app/useLibrary.d.ts +21 -0
- package/dist/app/useLibrary.js +81 -0
- package/dist/app/useOpenFromClipboard.d.ts +7 -0
- package/dist/app/useOpenFromClipboard.js +44 -0
- package/dist/app/usePaneLiveness.d.ts +29 -0
- package/dist/app/usePaneLiveness.js +199 -0
- package/dist/app/usePendingSavePaths.d.ts +19 -0
- package/dist/app/usePendingSavePaths.js +67 -0
- package/dist/app/useReadyShow.d.ts +12 -0
- package/dist/app/useReadyShow.js +142 -0
- package/dist/app/useSearchController.d.ts +22 -0
- package/dist/app/useSearchController.js +114 -0
- package/dist/app/useSetupRecovery.d.ts +41 -0
- package/dist/app/useSetupRecovery.js +153 -0
- package/dist/app/useTabChrome.d.ts +40 -0
- package/dist/app/useTabChrome.js +115 -0
- package/dist/app/useViewState.d.ts +27 -0
- package/dist/app/useViewState.js +160 -0
- package/dist/appName.d.ts +2 -0
- package/dist/appName.js +8 -0
- package/dist/documents/DocumentRouter.d.ts +102 -0
- package/dist/documents/DocumentRouter.js +153 -0
- package/dist/documents/FileTypeIcon.d.ts +6 -0
- package/dist/documents/FileTypeIcon.js +93 -0
- package/dist/documents/ImageDocument.d.ts +14 -0
- package/dist/documents/ImageDocument.js +61 -0
- package/dist/documents/capabilities.d.ts +17 -0
- package/dist/documents/capabilities.js +18 -0
- package/dist/documents/html/HtmlDocument.d.ts +15 -0
- package/dist/documents/html/HtmlDocument.js +584 -0
- package/dist/documents/html/HtmlDocumentLayout.d.ts +87 -0
- package/dist/documents/html/HtmlDocumentLayout.js +25 -0
- package/dist/documents/html/HtmlDocumentProps.d.ts +60 -0
- package/dist/documents/html/HtmlDocumentProps.js +1 -0
- package/dist/documents/html/HtmlDocumentSource.d.ts +34 -0
- package/dist/documents/html/HtmlDocumentSource.js +28 -0
- package/dist/documents/markdown/MarkdownSplitPane.d.ts +115 -0
- package/dist/documents/markdown/MarkdownSplitPane.js +352 -0
- package/dist/documents/mdx/MdxSplitPane.d.ts +32 -0
- package/dist/documents/mdx/MdxSplitPane.js +60 -0
- package/dist/documents/txt/LinkDialog.d.ts +8 -0
- package/dist/documents/txt/LinkDialog.js +34 -0
- package/dist/documents/txt/MarkdownSourceToolbar.d.ts +8 -0
- package/dist/documents/txt/MarkdownSourceToolbar.js +25 -0
- package/dist/documents/txt/SourceEditorToolbar.d.ts +12 -0
- package/dist/documents/txt/SourceEditorToolbar.js +4 -0
- package/dist/documents/txt/TxtEditor.d.ts +39 -0
- package/dist/documents/txt/TxtEditor.js +186 -0
- package/dist/documents/txt/sourceEditorTypes.d.ts +49 -0
- package/dist/documents/txt/sourceEditorTypes.js +1 -0
- package/dist/documents/txt/sourceExtensions.d.ts +38 -0
- package/dist/documents/txt/sourceExtensions.js +444 -0
- package/dist/documents/txt/sourceFormatting.d.ts +18 -0
- package/dist/documents/txt/sourceFormatting.js +183 -0
- package/dist/documents/txt/sourceTheme.d.ts +5 -0
- package/dist/documents/txt/sourceTheme.js +220 -0
- package/dist/documents/txt/useCodeMirrorSourceEditor.d.ts +69 -0
- package/dist/documents/txt/useCodeMirrorSourceEditor.js +546 -0
- package/dist/documents/txt/useSourceSearch.d.ts +39 -0
- package/dist/documents/txt/useSourceSearch.js +122 -0
- package/dist/documents/txt/useTextEditorCommands.d.ts +26 -0
- package/dist/documents/txt/useTextEditorCommands.js +151 -0
- package/dist/documents/web/LivePreviewPane.d.ts +11 -0
- package/dist/documents/web/LivePreviewPane.js +58 -0
- package/dist/env.d.ts +66 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.js +123 -0
- package/dist/settings/AppearanceMenu.d.ts +9 -0
- package/dist/settings/AppearanceMenu.js +54 -0
- package/dist/settings/SettingsPanel.d.ts +7 -0
- package/dist/settings/SettingsPanel.js +104 -0
- package/dist/settings/publishedSections.d.ts +22 -0
- package/dist/settings/publishedSections.js +79 -0
- package/dist/settings/sections/AboutSection.d.ts +1 -0
- package/dist/settings/sections/AboutSection.js +13 -0
- package/dist/settings/sections/DiagnosticsSection.d.ts +1 -0
- package/dist/settings/sections/DiagnosticsSection.js +42 -0
- package/dist/settings/sections/PrivacySection.d.ts +3 -0
- package/dist/settings/sections/PrivacySection.js +51 -0
- package/dist/settings/sections/UpdateLicenseBlock.d.ts +10 -0
- package/dist/settings/sections/UpdateLicenseBlock.js +158 -0
- package/dist/state/explorer.d.ts +32 -0
- package/dist/state/explorer.js +111 -0
- package/dist/state/legal.d.ts +51 -0
- package/dist/state/legal.js +130 -0
- package/dist/state/library.d.ts +33 -0
- package/dist/state/library.js +110 -0
- package/dist/state/updateLicense.d.ts +31 -0
- package/dist/state/updateLicense.js +340 -0
- package/dist/styles/explorer.css +219 -0
- package/dist/styles/index.css +1477 -0
- package/dist/styles/tabs.css +467 -0
- package/dist/styles.css +6 -0
- package/package.json +88 -3
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
import { fileFingerprint, parseHostError, readDocument, writeDocument, } from "@rightkit/document-core/state/tauriBridge";
|
|
3
|
+
import { errorMessage } from "@rightkit/document-core/state/errors";
|
|
4
|
+
import { baseName } from "@rightkit/document-core/documents/types";
|
|
5
|
+
import { incDiskWrites } from "@rightkit/document-core/documents/perfEventCounters";
|
|
6
|
+
/**
|
|
7
|
+
* HTML site sessions: the navigated SUB-page buffers (the entry page reuses the shared edits/onSave
|
|
8
|
+
* path), their per-page save/discard/dirty model, the page-aware close/save chooser, the sub-page
|
|
9
|
+
* on-disk conflict flow, and the external-change reconciliation for a session's sub-pages.
|
|
10
|
+
*
|
|
11
|
+
* Extracted verbatim from useDocuments so that hook stays a focused open/save/close/conflict surface.
|
|
12
|
+
* Bodies are unchanged from when this lived inline; the only edits are deps-array entries for the
|
|
13
|
+
* dialog/error setters, which are stable props here rather than this hook's own useState setters.
|
|
14
|
+
*/
|
|
15
|
+
export function useHtmlSession({ openDocs, editsRef, writeEdits, htmlPagesRef, writeHtmlPages, htmlActivePage, setHtmlActivePage, htmlConfirm, setHtmlConfirm, htmlConflict, setHtmlConflict, onSave, doClose, markSelfSaved, setError, onDirty, onSaveActivity, blockDrafts, }) {
|
|
16
|
+
// The entry page reuses the shared text path (edits + onSave); these handle the navigated SUB-pages,
|
|
17
|
+
// whose editable buffers live in htmlPages (App level — only the active doc is mounted).
|
|
18
|
+
// The dirty pages of one session: the entry (if its edits differ) plus every sub-page buffer that
|
|
19
|
+
// diverges from disk. Reads the synchronous mirrors so it's correct mid-keystroke.
|
|
20
|
+
const htmlDirtyPages = useCallback((entryPath) => {
|
|
21
|
+
const out = [];
|
|
22
|
+
const entryDoc = openDocs.find((d) => d.path === entryPath);
|
|
23
|
+
if (entryDoc && (editsRef.current[entryPath] ?? entryDoc.content) !== entryDoc.content) {
|
|
24
|
+
out.push({ path: entryPath, name: baseName(entryPath), isEntry: true });
|
|
25
|
+
}
|
|
26
|
+
const session = htmlPagesRef.current[entryPath] ?? {};
|
|
27
|
+
for (const [path, buf] of Object.entries(session)) {
|
|
28
|
+
if (buf.edited !== buf.original)
|
|
29
|
+
out.push({ path, name: baseName(path), isEntry: false });
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}, [openDocs, editsRef, htmlPagesRef]);
|
|
33
|
+
// Seed a sub-page buffer from disk on first visit (preserving any existing dirty buffer on a
|
|
34
|
+
// back/forward revisit). Called by HtmlDocument as the in-tab nav lands on a non-entry page.
|
|
35
|
+
const ensureHtmlPage = useCallback(async (entryPath, pagePath) => {
|
|
36
|
+
if (pagePath === entryPath)
|
|
37
|
+
return; // entry uses the shared edits buffer
|
|
38
|
+
if (htmlPagesRef.current[entryPath]?.[pagePath])
|
|
39
|
+
return; // already loaded — keep its (maybe dirty) buffer
|
|
40
|
+
try {
|
|
41
|
+
const d = await readDocument(pagePath);
|
|
42
|
+
if (htmlPagesRef.current[entryPath]?.[pagePath])
|
|
43
|
+
return; // raced another visit — keep existing
|
|
44
|
+
writeHtmlPages((p) => ({
|
|
45
|
+
...p,
|
|
46
|
+
[entryPath]: {
|
|
47
|
+
...(p[entryPath] ?? {}),
|
|
48
|
+
[pagePath]: {
|
|
49
|
+
original: d.content,
|
|
50
|
+
edited: d.content,
|
|
51
|
+
revision: 0,
|
|
52
|
+
encoding: d.encoding ?? "UTF-8",
|
|
53
|
+
fingerprint: d.fingerprint,
|
|
54
|
+
sourceSha256: d.sourceSha256,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
setError(errorMessage(e));
|
|
61
|
+
}
|
|
62
|
+
}, [htmlPagesRef, writeHtmlPages, setError]);
|
|
63
|
+
// Live edit of a sub-page buffer (the drawer editor's onChange for a non-entry page). The keystroke
|
|
64
|
+
// path: `immediate: false` debounces the React-state publish (App's htmlPropsFor re-render cascade)
|
|
65
|
+
// — see useDocumentTabs' writeHtmlPages. htmlPagesRef is still updated synchronously above, so every
|
|
66
|
+
// save/close/discard path (which reads the ref, not this state) is never stale.
|
|
67
|
+
const setHtmlPageEdited = useCallback((entryPath, pagePath, edited) => {
|
|
68
|
+
writeHtmlPages((p) => {
|
|
69
|
+
const buf = p[entryPath]?.[pagePath];
|
|
70
|
+
if (!buf || buf.edited === edited)
|
|
71
|
+
return p;
|
|
72
|
+
return {
|
|
73
|
+
...p,
|
|
74
|
+
[entryPath]: {
|
|
75
|
+
...p[entryPath],
|
|
76
|
+
[pagePath]: { ...buf, edited, revision: (buf.revision ?? 0) + 1 },
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}, { immediate: false });
|
|
80
|
+
onDirty?.(entryPath);
|
|
81
|
+
}, [onDirty, writeHtmlPages]);
|
|
82
|
+
// Record which in-site page a tab is currently showing (drives Ctrl+S targeting + dirty marker).
|
|
83
|
+
const setHtmlActive = useCallback((entryPath, pagePath) => {
|
|
84
|
+
setHtmlActivePage((s) => (s[entryPath] === pagePath ? s : { ...s, [entryPath]: pagePath }));
|
|
85
|
+
}, [setHtmlActivePage]);
|
|
86
|
+
// Revert one page's buffer to its on-disk content (entry → drop its edits; sub → edited = original).
|
|
87
|
+
const discardHtmlPage = useCallback((entryPath, pagePath) => {
|
|
88
|
+
if (pagePath === entryPath) {
|
|
89
|
+
writeEdits((e) => {
|
|
90
|
+
if (!(entryPath in e))
|
|
91
|
+
return e;
|
|
92
|
+
const n = { ...e };
|
|
93
|
+
delete n[entryPath];
|
|
94
|
+
return n;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
writeHtmlPages((p) => {
|
|
99
|
+
const buf = p[entryPath]?.[pagePath];
|
|
100
|
+
if (!buf)
|
|
101
|
+
return p;
|
|
102
|
+
return {
|
|
103
|
+
...p,
|
|
104
|
+
[entryPath]: {
|
|
105
|
+
...p[entryPath],
|
|
106
|
+
[pagePath]: { ...buf, edited: buf.original, revision: (buf.revision ?? 0) + 1 },
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}, [writeEdits, writeHtmlPages]);
|
|
112
|
+
// Save one page. Entry routes through the shared text save (encoding/fingerprint already handled).
|
|
113
|
+
// For a sub-page: a save-time fingerprint re-check guards against silently clobbering an external
|
|
114
|
+
// edit (defense-in-depth alongside the background watcher) — on a mismatch it raises the conflict
|
|
115
|
+
// dialog instead of writing. Returns true only when the page was actually written.
|
|
116
|
+
const htmlSaveSeqRef = useRef(new Map());
|
|
117
|
+
const saveHtmlPage = useCallback(async (entryPath, pagePath, options = {}) => {
|
|
118
|
+
// A manual page save resolves that page's block draft first (Done → commit → write). Autosave
|
|
119
|
+
// (silent) never commits a draft. A stale/failed draft blocks the write.
|
|
120
|
+
if (!options.silent && blockDrafts) {
|
|
121
|
+
const draft = blockDrafts.get({ documentPath: entryPath, pagePath });
|
|
122
|
+
if (draft) {
|
|
123
|
+
const result = await blockDrafts.resolve(draft.identity, "manual-save", "done");
|
|
124
|
+
if (result.outcome === "failed" || result.outcome === "cancelled")
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (pagePath === entryPath) {
|
|
129
|
+
const doc = openDocs.find((d) => d.path === entryPath);
|
|
130
|
+
if (!doc)
|
|
131
|
+
return false;
|
|
132
|
+
// Page-specific draft already resolved above; prevent onSave's generic draft resolution
|
|
133
|
+
// from double-committing other page drafts when this is Save Current.
|
|
134
|
+
const saved = await onSave(doc, { ...options, resolveDrafts: false });
|
|
135
|
+
return saved !== null;
|
|
136
|
+
}
|
|
137
|
+
const buf = htmlPagesRef.current[entryPath]?.[pagePath];
|
|
138
|
+
if (!buf)
|
|
139
|
+
return false;
|
|
140
|
+
if (buf.edited === buf.original)
|
|
141
|
+
return true; // already clean — nothing to write
|
|
142
|
+
const submitted = buf.edited; // bytes handed to the writer; completion marks only these clean (#362)
|
|
143
|
+
const saveKey = `${entryPath}\u0000${pagePath}`;
|
|
144
|
+
let saveSeq = 0;
|
|
145
|
+
onSaveActivity?.(entryPath, 1);
|
|
146
|
+
try {
|
|
147
|
+
const cur = await fileFingerprint(pagePath);
|
|
148
|
+
if (buf.fingerprint &&
|
|
149
|
+
cur &&
|
|
150
|
+
(cur.mtimeMs !== buf.fingerprint.mtimeMs || cur.size !== buf.fingerprint.size)) {
|
|
151
|
+
setHtmlConflict({ entryPath, pagePath });
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
// Sequence only saves that reach the writer, so a later save refused on conflict
|
|
155
|
+
// cannot orphan this completion's metadata.
|
|
156
|
+
saveSeq = (htmlSaveSeqRef.current.get(saveKey) ?? 0) + 1;
|
|
157
|
+
htmlSaveSeqRef.current.set(saveKey, saveSeq);
|
|
158
|
+
const result = await writeDocument(pagePath, submitted, buf.encoding, buf.sourceSha256);
|
|
159
|
+
const written = typeof result === "string" ? result : result.encoding;
|
|
160
|
+
const sourceSha256 = typeof result === "string" ? buf.sourceSha256 : result.sourceSha256 || buf.sourceSha256;
|
|
161
|
+
incDiskWrites(1);
|
|
162
|
+
markSelfSaved(pagePath);
|
|
163
|
+
const fp = await fileFingerprint(pagePath);
|
|
164
|
+
writeHtmlPages((p) => {
|
|
165
|
+
const cur2 = p[entryPath]?.[pagePath];
|
|
166
|
+
if (!cur2)
|
|
167
|
+
return p;
|
|
168
|
+
// An older completion must not overwrite metadata from a newer save of this page.
|
|
169
|
+
if (htmlSaveSeqRef.current.get(saveKey) !== saveSeq)
|
|
170
|
+
return p;
|
|
171
|
+
return {
|
|
172
|
+
...p,
|
|
173
|
+
[entryPath]: {
|
|
174
|
+
...p[entryPath],
|
|
175
|
+
[pagePath]: {
|
|
176
|
+
...cur2,
|
|
177
|
+
original: submitted,
|
|
178
|
+
stale: false,
|
|
179
|
+
encoding: written,
|
|
180
|
+
fingerprint: fp ?? cur2.fingerprint,
|
|
181
|
+
sourceSha256,
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
if (parseHostError(e).code === "write_conflict")
|
|
190
|
+
setHtmlConflict({ entryPath, pagePath });
|
|
191
|
+
else
|
|
192
|
+
setError(errorMessage(e));
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
onSaveActivity?.(entryPath, -1);
|
|
197
|
+
}
|
|
198
|
+
}, [
|
|
199
|
+
openDocs,
|
|
200
|
+
onSave,
|
|
201
|
+
htmlPagesRef,
|
|
202
|
+
writeHtmlPages,
|
|
203
|
+
markSelfSaved,
|
|
204
|
+
setHtmlConflict,
|
|
205
|
+
setError,
|
|
206
|
+
blockDrafts,
|
|
207
|
+
onSaveActivity,
|
|
208
|
+
]);
|
|
209
|
+
// Save every dirty page in the session, stopping (returning false) on the first that can't write
|
|
210
|
+
// (e.g. a conflict) so the caller doesn't then close the tab over unsaved work.
|
|
211
|
+
const saveAllHtmlPages = useCallback(async (entryPath, options = {}) => {
|
|
212
|
+
const canonicalDirty = htmlDirtyPages(entryPath);
|
|
213
|
+
// Manual save must also persist pages that have an open modified draft even if the
|
|
214
|
+
// canonical buffer hasn't changed yet (draft holds the edit). Autosave (silent) intentionally
|
|
215
|
+
// skips drafts — it saves canonical dirty content only and never commits a draft.
|
|
216
|
+
let pages = canonicalDirty;
|
|
217
|
+
if (!options.silent && blockDrafts) {
|
|
218
|
+
const draftPaths = new Set(blockDrafts.snapshotModified(entryPath).map((d) => d.identity.pagePath));
|
|
219
|
+
const already = new Set(canonicalDirty.map((p) => p.path));
|
|
220
|
+
for (const pagePath of draftPaths) {
|
|
221
|
+
if (!already.has(pagePath)) {
|
|
222
|
+
pages = [
|
|
223
|
+
...pages,
|
|
224
|
+
{ path: pagePath, name: baseName(pagePath), isEntry: pagePath === entryPath },
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const p of pages) {
|
|
230
|
+
if (!(await saveHtmlPage(entryPath, p.path, options)))
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
234
|
+
}, [htmlDirtyPages, saveHtmlPage, blockDrafts]);
|
|
235
|
+
const discardAllHtmlPages = useCallback((entryPath) => {
|
|
236
|
+
// Discard any open block drafts for this session first — Don't Save means discard edit, not keep.
|
|
237
|
+
if (blockDrafts) {
|
|
238
|
+
for (const draft of blockDrafts.snapshotModified(entryPath)) {
|
|
239
|
+
void blockDrafts.resolve(draft.identity, "close", "discard");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
writeEdits((e) => {
|
|
243
|
+
if (!(entryPath in e))
|
|
244
|
+
return e;
|
|
245
|
+
const n = { ...e };
|
|
246
|
+
delete n[entryPath];
|
|
247
|
+
return n;
|
|
248
|
+
});
|
|
249
|
+
writeHtmlPages((p) => {
|
|
250
|
+
const session = p[entryPath];
|
|
251
|
+
if (!session)
|
|
252
|
+
return p;
|
|
253
|
+
const next = {};
|
|
254
|
+
for (const k of Object.keys(session))
|
|
255
|
+
next[k] = {
|
|
256
|
+
...session[k],
|
|
257
|
+
edited: session[k].original,
|
|
258
|
+
revision: (session[k].revision ?? 0) + 1,
|
|
259
|
+
};
|
|
260
|
+
return { ...p, [entryPath]: next };
|
|
261
|
+
});
|
|
262
|
+
}, [writeEdits, writeHtmlPages, blockDrafts]);
|
|
263
|
+
// Ctrl+S on an HTML tab: one dirty page saves directly; several open the page-aware chooser.
|
|
264
|
+
const onHtmlSaveShortcut = useCallback((entryPath) => {
|
|
265
|
+
const canonical = htmlDirtyPages(entryPath);
|
|
266
|
+
// Include draft pages so Ctrl/Cmd+S inside a valid draft resolves once then saves once
|
|
267
|
+
// even when the canonical buffer is still clean (draft holds the edit).
|
|
268
|
+
let dirty = canonical;
|
|
269
|
+
if (blockDrafts) {
|
|
270
|
+
const draftPaths = new Set(blockDrafts.snapshotModified(entryPath).map((d) => d.identity.pagePath));
|
|
271
|
+
const already = new Set(canonical.map((p) => p.path));
|
|
272
|
+
for (const pagePath of draftPaths) {
|
|
273
|
+
if (!already.has(pagePath)) {
|
|
274
|
+
dirty = [
|
|
275
|
+
...dirty,
|
|
276
|
+
{ path: pagePath, name: baseName(pagePath), isEntry: pagePath === entryPath },
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (dirty.length === 0)
|
|
282
|
+
return;
|
|
283
|
+
if (dirty.length === 1) {
|
|
284
|
+
void saveHtmlPage(entryPath, dirty[0].path);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
setHtmlConfirm({ entryPath, mode: "save" });
|
|
288
|
+
}, [htmlDirtyPages, saveHtmlPage, setHtmlConfirm, blockDrafts]);
|
|
289
|
+
// Close/save chooser actions. In "close" mode each save path also closes the tab — but only once
|
|
290
|
+
// the save actually succeeds, so a conflict mid-close leaves the tab open with its conflict prompt.
|
|
291
|
+
const htmlSaveCurrent = useCallback(async () => {
|
|
292
|
+
const c = htmlConfirm;
|
|
293
|
+
if (!c)
|
|
294
|
+
return;
|
|
295
|
+
setHtmlConfirm(null);
|
|
296
|
+
const active = htmlActivePage[c.entryPath] ?? c.entryPath;
|
|
297
|
+
const ok = await saveHtmlPage(c.entryPath, active);
|
|
298
|
+
if (c.mode === "close" && ok)
|
|
299
|
+
doClose(c.entryPath);
|
|
300
|
+
}, [htmlConfirm, htmlActivePage, saveHtmlPage, doClose, setHtmlConfirm]);
|
|
301
|
+
const htmlSaveAll = useCallback(async () => {
|
|
302
|
+
const c = htmlConfirm;
|
|
303
|
+
if (!c)
|
|
304
|
+
return;
|
|
305
|
+
setHtmlConfirm(null);
|
|
306
|
+
const ok = await saveAllHtmlPages(c.entryPath);
|
|
307
|
+
if (c.mode === "close" && ok)
|
|
308
|
+
doClose(c.entryPath);
|
|
309
|
+
}, [htmlConfirm, saveAllHtmlPages, doClose, setHtmlConfirm]);
|
|
310
|
+
const htmlDiscardAll = useCallback(() => {
|
|
311
|
+
const c = htmlConfirm;
|
|
312
|
+
if (!c)
|
|
313
|
+
return;
|
|
314
|
+
setHtmlConfirm(null);
|
|
315
|
+
discardAllHtmlPages(c.entryPath);
|
|
316
|
+
if (c.mode === "close")
|
|
317
|
+
doClose(c.entryPath);
|
|
318
|
+
}, [htmlConfirm, discardAllHtmlPages, doClose, setHtmlConfirm]);
|
|
319
|
+
const htmlConfirmCancel = useCallback(() => setHtmlConfirm(null), [setHtmlConfirm]);
|
|
320
|
+
// Sub-page on-disk conflict (from the save-time guard or the watcher): reload re-reads the file
|
|
321
|
+
// into the buffer (discarding edits); keep dismisses and leaves the buffer as-is.
|
|
322
|
+
const htmlConflictReload = useCallback(async () => {
|
|
323
|
+
const c = htmlConflict;
|
|
324
|
+
if (!c)
|
|
325
|
+
return;
|
|
326
|
+
setHtmlConflict(null);
|
|
327
|
+
try {
|
|
328
|
+
const d = await readDocument(c.pagePath);
|
|
329
|
+
writeHtmlPages((p) => ({
|
|
330
|
+
...p,
|
|
331
|
+
[c.entryPath]: {
|
|
332
|
+
...(p[c.entryPath] ?? {}),
|
|
333
|
+
[c.pagePath]: {
|
|
334
|
+
original: d.content,
|
|
335
|
+
edited: d.content,
|
|
336
|
+
revision: (p[c.entryPath]?.[c.pagePath]?.revision ?? 0) + 1,
|
|
337
|
+
encoding: d.encoding ?? "UTF-8",
|
|
338
|
+
fingerprint: d.fingerprint,
|
|
339
|
+
sourceSha256: d.sourceSha256,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
setError(errorMessage(e));
|
|
346
|
+
}
|
|
347
|
+
}, [htmlConflict, writeHtmlPages, setHtmlConflict, setError]);
|
|
348
|
+
const htmlConflictKeep = useCallback(async () => {
|
|
349
|
+
const c = htmlConflict;
|
|
350
|
+
if (!c)
|
|
351
|
+
return;
|
|
352
|
+
try {
|
|
353
|
+
const d = await readDocument(c.pagePath);
|
|
354
|
+
if (!d.sourceSha256) {
|
|
355
|
+
setError("Couldn't refresh file identity before overwrite.");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
writeHtmlPages((p) => {
|
|
359
|
+
const session = p[c.entryPath];
|
|
360
|
+
const current = session?.[c.pagePath];
|
|
361
|
+
if (!session || !current)
|
|
362
|
+
return p;
|
|
363
|
+
return {
|
|
364
|
+
...p,
|
|
365
|
+
[c.entryPath]: {
|
|
366
|
+
...session,
|
|
367
|
+
[c.pagePath]: {
|
|
368
|
+
...current,
|
|
369
|
+
fingerprint: d.fingerprint ?? current.fingerprint,
|
|
370
|
+
sourceSha256: d.sourceSha256,
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
});
|
|
375
|
+
setHtmlConflict(null);
|
|
376
|
+
}
|
|
377
|
+
catch (e) {
|
|
378
|
+
setError(errorMessage(e));
|
|
379
|
+
}
|
|
380
|
+
}, [htmlConflict, setError, setHtmlConflict, writeHtmlPages]);
|
|
381
|
+
// A reloaded path that isn't an open tab may be a navigated sub-page of an open HTML site session.
|
|
382
|
+
// A dirty buffer raises the conflict chooser; a clean one silently refreshes from disk and rebuilds.
|
|
383
|
+
// No-op when the path belongs to no live session (the caller has already handled open tabs).
|
|
384
|
+
const reloadHtmlSubpage = useCallback((path) => {
|
|
385
|
+
for (const entryPath in htmlPagesRef.current) {
|
|
386
|
+
const buf = htmlPagesRef.current[entryPath]?.[path];
|
|
387
|
+
if (!buf)
|
|
388
|
+
continue;
|
|
389
|
+
if (buf.edited !== buf.original) {
|
|
390
|
+
// Dirty buffer — let the user choose, and keep the draft marked stale until it is reloaded or saved.
|
|
391
|
+
writeHtmlPages((p) => {
|
|
392
|
+
const cur = p[entryPath]?.[path];
|
|
393
|
+
if (!cur || cur.stale)
|
|
394
|
+
return p;
|
|
395
|
+
return { ...p, [entryPath]: { ...p[entryPath], [path]: { ...cur, stale: true } } };
|
|
396
|
+
});
|
|
397
|
+
setHtmlConflict({ entryPath, pagePath: path });
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
// Clean buffer — silently refresh it from disk and rebuild the preview.
|
|
401
|
+
const readOriginal = buf.original;
|
|
402
|
+
const readRevision = buf.revision;
|
|
403
|
+
let raced = false;
|
|
404
|
+
void readDocument(path)
|
|
405
|
+
.then((d) => {
|
|
406
|
+
writeHtmlPages((p) => {
|
|
407
|
+
const live = p[entryPath]?.[path];
|
|
408
|
+
if (!live)
|
|
409
|
+
return p; // closed / navigated away meanwhile
|
|
410
|
+
// Edits (or another reload) landed during the disk read: keep them, raise conflict (#363).
|
|
411
|
+
if (live.edited !== live.original ||
|
|
412
|
+
live.original !== readOriginal ||
|
|
413
|
+
live.revision !== readRevision) {
|
|
414
|
+
if (live.edited !== live.original) {
|
|
415
|
+
raced = true;
|
|
416
|
+
return { ...p, [entryPath]: { ...p[entryPath], [path]: { ...live, stale: true } } };
|
|
417
|
+
}
|
|
418
|
+
return p; // superseded by a newer clean refresh
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
...p,
|
|
422
|
+
[entryPath]: {
|
|
423
|
+
...p[entryPath],
|
|
424
|
+
[path]: {
|
|
425
|
+
original: d.content,
|
|
426
|
+
edited: d.content,
|
|
427
|
+
revision: (p[entryPath]?.[path]?.revision ?? 0) + 1,
|
|
428
|
+
encoding: d.encoding ?? "UTF-8",
|
|
429
|
+
fingerprint: d.fingerprint,
|
|
430
|
+
sourceSha256: d.sourceSha256,
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
});
|
|
435
|
+
if (raced)
|
|
436
|
+
setHtmlConflict({ entryPath, pagePath: path });
|
|
437
|
+
})
|
|
438
|
+
.catch(() => {
|
|
439
|
+
// A transient external reload failure is retried by the existing file watcher.
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
}, [htmlPagesRef, writeHtmlPages, setHtmlConflict]);
|
|
445
|
+
return {
|
|
446
|
+
ensureHtmlPage,
|
|
447
|
+
setHtmlPageEdited,
|
|
448
|
+
setHtmlActive,
|
|
449
|
+
discardHtmlPage,
|
|
450
|
+
saveHtmlPage,
|
|
451
|
+
saveAllHtmlPages,
|
|
452
|
+
discardAllHtmlPages,
|
|
453
|
+
onHtmlSaveShortcut,
|
|
454
|
+
htmlDirtyPages,
|
|
455
|
+
htmlSaveCurrent,
|
|
456
|
+
htmlSaveAll,
|
|
457
|
+
htmlDiscardAll,
|
|
458
|
+
htmlConfirmCancel,
|
|
459
|
+
htmlConflictReload,
|
|
460
|
+
htmlConflictKeep,
|
|
461
|
+
reloadHtmlSubpage,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type ComponentProps, type MutableRefObject } from "react";
|
|
2
|
+
import type { DocumentRouter } from "../documents/DocumentRouter.js";
|
|
3
|
+
import type { DocumentTabs } from "./useDocumentTabs.js";
|
|
4
|
+
import type { useHtmlSession } from "./useHtmlSession.js";
|
|
5
|
+
import type { useDocumentWatcher } from "./useDocumentWatcher.js";
|
|
6
|
+
type HtmlSession = ReturnType<typeof useHtmlSession>;
|
|
7
|
+
interface HtmlSessionPropsDeps {
|
|
8
|
+
htmlPages: DocumentTabs["htmlPages"];
|
|
9
|
+
/** App-owned durable scroll store (survives tab-switch unmounts); read/written per entry path. */
|
|
10
|
+
previewScrollStore: MutableRefObject<Map<string, number>>;
|
|
11
|
+
ensureHtmlPage: HtmlSession["ensureHtmlPage"];
|
|
12
|
+
setHtmlPageEdited: HtmlSession["setHtmlPageEdited"];
|
|
13
|
+
setHtmlActive: HtmlSession["setHtmlActive"];
|
|
14
|
+
saveHtmlPage: HtmlSession["saveHtmlPage"];
|
|
15
|
+
discardHtmlPage: HtmlSession["discardHtmlPage"];
|
|
16
|
+
htmlDirtyPages: HtmlSession["htmlDirtyPages"];
|
|
17
|
+
reload: ReturnType<typeof useDocumentWatcher>["reload"];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* HTML site-session props for the router. Sub-page buffers come from tabs; renderedRevision
|
|
21
|
+
* (checksum) is sole owner of preview reload. Per-html-tab session props (keyed by entry path), so
|
|
22
|
+
* each warm pane is wired to its OWN session. Cache a props object per entryPath and only replace it
|
|
23
|
+
* when THAT path's own pageBuffers/dirty slice actually changed (writeHtmlPages only replaces touched
|
|
24
|
+
* entryPath's sub-object, so unrelated tabs keep old reference — cache hits and same object returned).
|
|
25
|
+
* Extracted verbatim from App.tsx; bodies are unchanged.
|
|
26
|
+
* `dropHtmlProps` is the stable callback App's shared closed-tab eviction effect uses to drop a
|
|
27
|
+
* closed path's cache entry alongside its other per-path caches.
|
|
28
|
+
*/
|
|
29
|
+
export declare function useHtmlSessionProps({ htmlPages, previewScrollStore, ensureHtmlPage, setHtmlPageEdited, setHtmlActive, saveHtmlPage, discardHtmlPage, htmlDirtyPages, reload, }: HtmlSessionPropsDeps): {
|
|
30
|
+
htmlPropsFor: (entryPath: string) => NonNullable<ComponentProps<typeof DocumentRouter>["html"]>;
|
|
31
|
+
dropHtmlProps: (entryPath: string) => void;
|
|
32
|
+
};
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useCallback, useRef } from "react";
|
|
2
|
+
// Shared stable "no sub-pages yet" reference for htmlPropsFor's per-path cache below — a fresh `{}`
|
|
3
|
+
// literal on every miss would defeat the cache's identity check for a session with no sub-pages.
|
|
4
|
+
const EMPTY_HTML_SESSION_PAGE = {};
|
|
5
|
+
/**
|
|
6
|
+
* HTML site-session props for the router. Sub-page buffers come from tabs; renderedRevision
|
|
7
|
+
* (checksum) is sole owner of preview reload. Per-html-tab session props (keyed by entry path), so
|
|
8
|
+
* each warm pane is wired to its OWN session. Cache a props object per entryPath and only replace it
|
|
9
|
+
* when THAT path's own pageBuffers/dirty slice actually changed (writeHtmlPages only replaces touched
|
|
10
|
+
* entryPath's sub-object, so unrelated tabs keep old reference — cache hits and same object returned).
|
|
11
|
+
* Extracted verbatim from App.tsx; bodies are unchanged.
|
|
12
|
+
* `dropHtmlProps` is the stable callback App's shared closed-tab eviction effect uses to drop a
|
|
13
|
+
* closed path's cache entry alongside its other per-path caches.
|
|
14
|
+
*/
|
|
15
|
+
export function useHtmlSessionProps({ htmlPages, previewScrollStore, ensureHtmlPage, setHtmlPageEdited, setHtmlActive, saveHtmlPage, discardHtmlPage, htmlDirtyPages, reload, }) {
|
|
16
|
+
const htmlPropsCacheRef = useRef(new Map());
|
|
17
|
+
const htmlPropsFor = useCallback((entryPath) => {
|
|
18
|
+
const pageBuffers = htmlPages[entryPath] ?? EMPTY_HTML_SESSION_PAGE;
|
|
19
|
+
const dirty = htmlDirtyPages(entryPath).map((d) => d.path);
|
|
20
|
+
const dirtyKey = dirty.join(" ");
|
|
21
|
+
const cache = htmlPropsCacheRef.current;
|
|
22
|
+
const cached = cache.get(entryPath);
|
|
23
|
+
if (cached && cached.pageBuffers === pageBuffers && cached.dirtyKey === dirtyKey) {
|
|
24
|
+
return cached.props;
|
|
25
|
+
}
|
|
26
|
+
const props = {
|
|
27
|
+
pageBuffers,
|
|
28
|
+
dirtyPagePaths: dirty,
|
|
29
|
+
onEnsurePage: ensureHtmlPage,
|
|
30
|
+
onPageChange: setHtmlPageEdited,
|
|
31
|
+
onSetActivePage: setHtmlActive,
|
|
32
|
+
onSavePage: saveHtmlPage,
|
|
33
|
+
onDiscardPage: discardHtmlPage,
|
|
34
|
+
onReloadFromDisk: reload,
|
|
35
|
+
initialPreviewScrollY: previewScrollStore.current.get(entryPath) ?? 0,
|
|
36
|
+
onPreviewScroll: (y) => previewScrollStore.current.set(entryPath, y),
|
|
37
|
+
};
|
|
38
|
+
cache.set(entryPath, { pageBuffers, dirtyKey, props });
|
|
39
|
+
return props;
|
|
40
|
+
}, [
|
|
41
|
+
discardHtmlPage,
|
|
42
|
+
ensureHtmlPage,
|
|
43
|
+
htmlDirtyPages,
|
|
44
|
+
reload,
|
|
45
|
+
saveHtmlPage,
|
|
46
|
+
setHtmlActive,
|
|
47
|
+
setHtmlPageEdited,
|
|
48
|
+
htmlPages,
|
|
49
|
+
previewScrollStore,
|
|
50
|
+
]);
|
|
51
|
+
// Closed-tab eviction: drop a closed path's cached props entry alongside its other per-path
|
|
52
|
+
// caches. Stable identity so App's shared eviction effect can list it in deps.
|
|
53
|
+
const dropHtmlProps = useCallback((entryPath) => {
|
|
54
|
+
htmlPropsCacheRef.current.delete(entryPath);
|
|
55
|
+
}, []);
|
|
56
|
+
return { htmlPropsFor, dropHtmlProps };
|
|
57
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type FileEntry, type PinEntry } from "../state/library.js";
|
|
2
|
+
import { type LoadedDocument } from "@rightkit/document-core/documents/types";
|
|
3
|
+
/**
|
|
4
|
+
* Recents + pinned-files library state. Owns the recents list and the set of pinned paths, the
|
|
5
|
+
* refresh against the DB, pin toggling, and syncing a single path's pinned flag (used right after
|
|
6
|
+
* opening a file). Extracted from App so the shell isn't carrying library plumbing inline.
|
|
7
|
+
*
|
|
8
|
+
* Since the explorer sidebar, the DB also pins directories: `pinnedEntries` is the full pin list
|
|
9
|
+
* (dirs + files, in stored order) while `pinnedPaths` stays the flat Set used for pin-state lookup.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useLibrary(openDocs: LoadedDocument[]): {
|
|
12
|
+
onHistoryPurged: () => void;
|
|
13
|
+
recents: FileEntry[];
|
|
14
|
+
pinnedPaths: Set<string>;
|
|
15
|
+
pinnedEntries: PinEntry[];
|
|
16
|
+
refreshLibrary: () => Promise<void>;
|
|
17
|
+
syncPinned: (path: string) => Promise<void>;
|
|
18
|
+
onTogglePin: (path: string) => Promise<void>;
|
|
19
|
+
onTogglePinDir: (path: string, displayName: string) => Promise<void>;
|
|
20
|
+
onClearRecents: () => Promise<void>;
|
|
21
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { clearRecent, listRecent, listPins, togglePin as dbTogglePin, togglePinDir as dbTogglePinDir, isPinnedPath, } from "../state/library.js";
|
|
3
|
+
import { baseName, formatFromPath, isUntitled } from "@rightkit/document-core/documents/types";
|
|
4
|
+
import { isWebTabKey } from "@rightkit/document-core/documents/web/webUrl";
|
|
5
|
+
/**
|
|
6
|
+
* Recents + pinned-files library state. Owns the recents list and the set of pinned paths, the
|
|
7
|
+
* refresh against the DB, pin toggling, and syncing a single path's pinned flag (used right after
|
|
8
|
+
* opening a file). Extracted from App so the shell isn't carrying library plumbing inline.
|
|
9
|
+
*
|
|
10
|
+
* Since the explorer sidebar, the DB also pins directories: `pinnedEntries` is the full pin list
|
|
11
|
+
* (dirs + files, in stored order) while `pinnedPaths` stays the flat Set used for pin-state lookup.
|
|
12
|
+
*/
|
|
13
|
+
export function useLibrary(openDocs) {
|
|
14
|
+
const [recents, setRecents] = useState([]);
|
|
15
|
+
const [pinnedPaths, setPinnedPaths] = useState(() => new Set());
|
|
16
|
+
const [pinnedEntries, setPinnedEntries] = useState([]);
|
|
17
|
+
const refreshLibrary = useCallback(async () => {
|
|
18
|
+
const [recentRows, pinRows] = await Promise.all([listRecent(), listPins()]);
|
|
19
|
+
setRecents(recentRows);
|
|
20
|
+
setPinnedEntries(pinRows);
|
|
21
|
+
// Keep the flat path set in sync — but only for pins the DB knows about. Per-doc syncPinned
|
|
22
|
+
// calls (which reflect is_pinned_path right after open) merge into this set too, so a dir pin
|
|
23
|
+
// set here isn't lost: rebuild the set as union(db pins, existing non-DB-synced entries)? No —
|
|
24
|
+
// DB is the source of truth for pins; syncPinned only mirrors DB state anyway.
|
|
25
|
+
setPinnedPaths(new Set(pinRows.map((p) => p.path)));
|
|
26
|
+
}, []);
|
|
27
|
+
const setPinned = useCallback((path, pinned) => {
|
|
28
|
+
setPinnedPaths((prev) => {
|
|
29
|
+
if (prev.has(path) === pinned)
|
|
30
|
+
return prev;
|
|
31
|
+
const next = new Set(prev);
|
|
32
|
+
if (pinned)
|
|
33
|
+
next.add(path);
|
|
34
|
+
else
|
|
35
|
+
next.delete(path);
|
|
36
|
+
return next;
|
|
37
|
+
});
|
|
38
|
+
}, []);
|
|
39
|
+
/** Reflect the DB's pinned state for one path into local state (called after open). */
|
|
40
|
+
const syncPinned = useCallback(async (path) => {
|
|
41
|
+
setPinned(path, await isPinnedPath(path));
|
|
42
|
+
}, [setPinned]);
|
|
43
|
+
const onTogglePin = useCallback(async (path) => {
|
|
44
|
+
if (isUntitled(path) || isWebTabKey(path))
|
|
45
|
+
return; // can't pin an unsaved/virtual doc
|
|
46
|
+
const doc = openDocs.find((d) => d.path === path);
|
|
47
|
+
const entry = {
|
|
48
|
+
path,
|
|
49
|
+
displayName: baseName(path),
|
|
50
|
+
format: doc?.format ?? formatFromPath(path),
|
|
51
|
+
};
|
|
52
|
+
const pinnedNow = await dbTogglePin(entry);
|
|
53
|
+
setPinned(path, pinnedNow);
|
|
54
|
+
await refreshLibrary();
|
|
55
|
+
}, [openDocs, refreshLibrary, setPinned]);
|
|
56
|
+
const onTogglePinDir = useCallback(async (path, displayName) => {
|
|
57
|
+
const pinnedNow = await dbTogglePinDir(path, displayName);
|
|
58
|
+
setPinned(path, pinnedNow);
|
|
59
|
+
await refreshLibrary();
|
|
60
|
+
}, [refreshLibrary, setPinned]);
|
|
61
|
+
const onClearRecents = useCallback(async () => {
|
|
62
|
+
await clearRecent();
|
|
63
|
+
setRecents([]);
|
|
64
|
+
}, []);
|
|
65
|
+
const onHistoryPurged = useCallback(() => {
|
|
66
|
+
setRecents([]);
|
|
67
|
+
setPinnedEntries([]);
|
|
68
|
+
setPinnedPaths(new Set());
|
|
69
|
+
}, []);
|
|
70
|
+
return {
|
|
71
|
+
onHistoryPurged,
|
|
72
|
+
recents,
|
|
73
|
+
pinnedPaths,
|
|
74
|
+
pinnedEntries,
|
|
75
|
+
refreshLibrary,
|
|
76
|
+
syncPinned,
|
|
77
|
+
onTogglePin,
|
|
78
|
+
onTogglePinDir,
|
|
79
|
+
onClearRecents,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Open from clipboard: copied files (CF_HDROP) open like a drag-drop; nothing → a toast. */
|
|
2
|
+
export declare function useOpenFromClipboard({ open, openLiveUrl, duplicateAsDraft, setError, }: {
|
|
3
|
+
open: (path: string) => unknown;
|
|
4
|
+
openLiveUrl: (url: string) => unknown;
|
|
5
|
+
duplicateAsDraft: (text: string, title?: string) => unknown;
|
|
6
|
+
setError: (message: string | null) => void;
|
|
7
|
+
}): () => Promise<void>;
|