@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,511 @@
|
|
|
1
|
+
import { useCallback, useRef, useState, } from "react";
|
|
2
|
+
import { baseName, formatFromPath, isUntitled, safeFileStem, untitledTitle, } from "@rightkit/document-core/documents/types";
|
|
3
|
+
import { isWebDocument } from "@rightkit/document-core/documents/web/webUrl";
|
|
4
|
+
import { readDocument, writeDocument, writeDocumentBytes, copyDocument, saveFileDialog, regrantSavedTarget, fileFingerprint, parseHostError, } from "@rightkit/document-core/state/tauriBridge";
|
|
5
|
+
import { recordRecent } from "../state/library.js";
|
|
6
|
+
import { defaultImageSaveName, editableImageOutputForPath } from "@rightkit/media-ui";
|
|
7
|
+
import { clearDraft, clearFileDraft } from "./documentDrafts.js";
|
|
8
|
+
import { errorMessage } from "@rightkit/document-core/state/errors";
|
|
9
|
+
import { logEvent, logPerf } from "@rightkit/document-core/state/diagnostics";
|
|
10
|
+
import { toast } from "@rightkit/platform-ui/headless";
|
|
11
|
+
import { incDiskWrites } from "@rightkit/document-core/documents/perfEventCounters";
|
|
12
|
+
import { resolveDocumentDrafts } from "./draftResolution.js";
|
|
13
|
+
// Save-As default name for an untitled buffer: seed the dialog with the same first-line title shown in
|
|
14
|
+
// the tab/titlebar (filesystem-sanitized), or "untitled.<ext>" when the buffer is empty/unusable.
|
|
15
|
+
function untitledSaveName(text, ext) {
|
|
16
|
+
const stem = safeFileStem(untitledTitle(text));
|
|
17
|
+
return stem && stem !== "Untitled" ? `${stem}.${ext}` : `untitled.${ext}`;
|
|
18
|
+
}
|
|
19
|
+
/** Drop `path` from a dirty-flag set only when present (identity-preserving for React). */
|
|
20
|
+
function dropPathFromSet(setter, path) {
|
|
21
|
+
setter((current) => {
|
|
22
|
+
if (!current.has(path))
|
|
23
|
+
return current;
|
|
24
|
+
const next = new Set(current);
|
|
25
|
+
next.delete(path);
|
|
26
|
+
return next;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Re-add `path` to a dirty-flag set after a failed save — but only when it was already dirty
|
|
30
|
+
* before the attempt, so a failed save on a clean document doesn't fabricate dirty state. */
|
|
31
|
+
function restorePathToSet(setter, path, wasDirty) {
|
|
32
|
+
if (!wasDirty)
|
|
33
|
+
return;
|
|
34
|
+
setter((current) => (current.has(path) ? current : new Set(current).add(path)));
|
|
35
|
+
}
|
|
36
|
+
/** Keep a PDF dirty when disk commit succeeded but active memory still has newer edits. */
|
|
37
|
+
function retainPathAsDirty(setter, path) {
|
|
38
|
+
setter((current) => (current.has(path) ? current : new Set(current).add(path)));
|
|
39
|
+
}
|
|
40
|
+
/** Shared performSave success tail: perf log + success toast (unless silent), returning the
|
|
41
|
+
* saved path. */
|
|
42
|
+
function saveSucceeded(perfLabel, startedAt, savedPath, silent) {
|
|
43
|
+
logPerf(perfLabel, startedAt);
|
|
44
|
+
if (!silent)
|
|
45
|
+
toast(`Saved ${baseName(savedPath)}`, "success");
|
|
46
|
+
return savedPath;
|
|
47
|
+
}
|
|
48
|
+
/** Shared performSave failure tail: surface the error and log it, returning null. */
|
|
49
|
+
function saveFailed(setError, startedAt, error) {
|
|
50
|
+
const message = errorMessage(error);
|
|
51
|
+
setError(message);
|
|
52
|
+
logPerf("save:error", startedAt);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
function writeResult(result) {
|
|
56
|
+
if (typeof result === "string")
|
|
57
|
+
return { encoding: result };
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
/** Shared "build → commit → didCommit" sequence for the PDF save-plan path (Save and Save As). */
|
|
61
|
+
async function runPdfSavePlan(buildPlan, target) {
|
|
62
|
+
const plan = await buildPlan();
|
|
63
|
+
// Preserve the exact call shape: Save commits with no argument, Save As with a target.
|
|
64
|
+
const result = target === undefined ? await plan.commit() : await plan.commit(target);
|
|
65
|
+
await plan.didCommit(result);
|
|
66
|
+
incDiskWrites(1);
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Save / Save-As across every document format: text/markdown/mdx (writeDocument), DOCX (bytes +
|
|
71
|
+
* Save-As tab replacement), and images (payload write + Save-As tab replacement). Extracted verbatim
|
|
72
|
+
* from useDocuments — bodies, error handling, toasts, and fingerprint/dirty bookkeeping are unchanged.
|
|
73
|
+
* `markSelfSaved` + `refreshFingerprint` are owned here (this hook is the only writer of self-saved
|
|
74
|
+
* echo suppression and post-write fingerprints for save operations); `refreshFingerprint` is also
|
|
75
|
+
* exposed so the orchestrator's reload/reload-adjacent code can reuse the exact same fingerprint map.
|
|
76
|
+
*/
|
|
77
|
+
export function useDocumentSaver({ editsRef, writeEdits, setDocxDirty, setPdfDirty, pdfDirtyRef, setOpenDocs, setActivePath, setViews, setError, setConflict, fingerprints, refreshLibrary, imageSaver, imageSaversRef, docxSaver, docxSaversRef, pdfSaversRef, pdfSavePlansRef, clearImageDirty, writeImagePayload, releaseReplacedBackingPath, withTabKey, blockDrafts, }) {
|
|
78
|
+
const selfSaved = useRef(new Set());
|
|
79
|
+
const inFlightSaves = useRef(new Map());
|
|
80
|
+
const pendingSaveCounts = useRef(new Map());
|
|
81
|
+
// Synchronous mirror used by the file watcher before React can render the pending-path state.
|
|
82
|
+
// Update this before invoking any awaited writer, then clear it only after the operation settles.
|
|
83
|
+
const pendingSavePathsRef = useRef(new Set());
|
|
84
|
+
const pendingFingerprintRefreshes = useRef(new Map());
|
|
85
|
+
const [pendingSavePaths, setPendingSavePaths] = useState(() => new Set());
|
|
86
|
+
const waitForFingerprint = useCallback(async (path) => {
|
|
87
|
+
await pendingFingerprintRefreshes.current.get(path);
|
|
88
|
+
}, []);
|
|
89
|
+
const runSaveOnce = useCallback((path, key, operation) => {
|
|
90
|
+
const existing = inFlightSaves.current.get(key);
|
|
91
|
+
if (existing)
|
|
92
|
+
return existing;
|
|
93
|
+
pendingSaveCounts.current.set(path, (pendingSaveCounts.current.get(path) ?? 0) + 1);
|
|
94
|
+
const pendingNow = new Set(pendingSavePathsRef.current);
|
|
95
|
+
pendingNow.add(path);
|
|
96
|
+
pendingSavePathsRef.current = pendingNow;
|
|
97
|
+
setPendingSavePaths((prev) => (prev.has(path) ? prev : new Set(prev).add(path)));
|
|
98
|
+
const pending = operation()
|
|
99
|
+
.then(async (value) => {
|
|
100
|
+
await waitForFingerprint(path);
|
|
101
|
+
return value;
|
|
102
|
+
}, async (error) => {
|
|
103
|
+
await waitForFingerprint(path);
|
|
104
|
+
throw error;
|
|
105
|
+
})
|
|
106
|
+
.finally(() => {
|
|
107
|
+
if (inFlightSaves.current.get(key) === pending)
|
|
108
|
+
inFlightSaves.current.delete(key);
|
|
109
|
+
const remaining = (pendingSaveCounts.current.get(path) ?? 1) - 1;
|
|
110
|
+
if (remaining > 0)
|
|
111
|
+
return pendingSaveCounts.current.set(path, remaining);
|
|
112
|
+
pendingSaveCounts.current.delete(path);
|
|
113
|
+
const pendingNow = new Set(pendingSavePathsRef.current);
|
|
114
|
+
pendingNow.delete(path);
|
|
115
|
+
pendingSavePathsRef.current = pendingNow;
|
|
116
|
+
setPendingSavePaths((prev) => {
|
|
117
|
+
if (!prev.has(path))
|
|
118
|
+
return prev;
|
|
119
|
+
const next = new Set(prev);
|
|
120
|
+
next.delete(path);
|
|
121
|
+
return next;
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
inFlightSaves.current.set(key, pending);
|
|
125
|
+
return pending;
|
|
126
|
+
}, [waitForFingerprint]);
|
|
127
|
+
const markSelfSaved = useCallback((path) => {
|
|
128
|
+
selfSaved.current.add(path);
|
|
129
|
+
setTimeout(() => selfSaved.current.delete(path), 500);
|
|
130
|
+
}, []);
|
|
131
|
+
// Capture a file's current fingerprint after our own write so a later activation doesn't mistake
|
|
132
|
+
// our save for an external change. A miss just means the next activation reloads.
|
|
133
|
+
const refreshFingerprint = useCallback((path) => {
|
|
134
|
+
const refresh = fileFingerprint(path)
|
|
135
|
+
.then((fp) => {
|
|
136
|
+
// A miss must not leave the pre-write fingerprint behind: the watcher would read it as
|
|
137
|
+
// "differs from ours" and treat our own echo as external (or, worse, trust a stale base).
|
|
138
|
+
if (fp)
|
|
139
|
+
fingerprints.current.set(path, fp);
|
|
140
|
+
else
|
|
141
|
+
fingerprints.current.delete(path);
|
|
142
|
+
})
|
|
143
|
+
.catch(() => {
|
|
144
|
+
fingerprints.current.delete(path);
|
|
145
|
+
logEvent("fingerprint:error");
|
|
146
|
+
});
|
|
147
|
+
pendingFingerprintRefreshes.current.set(path, refresh);
|
|
148
|
+
void refresh.finally(() => {
|
|
149
|
+
if (pendingFingerprintRefreshes.current.get(path) === refresh)
|
|
150
|
+
pendingFingerprintRefreshes.current.delete(path);
|
|
151
|
+
});
|
|
152
|
+
}, [fingerprints]);
|
|
153
|
+
// Post-write bookkeeping for an in-place save: suppress the watcher echo and refresh the stored
|
|
154
|
+
// fingerprint so a later activation doesn't mistake our write for an external change.
|
|
155
|
+
const markSavedInPlace = useCallback((path) => {
|
|
156
|
+
markSelfSaved(path);
|
|
157
|
+
refreshFingerprint(path);
|
|
158
|
+
}, [markSelfSaved, refreshFingerprint]);
|
|
159
|
+
// Shared Save-As epilogue for the byte/payload writes (image, docx, pdf): re-grant the target so
|
|
160
|
+
// its stream stays readable, re-read it (fresh revocable token + fingerprint), swap the tab to
|
|
161
|
+
// the saved copy, clear the format's dirty flag, activate it, then update recents + library.
|
|
162
|
+
// Per-format differences come in through `opts`: whether the echo suppression marks the target,
|
|
163
|
+
// which dirty flag to clear, and whether to toast. `regrantPath` overrides the path re-granted
|
|
164
|
+
// when the writer reports a committed path that differs from `target` (PDF save plans).
|
|
165
|
+
const finishSaveAsSwap = useCallback(async (doc, target, opts = {}) => {
|
|
166
|
+
const readableTarget = (await regrantSavedTarget(opts.regrantPath ?? target)) ?? target;
|
|
167
|
+
if (opts.markSaved)
|
|
168
|
+
markSelfSaved(readableTarget);
|
|
169
|
+
const loaded = await readDocument(readableTarget);
|
|
170
|
+
if (loaded.fingerprint)
|
|
171
|
+
fingerprints.current.set(target, loaded.fingerprint);
|
|
172
|
+
releaseReplacedBackingPath(doc.path, target);
|
|
173
|
+
setOpenDocs((prev) => prev.map((d) => (d.path === doc.path ? withTabKey(loaded, d.tabKey) : d)));
|
|
174
|
+
setViews((v) => ({ ...v, [target]: v[doc.path] ?? "preview" }));
|
|
175
|
+
opts.clearDirty?.();
|
|
176
|
+
setActivePath(target);
|
|
177
|
+
await recordRecent(loaded);
|
|
178
|
+
await refreshLibrary();
|
|
179
|
+
if (opts.toastSaved)
|
|
180
|
+
toast(`Saved ${baseName(target)}`, "success");
|
|
181
|
+
return target;
|
|
182
|
+
}, [
|
|
183
|
+
fingerprints,
|
|
184
|
+
markSelfSaved,
|
|
185
|
+
refreshLibrary,
|
|
186
|
+
releaseReplacedBackingPath,
|
|
187
|
+
setActivePath,
|
|
188
|
+
setOpenDocs,
|
|
189
|
+
setViews,
|
|
190
|
+
withTabKey,
|
|
191
|
+
]);
|
|
192
|
+
const onImageSaveAs = useCallback(async (doc) => {
|
|
193
|
+
const requested = await saveFileDialog(defaultImageSaveName(doc.path), "image");
|
|
194
|
+
if (!requested)
|
|
195
|
+
return null;
|
|
196
|
+
const output = editableImageOutputForPath(requested);
|
|
197
|
+
if (!output) {
|
|
198
|
+
setError("Save image copies as .png, .jpg, .jpeg, or .webp.");
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const target = requested;
|
|
202
|
+
const payload = await imageSaver.current?.(output);
|
|
203
|
+
if (!payload)
|
|
204
|
+
return null;
|
|
205
|
+
try {
|
|
206
|
+
await writeImagePayload(target, payload);
|
|
207
|
+
incDiskWrites(1);
|
|
208
|
+
return await finishSaveAsSwap(doc, target, {
|
|
209
|
+
clearDirty: () => clearImageDirty(doc.path),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch (e) {
|
|
213
|
+
setError(errorMessage(e));
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}, [clearImageDirty, finishSaveAsSwap, imageSaver, setError, writeImagePayload]);
|
|
217
|
+
const onSaveAsRef = useRef(null);
|
|
218
|
+
// Shared PDF save skeleton for Save (in-place) and Save As (target): resolve the save plan or
|
|
219
|
+
// the live bytes, write them, then run the matching epilogue. `options.target` selects it —
|
|
220
|
+
// undefined = in-place (markSavedInPlace + saveSucceeded), defined = tab swap (finishSaveAsSwap).
|
|
221
|
+
// Any failure restores the pre-attempt dirty flag and logs the perf-tagged error.
|
|
222
|
+
const performPdfSave = useCallback(async (doc, startedAt, options = {}) => {
|
|
223
|
+
const { target, silent } = options;
|
|
224
|
+
// A failed save must not fabricate a dirty state: only re-add `doc.path` to `pdfDirty` on
|
|
225
|
+
// failure if it was ALREADY dirty before this attempt started (there are real unsaved
|
|
226
|
+
// edits to preserve). Pressing Save on an already-clean PDF that fails for unrelated
|
|
227
|
+
// reasons (e.g. a disk/permission error) must not flip its tab to "unsaved".
|
|
228
|
+
const wasDirty = pdfDirtyRef.current.has(doc.path);
|
|
229
|
+
const succeed = async (savedPath, regrantPath) => {
|
|
230
|
+
if (target === undefined) {
|
|
231
|
+
markSavedInPlace(savedPath);
|
|
232
|
+
dropPathFromSet(setPdfDirty, doc.path);
|
|
233
|
+
return saveSucceeded("save:pdf", startedAt, savedPath, silent);
|
|
234
|
+
}
|
|
235
|
+
return await finishSaveAsSwap(doc, target, {
|
|
236
|
+
regrantPath,
|
|
237
|
+
markSaved: true,
|
|
238
|
+
clearDirty: () => dropPathFromSet(setPdfDirty, doc.path),
|
|
239
|
+
toastSaved: true,
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
const buildPlan = pdfSavePlansRef.current.get(doc.path);
|
|
243
|
+
if (buildPlan) {
|
|
244
|
+
try {
|
|
245
|
+
const result = await runPdfSavePlan(buildPlan, target);
|
|
246
|
+
if (result.preservedUnsavedEdits) {
|
|
247
|
+
if (target === undefined || result.committedPath === doc.path)
|
|
248
|
+
markSavedInPlace(result.committedPath);
|
|
249
|
+
retainPathAsDirty(setPdfDirty, doc.path);
|
|
250
|
+
logPerf("save:pdf", startedAt);
|
|
251
|
+
if (!silent)
|
|
252
|
+
toast(`Saved ${baseName(result.committedPath)}; newer edits remain`, "success");
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
return await succeed(result.committedPath, result.committedPath);
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
restorePathToSet(setPdfDirty, doc.path, wasDirty);
|
|
259
|
+
return saveFailed(setError, startedAt, e);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const pdfSaver = pdfSaversRef.current.get(doc.path);
|
|
263
|
+
// Native PDFKit reading deliberately does not mount pdf.js, so a clean native PDF has no
|
|
264
|
+
// save plan or byte serializer. Save is still a successful no-op; only a dirty document needs
|
|
265
|
+
// a serializer. Save As uses the brokered byte-for-byte copy command so signatures,
|
|
266
|
+
// encryption, widgets, and other PDF features survive untouched.
|
|
267
|
+
if (!pdfSaver && !wasDirty) {
|
|
268
|
+
if (target === undefined) {
|
|
269
|
+
return saveSucceeded("save:pdf", startedAt, doc.path, silent);
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
if (target === doc.path) {
|
|
273
|
+
return saveSucceeded("save:pdf", startedAt, target, undefined);
|
|
274
|
+
}
|
|
275
|
+
await copyDocument(doc.path, target);
|
|
276
|
+
incDiskWrites(1);
|
|
277
|
+
return await succeed(target);
|
|
278
|
+
}
|
|
279
|
+
catch (e) {
|
|
280
|
+
return saveFailed(setError, startedAt, e);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
let bytes;
|
|
284
|
+
try {
|
|
285
|
+
bytes = await pdfSaver?.();
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
restorePathToSet(setPdfDirty, doc.path, wasDirty);
|
|
289
|
+
return saveFailed(setError, startedAt, e);
|
|
290
|
+
}
|
|
291
|
+
if (!bytes) {
|
|
292
|
+
restorePathToSet(setPdfDirty, doc.path, wasDirty);
|
|
293
|
+
setError("PDF edits could not be serialized.");
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const savedPath = target ?? doc.path;
|
|
298
|
+
await writeDocumentBytes(savedPath, bytes.slice().buffer);
|
|
299
|
+
incDiskWrites(1);
|
|
300
|
+
return await succeed(savedPath);
|
|
301
|
+
}
|
|
302
|
+
catch (e) {
|
|
303
|
+
restorePathToSet(setPdfDirty, doc.path, wasDirty);
|
|
304
|
+
return saveFailed(setError, startedAt, e);
|
|
305
|
+
}
|
|
306
|
+
}, [
|
|
307
|
+
finishSaveAsSwap,
|
|
308
|
+
markSavedInPlace,
|
|
309
|
+
pdfDirtyRef,
|
|
310
|
+
pdfSaversRef,
|
|
311
|
+
pdfSavePlansRef,
|
|
312
|
+
setError,
|
|
313
|
+
setPdfDirty,
|
|
314
|
+
]);
|
|
315
|
+
const performSave = useCallback(async (doc, options = {}) => {
|
|
316
|
+
const startedAt = performance.now();
|
|
317
|
+
logEvent("save:start");
|
|
318
|
+
if (isWebDocument(doc))
|
|
319
|
+
return null;
|
|
320
|
+
// A manual save resolves the target document's block drafts first (Done → commit → then the
|
|
321
|
+
// save writes the committed source). Autosave passes resolveDrafts:false — it must never
|
|
322
|
+
// commit a draft. Failed/cancelled resolution stops the write entirely.
|
|
323
|
+
if (options.resolveDrafts !== false && blockDrafts) {
|
|
324
|
+
const outcome = await resolveDocumentDrafts(blockDrafts, doc.path, "manual-save", "done");
|
|
325
|
+
if (outcome !== "resolved")
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
if (doc.format === "image") {
|
|
329
|
+
const output = editableImageOutputForPath(doc.path);
|
|
330
|
+
if (!output)
|
|
331
|
+
return onImageSaveAs(doc);
|
|
332
|
+
// Lifecycle routes can save a background tab. Its mounted saver remains registered by path,
|
|
333
|
+
// while imageSaver only mirrors the active tab.
|
|
334
|
+
const payload = await (imageSaversRef.current.get(doc.path) ?? imageSaver.current)?.(output);
|
|
335
|
+
if (!payload)
|
|
336
|
+
return null;
|
|
337
|
+
try {
|
|
338
|
+
await writeImagePayload(doc.path, payload);
|
|
339
|
+
incDiskWrites(1);
|
|
340
|
+
return saveSucceeded("save:image", startedAt, doc.path, options.silent);
|
|
341
|
+
}
|
|
342
|
+
catch (e) {
|
|
343
|
+
return saveFailed(setError, startedAt, e);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (doc.format === "pdf") {
|
|
347
|
+
return performPdfSave(doc, startedAt, { silent: options.silent });
|
|
348
|
+
}
|
|
349
|
+
const text = editsRef.current[doc.path] ?? doc.content;
|
|
350
|
+
if (isUntitled(doc.path))
|
|
351
|
+
return onSaveAsRef.current?.(doc) ?? null;
|
|
352
|
+
try {
|
|
353
|
+
const result = writeResult(await writeDocument(doc.path, text, doc.encoding, doc.sourceSha256));
|
|
354
|
+
const written = result.encoding;
|
|
355
|
+
incDiskWrites(1);
|
|
356
|
+
// The write is awaited, and the user can keep typing while it runs (a slow or network
|
|
357
|
+
// disk). Text typed in that window is newer than what was written: it must stay in the
|
|
358
|
+
// edit buffer and keep its recovery draft, never be replaced by the saved text.
|
|
359
|
+
const typedDuringSave = (editsRef.current[doc.path] ?? text) !== text;
|
|
360
|
+
markSavedInPlace(doc.path);
|
|
361
|
+
if (!typedDuringSave) {
|
|
362
|
+
clearDraft(doc.path);
|
|
363
|
+
clearFileDraft(doc.path);
|
|
364
|
+
}
|
|
365
|
+
setOpenDocs((prev) => prev.map((d) => d.path === doc.path
|
|
366
|
+
? {
|
|
367
|
+
...d,
|
|
368
|
+
content: text,
|
|
369
|
+
encoding: written,
|
|
370
|
+
sourceSha256: result.sourceSha256 ?? d.sourceSha256,
|
|
371
|
+
}
|
|
372
|
+
: d));
|
|
373
|
+
if (!typedDuringSave)
|
|
374
|
+
writeEdits((e) => ({ ...e, [doc.path]: text }));
|
|
375
|
+
return saveSucceeded("save:text", startedAt, doc.path, options.silent);
|
|
376
|
+
}
|
|
377
|
+
catch (e) {
|
|
378
|
+
if (parseHostError(e).code === "write_conflict") {
|
|
379
|
+
setConflict?.(doc.path);
|
|
380
|
+
logPerf("save:conflict", startedAt);
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
return saveFailed(setError, startedAt, e);
|
|
384
|
+
}
|
|
385
|
+
}, [
|
|
386
|
+
writeEdits,
|
|
387
|
+
editsRef,
|
|
388
|
+
setOpenDocs,
|
|
389
|
+
writeImagePayload,
|
|
390
|
+
onImageSaveAs,
|
|
391
|
+
imageSaver,
|
|
392
|
+
imageSaversRef,
|
|
393
|
+
markSavedInPlace,
|
|
394
|
+
setError,
|
|
395
|
+
setConflict,
|
|
396
|
+
blockDrafts,
|
|
397
|
+
performPdfSave,
|
|
398
|
+
]);
|
|
399
|
+
const onSave = useCallback((doc, options = {}) => runSaveOnce(doc.path, `save:${doc.path}`, () => performSave(doc, options)), [performSave, runSaveOnce]);
|
|
400
|
+
// DOCX Save As: grab the live bytes from the editor, write them to a chosen path, then REPLACE the
|
|
401
|
+
// active tab with the saved copy (mirrors text Save As — Word-style "you're now editing the new
|
|
402
|
+
// file", not two tabs). Re-reading the saved file mints a fresh revocable stream token.
|
|
403
|
+
const performDocxSaveAs = useCallback(async (doc, buffer) => {
|
|
404
|
+
// Save As may be reached from Close All/quit for a background DOCX tab. Read its path-owned
|
|
405
|
+
// saver instead of whichever tab is active.
|
|
406
|
+
const ab = buffer ?? (await (docxSaversRef.current.get(doc.path) ?? docxSaver.current)?.());
|
|
407
|
+
if (!ab)
|
|
408
|
+
return null;
|
|
409
|
+
const target = await saveFileDialog(isUntitled(doc.path)
|
|
410
|
+
? untitledSaveName(editsRef.current[doc.path] ?? "", "docx")
|
|
411
|
+
: baseName(doc.path), "docx");
|
|
412
|
+
if (!target)
|
|
413
|
+
return null;
|
|
414
|
+
try {
|
|
415
|
+
await writeDocumentBytes(target, ab);
|
|
416
|
+
incDiskWrites(1);
|
|
417
|
+
return await finishSaveAsSwap(doc, target, {
|
|
418
|
+
markSaved: true,
|
|
419
|
+
clearDirty: () => dropPathFromSet(setDocxDirty, doc.path),
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
catch (e) {
|
|
423
|
+
setError(errorMessage(e));
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}, [finishSaveAsSwap, setDocxDirty, docxSaver, docxSaversRef, editsRef, setError]);
|
|
427
|
+
const onDocxSaveAs = useCallback((doc, buffer) => runSaveOnce(doc.path, `docx-save-as:${doc.path}`, () => performDocxSaveAs(doc, buffer)), [performDocxSaveAs, runSaveOnce]);
|
|
428
|
+
const performSaveAs = useCallback(async (doc) => {
|
|
429
|
+
if (isWebDocument(doc))
|
|
430
|
+
return null;
|
|
431
|
+
if (doc.format === "docx")
|
|
432
|
+
return onDocxSaveAs(doc);
|
|
433
|
+
if (doc.format === "image")
|
|
434
|
+
return onImageSaveAs(doc);
|
|
435
|
+
if (doc.format === "pdf") {
|
|
436
|
+
const target = await saveFileDialog(baseName(doc.path), "pdf");
|
|
437
|
+
if (!target)
|
|
438
|
+
return null;
|
|
439
|
+
return performPdfSave(doc, performance.now(), { target });
|
|
440
|
+
}
|
|
441
|
+
const text = editsRef.current[doc.path] ?? doc.content;
|
|
442
|
+
const target = await saveFileDialog(isUntitled(doc.path)
|
|
443
|
+
? untitledSaveName(editsRef.current[doc.path] ?? "", doc.format === "mdx" ? "mdx" : doc.format)
|
|
444
|
+
: baseName(doc.path), doc.format);
|
|
445
|
+
if (!target)
|
|
446
|
+
return null;
|
|
447
|
+
try {
|
|
448
|
+
const result = writeResult(await writeDocument(target, text, doc.encoding));
|
|
449
|
+
const written = result.encoding;
|
|
450
|
+
incDiskWrites(1);
|
|
451
|
+
markSelfSaved(target); // suppress the watcher echo from our own write on the newly-watched target
|
|
452
|
+
refreshFingerprint(target);
|
|
453
|
+
releaseReplacedBackingPath(doc.path, target);
|
|
454
|
+
const newDoc = {
|
|
455
|
+
path: target,
|
|
456
|
+
displayName: baseName(target),
|
|
457
|
+
format: formatFromPath(target),
|
|
458
|
+
content: text,
|
|
459
|
+
encoding: written,
|
|
460
|
+
sourceSha256: result.sourceSha256,
|
|
461
|
+
};
|
|
462
|
+
setOpenDocs((prev) => prev.map((d) => (d.path === doc.path ? withTabKey(newDoc, d.tabKey) : d)));
|
|
463
|
+
setViews((v) => ({ ...v, [target]: v[doc.path] ?? "preview" }));
|
|
464
|
+
writeEdits((e) => {
|
|
465
|
+
const n = { ...e };
|
|
466
|
+
delete n[doc.path];
|
|
467
|
+
n[target] = text;
|
|
468
|
+
return n;
|
|
469
|
+
});
|
|
470
|
+
clearDraft(doc.path);
|
|
471
|
+
clearFileDraft(doc.path);
|
|
472
|
+
setActivePath(target);
|
|
473
|
+
await recordRecent(newDoc);
|
|
474
|
+
await refreshLibrary();
|
|
475
|
+
toast(`Saved ${baseName(target)}`, "success");
|
|
476
|
+
return target;
|
|
477
|
+
}
|
|
478
|
+
catch (e) {
|
|
479
|
+
setError(errorMessage(e));
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
}, [
|
|
483
|
+
refreshLibrary,
|
|
484
|
+
markSelfSaved,
|
|
485
|
+
releaseReplacedBackingPath,
|
|
486
|
+
writeEdits,
|
|
487
|
+
onDocxSaveAs,
|
|
488
|
+
onImageSaveAs,
|
|
489
|
+
editsRef,
|
|
490
|
+
setOpenDocs,
|
|
491
|
+
setViews,
|
|
492
|
+
setActivePath,
|
|
493
|
+
refreshFingerprint,
|
|
494
|
+
withTabKey,
|
|
495
|
+
setError,
|
|
496
|
+
performPdfSave,
|
|
497
|
+
]);
|
|
498
|
+
const onSaveAs = useCallback((doc) => runSaveOnce(doc.path, `save-as:${doc.path}`, () => performSaveAs(doc)), [performSaveAs, runSaveOnce]);
|
|
499
|
+
onSaveAsRef.current = onSaveAs;
|
|
500
|
+
return {
|
|
501
|
+
onSave,
|
|
502
|
+
onSaveAs,
|
|
503
|
+
onDocxSaveAs,
|
|
504
|
+
onImageSaveAs,
|
|
505
|
+
markSelfSaved,
|
|
506
|
+
refreshFingerprint,
|
|
507
|
+
selfSaved,
|
|
508
|
+
pendingSavePaths,
|
|
509
|
+
pendingSavePathsRef,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type SetStateAction } from "react";
|
|
2
|
+
import type { LoadedDocument } from "@rightkit/document-core/documents/types";
|
|
3
|
+
import type { HtmlSessionPages } from "@rightkit/html-editor/documents/html/sessionTypes";
|
|
4
|
+
/**
|
|
5
|
+
* The shared open-document state: tabs (openDocs + activePath), per-path unsaved TXT buffers
|
|
6
|
+
* (edits, with a synchronous editsRef mirror so a save/close in the same tick as a keystroke reads
|
|
7
|
+
* the latest text), the DOCX dirty set, and the per-HTML-session sub-page buffers. Owned separately
|
|
8
|
+
* so useViewState/useLibrary can take activePath/openDocs while useDocuments takes the setters
|
|
9
|
+
* (breaks the otherwise-circular wiring).
|
|
10
|
+
*/
|
|
11
|
+
export declare function useDocumentTabs(): {
|
|
12
|
+
openDocs: LoadedDocument[];
|
|
13
|
+
setOpenDocs: import("react").Dispatch<SetStateAction<LoadedDocument[]>>;
|
|
14
|
+
activePath: string | null;
|
|
15
|
+
setActivePath: import("react").Dispatch<SetStateAction<string | null>>;
|
|
16
|
+
editsRef: import("react").RefObject<Record<string, string>>;
|
|
17
|
+
writeEdits: (updater: (prev: Record<string, string>) => Record<string, string>) => void;
|
|
18
|
+
writeEditsLive: (path: string, text: string) => void;
|
|
19
|
+
subscribeEdits: (path: string, cb: () => void) => () => void;
|
|
20
|
+
getEditSnapshot: (path: string) => string;
|
|
21
|
+
getExternalSeq: (path: string) => number;
|
|
22
|
+
docxDirty: Set<string>;
|
|
23
|
+
docxDirtyRef: import("react").RefObject<Set<string>>;
|
|
24
|
+
setDocxDirty: (next: SetStateAction<Set<string>>) => void;
|
|
25
|
+
pdfDirty: Set<string>;
|
|
26
|
+
pdfDirtyRef: import("react").RefObject<Set<string>>;
|
|
27
|
+
setPdfDirty: (next: SetStateAction<Set<string>>) => void;
|
|
28
|
+
imageDirty: Set<string>;
|
|
29
|
+
imageDirtyRef: import("react").RefObject<Set<string>>;
|
|
30
|
+
setImageDirty: (next: SetStateAction<Set<string>>) => void;
|
|
31
|
+
htmlPages: HtmlSessionPages;
|
|
32
|
+
htmlPagesRef: import("react").RefObject<HtmlSessionPages>;
|
|
33
|
+
writeHtmlPages: (updater: (prev: HtmlSessionPages) => HtmlSessionPages, options?: {
|
|
34
|
+
immediate?: boolean;
|
|
35
|
+
}) => void;
|
|
36
|
+
htmlActivePage: Record<string, string>;
|
|
37
|
+
setHtmlActivePage: import("react").Dispatch<SetStateAction<Record<string, string>>>;
|
|
38
|
+
mdxGrantsRef: import("react").RefObject<Record<string, Set<string>>>;
|
|
39
|
+
trackMdxGrant: (entryPath: string, grantedPath: string) => void;
|
|
40
|
+
};
|
|
41
|
+
export type DocumentTabs = ReturnType<typeof useDocumentTabs>;
|