@tooee/view 0.11.0 → 0.13.0
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/dist/components/subviews/diff-subview.d.ts.map +1 -1
- package/dist/components/subviews/diff-subview.js +16 -4
- package/dist/components/subviews/diff-subview.js.map +1 -1
- package/dist/components/subviews/image-subview.d.ts.map +1 -1
- package/dist/components/subviews/image-subview.js +2 -1
- package/dist/components/subviews/image-subview.js.map +1 -1
- package/dist/components/subviews/index.d.ts.map +1 -1
- package/dist/components/subviews/index.js.map +1 -1
- package/dist/components/subviews/markdown-subview.d.ts +3 -1
- package/dist/components/subviews/markdown-subview.d.ts.map +1 -1
- package/dist/components/subviews/markdown-subview.js +21 -6
- package/dist/components/subviews/markdown-subview.js.map +1 -1
- package/dist/components/subviews/table-subview.d.ts.map +1 -1
- package/dist/components/subviews/table-subview.js +4 -3
- package/dist/components/subviews/table-subview.js.map +1 -1
- package/dist/components/view-screen.d.ts +3 -1
- package/dist/components/view-screen.d.ts.map +1 -1
- package/dist/components/view-screen.js +13 -3
- package/dist/components/view-screen.js.map +1 -1
- package/dist/content-loader-store.d.ts.map +1 -1
- package/dist/content-loader-store.js.map +1 -1
- package/dist/default-provider.d.ts.map +1 -1
- package/dist/default-provider.js.map +1 -1
- package/dist/directory-provider.d.ts.map +1 -1
- package/dist/directory-provider.js.map +1 -1
- package/dist/directory-view.d.ts.map +1 -1
- package/dist/directory-view.js +5 -2
- package/dist/directory-view.js.map +1 -1
- package/dist/folds/fold-decorations.d.ts.map +1 -1
- package/dist/folds/fold-decorations.js.map +1 -1
- package/dist/folds/fold-model.d.ts +5 -0
- package/dist/folds/fold-model.d.ts.map +1 -1
- package/dist/folds/fold-model.js +11 -0
- package/dist/folds/fold-model.js.map +1 -1
- package/dist/folds/markdown-heading-folds.d.ts +3 -0
- package/dist/folds/markdown-heading-folds.d.ts.map +1 -1
- package/dist/folds/markdown-heading-folds.js +6 -2
- package/dist/folds/markdown-heading-folds.js.map +1 -1
- package/dist/folds/use-folds.d.ts +30 -5
- package/dist/folds/use-folds.d.ts.map +1 -1
- package/dist/folds/use-folds.js +65 -75
- package/dist/folds/use-folds.js.map +1 -1
- package/dist/hooks/use-content-document.d.ts.map +1 -1
- package/dist/hooks/use-content-document.js +3 -2
- package/dist/hooks/use-content-document.js.map +1 -1
- package/dist/hooks/use-content-loader.d.ts.map +1 -1
- package/dist/hooks/use-content-loader.js.map +1 -1
- package/dist/hooks/use-view-command-context.d.ts.map +1 -1
- package/dist/hooks/use-view-command-context.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/launch.d.ts +9 -1
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js.map +1 -1
- package/dist/link-handlers.d.ts.map +1 -1
- package/dist/link-handlers.js.map +1 -1
- package/dist/markdown-links.d.ts.map +1 -1
- package/dist/markdown-links.js.map +1 -1
- package/dist/outline/markdown-outline.d.ts +24 -0
- package/dist/outline/markdown-outline.d.ts.map +1 -0
- package/dist/outline/markdown-outline.js +25 -0
- package/dist/outline/markdown-outline.js.map +1 -0
- package/dist/outline/outline-panel.d.ts +17 -0
- package/dist/outline/outline-panel.d.ts.map +1 -0
- package/dist/outline/outline-panel.js +106 -0
- package/dist/outline/outline-panel.js.map +1 -0
- package/dist/outline/use-outline.d.ts +53 -0
- package/dist/outline/use-outline.d.ts.map +1 -0
- package/dist/outline/use-outline.js +131 -0
- package/dist/outline/use-outline.js.map +1 -0
- package/dist/standalone-router.d.ts.map +1 -1
- package/dist/standalone-router.js +4 -1
- package/dist/standalone-router.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/view.d.ts +3 -1
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +2 -2
- package/dist/view.js.map +1 -1
- package/package.json +14 -13
- package/src/components/subviews/diff-subview.tsx +21 -4
- package/src/components/subviews/image-subview.tsx +2 -1
- package/src/components/subviews/index.ts +6 -0
- package/src/components/subviews/markdown-subview.tsx +33 -4
- package/src/components/subviews/table-subview.tsx +6 -3
- package/src/components/view-screen.tsx +15 -1
- package/src/content-loader-store.ts +23 -0
- package/src/default-provider.ts +18 -0
- package/src/directory-provider.ts +7 -0
- package/src/directory-view.tsx +7 -2
- package/src/folds/fold-decorations.ts +4 -0
- package/src/folds/fold-model.ts +21 -0
- package/src/folds/markdown-heading-folds.ts +14 -2
- package/src/folds/use-folds.ts +110 -78
- package/src/hooks/use-content-document.ts +5 -2
- package/src/hooks/use-content-loader.ts +7 -0
- package/src/hooks/use-view-command-context.ts +1 -0
- package/src/index.ts +17 -0
- package/src/launch.tsx +11 -1
- package/src/link-handlers.ts +10 -0
- package/src/markdown-links.ts +16 -0
- package/src/outline/markdown-outline.ts +48 -0
- package/src/outline/outline-panel.tsx +184 -0
- package/src/outline/use-outline.ts +212 -0
- package/src/standalone-router.tsx +24 -2
- package/src/types.ts +11 -0
- package/src/view.tsx +10 -0
package/src/folds/use-folds.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { CommandContext } from "@tooee/commands";
|
|
1
|
+
import { useActions, useCommandGroup } from "@tooee/commands";
|
|
2
|
+
import type { ActionDefinition, CommandContext, CommandHandler } from "@tooee/commands";
|
|
3
3
|
import type { DecorationLayer } from "@tooee/renderers";
|
|
4
4
|
import type { NavigationState } from "@tooee/shell";
|
|
5
5
|
import { useMemo, useState } from "react";
|
|
@@ -39,10 +39,12 @@ export const useFoldState = function useFoldState<T>(
|
|
|
39
39
|
decorations: DecorationLayer[]
|
|
40
40
|
): FoldState<T> {
|
|
41
41
|
const [closedKeys, setClosedKeys] = useState<ReadonlySet<Key>>(EMPTY_KEYS);
|
|
42
|
+
|
|
42
43
|
const view = useMemo(
|
|
43
44
|
() => computeFoldView(rows.length, ranges, closedKeys),
|
|
44
45
|
[rows.length, ranges, closedKeys]
|
|
45
46
|
);
|
|
47
|
+
|
|
46
48
|
const folded = view.visibleToSource.length < rows.length;
|
|
47
49
|
|
|
48
50
|
const visibleRows = useMemo(
|
|
@@ -50,15 +52,18 @@ export const useFoldState = function useFoldState<T>(
|
|
|
50
52
|
folded
|
|
51
53
|
? view.visibleToSource.flatMap((index) => {
|
|
52
54
|
const row = rows[index];
|
|
55
|
+
|
|
53
56
|
return row === undefined ? [] : [row];
|
|
54
57
|
})
|
|
55
58
|
: rows,
|
|
56
59
|
[folded, rows, view]
|
|
57
60
|
);
|
|
61
|
+
|
|
58
62
|
const visibleDecorations = useMemo(
|
|
59
63
|
() => (folded ? projectDecorationLayers(decorations, view) : decorations),
|
|
60
64
|
[folded, decorations, view]
|
|
61
65
|
);
|
|
66
|
+
|
|
62
67
|
const rowNumbers = useMemo(
|
|
63
68
|
() => (folded ? view.visibleToSource.map((index) => index + 1) : undefined),
|
|
64
69
|
[folded, view]
|
|
@@ -76,114 +81,141 @@ export const useFoldState = function useFoldState<T>(
|
|
|
76
81
|
};
|
|
77
82
|
};
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
/** Fold operations. Row arguments are full-array row indices. */
|
|
85
|
+
export interface FoldActions {
|
|
86
|
+
/** Toggle the innermost fold that contains `row`. Returns false when no fold contains it. */
|
|
87
|
+
toggle: (row: number) => boolean;
|
|
88
|
+
/** Close the innermost open fold that contains `row`. Returns false when there is none. */
|
|
89
|
+
close: (row: number) => boolean;
|
|
90
|
+
/** Open the innermost closed fold that contains `row`. Returns false when there is none. */
|
|
91
|
+
open: (row: number) => boolean;
|
|
92
|
+
/** Open a fold at `row` when one is closed, else close the innermost open fold. */
|
|
93
|
+
toggleBlock: (row: number) => boolean;
|
|
94
|
+
closeAll: () => void;
|
|
95
|
+
openAll: () => void;
|
|
96
|
+
}
|
|
82
97
|
|
|
83
98
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
99
|
+
* Fold operations over a fold state. Any change that hides the document
|
|
100
|
+
* cursor row first moves the cursor to the header of the outermost closed
|
|
101
|
+
* fold that hides it, and key preservation keeps it there. This holds for
|
|
102
|
+
* every target row, so a fold closed from the outline also keeps the cursor
|
|
103
|
+
* visible.
|
|
88
104
|
*/
|
|
89
|
-
export const
|
|
105
|
+
export const useFoldActions = function useFoldActions<T>(
|
|
90
106
|
folds: FoldState<T>,
|
|
91
107
|
navigation: Pick<NavigationState, "cursor" | "setCursor">
|
|
92
|
-
):
|
|
108
|
+
): FoldActions {
|
|
93
109
|
const { closedKeys, ranges, rowCount, setClosedKeys, view } = folds;
|
|
94
110
|
const { cursor, setCursor } = navigation;
|
|
95
|
-
const
|
|
96
|
-
const atCursor = sourceRow === undefined ? [] : foldsAt(ranges, sourceRow);
|
|
111
|
+
const cursorRow = cursor === null ? undefined : view.visibleToSource[cursor];
|
|
97
112
|
|
|
98
113
|
const apply = (next: ReadonlySet<Key>) => {
|
|
99
|
-
if (
|
|
100
|
-
const anchor = visibleAnchorRow(computeFoldView(rowCount, ranges, next),
|
|
114
|
+
if (cursorRow !== undefined) {
|
|
115
|
+
const anchor = visibleAnchorRow(computeFoldView(rowCount, ranges, next), cursorRow);
|
|
101
116
|
const anchorIndex = view.sourceToVisible[anchor] ?? -1;
|
|
102
|
-
|
|
117
|
+
|
|
118
|
+
if (anchor !== cursorRow && anchorIndex !== -1) {
|
|
103
119
|
setCursor(anchorIndex);
|
|
104
120
|
}
|
|
105
121
|
}
|
|
122
|
+
|
|
106
123
|
setClosedKeys(next);
|
|
107
124
|
};
|
|
108
|
-
|
|
125
|
+
|
|
126
|
+
const withKey = (key: Key | undefined, closed: boolean): boolean => {
|
|
127
|
+
if (key === undefined) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
109
131
|
const next = new Set(closedKeys);
|
|
132
|
+
|
|
110
133
|
if (closed) {
|
|
111
134
|
next.add(key);
|
|
112
135
|
} else {
|
|
113
136
|
next.delete(key);
|
|
114
137
|
}
|
|
138
|
+
|
|
115
139
|
apply(next);
|
|
140
|
+
|
|
141
|
+
return true;
|
|
116
142
|
};
|
|
117
143
|
|
|
118
|
-
|
|
144
|
+
const close = (row: number) =>
|
|
145
|
+
withKey(foldsAt(ranges, row).find((range) => !closedKeys.has(range.key))?.key, true);
|
|
119
146
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
withKey(innermost.key, !closedKeys.has(innermost.key));
|
|
147
|
+
const open = (row: number) =>
|
|
148
|
+
withKey(foldsAt(ranges, row).find((range) => closedKeys.has(range.key))?.key, false);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
close,
|
|
152
|
+
closeAll: () => {
|
|
153
|
+
apply(new Set(ranges.map((range) => range.key)));
|
|
129
154
|
},
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
155
|
+
open,
|
|
156
|
+
openAll: () => {
|
|
157
|
+
apply(EMPTY_KEYS);
|
|
158
|
+
},
|
|
159
|
+
toggle: (row) => {
|
|
160
|
+
const [innermost] = foldsAt(ranges, row);
|
|
135
161
|
|
|
136
|
-
|
|
137
|
-
category: "Fold",
|
|
138
|
-
handler: (ctx) => {
|
|
139
|
-
const target = atCursor.find((range) => !closedKeys.has(range.key));
|
|
140
|
-
if (target === undefined) {
|
|
141
|
-
warnNoFold(ctx);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
withKey(target.key, true);
|
|
162
|
+
return withKey(innermost?.key, innermost !== undefined && !closedKeys.has(innermost.key));
|
|
145
163
|
},
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
});
|
|
164
|
+
toggleBlock: (row) =>
|
|
165
|
+
foldsAt(ranges, row).some((range) => closedKeys.has(range.key)) ? open(row) : close(row),
|
|
166
|
+
};
|
|
167
|
+
};
|
|
151
168
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
169
|
+
/**
|
|
170
|
+
* The Vim fold commands (`z a`, `z c`, `z o`, `z z`, `z shift+m`,
|
|
171
|
+
* `z shift+r`) as action definitions. The row commands act on `targetRow()`:
|
|
172
|
+
* the document cursor row on the root surface, or the selected heading in the
|
|
173
|
+
* outline. `idPrefix` keeps the ids apart on each surface, and `noFoldMessage`
|
|
174
|
+
* is the warning when no fold contains the target row.
|
|
175
|
+
*/
|
|
176
|
+
export const foldActionDefinitions = function foldActionDefinitions(
|
|
177
|
+
actions: FoldActions,
|
|
178
|
+
targetRow: () => number | undefined,
|
|
179
|
+
idPrefix: string,
|
|
180
|
+
noFoldMessage: string
|
|
181
|
+
): ActionDefinition[] {
|
|
182
|
+
const atTarget =
|
|
183
|
+
(run: (row: number) => boolean) =>
|
|
184
|
+
(ctx: CommandContext): void => {
|
|
185
|
+
const row = targetRow();
|
|
186
|
+
|
|
187
|
+
if (row === undefined || !run(row)) {
|
|
188
|
+
ctx.toast?.toast({ level: "warning", message: noFoldMessage });
|
|
159
189
|
}
|
|
160
|
-
|
|
161
|
-
},
|
|
162
|
-
hotkey: "z o",
|
|
163
|
-
id: "fold.open",
|
|
164
|
-
modes: ["cursor"],
|
|
165
|
-
title: "Open fold",
|
|
166
|
-
});
|
|
190
|
+
};
|
|
167
191
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
title: "Close all folds",
|
|
177
|
-
});
|
|
192
|
+
const definitions: [hotkey: string, id: string, title: string, handler: CommandHandler][] = [
|
|
193
|
+
["z a", "toggle", "Toggle fold", atTarget(actions.toggle)],
|
|
194
|
+
["z c", "close", "Close fold", atTarget(actions.close)],
|
|
195
|
+
["z o", "open", "Open fold", atTarget(actions.open)],
|
|
196
|
+
["z z", "toggle-block", "Open or close fold", atTarget(actions.toggleBlock)],
|
|
197
|
+
["z shift+m", "close-all", "Close all folds", actions.closeAll],
|
|
198
|
+
["z shift+r", "open-all", "Open all folds", actions.openAll],
|
|
199
|
+
];
|
|
178
200
|
|
|
179
|
-
|
|
201
|
+
return definitions.map(([hotkey, id, title, handler]) => ({
|
|
180
202
|
category: "Fold",
|
|
181
|
-
handler
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
hotkey: "z shift+r",
|
|
185
|
-
id: "fold.open-all",
|
|
203
|
+
handler,
|
|
204
|
+
hotkey,
|
|
205
|
+
id: `${idPrefix}.${id}`,
|
|
186
206
|
modes: ["cursor"],
|
|
187
|
-
title
|
|
188
|
-
});
|
|
207
|
+
title,
|
|
208
|
+
}));
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/** Register the fold commands on the current surface, acting on the document cursor row. */
|
|
212
|
+
export const useFoldCommands = function useFoldCommands<T>(
|
|
213
|
+
folds: FoldState<T>,
|
|
214
|
+
actions: FoldActions,
|
|
215
|
+
navigation: Pick<NavigationState, "cursor">
|
|
216
|
+
): void {
|
|
217
|
+
const { cursor } = navigation;
|
|
218
|
+
const cursorRow = cursor === null ? undefined : folds.view.visibleToSource[cursor];
|
|
219
|
+
useCommandGroup({ id: "fold", prefix: "z", title: "Fold" });
|
|
220
|
+
useActions(foldActionDefinitions(actions, () => cursorRow, "fold", "No fold at cursor"));
|
|
189
221
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ActionDefinition } from "@tooee/commands";
|
|
2
|
+
import { StatusPriority } from "@tooee/layout";
|
|
2
3
|
import type { StatusBarItem } from "@tooee/layout";
|
|
3
4
|
import type { DecorationLayer } from "@tooee/renderers";
|
|
4
5
|
import { useDocumentController } from "@tooee/shell";
|
|
@@ -37,6 +38,7 @@ export const useContentDocument = function useContentDocument<T>(
|
|
|
37
38
|
options: ContentDocumentOptions<T>
|
|
38
39
|
): ContentDocumentResult<T> {
|
|
39
40
|
const { showLineNumbers } = useContentCommands({ content, textContent });
|
|
41
|
+
|
|
40
42
|
const document = useDocumentController<T>({
|
|
41
43
|
adapter,
|
|
42
44
|
contextMenu: options.contextMenu === false ? false : actions,
|
|
@@ -45,10 +47,11 @@ export const useContentDocument = function useContentDocument<T>(
|
|
|
45
47
|
preserveCursorByKey: options.preserveCursorByKey,
|
|
46
48
|
rows,
|
|
47
49
|
});
|
|
50
|
+
|
|
48
51
|
const statusItems = options.buildStatusItems?.(document) ??
|
|
49
52
|
options.statusItems ?? [
|
|
50
|
-
{ label: "Format:", value: content.format },
|
|
51
|
-
{ label: "Lines:", value: String(rows.length) },
|
|
53
|
+
{ label: "Format:", priority: StatusPriority.content, value: content.format },
|
|
54
|
+
{ label: "Lines:", priority: StatusPriority.content, value: String(rows.length) },
|
|
52
55
|
];
|
|
53
56
|
|
|
54
57
|
return { document, showLineNumbers, statusItems };
|
|
@@ -39,6 +39,7 @@ export const useContentLoader = function useContentLoader(
|
|
|
39
39
|
});
|
|
40
40
|
const { requestId } = store.getSnapshot().context;
|
|
41
41
|
const loaded = contentProvider.load();
|
|
42
|
+
|
|
42
43
|
let cleanup: () => void = () => {
|
|
43
44
|
// Synchronous providers have no pending work to cancel.
|
|
44
45
|
};
|
|
@@ -55,16 +56,20 @@ export const useContentLoader = function useContentLoader(
|
|
|
55
56
|
// preserve sequential stream consumption for ordered backpressure.
|
|
56
57
|
// oxlint-disable-next-line no-await-in-loop -- each chunk must be consumed in iterator order
|
|
57
58
|
const result = await iterator.next();
|
|
59
|
+
|
|
58
60
|
if (result.done === true) {
|
|
59
61
|
break;
|
|
60
62
|
}
|
|
63
|
+
|
|
61
64
|
store.trigger.chunkReceived({ chunk: result.value, requestId });
|
|
62
65
|
}
|
|
66
|
+
|
|
63
67
|
store.trigger.streamEnded({ requestId });
|
|
64
68
|
} catch (error) {
|
|
65
69
|
store.trigger.loadFailed({ error: normalizeError(error), requestId });
|
|
66
70
|
}
|
|
67
71
|
})();
|
|
72
|
+
|
|
68
73
|
cleanup = () => {
|
|
69
74
|
store.trigger.loadCancelled({ requestId });
|
|
70
75
|
void (async () => {
|
|
@@ -84,6 +89,7 @@ export const useContentLoader = function useContentLoader(
|
|
|
84
89
|
store.trigger.loadFailed({ error: normalizeError(error), requestId });
|
|
85
90
|
}
|
|
86
91
|
})();
|
|
92
|
+
|
|
87
93
|
cleanup = () => {
|
|
88
94
|
store.trigger.loadCancelled({ requestId });
|
|
89
95
|
};
|
|
@@ -99,6 +105,7 @@ export const useContentLoader = function useContentLoader(
|
|
|
99
105
|
const error = useSelector(store, (snapshot) => selectError(snapshot.context));
|
|
100
106
|
const providerMarks = useSelector(store, (snapshot) => selectProviderMarks(snapshot.context));
|
|
101
107
|
const status = useSelector(store, (snapshot) => selectStatus(snapshot.context));
|
|
108
|
+
|
|
102
109
|
const reload = useCallback(() => {
|
|
103
110
|
store.trigger.reloadRequested({});
|
|
104
111
|
}, [store]);
|
|
@@ -72,6 +72,7 @@ export const createViewCommandContext = function createViewCommandContext({
|
|
|
72
72
|
marks,
|
|
73
73
|
}: CreateViewCommandContextOptions): ViewCommandContext {
|
|
74
74
|
const resolvedFormat = format ?? content?.format ?? "custom";
|
|
75
|
+
|
|
75
76
|
const resolvedContent: AnyContent =
|
|
76
77
|
content ??
|
|
77
78
|
({
|
package/src/index.ts
CHANGED
|
@@ -1,27 +1,39 @@
|
|
|
1
1
|
export { View } from "./view.js";
|
|
2
|
+
|
|
2
3
|
export { DirectoryView } from "./directory-view.js";
|
|
4
|
+
|
|
3
5
|
export { launch, launchDirectory } from "./launch.js";
|
|
6
|
+
|
|
4
7
|
export type { ViewLaunchOptions } from "./launch.js";
|
|
8
|
+
|
|
5
9
|
export {
|
|
6
10
|
createFileProvider,
|
|
7
11
|
createStdinProvider,
|
|
8
12
|
createTableFileProvider,
|
|
9
13
|
createTableStdinProvider,
|
|
10
14
|
} from "./default-provider.js";
|
|
15
|
+
|
|
11
16
|
export type { CreateProviderOptions } from "./default-provider.js";
|
|
17
|
+
|
|
12
18
|
export { listDirectoryFiles } from "./directory-provider.js";
|
|
19
|
+
|
|
13
20
|
export type { DirectoryEntry } from "./directory-provider.js";
|
|
21
|
+
|
|
14
22
|
export {
|
|
15
23
|
createViewCommandContext,
|
|
16
24
|
useProvideViewCommandContext,
|
|
17
25
|
} from "./hooks/use-view-command-context.js";
|
|
26
|
+
|
|
18
27
|
export type {
|
|
19
28
|
CreateViewCommandContextOptions,
|
|
20
29
|
ProvideViewCommandContextOptions,
|
|
21
30
|
ViewCommandContext,
|
|
22
31
|
} from "./hooks/use-view-command-context.js";
|
|
32
|
+
|
|
23
33
|
export type { ContentLoaderState } from "./hooks/use-content-loader.js";
|
|
34
|
+
|
|
24
35
|
export type { ContentCommandsResult } from "./hooks/use-content-commands.js";
|
|
36
|
+
|
|
25
37
|
export type {
|
|
26
38
|
AnyContent,
|
|
27
39
|
Content,
|
|
@@ -43,8 +55,13 @@ export type {
|
|
|
43
55
|
ColumnDef,
|
|
44
56
|
TableRow,
|
|
45
57
|
} from "./types.js";
|
|
58
|
+
|
|
46
59
|
export { getTextContent, isBuiltinContent, isCustomContent } from "./types.js";
|
|
60
|
+
|
|
47
61
|
export { CodeBlockChrome } from "@tooee/renderers";
|
|
62
|
+
|
|
48
63
|
export type { CodeBlockRenderer, CodeBlockRendererProps, CodeBlockHScroll } from "@tooee/renderers";
|
|
64
|
+
|
|
49
65
|
export { localFileLinkHandler } from "./link-handlers.js";
|
|
66
|
+
|
|
50
67
|
export type { LinkHandler, LinkHandlerContext, LinkTarget } from "./link-handlers.js";
|
package/src/launch.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import type { ReactNode } from "react";
|
|
|
7
7
|
import { DirectoryView } from "./directory-view.js";
|
|
8
8
|
import type { LinkHandler } from "./link-handlers.js";
|
|
9
9
|
import { createStandaloneRouter } from "./standalone-router.js";
|
|
10
|
-
import type { ContentProvider, ContentRenderer } from "./types.js";
|
|
10
|
+
import type { ContentProvider, ContentRenderer, MarkdownLinkActivateHandler } from "./types.js";
|
|
11
11
|
|
|
12
12
|
export interface ViewLaunchOptions {
|
|
13
13
|
contentProvider: ContentProvider;
|
|
@@ -15,6 +15,12 @@ export interface ViewLaunchOptions {
|
|
|
15
15
|
filePath?: string;
|
|
16
16
|
/** Synchronous handlers tried before local file navigation. */
|
|
17
17
|
linkHandlers?: readonly LinkHandler[];
|
|
18
|
+
/**
|
|
19
|
+
* Handles activation of a Markdown link, by mouse or by Enter on the cursor
|
|
20
|
+
* line. When set, it replaces the default handling: `linkHandlers`, local
|
|
21
|
+
* file navigation, and the stdin "Links need a source file" toast.
|
|
22
|
+
*/
|
|
23
|
+
onMarkdownLinkActivate?: MarkdownLinkActivateHandler;
|
|
18
24
|
actions?: ActionDefinition[];
|
|
19
25
|
renderers?: Record<string, ContentRenderer>;
|
|
20
26
|
/**
|
|
@@ -23,6 +29,8 @@ export interface ViewLaunchOptions {
|
|
|
23
29
|
* Unmatched types fall back to the default syntax-highlighted code block.
|
|
24
30
|
*/
|
|
25
31
|
codeBlockRenderers?: Record<string, CodeBlockRenderer>;
|
|
32
|
+
/** Open the heading outline beside Markdown content at start. */
|
|
33
|
+
outline?: boolean;
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
export interface DirectoryLaunchOptions {
|
|
@@ -33,9 +41,11 @@ export interface DirectoryLaunchOptions {
|
|
|
33
41
|
export const launch = async function launch(options: ViewLaunchOptions): Promise<void> {
|
|
34
42
|
const { router } = createStandaloneRouter(options);
|
|
35
43
|
const startup = await router.start();
|
|
44
|
+
|
|
36
45
|
if (startup.status !== "committed") {
|
|
37
46
|
throw new Error(`View router startup ${startup.status}`);
|
|
38
47
|
}
|
|
48
|
+
|
|
39
49
|
await runCliSession<undefined>(
|
|
40
50
|
(): ReactNode => (
|
|
41
51
|
<RouterProvider router={router}>
|
package/src/link-handlers.ts
CHANGED
|
@@ -24,22 +24,29 @@ export type LinkHandler = (link: LinkTarget, context: LinkHandlerContext) => boo
|
|
|
24
24
|
|
|
25
25
|
export const localFileLinkHandler: LinkHandler = function localFileLinkHandler(link, context) {
|
|
26
26
|
const result = resolveMarkdownLink(link.href, link.baseDir, link.currentPath);
|
|
27
|
+
|
|
27
28
|
if (result.status === "unsupported") {
|
|
28
29
|
return false;
|
|
29
30
|
}
|
|
31
|
+
|
|
30
32
|
if (result.status === "file") {
|
|
31
33
|
void context.navigate.push(context.documentRoute, { path: result.path });
|
|
34
|
+
|
|
32
35
|
return true;
|
|
33
36
|
}
|
|
37
|
+
|
|
34
38
|
const relative = path.relative(link.baseDir, result.path);
|
|
39
|
+
|
|
35
40
|
const target =
|
|
36
41
|
relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)
|
|
37
42
|
? result.path
|
|
38
43
|
: relative || ".";
|
|
44
|
+
|
|
39
45
|
context.command.toast?.toast({
|
|
40
46
|
level: "warning",
|
|
41
47
|
message: `${result.status === "missing" ? "File not found" : "Not a file"}: ${target}`,
|
|
42
48
|
});
|
|
49
|
+
|
|
43
50
|
return true;
|
|
44
51
|
};
|
|
45
52
|
|
|
@@ -54,9 +61,12 @@ export const runLinkHandlers = function runLinkHandlers(
|
|
|
54
61
|
return true;
|
|
55
62
|
}
|
|
56
63
|
}
|
|
64
|
+
|
|
57
65
|
if (localFileLinkHandler(link, context)) {
|
|
58
66
|
return true;
|
|
59
67
|
}
|
|
68
|
+
|
|
60
69
|
context.command.toast?.toast({ level: "warning", message: `Unsupported link: ${link.href}` });
|
|
70
|
+
|
|
61
71
|
return true;
|
|
62
72
|
};
|
package/src/markdown-links.ts
CHANGED
|
@@ -19,39 +19,53 @@ export const resolveMarkdownLink = function resolveMarkdownLink(
|
|
|
19
19
|
): LinkResolution {
|
|
20
20
|
try {
|
|
21
21
|
const decoded = decodeURIComponent(href);
|
|
22
|
+
|
|
22
23
|
if (/[\p{Cc}\\]/u.test(decoded)) {
|
|
23
24
|
return { status: "unsupported" };
|
|
24
25
|
}
|
|
26
|
+
|
|
25
27
|
const target = href.trim();
|
|
28
|
+
|
|
26
29
|
if (!target || target.startsWith("//")) {
|
|
27
30
|
return { status: "unsupported" };
|
|
28
31
|
}
|
|
32
|
+
|
|
29
33
|
const [rawPath] = target.split("#", 1);
|
|
34
|
+
|
|
30
35
|
if (rawPath.includes("?")) {
|
|
31
36
|
return { status: "unsupported" };
|
|
32
37
|
}
|
|
38
|
+
|
|
33
39
|
let path: string;
|
|
40
|
+
|
|
34
41
|
if (/^[a-zA-Z][a-zA-Z\d+.-]*:/u.test(rawPath)) {
|
|
35
42
|
// Check the authority before URL normalizes localhost into an empty host.
|
|
36
43
|
if (!/^file:\/\/\//iu.test(rawPath)) {
|
|
37
44
|
return { status: "unsupported" };
|
|
38
45
|
}
|
|
46
|
+
|
|
39
47
|
const url = new URL(rawPath);
|
|
48
|
+
|
|
40
49
|
if (url.hostname || url.search) {
|
|
41
50
|
return { status: "unsupported" };
|
|
42
51
|
}
|
|
52
|
+
|
|
43
53
|
path = fileURLToPath(url);
|
|
44
54
|
} else {
|
|
45
55
|
path = rawPath
|
|
46
56
|
? nodePath.resolve(baseDir, decodeURIComponent(rawPath))
|
|
47
57
|
: (currentPath ?? baseDir);
|
|
48
58
|
}
|
|
59
|
+
|
|
49
60
|
const absolute = nodePath.resolve(path);
|
|
61
|
+
|
|
50
62
|
try {
|
|
51
63
|
const stat = statSync(absolute);
|
|
64
|
+
|
|
52
65
|
if (stat.isFile()) {
|
|
53
66
|
return { path: absolute, status: "file" };
|
|
54
67
|
}
|
|
68
|
+
|
|
55
69
|
return stat.isDirectory()
|
|
56
70
|
? { path: absolute, status: "directory" }
|
|
57
71
|
: { status: "unsupported" };
|
|
@@ -82,8 +96,10 @@ const collect = (tokens: Token[], links: MarkdownLink[]): void => {
|
|
|
82
96
|
}
|
|
83
97
|
}
|
|
84
98
|
};
|
|
99
|
+
|
|
85
100
|
export const markdownLinks = function markdownLinks(line: string): MarkdownLink[] {
|
|
86
101
|
const links: MarkdownLink[] = [];
|
|
87
102
|
collect(Lexer.lexInline(line), links);
|
|
103
|
+
|
|
88
104
|
return links;
|
|
89
105
|
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getPlainText } from "@tooee/renderers";
|
|
2
|
+
import type { FlatBlock } from "@tooee/renderers";
|
|
3
|
+
|
|
4
|
+
import { headingToken } from "../folds/markdown-heading-folds.js";
|
|
5
|
+
|
|
6
|
+
/** One heading in a document outline. */
|
|
7
|
+
export interface OutlineEntry {
|
|
8
|
+
/** The heading's index in the full row array, the same key folds use. */
|
|
9
|
+
row: number;
|
|
10
|
+
/** Heading level, 1 for `#`. */
|
|
11
|
+
depth: number;
|
|
12
|
+
/** Indent level in the outline: depth relative to the shallowest heading. */
|
|
13
|
+
level: number;
|
|
14
|
+
/** Unstyled heading text. */
|
|
15
|
+
text: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The headings of flattened Markdown blocks, in document order. Levels start
|
|
20
|
+
* at 0 for the shallowest heading in the document, so a document that starts
|
|
21
|
+
* at `##` is not indented.
|
|
22
|
+
*/
|
|
23
|
+
export const markdownOutline = function markdownOutline(
|
|
24
|
+
blocks: readonly FlatBlock[]
|
|
25
|
+
): OutlineEntry[] {
|
|
26
|
+
const headings = blocks.flatMap((block, row) => {
|
|
27
|
+
const heading = headingToken(block);
|
|
28
|
+
|
|
29
|
+
return heading === null
|
|
30
|
+
? []
|
|
31
|
+
: [{ depth: heading.depth, row, text: getPlainText(heading.tokens).trim() }];
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const minDepth = Math.min(...headings.map((heading) => heading.depth));
|
|
35
|
+
|
|
36
|
+
return headings.map((heading) => ({ ...heading, level: heading.depth - minDepth }));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Index of the entry for the heading that contains `row`: the last heading at
|
|
41
|
+
* or before it. Returns `-1` before the first heading or for an empty outline.
|
|
42
|
+
*/
|
|
43
|
+
export const outlineIndexAt = function outlineIndexAt(
|
|
44
|
+
entries: readonly OutlineEntry[],
|
|
45
|
+
row: number
|
|
46
|
+
): number {
|
|
47
|
+
return entries.findLastIndex((entry) => entry.row <= row);
|
|
48
|
+
};
|