@tooee/view 0.10.2 → 0.11.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/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/folds/fold-decorations.d.ts +8 -0
- package/dist/folds/fold-decorations.d.ts.map +1 -0
- package/dist/folds/fold-decorations.js +24 -0
- package/dist/folds/fold-decorations.js.map +1 -0
- package/dist/folds/fold-model.d.ts +36 -0
- package/dist/folds/fold-model.d.ts.map +1 -0
- package/dist/folds/fold-model.js +51 -0
- package/dist/folds/fold-model.js.map +1 -0
- package/dist/folds/markdown-heading-folds.d.ts +10 -0
- package/dist/folds/markdown-heading-folds.d.ts.map +1 -0
- package/dist/folds/markdown-heading-folds.js +41 -0
- package/dist/folds/markdown-heading-folds.js.map +1 -0
- package/dist/folds/use-folds.d.ts +34 -0
- package/dist/folds/use-folds.d.ts.map +1 -0
- package/dist/folds/use-folds.js +136 -0
- package/dist/folds/use-folds.js.map +1 -0
- package/package.json +13 -13
- package/src/components/subviews/markdown-subview.tsx +27 -8
- package/src/folds/fold-decorations.ts +30 -0
- package/src/folds/fold-model.ts +85 -0
- package/src/folds/markdown-heading-folds.ts +50 -0
- package/src/folds/use-folds.ts +189 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown-subview.d.ts","sourceRoot":"","sources":["../../../src/components/subviews/markdown-subview.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAa,MAAM,kBAAkB,CAAC;AAGrE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"markdown-subview.d.ts","sourceRoot":"","sources":["../../../src/components/subviews/markdown-subview.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAa,MAAM,kBAAkB,CAAC;AAGrE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAKvC,OAAO,KAAK,EAAE,eAAe,EAAE,2BAA2B,EAAE,MAAM,gBAAgB,CAAC;AAEnF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,UAAU,oBAAqB,SAAQ,YAAY;IACjD,OAAO,EAAE,eAAe,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IACvD,cAAc,CAAC,EAAE,2BAA2B,CAAC;CAC9C;AAuBD,eAAO,MAAM,eAAe,qFAOzB,oBAAoB,KAAG,SAmFzB,CAAC"}
|
|
@@ -2,6 +2,8 @@ import { jsx as _jsx } from "@opentui/react/jsx-runtime";
|
|
|
2
2
|
import { useBuildCommandContext, useCommand } from "@tooee/commands";
|
|
3
3
|
import { MarkdownView, flattenMarkdown, getFlatBlockText } from "@tooee/renderers";
|
|
4
4
|
import { useMemo, useRef } from "react";
|
|
5
|
+
import { markdownHeadingFoldRanges } from "../../folds/markdown-heading-folds.js";
|
|
6
|
+
import { useFoldCommands, useFoldState } from "../../folds/use-folds.js";
|
|
5
7
|
import { useContentDocument } from "../../hooks/use-content-document.js";
|
|
6
8
|
import { ViewScreen } from "../view-screen.js";
|
|
7
9
|
/** Columns moved per h/l press when scrolling a wide block horizontally. */
|
|
@@ -9,23 +11,36 @@ const BLOCK_HSCROLL_STEP = 4;
|
|
|
9
11
|
/**
|
|
10
12
|
* Blocks are the row unit. `getFlatBlockText` keeps search/copy in step with the
|
|
11
13
|
* source mapping (notably for synthetic bullet rows), and `getSource` projects
|
|
12
|
-
* each block's Markdown provenance onto the controller's anchors.
|
|
14
|
+
* each block's Markdown provenance onto the controller's anchors. The key is
|
|
15
|
+
* the block's index in the full flattened document, so it stays the same when
|
|
16
|
+
* folds hide the blocks around it.
|
|
13
17
|
*/
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
const markdownBlockAdapter = function markdownBlockAdapter(blocks) {
|
|
19
|
+
const indices = new Map(blocks.map((block, index) => [block, index]));
|
|
20
|
+
return {
|
|
21
|
+
getKey: (block, index) => indices.get(block) ?? index,
|
|
22
|
+
getSource: (block) => block.source,
|
|
23
|
+
getText: (block) => getFlatBlockText(block),
|
|
24
|
+
};
|
|
17
25
|
};
|
|
18
26
|
export const MarkdownSubview = function MarkdownSubview({ content, codeBlockRenderers, onLinkActivate, decorations, actions, ...screen }) {
|
|
19
27
|
const textContent = content.markdown;
|
|
20
28
|
const lineCount = useMemo(() => textContent.split("\n").length, [textContent]);
|
|
21
29
|
const blocks = useMemo(() => flattenMarkdown(content.markdown), [content.markdown]);
|
|
22
|
-
const
|
|
30
|
+
const adapter = useMemo(() => markdownBlockAdapter(blocks), [blocks]);
|
|
31
|
+
const foldRanges = useMemo(() => markdownHeadingFoldRanges(blocks), [blocks]);
|
|
32
|
+
// Closed heading folds filter `blocks`; `folds.rows` is the one array both
|
|
33
|
+
// the controller and the renderer see.
|
|
34
|
+
const folds = useFoldState(blocks, foldRanges, decorations);
|
|
35
|
+
const { document, showLineNumbers, statusItems } = useContentDocument(folds.rows, adapter, { actions, content, decorations: folds.decorations, textContent }, {
|
|
23
36
|
multiSelect: true,
|
|
37
|
+
preserveCursorByKey: true,
|
|
24
38
|
statusItems: [
|
|
25
39
|
{ label: "Format:", value: content.format },
|
|
26
40
|
{ label: "Lines:", value: String(lineCount) },
|
|
27
41
|
],
|
|
28
42
|
});
|
|
43
|
+
useFoldCommands(folds, document.navigation);
|
|
29
44
|
const buildCommandContext = useBuildCommandContext();
|
|
30
45
|
const handleLinkActivate = onLinkActivate
|
|
31
46
|
? (href) => onLinkActivate(href, buildCommandContext())
|
|
@@ -60,6 +75,6 @@ export const MarkdownSubview = function MarkdownSubview({ content, codeBlockRend
|
|
|
60
75
|
title: "Scroll block right",
|
|
61
76
|
when: () => cursorScrollable() !== undefined,
|
|
62
77
|
});
|
|
63
|
-
return (_jsx(ViewScreen, { content: content, controller: document, actions: actions, statusItems: statusItems, ...screen, children: _jsx(MarkdownView, { content: content.markdown, blocks:
|
|
78
|
+
return (_jsx(ViewScreen, { content: content, controller: document, actions: actions, statusItems: statusItems, ...screen, children: _jsx(MarkdownView, { content: content.markdown, blocks: folds.rows, rowNumbers: folds.rowNumbers, foldedBlocks: folds.view.hiddenCounts, showLineNumbers: showLineNumbers, document: document, hScrollableBlocksRef: hScrollableBlocksRef, codeBlockRenderers: codeBlockRenderers, onLinkActivate: handleLinkActivate, imageBasePath: content.imageBasePath }) }));
|
|
64
79
|
};
|
|
65
80
|
//# sourceMappingURL=markdown-subview.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown-subview.js","sourceRoot":"","sources":["../../../src/components/subviews/markdown-subview.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACrE,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGnF,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAGxC,OAAO,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAEzE,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAS/C,4EAA4E;AAC5E,MAAM,kBAAkB,GAAG,CAAC,CAAC;AAE7B
|
|
1
|
+
{"version":3,"file":"markdown-subview.js","sourceRoot":"","sources":["../../../src/components/subviews/markdown-subview.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DecorationLayer } from "@tooee/renderers";
|
|
2
|
+
import type { FoldView } from "./fold-model.js";
|
|
3
|
+
/**
|
|
4
|
+
* Map decoration layers written against full-array rows onto the visible rows
|
|
5
|
+
* of `view`. Decorations on rows that a closed fold hides are dropped.
|
|
6
|
+
*/
|
|
7
|
+
export declare const projectDecorationLayers: (layers: readonly DecorationLayer[], view: FoldView) => DecorationLayer[];
|
|
8
|
+
//# sourceMappingURL=fold-decorations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fold-decorations.d.ts","sourceRoot":"","sources":["../../src/folds/fold-decorations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAiB,MAAM,kBAAkB,CAAC;AAEvE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEhD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,WAC1B,SAAS,eAAe,EAAE,QAC5B,QAAQ,KACb,eAAe,EAkBjB,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map decoration layers written against full-array rows onto the visible rows
|
|
3
|
+
* of `view`. Decorations on rows that a closed fold hides are dropped.
|
|
4
|
+
*/
|
|
5
|
+
export const projectDecorationLayers = function projectDecorationLayers(layers, view) {
|
|
6
|
+
const { visibleToSource, sourceToVisible } = view;
|
|
7
|
+
return layers.map((layer) => ({
|
|
8
|
+
*forVisibleRows(from, to) {
|
|
9
|
+
const sourceFrom = visibleToSource[from];
|
|
10
|
+
const sourceTo = visibleToSource[Math.min(to, visibleToSource.length - 1)];
|
|
11
|
+
if (sourceFrom === undefined || sourceTo === undefined) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
for (const decoration of layer.forVisibleRows(sourceFrom, sourceTo)) {
|
|
15
|
+
const row = sourceToVisible[decoration.row] ?? -1;
|
|
16
|
+
if (row !== -1) {
|
|
17
|
+
yield { ...decoration, row };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
priority: layer.priority,
|
|
22
|
+
}));
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=fold-decorations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fold-decorations.js","sourceRoot":"","sources":["../../src/folds/fold-decorations.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,SAAS,uBAAuB,CACrE,MAAkC,EAClC,IAAc;IAEd,MAAM,EAAE,eAAe,EAAE,eAAe,EAAE,GAAG,IAAI,CAAC;IAClD,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAmB,EAAE,CAAC,CAAC;QAC7C,CAAC,cAAc,CAAC,IAAY,EAAE,EAAU;YACtC,MAAM,UAAU,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;YACzC,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,eAAe,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;YAC3E,IAAI,UAAU,KAAK,SAAS,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBACvD,OAAO;YACT,CAAC;YACD,KAAK,MAAM,UAAU,IAAI,KAAK,CAAC,cAAc,CAAC,UAAU,EAAE,QAAQ,CAAC,EAAE,CAAC;gBACpE,MAAM,GAAG,GAAG,eAAe,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;gBAClD,IAAI,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC;oBACf,MAAM,EAAE,GAAG,UAAU,EAAE,GAAG,EAAE,CAAC;gBAC/B,CAAC;YACH,CAAC;QACH,CAAC;QACD,QAAQ,EAAE,KAAK,CAAC,QAAQ;KACzB,CAAC,CAAC,CAAC;AACN,CAAC,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Key } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* A foldable range of rows. `start` is the header row that stays visible;
|
|
4
|
+
* `start + 1 ..= end` are the rows a closed fold hides. Indices point into the
|
|
5
|
+
* full row array, and `key` is the header row's key, so closed state survives
|
|
6
|
+
* row changes that keep keys stable.
|
|
7
|
+
*/
|
|
8
|
+
export interface FoldRange {
|
|
9
|
+
key: Key;
|
|
10
|
+
start: number;
|
|
11
|
+
end: number;
|
|
12
|
+
}
|
|
13
|
+
/** The visible projection of a full row array under a set of closed folds. */
|
|
14
|
+
export interface FoldView {
|
|
15
|
+
/** Full-array index of each visible row, in order. */
|
|
16
|
+
visibleToSource: readonly number[];
|
|
17
|
+
/** Visible index of each full-array row, or `-1` when a closed fold hides it. */
|
|
18
|
+
sourceToVisible: readonly number[];
|
|
19
|
+
/** Hidden row count for each closed header, keyed by the header's visible index. */
|
|
20
|
+
hiddenCounts: ReadonlyMap<number, number>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Project `rowCount` rows through the closed folds. A closed range hides its
|
|
24
|
+
* body even when it sits inside another closed range; the outermost header
|
|
25
|
+
* shown carries the count of every row hidden below it.
|
|
26
|
+
*/
|
|
27
|
+
export declare const computeFoldView: (rowCount: number, ranges: readonly FoldRange[], closedKeys: ReadonlySet<Key>) => FoldView;
|
|
28
|
+
/** Ranges that contain `row` (header included), innermost first. */
|
|
29
|
+
export declare const foldsAt: (ranges: readonly FoldRange[], row: number) => readonly FoldRange[];
|
|
30
|
+
/**
|
|
31
|
+
* The full-array row the cursor should rest on once `view` applies: `row`
|
|
32
|
+
* itself when it stays visible, otherwise the nearest visible row before it,
|
|
33
|
+
* which is the header of the outermost closed fold that hides it.
|
|
34
|
+
*/
|
|
35
|
+
export declare const visibleAnchorRow: (view: FoldView, row: number) => number;
|
|
36
|
+
//# sourceMappingURL=fold-model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fold-model.d.ts","sourceRoot":"","sources":["../../src/folds/fold-model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAEjC;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,GAAG,CAAC;IACT,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,8EAA8E;AAC9E,MAAM,WAAW,QAAQ;IACvB,sDAAsD;IACtD,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,iFAAiF;IACjF,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,oFAAoF;IACpF,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,aAChB,MAAM,UACR,SAAS,SAAS,EAAE,cAChB,WAAW,CAAC,GAAG,CAAC,KAC3B,QA2BF,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,OAAO,WACV,SAAS,SAAS,EAAE,OACvB,MAAM,KACV,SAAS,SAAS,EAIpB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,SAAmC,QAAQ,OAAO,MAAM,KAAG,MAOvF,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project `rowCount` rows through the closed folds. A closed range hides its
|
|
3
|
+
* body even when it sits inside another closed range; the outermost header
|
|
4
|
+
* shown carries the count of every row hidden below it.
|
|
5
|
+
*/
|
|
6
|
+
export const computeFoldView = function computeFoldView(rowCount, ranges, closedKeys) {
|
|
7
|
+
const closedEnds = new Map();
|
|
8
|
+
for (const range of ranges) {
|
|
9
|
+
if (range.end > range.start && closedKeys.has(range.key)) {
|
|
10
|
+
closedEnds.set(range.start, Math.max(closedEnds.get(range.start) ?? range.end, range.end));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const visibleToSource = [];
|
|
14
|
+
const sourceToVisible = Array.from({ length: rowCount }, () => -1);
|
|
15
|
+
const hiddenCounts = new Map();
|
|
16
|
+
let hiddenUntil = -1;
|
|
17
|
+
for (let index = 0; index < rowCount; index += 1) {
|
|
18
|
+
if (index <= hiddenUntil) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const visibleIndex = visibleToSource.length;
|
|
22
|
+
visibleToSource.push(index);
|
|
23
|
+
sourceToVisible[index] = visibleIndex;
|
|
24
|
+
const end = closedEnds.get(index);
|
|
25
|
+
if (end !== undefined) {
|
|
26
|
+
hiddenUntil = Math.min(end, rowCount - 1);
|
|
27
|
+
hiddenCounts.set(visibleIndex, hiddenUntil - index);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { hiddenCounts, sourceToVisible, visibleToSource };
|
|
31
|
+
};
|
|
32
|
+
/** Ranges that contain `row` (header included), innermost first. */
|
|
33
|
+
export const foldsAt = function foldsAt(ranges, row) {
|
|
34
|
+
return ranges
|
|
35
|
+
.filter((range) => range.end > range.start && range.start <= row && row <= range.end)
|
|
36
|
+
.toSorted((left, right) => right.start - left.start || left.end - right.end);
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The full-array row the cursor should rest on once `view` applies: `row`
|
|
40
|
+
* itself when it stays visible, otherwise the nearest visible row before it,
|
|
41
|
+
* which is the header of the outermost closed fold that hides it.
|
|
42
|
+
*/
|
|
43
|
+
export const visibleAnchorRow = function visibleAnchorRow(view, row) {
|
|
44
|
+
for (let index = Math.min(row, view.sourceToVisible.length - 1); index >= 0; index -= 1) {
|
|
45
|
+
if ((view.sourceToVisible[index] ?? -1) !== -1) {
|
|
46
|
+
return index;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return row;
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=fold-model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fold-model.js","sourceRoot":"","sources":["../../src/folds/fold-model.ts"],"names":[],"mappings":"AAwBA;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,SAAS,eAAe,CACrD,QAAgB,EAChB,MAA4B,EAC5B,UAA4B;IAE5B,MAAM,UAAU,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC7C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC,KAAK,IAAI,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;YACzD,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QAC7F,CAAC;IACH,CAAC;IAED,MAAM,eAAe,GAAa,EAAE,CAAC;IACrC,MAAM,eAAe,GAAa,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7E,MAAM,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC/C,IAAI,WAAW,GAAG,CAAC,CAAC,CAAC;IACrB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,QAAQ,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACjD,IAAI,KAAK,IAAI,WAAW,EAAE,CAAC;YACzB,SAAS;QACX,CAAC;QACD,MAAM,YAAY,GAAG,eAAe,CAAC,MAAM,CAAC;QAC5C,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5B,eAAe,CAAC,KAAK,CAAC,GAAG,YAAY,CAAC;QACtC,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,GAAG,CAAC,CAAC,CAAC;YAC1C,YAAY,CAAC,GAAG,CAAC,YAAY,EAAE,WAAW,GAAG,KAAK,CAAC,CAAC;QACtD,CAAC;IACH,CAAC;IAED,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,CAAC;AAC5D,CAAC,CAAC;AAEF,oEAAoE;AACpE,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,OAAO,CACrC,MAA4B,EAC5B,GAAW;IAEX,OAAO,MAAM;SACV,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,GAAG,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,IAAI,GAAG,IAAI,GAAG,IAAI,KAAK,CAAC,GAAG,CAAC;SACpF,QAAQ,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;AACjF,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,SAAS,gBAAgB,CAAC,IAAc,EAAE,GAAW;IACnF,KAAK,IAAI,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACxF,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YAC/C,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FlatBlock } from "@tooee/renderers";
|
|
2
|
+
import type { FoldRange } from "./fold-model.js";
|
|
3
|
+
/**
|
|
4
|
+
* Heading fold ranges over flattened Markdown blocks. A heading at depth `d`
|
|
5
|
+
* folds every following block up to the next heading of depth `d` or less, or
|
|
6
|
+
* the end of the document. A heading with no body yields no range. Keys are
|
|
7
|
+
* the heading's block index, the same key the Markdown row adapter uses.
|
|
8
|
+
*/
|
|
9
|
+
export declare const markdownHeadingFoldRanges: (blocks: readonly FlatBlock[]) => FoldRange[];
|
|
10
|
+
//# sourceMappingURL=markdown-heading-folds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-heading-folds.d.ts","sourceRoot":"","sources":["../../src/folds/markdown-heading-folds.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAWjD;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,WAC5B,SAAS,SAAS,EAAE,KAC3B,SAAS,EA2BX,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const headingDepth = function headingDepth(block) {
|
|
2
|
+
if (block.token.type !== "heading") {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
// SAFETY: Marked creates a Heading token for the checked "heading" discriminator.
|
|
6
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Marked's Generic fallback prevents discriminator narrowing
|
|
7
|
+
return block.token.depth;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Heading fold ranges over flattened Markdown blocks. A heading at depth `d`
|
|
11
|
+
* folds every following block up to the next heading of depth `d` or less, or
|
|
12
|
+
* the end of the document. A heading with no body yields no range. Keys are
|
|
13
|
+
* the heading's block index, the same key the Markdown row adapter uses.
|
|
14
|
+
*/
|
|
15
|
+
export const markdownHeadingFoldRanges = function markdownHeadingFoldRanges(blocks) {
|
|
16
|
+
const ranges = [];
|
|
17
|
+
const open = [];
|
|
18
|
+
const close = (start, end) => {
|
|
19
|
+
if (end > start) {
|
|
20
|
+
ranges.push({ end, key: start, start });
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
for (const [index, block] of blocks.entries()) {
|
|
24
|
+
const depth = headingDepth(block);
|
|
25
|
+
if (depth === null) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
while (open.length > 0 && (open.at(-1)?.depth ?? 0) >= depth) {
|
|
29
|
+
const finished = open.pop();
|
|
30
|
+
if (finished) {
|
|
31
|
+
close(finished.start, index - 1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
open.push({ depth, start: index });
|
|
35
|
+
}
|
|
36
|
+
for (const finished of open) {
|
|
37
|
+
close(finished.start, blocks.length - 1);
|
|
38
|
+
}
|
|
39
|
+
return ranges.toSorted((left, right) => left.start - right.start);
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=markdown-heading-folds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-heading-folds.js","sourceRoot":"","sources":["../../src/folds/markdown-heading-folds.ts"],"names":[],"mappings":"AAKA,MAAM,YAAY,GAAG,SAAS,YAAY,CAAC,KAAgB;IACzD,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QACnC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,kFAAkF;IAClF,6HAA6H;IAC7H,OAAQ,KAAK,CAAC,KAAwB,CAAC,KAAK,CAAC;AAC/C,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,SAAS,yBAAyB,CACzE,MAA4B;IAE5B,MAAM,MAAM,GAAgB,EAAE,CAAC;IAC/B,MAAM,IAAI,GAAuC,EAAE,CAAC;IACpD,MAAM,KAAK,GAAG,CAAC,KAAa,EAAE,GAAW,EAAE,EAAE;QAC3C,IAAI,GAAG,GAAG,KAAK,EAAE,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC,CAAC;IAEF,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,EAAE,EAAE,CAAC;QAC9C,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;QAClC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,SAAS;QACX,CAAC;QACD,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC;YAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAC5B,IAAI,QAAQ,EAAE,CAAC;gBACb,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IACrC,CAAC;IACD,KAAK,MAAM,QAAQ,IAAI,IAAI,EAAE,CAAC;QAC5B,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;AACpE,CAAC,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DecorationLayer } from "@tooee/renderers";
|
|
2
|
+
import type { NavigationState } from "@tooee/shell";
|
|
3
|
+
import type { Key } from "react";
|
|
4
|
+
import type { FoldRange, FoldView } from "./fold-model.js";
|
|
5
|
+
/** Fold state over a full row array and the visible projection it produces. */
|
|
6
|
+
export interface FoldState<T> {
|
|
7
|
+
/** The visible rows: pass this array to the document controller and the renderer. */
|
|
8
|
+
rows: readonly T[];
|
|
9
|
+
/** The full-array row count. */
|
|
10
|
+
rowCount: number;
|
|
11
|
+
ranges: readonly FoldRange[];
|
|
12
|
+
view: FoldView;
|
|
13
|
+
closedKeys: ReadonlySet<Key>;
|
|
14
|
+
setClosedKeys: (keys: ReadonlySet<Key>) => void;
|
|
15
|
+
/** External decoration layers projected onto the visible rows. */
|
|
16
|
+
decorations: DecorationLayer[];
|
|
17
|
+
/** One-based full-array row number for each visible row, or `undefined` when nothing is hidden. */
|
|
18
|
+
rowNumbers: readonly number[] | undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Hold the closed fold keys for `rows` and project the rows, decorations and
|
|
22
|
+
* gutter numbers onto what stays visible. `ranges` and `decorations` use
|
|
23
|
+
* full-array row indices. With no closed fold, `rows` and `decorations` pass
|
|
24
|
+
* through unchanged.
|
|
25
|
+
*/
|
|
26
|
+
export declare const useFoldState: <T>(rows: readonly T[], ranges: readonly FoldRange[], decorations: DecorationLayer[]) => FoldState<T>;
|
|
27
|
+
/**
|
|
28
|
+
* Register the Vim fold commands (`z a`, `z c`, `z o`, `z shift+m`,
|
|
29
|
+
* `z shift+r`) against a fold state and the controller's navigation. When a
|
|
30
|
+
* close hides the cursor row, the cursor moves first to the header of the
|
|
31
|
+
* outermost closed fold, and key preservation keeps it there.
|
|
32
|
+
*/
|
|
33
|
+
export declare const useFoldCommands: <T>(folds: FoldState<T>, navigation: Pick<NavigationState, "cursor" | "setCursor">) => void;
|
|
34
|
+
//# sourceMappingURL=use-folds.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-folds.d.ts","sourceRoot":"","sources":["../../src/folds/use-folds.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAIjC,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAI3D,+EAA+E;AAC/E,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,qFAAqF;IACrF,IAAI,EAAE,SAAS,CAAC,EAAE,CAAC;IACnB,gCAAgC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;IAC7B,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC;IAC7B,aAAa,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC;IAChD,kEAAkE;IAClE,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,mGAAmG;IACnG,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,CAAC;CAC3C;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAyB,CAAC,QAC3C,SAAS,CAAC,EAAE,UACV,SAAS,SAAS,EAAE,eACf,eAAe,EAAE,KAC7B,SAAS,CAAC,CAAC,CAqCb,CAAC;AAMF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAA4B,CAAC,SAChD,SAAS,CAAC,CAAC,CAAC,cACP,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,WAAW,CAAC,KACxD,IAiGF,CAAC"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useCommand, useCommandGroup } from "@tooee/commands";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { projectDecorationLayers } from "./fold-decorations.js";
|
|
4
|
+
import { computeFoldView, foldsAt, visibleAnchorRow } from "./fold-model.js";
|
|
5
|
+
const EMPTY_KEYS = new Set();
|
|
6
|
+
/**
|
|
7
|
+
* Hold the closed fold keys for `rows` and project the rows, decorations and
|
|
8
|
+
* gutter numbers onto what stays visible. `ranges` and `decorations` use
|
|
9
|
+
* full-array row indices. With no closed fold, `rows` and `decorations` pass
|
|
10
|
+
* through unchanged.
|
|
11
|
+
*/
|
|
12
|
+
export const useFoldState = function useFoldState(rows, ranges, decorations) {
|
|
13
|
+
const [closedKeys, setClosedKeys] = useState(EMPTY_KEYS);
|
|
14
|
+
const view = useMemo(() => computeFoldView(rows.length, ranges, closedKeys), [rows.length, ranges, closedKeys]);
|
|
15
|
+
const folded = view.visibleToSource.length < rows.length;
|
|
16
|
+
const visibleRows = useMemo(() => folded
|
|
17
|
+
? view.visibleToSource.flatMap((index) => {
|
|
18
|
+
const row = rows[index];
|
|
19
|
+
return row === undefined ? [] : [row];
|
|
20
|
+
})
|
|
21
|
+
: rows, [folded, rows, view]);
|
|
22
|
+
const visibleDecorations = useMemo(() => (folded ? projectDecorationLayers(decorations, view) : decorations), [folded, decorations, view]);
|
|
23
|
+
const rowNumbers = useMemo(() => (folded ? view.visibleToSource.map((index) => index + 1) : undefined), [folded, view]);
|
|
24
|
+
return {
|
|
25
|
+
closedKeys,
|
|
26
|
+
decorations: visibleDecorations,
|
|
27
|
+
ranges,
|
|
28
|
+
rowCount: rows.length,
|
|
29
|
+
rowNumbers,
|
|
30
|
+
rows: visibleRows,
|
|
31
|
+
setClosedKeys,
|
|
32
|
+
view,
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
const warnNoFold = function warnNoFold(ctx) {
|
|
36
|
+
ctx.toast?.toast({ level: "warning", message: "No fold at cursor" });
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Register the Vim fold commands (`z a`, `z c`, `z o`, `z shift+m`,
|
|
40
|
+
* `z shift+r`) against a fold state and the controller's navigation. When a
|
|
41
|
+
* close hides the cursor row, the cursor moves first to the header of the
|
|
42
|
+
* outermost closed fold, and key preservation keeps it there.
|
|
43
|
+
*/
|
|
44
|
+
export const useFoldCommands = function useFoldCommands(folds, navigation) {
|
|
45
|
+
const { closedKeys, ranges, rowCount, setClosedKeys, view } = folds;
|
|
46
|
+
const { cursor, setCursor } = navigation;
|
|
47
|
+
const sourceRow = cursor === null ? undefined : view.visibleToSource[cursor];
|
|
48
|
+
const atCursor = sourceRow === undefined ? [] : foldsAt(ranges, sourceRow);
|
|
49
|
+
const apply = (next) => {
|
|
50
|
+
if (sourceRow !== undefined) {
|
|
51
|
+
const anchor = visibleAnchorRow(computeFoldView(rowCount, ranges, next), sourceRow);
|
|
52
|
+
const anchorIndex = view.sourceToVisible[anchor] ?? -1;
|
|
53
|
+
if (anchor !== sourceRow && anchorIndex !== -1) {
|
|
54
|
+
setCursor(anchorIndex);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
setClosedKeys(next);
|
|
58
|
+
};
|
|
59
|
+
const withKey = (key, closed) => {
|
|
60
|
+
const next = new Set(closedKeys);
|
|
61
|
+
if (closed) {
|
|
62
|
+
next.add(key);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
next.delete(key);
|
|
66
|
+
}
|
|
67
|
+
apply(next);
|
|
68
|
+
};
|
|
69
|
+
useCommandGroup({ id: "fold", prefix: "z", title: "Fold" });
|
|
70
|
+
useCommand({
|
|
71
|
+
category: "Fold",
|
|
72
|
+
handler: (ctx) => {
|
|
73
|
+
const [innermost] = atCursor;
|
|
74
|
+
if (innermost === undefined) {
|
|
75
|
+
warnNoFold(ctx);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
withKey(innermost.key, !closedKeys.has(innermost.key));
|
|
79
|
+
},
|
|
80
|
+
hotkey: "z a",
|
|
81
|
+
id: "fold.toggle",
|
|
82
|
+
modes: ["cursor"],
|
|
83
|
+
title: "Toggle fold",
|
|
84
|
+
});
|
|
85
|
+
useCommand({
|
|
86
|
+
category: "Fold",
|
|
87
|
+
handler: (ctx) => {
|
|
88
|
+
const target = atCursor.find((range) => !closedKeys.has(range.key));
|
|
89
|
+
if (target === undefined) {
|
|
90
|
+
warnNoFold(ctx);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
withKey(target.key, true);
|
|
94
|
+
},
|
|
95
|
+
hotkey: "z c",
|
|
96
|
+
id: "fold.close",
|
|
97
|
+
modes: ["cursor"],
|
|
98
|
+
title: "Close fold",
|
|
99
|
+
});
|
|
100
|
+
useCommand({
|
|
101
|
+
category: "Fold",
|
|
102
|
+
handler: (ctx) => {
|
|
103
|
+
const target = atCursor.find((range) => closedKeys.has(range.key));
|
|
104
|
+
if (target === undefined) {
|
|
105
|
+
warnNoFold(ctx);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
withKey(target.key, false);
|
|
109
|
+
},
|
|
110
|
+
hotkey: "z o",
|
|
111
|
+
id: "fold.open",
|
|
112
|
+
modes: ["cursor"],
|
|
113
|
+
title: "Open fold",
|
|
114
|
+
});
|
|
115
|
+
useCommand({
|
|
116
|
+
category: "Fold",
|
|
117
|
+
handler: () => {
|
|
118
|
+
apply(new Set(ranges.map((range) => range.key)));
|
|
119
|
+
},
|
|
120
|
+
hotkey: "z shift+m",
|
|
121
|
+
id: "fold.close-all",
|
|
122
|
+
modes: ["cursor"],
|
|
123
|
+
title: "Close all folds",
|
|
124
|
+
});
|
|
125
|
+
useCommand({
|
|
126
|
+
category: "Fold",
|
|
127
|
+
handler: () => {
|
|
128
|
+
apply(EMPTY_KEYS);
|
|
129
|
+
},
|
|
130
|
+
hotkey: "z shift+r",
|
|
131
|
+
id: "fold.open-all",
|
|
132
|
+
modes: ["cursor"],
|
|
133
|
+
title: "Open all folds",
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
//# sourceMappingURL=use-folds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-folds.js","sourceRoot":"","sources":["../../src/folds/use-folds.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tooee/view",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Terminal content viewer for markdown, code, text, images, and tables",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -40,18 +40,18 @@
|
|
|
40
40
|
"typecheck": "tsc --noEmit"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@tooee/choose": "0.
|
|
44
|
-
"@tooee/commands": "0.
|
|
45
|
-
"@tooee/config": "0.
|
|
46
|
-
"@tooee/diff": "0.
|
|
47
|
-
"@tooee/layout": "0.
|
|
48
|
-
"@tooee/marks": "0.
|
|
49
|
-
"@tooee/overlays": "0.
|
|
50
|
-
"@tooee/renderers": "0.
|
|
51
|
-
"@tooee/router": "0.
|
|
52
|
-
"@tooee/search": "0.
|
|
53
|
-
"@tooee/shell": "0.
|
|
54
|
-
"@tooee/themes": "0.
|
|
43
|
+
"@tooee/choose": "0.11.0",
|
|
44
|
+
"@tooee/commands": "0.11.0",
|
|
45
|
+
"@tooee/config": "0.11.0",
|
|
46
|
+
"@tooee/diff": "0.11.0",
|
|
47
|
+
"@tooee/layout": "0.11.0",
|
|
48
|
+
"@tooee/marks": "0.11.0",
|
|
49
|
+
"@tooee/overlays": "0.11.0",
|
|
50
|
+
"@tooee/renderers": "0.11.0",
|
|
51
|
+
"@tooee/router": "0.11.0",
|
|
52
|
+
"@tooee/search": "0.11.0",
|
|
53
|
+
"@tooee/shell": "0.11.0",
|
|
54
|
+
"@tooee/themes": "0.11.0",
|
|
55
55
|
"@xstate/store": "^4.2.3",
|
|
56
56
|
"@xstate/store-react": "^2",
|
|
57
57
|
"marked": "^18.0.14"
|
|
@@ -6,6 +6,8 @@ import type { DocumentRowAdapter } from "@tooee/shell";
|
|
|
6
6
|
import { useMemo, useRef } from "react";
|
|
7
7
|
import type { ReactNode } from "react";
|
|
8
8
|
|
|
9
|
+
import { markdownHeadingFoldRanges } from "../../folds/markdown-heading-folds.js";
|
|
10
|
+
import { useFoldCommands, useFoldState } from "../../folds/use-folds.js";
|
|
9
11
|
import { useContentDocument } from "../../hooks/use-content-document.js";
|
|
10
12
|
import type { MarkdownContent, MarkdownLinkActivateHandler } from "../../types.js";
|
|
11
13
|
import { ViewScreen } from "../view-screen.js";
|
|
@@ -23,11 +25,19 @@ const BLOCK_HSCROLL_STEP = 4;
|
|
|
23
25
|
/**
|
|
24
26
|
* Blocks are the row unit. `getFlatBlockText` keeps search/copy in step with the
|
|
25
27
|
* source mapping (notably for synthetic bullet rows), and `getSource` projects
|
|
26
|
-
* each block's Markdown provenance onto the controller's anchors.
|
|
28
|
+
* each block's Markdown provenance onto the controller's anchors. The key is
|
|
29
|
+
* the block's index in the full flattened document, so it stays the same when
|
|
30
|
+
* folds hide the blocks around it.
|
|
27
31
|
*/
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
const markdownBlockAdapter = function markdownBlockAdapter(
|
|
33
|
+
blocks: readonly FlatBlock[]
|
|
34
|
+
): DocumentRowAdapter<FlatBlock> {
|
|
35
|
+
const indices = new Map(blocks.map((block, index) => [block, index]));
|
|
36
|
+
return {
|
|
37
|
+
getKey: (block, index) => indices.get(block) ?? index,
|
|
38
|
+
getSource: (block) => block.source,
|
|
39
|
+
getText: (block) => getFlatBlockText(block),
|
|
40
|
+
};
|
|
31
41
|
};
|
|
32
42
|
|
|
33
43
|
export const MarkdownSubview = function MarkdownSubview({
|
|
@@ -41,19 +51,26 @@ export const MarkdownSubview = function MarkdownSubview({
|
|
|
41
51
|
const textContent = content.markdown;
|
|
42
52
|
const lineCount = useMemo(() => textContent.split("\n").length, [textContent]);
|
|
43
53
|
const blocks = useMemo(() => flattenMarkdown(content.markdown), [content.markdown]);
|
|
54
|
+
const adapter = useMemo(() => markdownBlockAdapter(blocks), [blocks]);
|
|
55
|
+
const foldRanges = useMemo(() => markdownHeadingFoldRanges(blocks), [blocks]);
|
|
56
|
+
// Closed heading folds filter `blocks`; `folds.rows` is the one array both
|
|
57
|
+
// the controller and the renderer see.
|
|
58
|
+
const folds = useFoldState(blocks, foldRanges, decorations);
|
|
44
59
|
|
|
45
60
|
const { document, showLineNumbers, statusItems } = useContentDocument<FlatBlock>(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{ actions, content, decorations, textContent },
|
|
61
|
+
folds.rows,
|
|
62
|
+
adapter,
|
|
63
|
+
{ actions, content, decorations: folds.decorations, textContent },
|
|
49
64
|
{
|
|
50
65
|
multiSelect: true,
|
|
66
|
+
preserveCursorByKey: true,
|
|
51
67
|
statusItems: [
|
|
52
68
|
{ label: "Format:", value: content.format },
|
|
53
69
|
{ label: "Lines:", value: String(lineCount) },
|
|
54
70
|
],
|
|
55
71
|
}
|
|
56
72
|
);
|
|
73
|
+
useFoldCommands(folds, document.navigation);
|
|
57
74
|
const buildCommandContext = useBuildCommandContext();
|
|
58
75
|
const handleLinkActivate = onLinkActivate
|
|
59
76
|
? (href: string) => onLinkActivate(href, buildCommandContext())
|
|
@@ -101,7 +118,9 @@ export const MarkdownSubview = function MarkdownSubview({
|
|
|
101
118
|
>
|
|
102
119
|
<MarkdownView
|
|
103
120
|
content={content.markdown}
|
|
104
|
-
blocks={
|
|
121
|
+
blocks={folds.rows}
|
|
122
|
+
rowNumbers={folds.rowNumbers}
|
|
123
|
+
foldedBlocks={folds.view.hiddenCounts}
|
|
105
124
|
showLineNumbers={showLineNumbers}
|
|
106
125
|
document={document}
|
|
107
126
|
hScrollableBlocksRef={hScrollableBlocksRef}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { DecorationLayer, RowDecoration } from "@tooee/renderers";
|
|
2
|
+
|
|
3
|
+
import type { FoldView } from "./fold-model.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Map decoration layers written against full-array rows onto the visible rows
|
|
7
|
+
* of `view`. Decorations on rows that a closed fold hides are dropped.
|
|
8
|
+
*/
|
|
9
|
+
export const projectDecorationLayers = function projectDecorationLayers(
|
|
10
|
+
layers: readonly DecorationLayer[],
|
|
11
|
+
view: FoldView
|
|
12
|
+
): DecorationLayer[] {
|
|
13
|
+
const { visibleToSource, sourceToVisible } = view;
|
|
14
|
+
return layers.map((layer): DecorationLayer => ({
|
|
15
|
+
*forVisibleRows(from: number, to: number): Generator<RowDecoration> {
|
|
16
|
+
const sourceFrom = visibleToSource[from];
|
|
17
|
+
const sourceTo = visibleToSource[Math.min(to, visibleToSource.length - 1)];
|
|
18
|
+
if (sourceFrom === undefined || sourceTo === undefined) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
for (const decoration of layer.forVisibleRows(sourceFrom, sourceTo)) {
|
|
22
|
+
const row = sourceToVisible[decoration.row] ?? -1;
|
|
23
|
+
if (row !== -1) {
|
|
24
|
+
yield { ...decoration, row };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
priority: layer.priority,
|
|
29
|
+
}));
|
|
30
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Key } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A foldable range of rows. `start` is the header row that stays visible;
|
|
5
|
+
* `start + 1 ..= end` are the rows a closed fold hides. Indices point into the
|
|
6
|
+
* full row array, and `key` is the header row's key, so closed state survives
|
|
7
|
+
* row changes that keep keys stable.
|
|
8
|
+
*/
|
|
9
|
+
export interface FoldRange {
|
|
10
|
+
key: Key;
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The visible projection of a full row array under a set of closed folds. */
|
|
16
|
+
export interface FoldView {
|
|
17
|
+
/** Full-array index of each visible row, in order. */
|
|
18
|
+
visibleToSource: readonly number[];
|
|
19
|
+
/** Visible index of each full-array row, or `-1` when a closed fold hides it. */
|
|
20
|
+
sourceToVisible: readonly number[];
|
|
21
|
+
/** Hidden row count for each closed header, keyed by the header's visible index. */
|
|
22
|
+
hiddenCounts: ReadonlyMap<number, number>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Project `rowCount` rows through the closed folds. A closed range hides its
|
|
27
|
+
* body even when it sits inside another closed range; the outermost header
|
|
28
|
+
* shown carries the count of every row hidden below it.
|
|
29
|
+
*/
|
|
30
|
+
export const computeFoldView = function computeFoldView(
|
|
31
|
+
rowCount: number,
|
|
32
|
+
ranges: readonly FoldRange[],
|
|
33
|
+
closedKeys: ReadonlySet<Key>
|
|
34
|
+
): FoldView {
|
|
35
|
+
const closedEnds = new Map<number, number>();
|
|
36
|
+
for (const range of ranges) {
|
|
37
|
+
if (range.end > range.start && closedKeys.has(range.key)) {
|
|
38
|
+
closedEnds.set(range.start, Math.max(closedEnds.get(range.start) ?? range.end, range.end));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const visibleToSource: number[] = [];
|
|
43
|
+
const sourceToVisible: number[] = Array.from({ length: rowCount }, () => -1);
|
|
44
|
+
const hiddenCounts = new Map<number, number>();
|
|
45
|
+
let hiddenUntil = -1;
|
|
46
|
+
for (let index = 0; index < rowCount; index += 1) {
|
|
47
|
+
if (index <= hiddenUntil) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const visibleIndex = visibleToSource.length;
|
|
51
|
+
visibleToSource.push(index);
|
|
52
|
+
sourceToVisible[index] = visibleIndex;
|
|
53
|
+
const end = closedEnds.get(index);
|
|
54
|
+
if (end !== undefined) {
|
|
55
|
+
hiddenUntil = Math.min(end, rowCount - 1);
|
|
56
|
+
hiddenCounts.set(visibleIndex, hiddenUntil - index);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { hiddenCounts, sourceToVisible, visibleToSource };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Ranges that contain `row` (header included), innermost first. */
|
|
64
|
+
export const foldsAt = function foldsAt(
|
|
65
|
+
ranges: readonly FoldRange[],
|
|
66
|
+
row: number
|
|
67
|
+
): readonly FoldRange[] {
|
|
68
|
+
return ranges
|
|
69
|
+
.filter((range) => range.end > range.start && range.start <= row && row <= range.end)
|
|
70
|
+
.toSorted((left, right) => right.start - left.start || left.end - right.end);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The full-array row the cursor should rest on once `view` applies: `row`
|
|
75
|
+
* itself when it stays visible, otherwise the nearest visible row before it,
|
|
76
|
+
* which is the header of the outermost closed fold that hides it.
|
|
77
|
+
*/
|
|
78
|
+
export const visibleAnchorRow = function visibleAnchorRow(view: FoldView, row: number): number {
|
|
79
|
+
for (let index = Math.min(row, view.sourceToVisible.length - 1); index >= 0; index -= 1) {
|
|
80
|
+
if ((view.sourceToVisible[index] ?? -1) !== -1) {
|
|
81
|
+
return index;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return row;
|
|
85
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { FlatBlock } from "@tooee/renderers";
|
|
2
|
+
import type { Tokens } from "marked";
|
|
3
|
+
|
|
4
|
+
import type { FoldRange } from "./fold-model.js";
|
|
5
|
+
|
|
6
|
+
const headingDepth = function headingDepth(block: FlatBlock): number | null {
|
|
7
|
+
if (block.token.type !== "heading") {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
// SAFETY: Marked creates a Heading token for the checked "heading" discriminator.
|
|
11
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Marked's Generic fallback prevents discriminator narrowing
|
|
12
|
+
return (block.token as Tokens.Heading).depth;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Heading fold ranges over flattened Markdown blocks. A heading at depth `d`
|
|
17
|
+
* folds every following block up to the next heading of depth `d` or less, or
|
|
18
|
+
* the end of the document. A heading with no body yields no range. Keys are
|
|
19
|
+
* the heading's block index, the same key the Markdown row adapter uses.
|
|
20
|
+
*/
|
|
21
|
+
export const markdownHeadingFoldRanges = function markdownHeadingFoldRanges(
|
|
22
|
+
blocks: readonly FlatBlock[]
|
|
23
|
+
): FoldRange[] {
|
|
24
|
+
const ranges: FoldRange[] = [];
|
|
25
|
+
const open: { depth: number; start: number }[] = [];
|
|
26
|
+
const close = (start: number, end: number) => {
|
|
27
|
+
if (end > start) {
|
|
28
|
+
ranges.push({ end, key: start, start });
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
for (const [index, block] of blocks.entries()) {
|
|
33
|
+
const depth = headingDepth(block);
|
|
34
|
+
if (depth === null) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
while (open.length > 0 && (open.at(-1)?.depth ?? 0) >= depth) {
|
|
38
|
+
const finished = open.pop();
|
|
39
|
+
if (finished) {
|
|
40
|
+
close(finished.start, index - 1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
open.push({ depth, start: index });
|
|
44
|
+
}
|
|
45
|
+
for (const finished of open) {
|
|
46
|
+
close(finished.start, blocks.length - 1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return ranges.toSorted((left, right) => left.start - right.start);
|
|
50
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { useCommand, useCommandGroup } from "@tooee/commands";
|
|
2
|
+
import type { CommandContext } from "@tooee/commands";
|
|
3
|
+
import type { DecorationLayer } from "@tooee/renderers";
|
|
4
|
+
import type { NavigationState } from "@tooee/shell";
|
|
5
|
+
import { useMemo, useState } from "react";
|
|
6
|
+
import type { Key } from "react";
|
|
7
|
+
|
|
8
|
+
import { projectDecorationLayers } from "./fold-decorations.js";
|
|
9
|
+
import { computeFoldView, foldsAt, visibleAnchorRow } from "./fold-model.js";
|
|
10
|
+
import type { FoldRange, FoldView } from "./fold-model.js";
|
|
11
|
+
|
|
12
|
+
const EMPTY_KEYS: ReadonlySet<Key> = new Set();
|
|
13
|
+
|
|
14
|
+
/** Fold state over a full row array and the visible projection it produces. */
|
|
15
|
+
export interface FoldState<T> {
|
|
16
|
+
/** The visible rows: pass this array to the document controller and the renderer. */
|
|
17
|
+
rows: readonly T[];
|
|
18
|
+
/** The full-array row count. */
|
|
19
|
+
rowCount: number;
|
|
20
|
+
ranges: readonly FoldRange[];
|
|
21
|
+
view: FoldView;
|
|
22
|
+
closedKeys: ReadonlySet<Key>;
|
|
23
|
+
setClosedKeys: (keys: ReadonlySet<Key>) => void;
|
|
24
|
+
/** External decoration layers projected onto the visible rows. */
|
|
25
|
+
decorations: DecorationLayer[];
|
|
26
|
+
/** One-based full-array row number for each visible row, or `undefined` when nothing is hidden. */
|
|
27
|
+
rowNumbers: readonly number[] | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Hold the closed fold keys for `rows` and project the rows, decorations and
|
|
32
|
+
* gutter numbers onto what stays visible. `ranges` and `decorations` use
|
|
33
|
+
* full-array row indices. With no closed fold, `rows` and `decorations` pass
|
|
34
|
+
* through unchanged.
|
|
35
|
+
*/
|
|
36
|
+
export const useFoldState = function useFoldState<T>(
|
|
37
|
+
rows: readonly T[],
|
|
38
|
+
ranges: readonly FoldRange[],
|
|
39
|
+
decorations: DecorationLayer[]
|
|
40
|
+
): FoldState<T> {
|
|
41
|
+
const [closedKeys, setClosedKeys] = useState<ReadonlySet<Key>>(EMPTY_KEYS);
|
|
42
|
+
const view = useMemo(
|
|
43
|
+
() => computeFoldView(rows.length, ranges, closedKeys),
|
|
44
|
+
[rows.length, ranges, closedKeys]
|
|
45
|
+
);
|
|
46
|
+
const folded = view.visibleToSource.length < rows.length;
|
|
47
|
+
|
|
48
|
+
const visibleRows = useMemo(
|
|
49
|
+
() =>
|
|
50
|
+
folded
|
|
51
|
+
? view.visibleToSource.flatMap((index) => {
|
|
52
|
+
const row = rows[index];
|
|
53
|
+
return row === undefined ? [] : [row];
|
|
54
|
+
})
|
|
55
|
+
: rows,
|
|
56
|
+
[folded, rows, view]
|
|
57
|
+
);
|
|
58
|
+
const visibleDecorations = useMemo(
|
|
59
|
+
() => (folded ? projectDecorationLayers(decorations, view) : decorations),
|
|
60
|
+
[folded, decorations, view]
|
|
61
|
+
);
|
|
62
|
+
const rowNumbers = useMemo(
|
|
63
|
+
() => (folded ? view.visibleToSource.map((index) => index + 1) : undefined),
|
|
64
|
+
[folded, view]
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
closedKeys,
|
|
69
|
+
decorations: visibleDecorations,
|
|
70
|
+
ranges,
|
|
71
|
+
rowCount: rows.length,
|
|
72
|
+
rowNumbers,
|
|
73
|
+
rows: visibleRows,
|
|
74
|
+
setClosedKeys,
|
|
75
|
+
view,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const warnNoFold = function warnNoFold(ctx: CommandContext): void {
|
|
80
|
+
ctx.toast?.toast({ level: "warning", message: "No fold at cursor" });
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Register the Vim fold commands (`z a`, `z c`, `z o`, `z shift+m`,
|
|
85
|
+
* `z shift+r`) against a fold state and the controller's navigation. When a
|
|
86
|
+
* close hides the cursor row, the cursor moves first to the header of the
|
|
87
|
+
* outermost closed fold, and key preservation keeps it there.
|
|
88
|
+
*/
|
|
89
|
+
export const useFoldCommands = function useFoldCommands<T>(
|
|
90
|
+
folds: FoldState<T>,
|
|
91
|
+
navigation: Pick<NavigationState, "cursor" | "setCursor">
|
|
92
|
+
): void {
|
|
93
|
+
const { closedKeys, ranges, rowCount, setClosedKeys, view } = folds;
|
|
94
|
+
const { cursor, setCursor } = navigation;
|
|
95
|
+
const sourceRow = cursor === null ? undefined : view.visibleToSource[cursor];
|
|
96
|
+
const atCursor = sourceRow === undefined ? [] : foldsAt(ranges, sourceRow);
|
|
97
|
+
|
|
98
|
+
const apply = (next: ReadonlySet<Key>) => {
|
|
99
|
+
if (sourceRow !== undefined) {
|
|
100
|
+
const anchor = visibleAnchorRow(computeFoldView(rowCount, ranges, next), sourceRow);
|
|
101
|
+
const anchorIndex = view.sourceToVisible[anchor] ?? -1;
|
|
102
|
+
if (anchor !== sourceRow && anchorIndex !== -1) {
|
|
103
|
+
setCursor(anchorIndex);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
setClosedKeys(next);
|
|
107
|
+
};
|
|
108
|
+
const withKey = (key: Key, closed: boolean) => {
|
|
109
|
+
const next = new Set(closedKeys);
|
|
110
|
+
if (closed) {
|
|
111
|
+
next.add(key);
|
|
112
|
+
} else {
|
|
113
|
+
next.delete(key);
|
|
114
|
+
}
|
|
115
|
+
apply(next);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
useCommandGroup({ id: "fold", prefix: "z", title: "Fold" });
|
|
119
|
+
|
|
120
|
+
useCommand({
|
|
121
|
+
category: "Fold",
|
|
122
|
+
handler: (ctx) => {
|
|
123
|
+
const [innermost] = atCursor;
|
|
124
|
+
if (innermost === undefined) {
|
|
125
|
+
warnNoFold(ctx);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
withKey(innermost.key, !closedKeys.has(innermost.key));
|
|
129
|
+
},
|
|
130
|
+
hotkey: "z a",
|
|
131
|
+
id: "fold.toggle",
|
|
132
|
+
modes: ["cursor"],
|
|
133
|
+
title: "Toggle fold",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
useCommand({
|
|
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);
|
|
145
|
+
},
|
|
146
|
+
hotkey: "z c",
|
|
147
|
+
id: "fold.close",
|
|
148
|
+
modes: ["cursor"],
|
|
149
|
+
title: "Close fold",
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
useCommand({
|
|
153
|
+
category: "Fold",
|
|
154
|
+
handler: (ctx) => {
|
|
155
|
+
const target = atCursor.find((range) => closedKeys.has(range.key));
|
|
156
|
+
if (target === undefined) {
|
|
157
|
+
warnNoFold(ctx);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
withKey(target.key, false);
|
|
161
|
+
},
|
|
162
|
+
hotkey: "z o",
|
|
163
|
+
id: "fold.open",
|
|
164
|
+
modes: ["cursor"],
|
|
165
|
+
title: "Open fold",
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
useCommand({
|
|
169
|
+
category: "Fold",
|
|
170
|
+
handler: () => {
|
|
171
|
+
apply(new Set(ranges.map((range) => range.key)));
|
|
172
|
+
},
|
|
173
|
+
hotkey: "z shift+m",
|
|
174
|
+
id: "fold.close-all",
|
|
175
|
+
modes: ["cursor"],
|
|
176
|
+
title: "Close all folds",
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
useCommand({
|
|
180
|
+
category: "Fold",
|
|
181
|
+
handler: () => {
|
|
182
|
+
apply(EMPTY_KEYS);
|
|
183
|
+
},
|
|
184
|
+
hotkey: "z shift+r",
|
|
185
|
+
id: "fold.open-all",
|
|
186
|
+
modes: ["cursor"],
|
|
187
|
+
title: "Open all folds",
|
|
188
|
+
});
|
|
189
|
+
};
|