@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,387 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// The explorer directory tree: lazy one-level listing per expanded dir (explorer_list_dir),
|
|
3
|
+
// non-recursive watches while a dir is expanded (explorer_watch_dir / explorer_unwatch_dir +
|
|
4
|
+
// "explorer:changed" re-lists just that dir), ARIA tree semantics with roving tabindex and
|
|
5
|
+
// arrow-key navigation, and the shared row chrome (caret, folder/format icon, name, "…" button).
|
|
6
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
7
|
+
import { CaretDown } from "@phosphor-icons/react/dist/icons/CaretDown";
|
|
8
|
+
import { CaretRight } from "@phosphor-icons/react/dist/icons/CaretRight";
|
|
9
|
+
import { Cloud } from "@phosphor-icons/react/dist/icons/Cloud";
|
|
10
|
+
import { Desktop } from "@phosphor-icons/react/dist/icons/Desktop";
|
|
11
|
+
import { DotsThree } from "@phosphor-icons/react/dist/icons/DotsThree";
|
|
12
|
+
import { DownloadSimple } from "@phosphor-icons/react/dist/icons/DownloadSimple";
|
|
13
|
+
import { File } from "@phosphor-icons/react/dist/icons/File";
|
|
14
|
+
import { FileText } from "@phosphor-icons/react/dist/icons/FileText";
|
|
15
|
+
import { Folder } from "@phosphor-icons/react/dist/icons/Folder";
|
|
16
|
+
import { HardDrives } from "@phosphor-icons/react/dist/icons/HardDrives";
|
|
17
|
+
import { House } from "@phosphor-icons/react/dist/icons/House";
|
|
18
|
+
import { FileTypeIcon } from "../../../documents/FileTypeIcon.js";
|
|
19
|
+
import { explorerListDir, explorerUnwatchDir, explorerWatchDir, onExplorerChanged, } from "../../../state/explorer.js";
|
|
20
|
+
import { parseHostError } from "@rightkit/document-core/state/tauriBridge";
|
|
21
|
+
import { childNameOnPath, pathInside, rowKindOf } from "./explorerState.js";
|
|
22
|
+
import { hostAppName } from "../../../appName.js";
|
|
23
|
+
function explorerErrorMessage(e) {
|
|
24
|
+
// Commands reject with the typed {code,message} envelope (e.g. "permission_denied") — show its
|
|
25
|
+
// message inline in the tree rather than toasting or crashing.
|
|
26
|
+
return parseHostError(e).message;
|
|
27
|
+
}
|
|
28
|
+
/** `wantedPath` is the active file; a folder on its way is listed with the next segment's name so
|
|
29
|
+
* the backend keeps that row even past its entry cap (otherwise the reveal stops at that folder). */
|
|
30
|
+
export function useExplorerTree(wantedPath = null) {
|
|
31
|
+
const [expanded, setExpanded] = useState(() => new Set());
|
|
32
|
+
const [dirs, setDirs] = useState(() => new Map());
|
|
33
|
+
const expandedRef = useRef(expanded);
|
|
34
|
+
expandedRef.current = expanded;
|
|
35
|
+
const dirsRef = useRef(dirs);
|
|
36
|
+
dirsRef.current = dirs;
|
|
37
|
+
const wantedRef = useRef(wantedPath);
|
|
38
|
+
wantedRef.current = wantedPath;
|
|
39
|
+
// Synchronous guards so a double-fired effect (StrictMode) or rapid toggle can't double-list or
|
|
40
|
+
// double-watch a dir before the state update lands.
|
|
41
|
+
const watchedRef = useRef(new Set());
|
|
42
|
+
const loadingRef = useRef(new Set());
|
|
43
|
+
const setDir = useCallback((path, next) => {
|
|
44
|
+
setDirs((prev) => {
|
|
45
|
+
const m = new Map(prev);
|
|
46
|
+
m.set(path, next);
|
|
47
|
+
return m;
|
|
48
|
+
});
|
|
49
|
+
}, []);
|
|
50
|
+
const fetchDir = useCallback((path) => {
|
|
51
|
+
if (loadingRef.current.has(path))
|
|
52
|
+
return;
|
|
53
|
+
loadingRef.current.add(path);
|
|
54
|
+
const cur = dirsRef.current.get(path);
|
|
55
|
+
setDir(path, {
|
|
56
|
+
status: "loading",
|
|
57
|
+
entries: cur?.entries ?? [],
|
|
58
|
+
truncated: cur?.truncated ?? false,
|
|
59
|
+
});
|
|
60
|
+
void explorerListDir(path, childNameOnPath(path, wantedRef.current)).then((listing) => {
|
|
61
|
+
loadingRef.current.delete(path);
|
|
62
|
+
setDir(path, {
|
|
63
|
+
status: "ready",
|
|
64
|
+
// A folder never lists itself: a child carrying its parent's path (a link that
|
|
65
|
+
// resolves back to the folder) would make an expanded folder render inside itself
|
|
66
|
+
// without end.
|
|
67
|
+
entries: keepUnchangedEntries(dirsRef.current.get(path)?.entries, listing.entries.filter((e) => e.path !== path)),
|
|
68
|
+
truncated: listing.truncated,
|
|
69
|
+
});
|
|
70
|
+
}, (e) => {
|
|
71
|
+
loadingRef.current.delete(path);
|
|
72
|
+
setDir(path, {
|
|
73
|
+
status: "error",
|
|
74
|
+
entries: cur?.entries ?? [],
|
|
75
|
+
truncated: false,
|
|
76
|
+
message: explorerErrorMessage(e),
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}, [setDir]);
|
|
80
|
+
/** List a dir on first expand; a later manual relistDir forces a fetch. */
|
|
81
|
+
const loadDir = useCallback((path) => {
|
|
82
|
+
const cur = dirsRef.current.get(path);
|
|
83
|
+
if (cur && cur.status !== "error")
|
|
84
|
+
return;
|
|
85
|
+
fetchDir(path);
|
|
86
|
+
}, [fetchDir]);
|
|
87
|
+
const watch = useCallback((path) => {
|
|
88
|
+
if (watchedRef.current.has(path))
|
|
89
|
+
return;
|
|
90
|
+
watchedRef.current.add(path);
|
|
91
|
+
void explorerWatchDir(path);
|
|
92
|
+
}, []);
|
|
93
|
+
const unwatch = useCallback((path) => {
|
|
94
|
+
if (!watchedRef.current.delete(path))
|
|
95
|
+
return;
|
|
96
|
+
void explorerUnwatchDir(path);
|
|
97
|
+
}, []);
|
|
98
|
+
const expandDir = useCallback((path) => {
|
|
99
|
+
setExpanded((prev) => {
|
|
100
|
+
if (prev.has(path))
|
|
101
|
+
return prev;
|
|
102
|
+
const next = new Set(prev);
|
|
103
|
+
next.add(path);
|
|
104
|
+
return next;
|
|
105
|
+
});
|
|
106
|
+
watch(path);
|
|
107
|
+
loadDir(path);
|
|
108
|
+
}, [watch, loadDir]);
|
|
109
|
+
const collapseDir = useCallback((path) => {
|
|
110
|
+
setExpanded((prev) => {
|
|
111
|
+
if (!prev.has(path))
|
|
112
|
+
return prev;
|
|
113
|
+
const next = new Set(prev);
|
|
114
|
+
next.delete(path);
|
|
115
|
+
return next;
|
|
116
|
+
});
|
|
117
|
+
unwatch(path);
|
|
118
|
+
}, [unwatch]);
|
|
119
|
+
const toggleDir = useCallback((path) => {
|
|
120
|
+
if (expandedRef.current.has(path))
|
|
121
|
+
collapseDir(path);
|
|
122
|
+
else
|
|
123
|
+
expandDir(path);
|
|
124
|
+
}, [expandDir, collapseDir]);
|
|
125
|
+
const expandPaths = useCallback((paths) => {
|
|
126
|
+
for (const p of paths)
|
|
127
|
+
expandDir(p);
|
|
128
|
+
}, [expandDir]);
|
|
129
|
+
// One global "explorer:changed" subscription: re-list only the changed dir, and only while it's
|
|
130
|
+
// expanded (its listing is on screen). On unmount, drop the listener and every dir watch.
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
let live = true;
|
|
133
|
+
let un;
|
|
134
|
+
void onExplorerChanged((dir) => {
|
|
135
|
+
if (!live || !expandedRef.current.has(dir))
|
|
136
|
+
return;
|
|
137
|
+
fetchDir(dir);
|
|
138
|
+
}).then((unsub) => {
|
|
139
|
+
if (live)
|
|
140
|
+
un = unsub;
|
|
141
|
+
else
|
|
142
|
+
unsub();
|
|
143
|
+
});
|
|
144
|
+
return () => {
|
|
145
|
+
live = false;
|
|
146
|
+
un?.();
|
|
147
|
+
for (const w of watchedRef.current)
|
|
148
|
+
void explorerUnwatchDir(w);
|
|
149
|
+
watchedRef.current.clear();
|
|
150
|
+
};
|
|
151
|
+
}, [fetchDir]);
|
|
152
|
+
return useMemo(() => ({
|
|
153
|
+
expanded,
|
|
154
|
+
dirs,
|
|
155
|
+
expandDir,
|
|
156
|
+
collapseDir,
|
|
157
|
+
toggleDir,
|
|
158
|
+
expandPaths,
|
|
159
|
+
relistDir: fetchDir,
|
|
160
|
+
}), [expanded, dirs, expandDir, collapseDir, toggleDir, expandPaths, fetchDir]);
|
|
161
|
+
}
|
|
162
|
+
/** A re-list returns fresh objects for every entry. Keep the previous object for each entry that
|
|
163
|
+
* did not change, so memoised rows skip it: a watched folder that changes constantly (a temp or
|
|
164
|
+
* build directory) otherwise re-rendered every one of its rows on every change. */
|
|
165
|
+
function keepUnchangedEntries(previous, next) {
|
|
166
|
+
if (!previous || previous.length === 0)
|
|
167
|
+
return next;
|
|
168
|
+
const byPath = new Map(previous.map((e) => [e.path, e]));
|
|
169
|
+
return next.map((e) => {
|
|
170
|
+
const old = byPath.get(e.path);
|
|
171
|
+
return old &&
|
|
172
|
+
old.name === e.name &&
|
|
173
|
+
old.kind === e.kind &&
|
|
174
|
+
old.modified === e.modified &&
|
|
175
|
+
old.size === e.size
|
|
176
|
+
? old
|
|
177
|
+
: e;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/** Rows of a folder mount in batches of this size, one batch per frame, so expanding a folder
|
|
181
|
+
* with thousands of entries never blocks the page in a single render. */
|
|
182
|
+
const ROW_MOUNT_BATCH = 200;
|
|
183
|
+
function locationIcon(icon) {
|
|
184
|
+
const props = { size: 15, weight: "regular", "aria-hidden": true };
|
|
185
|
+
switch (icon) {
|
|
186
|
+
case "home":
|
|
187
|
+
return _jsx(House, { ...props });
|
|
188
|
+
case "desktop":
|
|
189
|
+
return _jsx(Desktop, { ...props });
|
|
190
|
+
case "documents":
|
|
191
|
+
return _jsx(FileText, { ...props });
|
|
192
|
+
case "downloads":
|
|
193
|
+
return _jsx(DownloadSimple, { ...props });
|
|
194
|
+
case "icloud":
|
|
195
|
+
case "onedrive":
|
|
196
|
+
return _jsx(Cloud, { ...props });
|
|
197
|
+
case "volume":
|
|
198
|
+
case "root":
|
|
199
|
+
return _jsx(HardDrives, { ...props });
|
|
200
|
+
default:
|
|
201
|
+
return _jsx(Folder, { ...props });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function nodeIcon(node) {
|
|
205
|
+
if (node.kind === "dir") {
|
|
206
|
+
if (node.icon !== undefined)
|
|
207
|
+
return locationIcon(node.icon);
|
|
208
|
+
return _jsx(Folder, { size: 15, weight: "fill", "aria-hidden": true });
|
|
209
|
+
}
|
|
210
|
+
if (node.kind === "file") {
|
|
211
|
+
return (_jsx(FileTypeIcon, { format: (node.format ?? "unknown"), path: node.path, size: 14 }));
|
|
212
|
+
}
|
|
213
|
+
return _jsx(File, { size: 15, weight: "regular", "aria-hidden": true });
|
|
214
|
+
}
|
|
215
|
+
const TreeRow = memo(function TreeRow({ node, depth, parentPath, expanded, selected, open, tabbed, actions, }) {
|
|
216
|
+
const expandable = node.kind === "dir";
|
|
217
|
+
const unsupported = node.kind === "other";
|
|
218
|
+
return (_jsxs("div", { role: "treeitem", "aria-label": node.name, "aria-description": open ? "Open in a tab" : undefined, "aria-expanded": expandable ? expanded : undefined, "aria-selected": selected, "aria-disabled": unsupported || undefined, "data-vr-tree-path": node.path, "data-vr-parent": parentPath, tabIndex: tabbed ? 0 : -1, title: unsupported ? `${node.path} — ${hostAppName()} can't open this file type` : node.path, className: `vr-x-row${unsupported ? " vr-x-dim" : ""}`, style: { paddingLeft: 8 + depth * 14 }, onClick: () => {
|
|
219
|
+
if (expandable)
|
|
220
|
+
actions.toggleDir(node.path);
|
|
221
|
+
else if (node.kind === "file")
|
|
222
|
+
actions.onActivateRow(node);
|
|
223
|
+
}, onFocus: () => actions.setFocusPath(node.path), onContextMenu: (e) => {
|
|
224
|
+
e.preventDefault();
|
|
225
|
+
actions.onOpenMenu(node, e.clientX, e.clientY);
|
|
226
|
+
}, children: [_jsx("span", { className: "vr-x-caret", "aria-hidden": true, children: expandable ? (expanded ? (_jsx(CaretDown, { size: 10, weight: "regular" })) : (_jsx(CaretRight, { size: 10, weight: "regular" }))) : null }), _jsx("span", { className: "vr-x-icn", "aria-hidden": true, children: nodeIcon(node) }), _jsx("span", { className: "vr-x-name", children: node.name }), open && _jsx("span", { className: "vr-x-open", title: "Open in a tab", "aria-hidden": true }), _jsx("button", { type: "button", tabIndex: -1, "aria-label": `Actions for ${node.name}`, className: "vr-x-dots vr-press vr-focus", onClick: (e) => {
|
|
227
|
+
e.stopPropagation();
|
|
228
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
229
|
+
actions.onOpenMenu(node, r.right, r.bottom + 2);
|
|
230
|
+
}, onContextMenu: (e) => {
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
e.stopPropagation();
|
|
233
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
234
|
+
actions.onOpenMenu(node, r.right, r.bottom + 2);
|
|
235
|
+
}, children: _jsx(DotsThree, { size: 14, weight: "bold", "aria-hidden": true }) })] }));
|
|
236
|
+
});
|
|
237
|
+
// One spec object per listing entry, so a memoised row sees the same `node` on every render.
|
|
238
|
+
const specByEntry = new WeakMap();
|
|
239
|
+
function entryToSpec(e) {
|
|
240
|
+
const cached = specByEntry.get(e);
|
|
241
|
+
if (cached)
|
|
242
|
+
return cached;
|
|
243
|
+
const kind = rowKindOf(e.kind);
|
|
244
|
+
const spec = {
|
|
245
|
+
path: e.path,
|
|
246
|
+
name: e.name,
|
|
247
|
+
kind,
|
|
248
|
+
format: kind === "file" ? e.kind : undefined,
|
|
249
|
+
};
|
|
250
|
+
specByEntry.set(e, spec);
|
|
251
|
+
return spec;
|
|
252
|
+
}
|
|
253
|
+
function ChildrenRows({ dir, depth, ctx }) {
|
|
254
|
+
const st = ctx.tree.dirs.get(dir);
|
|
255
|
+
const pad = { paddingLeft: 24 + depth * 14 };
|
|
256
|
+
const total = st?.entries.length ?? 0;
|
|
257
|
+
const [mounted, setMounted] = useState(ROW_MOUNT_BATCH);
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
if (mounted >= total)
|
|
260
|
+
return;
|
|
261
|
+
const frame = requestAnimationFrame(() => setMounted((count) => Math.min(total, count + ROW_MOUNT_BATCH)));
|
|
262
|
+
return () => cancelAnimationFrame(frame);
|
|
263
|
+
}, [mounted, total]);
|
|
264
|
+
// The row on the way to the file being revealed (or the active file) must exist at once, so the
|
|
265
|
+
// reveal can scroll to it; rows mounted for it stay mounted once the reveal has finished.
|
|
266
|
+
const wanted = ctx.revealPath ?? ctx.activePath;
|
|
267
|
+
const wantedIndex = useMemo(() => {
|
|
268
|
+
if (!st || !wanted || total <= ROW_MOUNT_BATCH)
|
|
269
|
+
return -1;
|
|
270
|
+
return st.entries.findIndex((e) => e.path === wanted || pathInside(e.path, wanted));
|
|
271
|
+
}, [st, wanted, total]);
|
|
272
|
+
const limit = Math.min(total, Math.max(mounted, wantedIndex + 1));
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
if (limit > mounted)
|
|
275
|
+
setMounted(limit);
|
|
276
|
+
}, [limit, mounted]);
|
|
277
|
+
if (!st || (st.status === "loading" && st.entries.length === 0)) {
|
|
278
|
+
return (_jsx("div", { className: "vr-x-empty", style: pad, children: "Loading\u2026" }));
|
|
279
|
+
}
|
|
280
|
+
if (st.status === "error") {
|
|
281
|
+
return (_jsx("div", { className: "vr-x-note", role: "note", style: pad, children: st.message ?? "Couldn't list this folder." }));
|
|
282
|
+
}
|
|
283
|
+
return (_jsxs(_Fragment, { children: [st.entries.length === 0 && (_jsx("div", { className: "vr-x-empty", style: pad, children: "Empty folder" })), (limit < total ? st.entries.slice(0, limit) : st.entries).map((e) => (_jsx(ExplorerNode, { node: entryToSpec(e), depth: depth, parentPath: dir, ctx: ctx }, e.path))), st.truncated && limit >= total && (_jsx("div", { className: "vr-x-empty", style: pad, children: "\u2026 (more)" }))] }));
|
|
284
|
+
}
|
|
285
|
+
function ExplorerNode({ node, depth, parentPath, ctx, }) {
|
|
286
|
+
const expanded = node.kind === "dir" && ctx.tree.expanded.has(node.path);
|
|
287
|
+
const selected = node.path === ctx.activePath;
|
|
288
|
+
const open = !selected && node.kind !== "dir" && ctx.openPaths.has(node.path);
|
|
289
|
+
const tabbed = ctx.focusPath === node.path || (ctx.focusPath == null && ctx.firstPath === node.path);
|
|
290
|
+
return (_jsxs(_Fragment, { children: [_jsx(TreeRow, { node: node, depth: depth, parentPath: parentPath, expanded: expanded, selected: selected, open: open, tabbed: tabbed, actions: ctx.actions }), expanded && (_jsx("div", { role: "group", children: _jsx(ChildrenRows, { dir: node.path, depth: depth + 1, ctx: ctx }) }))] }));
|
|
291
|
+
}
|
|
292
|
+
/** One ARIA tree (roving tabindex, arrow keys) over a set of root nodes. Used for the Locations
|
|
293
|
+
* section (roots = explorer_locations) and for the Pinned section (roots = pinned dirs+files). */
|
|
294
|
+
export function ExplorerTree({ ariaLabel, roots, tree, activePath, revealPath = null, openPaths, onActivateRow, onOpenMenu, }) {
|
|
295
|
+
const ref = useRef(null);
|
|
296
|
+
const [focusPath, setFocusPath] = useState(null);
|
|
297
|
+
// The latest callbacks behind one object whose identity never changes (see RowActions).
|
|
298
|
+
const latest = useRef({ toggleDir: tree.toggleDir, onActivateRow, onOpenMenu });
|
|
299
|
+
latest.current = { toggleDir: tree.toggleDir, onActivateRow, onOpenMenu };
|
|
300
|
+
const actions = useMemo(() => ({
|
|
301
|
+
toggleDir: (path) => latest.current.toggleDir(path),
|
|
302
|
+
setFocusPath,
|
|
303
|
+
onActivateRow: (node) => latest.current.onActivateRow(node),
|
|
304
|
+
onOpenMenu: (node, x, y) => latest.current.onOpenMenu(node, x, y),
|
|
305
|
+
}), []);
|
|
306
|
+
const ctx = useMemo(() => ({
|
|
307
|
+
tree,
|
|
308
|
+
focusPath,
|
|
309
|
+
firstPath: roots[0]?.path,
|
|
310
|
+
activePath,
|
|
311
|
+
revealPath,
|
|
312
|
+
openPaths,
|
|
313
|
+
actions,
|
|
314
|
+
}), [tree, focusPath, roots, activePath, revealPath, openPaths, actions]);
|
|
315
|
+
const onKeyDown = (e) => {
|
|
316
|
+
const container = ref.current;
|
|
317
|
+
if (!container)
|
|
318
|
+
return;
|
|
319
|
+
const items = Array.from(container.querySelectorAll('[role="treeitem"]'));
|
|
320
|
+
if (items.length === 0)
|
|
321
|
+
return;
|
|
322
|
+
const activeEl = document.activeElement;
|
|
323
|
+
let idx = items.indexOf(activeEl);
|
|
324
|
+
if (idx < 0)
|
|
325
|
+
idx = items.findIndex((el) => el.dataset.vrTreePath === focusPath);
|
|
326
|
+
if (idx < 0)
|
|
327
|
+
idx = 0;
|
|
328
|
+
const cur = items[idx];
|
|
329
|
+
const focusItem = (el) => {
|
|
330
|
+
if (!el)
|
|
331
|
+
return;
|
|
332
|
+
el.focus();
|
|
333
|
+
el.scrollIntoView?.({ block: "nearest" });
|
|
334
|
+
};
|
|
335
|
+
switch (e.key) {
|
|
336
|
+
case "ArrowDown":
|
|
337
|
+
e.preventDefault();
|
|
338
|
+
focusItem(items[Math.min(items.length - 1, idx + 1)]);
|
|
339
|
+
break;
|
|
340
|
+
case "ArrowUp":
|
|
341
|
+
e.preventDefault();
|
|
342
|
+
focusItem(items[Math.max(0, idx - 1)]);
|
|
343
|
+
break;
|
|
344
|
+
case "Home":
|
|
345
|
+
e.preventDefault();
|
|
346
|
+
focusItem(items[0]);
|
|
347
|
+
break;
|
|
348
|
+
case "End":
|
|
349
|
+
e.preventDefault();
|
|
350
|
+
focusItem(items[items.length - 1]);
|
|
351
|
+
break;
|
|
352
|
+
case "ArrowRight": {
|
|
353
|
+
e.preventDefault();
|
|
354
|
+
const state = cur.getAttribute("aria-expanded");
|
|
355
|
+
const path = cur.dataset.vrTreePath;
|
|
356
|
+
if (state === "false" && path)
|
|
357
|
+
tree.expandDir(path);
|
|
358
|
+
else if (state === "true")
|
|
359
|
+
focusItem(items[idx + 1]);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
case "ArrowLeft": {
|
|
363
|
+
e.preventDefault();
|
|
364
|
+
const state = cur.getAttribute("aria-expanded");
|
|
365
|
+
const path = cur.dataset.vrTreePath;
|
|
366
|
+
if (state === "true" && path) {
|
|
367
|
+
tree.collapseDir(path);
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
const parent = cur.dataset.vrParent;
|
|
371
|
+
if (parent) {
|
|
372
|
+
const parentEl = items.find((el) => el.dataset.vrTreePath === parent);
|
|
373
|
+
if (parentEl)
|
|
374
|
+
focusItem(parentEl);
|
|
375
|
+
}
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
case "Enter":
|
|
379
|
+
case " ": {
|
|
380
|
+
e.preventDefault();
|
|
381
|
+
cur.click();
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
return (_jsx("div", { role: "tree", "aria-label": ariaLabel, ref: ref, onKeyDown: onKeyDown, className: "vr-x-tree", children: roots.map((r, i) => (_jsx(ExplorerNode, { node: r, depth: 0, parentPath: undefined, ctx: ctx }, `${i}:${r.path}`))) }));
|
|
387
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const EXPLORER_SETTINGS_KEY = "vr.explorer.v1";
|
|
2
|
+
export interface ExplorerSectionState {
|
|
3
|
+
pinned: boolean;
|
|
4
|
+
recent: boolean;
|
|
5
|
+
locations: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface ExplorerPersisted {
|
|
8
|
+
/** Dir paths currently expanded in the Locations tree / Pinned folders. */
|
|
9
|
+
expanded: string[];
|
|
10
|
+
sections: ExplorerSectionState;
|
|
11
|
+
/** Sidebar pixel width (also mirrored to localStorage "vr.sidebar.width"). */
|
|
12
|
+
width: number;
|
|
13
|
+
/** Explorer scroll offset (scrollTop of the nav container). */
|
|
14
|
+
scroll: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function defaultExplorerPersisted(): ExplorerPersisted;
|
|
17
|
+
/** Load the persisted explorer record once (SQLite mirror via settings_get); merges over defaults
|
|
18
|
+
* and caches the result so the sidebar shell and the sections share one IPC read. */
|
|
19
|
+
export declare function loadExplorerPersisted(): Promise<ExplorerPersisted>;
|
|
20
|
+
/** Synchronous read of the last-loaded record (defaults before hydration resolves). */
|
|
21
|
+
export declare function explorerPersistedSync(): ExplorerPersisted;
|
|
22
|
+
/** Merge a patch into the record and schedule a debounced (~300ms) mirror write. */
|
|
23
|
+
export declare function saveExplorerPersisted(patch: Partial<ExplorerPersisted>): void;
|
|
24
|
+
/** Case-insensitive "is `child` strictly inside `dir`" on path segments. */
|
|
25
|
+
export declare function pathInside(dir: string, child: string): boolean;
|
|
26
|
+
/** The chain of ancestor directories of `filePath` from `rootDir` (inclusive) down to the file's
|
|
27
|
+
* parent dir — used by "reveal the active file" to expand each level. Returns [] when the file is
|
|
28
|
+
* not under rootDir. Ancestors are rebuilt from the file path's own segments so they keep the
|
|
29
|
+
* backend's canonical casing (they must string-match the `path` keys of tree dir entries). */
|
|
30
|
+
export declare function ancestorDirs(filePath: string, rootDir: string): string[];
|
|
31
|
+
/** The name of the child of `dir` on the way to `target` (the target itself when it is a direct
|
|
32
|
+
* child), or undefined when `dir` is not a strict ancestor of `target`. Separator-agnostic:
|
|
33
|
+
* `C:\Temp` with `C:\Temp\abc\x.md` gives "abc". */
|
|
34
|
+
export declare function childNameOnPath(dir: string, target: string | null): string | undefined;
|
|
35
|
+
/** ExplorerEntry.kind → row kind. "dir" stays "dir", a supported format id becomes "file"
|
|
36
|
+
* (openable, broker-granted path), anything else is "other" (real path, no grant — never open). */
|
|
37
|
+
export declare function rowKindOf(kind: string): "dir" | "file" | "other";
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Shared state for the explorer sidebar (Part C): the "vr.explorer.v1" settings-mirror record
|
|
2
|
+
// (expanded dirs, section open/closed, sidebar width, scroll position) with a module-level cache +
|
|
3
|
+
// debounced write, plus the small path helpers the lazy tree and the active-file reveal need.
|
|
4
|
+
import { getSettingsMirror, setSettingsMirror } from "@rightkit/document-core/state/tauriBridge";
|
|
5
|
+
import { splitPath } from "../SidebarPathUtils.js";
|
|
6
|
+
export const EXPLORER_SETTINGS_KEY = "vr.explorer.v1";
|
|
7
|
+
export function defaultExplorerPersisted() {
|
|
8
|
+
return {
|
|
9
|
+
expanded: [],
|
|
10
|
+
sections: { pinned: true, recent: true, locations: true },
|
|
11
|
+
width: 0,
|
|
12
|
+
scroll: 0,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
let cache = null;
|
|
16
|
+
/** Set once any save has landed — a late-resolving load must not stomp a fresher write. */
|
|
17
|
+
let mutated = false;
|
|
18
|
+
let writeTimer = null;
|
|
19
|
+
function sanitize(raw) {
|
|
20
|
+
const fallback = defaultExplorerPersisted();
|
|
21
|
+
if (raw === null || typeof raw !== "object")
|
|
22
|
+
return fallback;
|
|
23
|
+
const o = raw;
|
|
24
|
+
const s = o.sections;
|
|
25
|
+
return {
|
|
26
|
+
expanded: Array.isArray(o.expanded)
|
|
27
|
+
? o.expanded.filter((p) => typeof p === "string")
|
|
28
|
+
: [],
|
|
29
|
+
sections: {
|
|
30
|
+
pinned: typeof s?.pinned === "boolean" ? s.pinned : true,
|
|
31
|
+
recent: typeof s?.recent === "boolean" ? s.recent : true,
|
|
32
|
+
locations: typeof s?.locations === "boolean" ? s.locations : true,
|
|
33
|
+
},
|
|
34
|
+
width: typeof o.width === "number" && Number.isFinite(o.width) ? o.width : 0,
|
|
35
|
+
scroll: typeof o.scroll === "number" && Number.isFinite(o.scroll) ? o.scroll : 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Load the persisted explorer record once (SQLite mirror via settings_get); merges over defaults
|
|
39
|
+
* and caches the result so the sidebar shell and the sections share one IPC read. */
|
|
40
|
+
export async function loadExplorerPersisted() {
|
|
41
|
+
if (cache)
|
|
42
|
+
return cache;
|
|
43
|
+
const raw = await getSettingsMirror(EXPLORER_SETTINGS_KEY);
|
|
44
|
+
if (mutated && cache)
|
|
45
|
+
return cache;
|
|
46
|
+
let parsed = defaultExplorerPersisted();
|
|
47
|
+
if (raw != null) {
|
|
48
|
+
try {
|
|
49
|
+
parsed = sanitize(JSON.parse(raw));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
/* corrupt mirror value — keep defaults */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!mutated)
|
|
56
|
+
cache = parsed;
|
|
57
|
+
return cache ?? parsed;
|
|
58
|
+
}
|
|
59
|
+
/** Synchronous read of the last-loaded record (defaults before hydration resolves). */
|
|
60
|
+
export function explorerPersistedSync() {
|
|
61
|
+
return cache ?? defaultExplorerPersisted();
|
|
62
|
+
}
|
|
63
|
+
/** Merge a patch into the record and schedule a debounced (~300ms) mirror write. */
|
|
64
|
+
export function saveExplorerPersisted(patch) {
|
|
65
|
+
mutated = true;
|
|
66
|
+
cache = { ...(cache ?? defaultExplorerPersisted()), ...patch };
|
|
67
|
+
if (writeTimer != null)
|
|
68
|
+
window.clearTimeout(writeTimer);
|
|
69
|
+
writeTimer = window.setTimeout(() => {
|
|
70
|
+
writeTimer = null;
|
|
71
|
+
if (cache)
|
|
72
|
+
void setSettingsMirror(EXPLORER_SETTINGS_KEY, JSON.stringify(cache));
|
|
73
|
+
}, 300);
|
|
74
|
+
}
|
|
75
|
+
// ── Path helpers (separator-agnostic; the tree sees both / and \ paths) ──────
|
|
76
|
+
/** Case-insensitive "is `child` strictly inside `dir`" on path segments. */
|
|
77
|
+
export function pathInside(dir, child) {
|
|
78
|
+
const d = splitPath(dir).join("/").toLowerCase();
|
|
79
|
+
const c = splitPath(child).join("/").toLowerCase();
|
|
80
|
+
return c.length > d.length && c.startsWith(d + "/");
|
|
81
|
+
}
|
|
82
|
+
/** The chain of ancestor directories of `filePath` from `rootDir` (inclusive) down to the file's
|
|
83
|
+
* parent dir — used by "reveal the active file" to expand each level. Returns [] when the file is
|
|
84
|
+
* not under rootDir. Ancestors are rebuilt from the file path's own segments so they keep the
|
|
85
|
+
* backend's canonical casing (they must string-match the `path` keys of tree dir entries). */
|
|
86
|
+
export function ancestorDirs(filePath, rootDir) {
|
|
87
|
+
if (!pathInside(rootDir, filePath))
|
|
88
|
+
return [];
|
|
89
|
+
const rootSegs = splitPath(rootDir);
|
|
90
|
+
const fileSegs = splitPath(filePath);
|
|
91
|
+
const sep = filePath.includes("\\") ? "\\" : "/";
|
|
92
|
+
const out = [];
|
|
93
|
+
for (let i = rootSegs.length; i < fileSegs.length; i++) {
|
|
94
|
+
if (i === rootSegs.length) {
|
|
95
|
+
out.push(rootDir);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const head = fileSegs[0] ?? "";
|
|
99
|
+
if (/^[A-Za-z]:$/.test(head)) {
|
|
100
|
+
out.push(head + sep + fileSegs.slice(1, i).join(sep));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
out.push((filePath.startsWith("/") ? "/" : "") + fileSegs.slice(0, i).join(sep));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
/** The name of the child of `dir` on the way to `target` (the target itself when it is a direct
|
|
109
|
+
* child), or undefined when `dir` is not a strict ancestor of `target`. Separator-agnostic:
|
|
110
|
+
* `C:\Temp` with `C:\Temp\abc\x.md` gives "abc". */
|
|
111
|
+
export function childNameOnPath(dir, target) {
|
|
112
|
+
if (!target || !pathInside(dir, target))
|
|
113
|
+
return undefined;
|
|
114
|
+
return splitPath(target)[splitPath(dir).length];
|
|
115
|
+
}
|
|
116
|
+
const KNOWN_FORMATS = new Set([
|
|
117
|
+
"md",
|
|
118
|
+
"mdx",
|
|
119
|
+
"txt",
|
|
120
|
+
"pdf",
|
|
121
|
+
"html",
|
|
122
|
+
"docx",
|
|
123
|
+
"image",
|
|
124
|
+
"web",
|
|
125
|
+
"unknown",
|
|
126
|
+
]);
|
|
127
|
+
/** ExplorerEntry.kind → row kind. "dir" stays "dir", a supported format id becomes "file"
|
|
128
|
+
* (openable, broker-granted path), anything else is "other" (real path, no grant — never open). */
|
|
129
|
+
export function rowKindOf(kind) {
|
|
130
|
+
if (kind === "dir")
|
|
131
|
+
return "dir";
|
|
132
|
+
if (kind === "other" || !KNOWN_FORMATS.has(kind))
|
|
133
|
+
return "other";
|
|
134
|
+
return "file";
|
|
135
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type DocumentFormat } from "@rightkit/document-core/app/resourceCensus";
|
|
2
|
+
/** Maximum number of ordinary document routers kept paint-warm (DocumentRouterTable's own limit). */
|
|
3
|
+
export declare const ORDINARY_LIVE_PANE_LIMIT = 2;
|
|
4
|
+
export declare const LIVE_TAB_LIMIT = 8;
|
|
5
|
+
export type TabMountPolicyInput = Readonly<{
|
|
6
|
+
activePath: string | null;
|
|
7
|
+
previousActivePath: string | null;
|
|
8
|
+
protectedPaths: ReadonlySet<string>;
|
|
9
|
+
/** Recently-active paths, most-recent-first. Only the first LIVE_TAB_LIMIT entries stay live. */
|
|
10
|
+
recentPaths?: readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* Document format per path, for the intrinsic-cost floor in `candidateWeight`. Optional and
|
|
13
|
+
* missing entries fall back to the census-only signal (matches prior behavior) so existing
|
|
14
|
+
* callers/tests that don't care about the eviction-cost-decay case keep working unchanged.
|
|
15
|
+
*/
|
|
16
|
+
formatByPath?: ReadonlyMap<string, DocumentFormat>;
|
|
17
|
+
}>;
|
|
18
|
+
export type TabMountCounters = Readonly<{
|
|
19
|
+
shellCount: number;
|
|
20
|
+
liveCount: number;
|
|
21
|
+
protectedLiveCount: number;
|
|
22
|
+
ordinaryLiveLimit: number;
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* Dirty text sessions may leave the live set only after their authoritative source (or a
|
|
26
|
+
* structured draft snapshot) is owned above the router. Binary editors deliberately never get
|
|
27
|
+
* this escape hatch: their mounted runtime remains their only authoritative dirty state.
|
|
28
|
+
*/
|
|
29
|
+
export declare function dirtyPaneMayShell(format: string, hasExternalSourceOrDraftOwner: boolean): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Active + previous-active + a resource-weighted MRU window of recently-active panes stay live.
|
|
32
|
+
* `protectedPaths` protection is deliberately additive and unbounded: a draft or pending save
|
|
33
|
+
* cannot be evicted merely because another tab became active or aged out of the MRU window.
|
|
34
|
+
*
|
|
35
|
+
* Beyond that, every candidate is checked against the resource census before it can age out:
|
|
36
|
+
* a document reporting pinned state (dirty with no external owner — `documentHasPinnedState`)
|
|
37
|
+
* is kept live no matter its MRU rank or weight, full stop — there is no limit or heuristic that
|
|
38
|
+
* overrides this. A document whose state is dirty-but-externalized, or simply clean, competes for
|
|
39
|
+
* the MRU budget like any other, weighted by `candidateWeight` so heavyweight panes shell out
|
|
40
|
+
* before lightweight ones at the same recency — and stay counted as heavyweight even once shelled,
|
|
41
|
+
* via `formatByPath`'s intrinsic-cost floor (see `candidateWeight`'s own doc comment).
|
|
42
|
+
*/
|
|
43
|
+
export declare function liveDocumentPaths({ activePath, previousActivePath, protectedPaths, recentPaths, formatByPath, }: TabMountPolicyInput): Set<string>;
|
|
44
|
+
/** Deterministic observability for stress tests & diagnostics; never reads wall-clock time. */
|
|
45
|
+
export declare function tabMountCounters(documentPaths: readonly string[], livePaths: ReadonlySet<string>, protectedPaths: ReadonlySet<string>): TabMountCounters;
|