@tooee/view 0.7.3 → 0.8.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/code-subview.d.ts +2 -1
- package/dist/components/subviews/code-subview.d.ts.map +1 -1
- package/dist/components/subviews/code-subview.js +2 -15
- package/dist/components/subviews/code-subview.js.map +1 -1
- package/dist/components/subviews/custom-subview.d.ts +2 -1
- package/dist/components/subviews/custom-subview.d.ts.map +1 -1
- package/dist/components/subviews/custom-subview.js +3 -13
- package/dist/components/subviews/custom-subview.js.map +1 -1
- package/dist/components/subviews/diff-subview.d.ts +2 -1
- package/dist/components/subviews/diff-subview.d.ts.map +1 -1
- package/dist/components/subviews/diff-subview.js +23 -33
- package/dist/components/subviews/diff-subview.js.map +1 -1
- package/dist/components/subviews/image-subview.d.ts +2 -1
- package/dist/components/subviews/image-subview.d.ts.map +1 -1
- package/dist/components/subviews/image-subview.js +4 -9
- package/dist/components/subviews/image-subview.js.map +1 -1
- package/dist/components/subviews/markdown-subview.d.ts +2 -1
- package/dist/components/subviews/markdown-subview.d.ts.map +1 -1
- package/dist/components/subviews/markdown-subview.js +8 -15
- package/dist/components/subviews/markdown-subview.js.map +1 -1
- package/dist/components/subviews/table-subview.d.ts +2 -1
- package/dist/components/subviews/table-subview.d.ts.map +1 -1
- package/dist/components/subviews/table-subview.js +9 -48
- package/dist/components/subviews/table-subview.js.map +1 -1
- package/dist/components/view-state.d.ts +2 -1
- package/dist/components/view-state.d.ts.map +1 -1
- package/dist/components/view-state.js.map +1 -1
- package/dist/content-loader-store.d.ts +3 -2
- package/dist/content-loader-store.d.ts.map +1 -1
- package/dist/content-loader-store.js +5 -4
- package/dist/content-loader-store.js.map +1 -1
- package/dist/default-provider.d.ts.map +1 -1
- package/dist/default-provider.js +29 -29
- package/dist/default-provider.js.map +1 -1
- package/dist/directory-view.d.ts +2 -1
- package/dist/directory-view.d.ts.map +1 -1
- package/dist/directory-view.js +2 -1
- package/dist/directory-view.js.map +1 -1
- package/dist/hooks/use-content-commands.d.ts +5 -3
- package/dist/hooks/use-content-commands.d.ts.map +1 -1
- package/dist/hooks/use-content-commands.js.map +1 -1
- package/dist/hooks/use-content-document.d.ts +28 -0
- package/dist/hooks/use-content-document.d.ts.map +1 -0
- package/dist/hooks/use-content-document.js +21 -0
- package/dist/hooks/use-content-document.js.map +1 -0
- package/dist/hooks/use-content-loader.d.ts +10 -6
- package/dist/hooks/use-content-loader.d.ts.map +1 -1
- package/dist/hooks/use-content-loader.js +1 -1
- package/dist/hooks/use-content-loader.js.map +1 -1
- package/dist/hooks/use-view-command-context.d.ts +4 -2
- package/dist/hooks/use-view-command-context.d.ts.map +1 -1
- package/dist/hooks/use-view-command-context.js +6 -1
- package/dist/hooks/use-view-command-context.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js +2 -2
- package/dist/launch.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -33
- package/dist/types.js.map +1 -1
- package/dist/view.d.ts +2 -1
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js.map +1 -1
- package/package.json +12 -11
- package/src/components/subviews/code-subview.tsx +8 -20
- package/src/components/subviews/custom-subview.tsx +9 -19
- package/src/components/subviews/diff-subview.tsx +35 -43
- package/src/components/subviews/image-subview.tsx +12 -11
- package/src/components/subviews/markdown-subview.tsx +16 -22
- package/src/components/subviews/table-subview.tsx +16 -56
- package/src/components/view-state.tsx +2 -1
- package/src/content-loader-store.ts +8 -5
- package/src/default-provider.ts +34 -32
- package/src/directory-view.tsx +4 -2
- package/src/hooks/use-content-commands.ts +6 -1
- package/src/hooks/use-content-document.ts +54 -0
- package/src/hooks/use-content-loader.ts +17 -3
- package/src/hooks/use-view-command-context.ts +16 -4
- package/src/index.ts +2 -0
- package/src/launch.tsx +11 -8
- package/src/types.ts +4 -35
- package/src/view.tsx +2 -1
- package/dist/hooks/index.d.ts +0 -5
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/index.js +0 -4
- package/dist/hooks/index.js.map +0 -1
- package/src/hooks/index.ts +0 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tooee/view",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Terminal content viewer for markdown, code, text, images, and tables",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -40,16 +40,16 @@
|
|
|
40
40
|
"typecheck": "tsc --noEmit"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@tooee/commands": "0.
|
|
44
|
-
"@tooee/config": "0.
|
|
45
|
-
"@tooee/diff": "0.
|
|
46
|
-
"@tooee/layout": "0.
|
|
47
|
-
"@tooee/marks": "0.
|
|
48
|
-
"@tooee/overlays": "0.
|
|
49
|
-
"@tooee/renderers": "0.
|
|
50
|
-
"@tooee/search": "0.
|
|
51
|
-
"@tooee/shell": "0.
|
|
52
|
-
"@tooee/themes": "0.
|
|
43
|
+
"@tooee/commands": "0.8.0",
|
|
44
|
+
"@tooee/config": "0.8.0",
|
|
45
|
+
"@tooee/diff": "0.8.0",
|
|
46
|
+
"@tooee/layout": "0.8.0",
|
|
47
|
+
"@tooee/marks": "0.8.0",
|
|
48
|
+
"@tooee/overlays": "0.8.0",
|
|
49
|
+
"@tooee/renderers": "0.8.0",
|
|
50
|
+
"@tooee/search": "0.8.0",
|
|
51
|
+
"@tooee/shell": "0.8.0",
|
|
52
|
+
"@tooee/themes": "0.8.0",
|
|
53
53
|
"@xstate/store": "^4",
|
|
54
54
|
"@xstate/store-react": "^2",
|
|
55
55
|
"marked": "^17.0.4"
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@opentui/core": "^0.5.1",
|
|
59
59
|
"@opentui/react": "^0.5.1",
|
|
60
|
+
"@tooee/test-support": "0.7.3",
|
|
60
61
|
"@types/bun": "^1.3.10",
|
|
61
62
|
"@types/react": "^19.2.14",
|
|
62
63
|
"typescript": "^5.9.3"
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { CodeView, sourceLines, sourceLineAdapter } from "@tooee/renderers";
|
|
3
3
|
import type { SourceLineRow } from "@tooee/renderers";
|
|
4
|
-
import { useDocumentController } from "@tooee/shell";
|
|
5
4
|
import type { CodeContent, TextContent } from "../../types.js";
|
|
6
|
-
import {
|
|
5
|
+
import { useContentDocument } from "../../hooks/use-content-document.js";
|
|
7
6
|
import { ViewScreen } from "../view-screen.js";
|
|
8
7
|
import type { SubviewProps } from "./types.js";
|
|
8
|
+
import type { ReactNode } from "react";
|
|
9
9
|
|
|
10
10
|
interface CodeSubviewProps extends SubviewProps {
|
|
11
11
|
content: CodeContent | TextContent;
|
|
@@ -16,28 +16,16 @@ export const CodeSubview = function CodeSubview({
|
|
|
16
16
|
decorations,
|
|
17
17
|
actions,
|
|
18
18
|
...screen
|
|
19
|
-
}: CodeSubviewProps):
|
|
19
|
+
}: CodeSubviewProps): ReactNode {
|
|
20
20
|
const textContent = content.format === "code" ? content.code : content.text;
|
|
21
21
|
// One navigation row per physical source line; row index is the source line.
|
|
22
22
|
const lineRows = useMemo(() => sourceLines(textContent), [textContent]);
|
|
23
23
|
|
|
24
|
-
const { showLineNumbers } =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
contextMenu: actions,
|
|
30
|
-
decorations,
|
|
31
|
-
multiSelect: true,
|
|
32
|
-
rows: lineRows,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const statusItems = useMemo(
|
|
36
|
-
() => [
|
|
37
|
-
{ label: "Format:", value: content.format },
|
|
38
|
-
{ label: "Lines:", value: String(lineRows.length) },
|
|
39
|
-
],
|
|
40
|
-
[content.format, lineRows.length],
|
|
24
|
+
const { document, showLineNumbers, statusItems } = useContentDocument<SourceLineRow>(
|
|
25
|
+
lineRows,
|
|
26
|
+
sourceLineAdapter,
|
|
27
|
+
{ actions, content, decorations, textContent },
|
|
28
|
+
{ multiSelect: true },
|
|
41
29
|
);
|
|
42
30
|
|
|
43
31
|
return (
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { CodeView, sourceLines, sourceLineAdapter } from "@tooee/renderers";
|
|
3
3
|
import type { SourceLineRow } from "@tooee/renderers";
|
|
4
|
-
import { useDocumentController } from "@tooee/shell";
|
|
5
4
|
import { getTextContent } from "../../types.js";
|
|
6
5
|
import type { CustomContent, ContentRenderer } from "../../types.js";
|
|
7
|
-
import {
|
|
6
|
+
import { useContentDocument } from "../../hooks/use-content-document.js";
|
|
8
7
|
import { ViewScreen } from "../view-screen.js";
|
|
9
8
|
import type { SubviewProps } from "./types.js";
|
|
9
|
+
import type { ReactNode } from "react";
|
|
10
10
|
|
|
11
11
|
interface CustomSubviewProps extends SubviewProps {
|
|
12
12
|
content: CustomContent;
|
|
@@ -19,27 +19,17 @@ export const CustomSubview = function CustomSubview({
|
|
|
19
19
|
actions,
|
|
20
20
|
renderers,
|
|
21
21
|
...screen
|
|
22
|
-
}: CustomSubviewProps):
|
|
22
|
+
}: CustomSubviewProps): ReactNode {
|
|
23
23
|
const textContent = useMemo(() => getTextContent(content), [content]);
|
|
24
24
|
// Fallback rows are source lines; row index is the source line.
|
|
25
25
|
const lineRows = useMemo(() => sourceLines(textContent), [textContent]);
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
multiSelect: true,
|
|
34
|
-
rows: lineRows,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const statusItems = useMemo(
|
|
38
|
-
() => [
|
|
39
|
-
{ label: "Format:", value: content.format },
|
|
40
|
-
{ label: "Lines:", value: String(lineRows.length) },
|
|
41
|
-
],
|
|
42
|
-
[content.format, lineRows.length],
|
|
27
|
+
const { document, statusItems } = useContentDocument<SourceLineRow>(
|
|
28
|
+
lineRows,
|
|
29
|
+
sourceLineAdapter,
|
|
30
|
+
{ actions, content, decorations, textContent },
|
|
31
|
+
// Custom content has no action rows of its own, so no context menu is bound.
|
|
32
|
+
{ contextMenu: false, multiSelect: true },
|
|
43
33
|
);
|
|
44
34
|
|
|
45
35
|
const customRenderer = renderers?.[content.format];
|
|
@@ -5,13 +5,13 @@ import { useCommand } from "@tooee/commands";
|
|
|
5
5
|
import { useConfig } from "@tooee/config";
|
|
6
6
|
import { useOverlay } from "@tooee/overlays";
|
|
7
7
|
import type { OverlayCloseReason } from "@tooee/overlays";
|
|
8
|
-
import { useDocumentController } from "@tooee/shell";
|
|
9
8
|
import type { DocumentRowAdapter } from "@tooee/shell";
|
|
10
9
|
import type { DiffContent } from "../../types.js";
|
|
11
|
-
import {
|
|
10
|
+
import { useContentDocument } from "../../hooks/use-content-document.js";
|
|
12
11
|
import { ViewScreen } from "../view-screen.js";
|
|
13
12
|
import { DiffFilePickerOverlay } from "../diff-file-picker.js";
|
|
14
13
|
import type { SubviewProps } from "./types.js";
|
|
14
|
+
import type { ReactNode } from "react";
|
|
15
15
|
|
|
16
16
|
interface DiffSubviewProps extends SubviewProps {
|
|
17
17
|
content: DiffContent;
|
|
@@ -41,7 +41,7 @@ export const DiffSubview = function DiffSubview({
|
|
|
41
41
|
decorations,
|
|
42
42
|
actions,
|
|
43
43
|
...screen
|
|
44
|
-
}: DiffSubviewProps):
|
|
44
|
+
}: DiffSubviewProps): ReactNode {
|
|
45
45
|
const config = useConfig();
|
|
46
46
|
const overlay = useOverlay();
|
|
47
47
|
const textContent = content.patch;
|
|
@@ -53,17 +53,38 @@ export const DiffSubview = function DiffSubview({
|
|
|
53
53
|
const [wrapLines, setWrapLines] = useState(config.view?.wrap ?? false);
|
|
54
54
|
const [horizontalOffset, setHorizontalOffset] = useState(0);
|
|
55
55
|
|
|
56
|
-
const { showLineNumbers } =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
const { document, showLineNumbers, statusItems } = useContentDocument<DiffRow>(
|
|
57
|
+
model.rows,
|
|
58
|
+
DIFF_ROW_ADAPTER,
|
|
59
|
+
{ actions, content, decorations, textContent },
|
|
60
|
+
{
|
|
61
|
+
buildStatusItems: (currentDocument) => {
|
|
62
|
+
const activeRow =
|
|
63
|
+
currentDocument.activeIndex === null
|
|
64
|
+
? undefined
|
|
65
|
+
: model.rows[currentDocument.activeIndex];
|
|
66
|
+
return [
|
|
67
|
+
{ label: "Format:", value: content.format },
|
|
68
|
+
{ label: "Files:", value: String(model.files.length) },
|
|
69
|
+
{ label: "Changes:", value: `+${model.stats.additions} -${model.stats.deletions}` },
|
|
70
|
+
{ label: "Layout:", value: layout },
|
|
71
|
+
...(activeRow
|
|
72
|
+
? [
|
|
73
|
+
{
|
|
74
|
+
label: "At:",
|
|
75
|
+
value:
|
|
76
|
+
activeRow.hunkIndex >= 0
|
|
77
|
+
? `${activeRow.parent.path ?? activeRow.fileId}:${activeRow.hunkIndex + 1}`
|
|
78
|
+
: (activeRow.parent.path ?? activeRow.fileId),
|
|
79
|
+
},
|
|
80
|
+
]
|
|
81
|
+
: []),
|
|
82
|
+
];
|
|
83
|
+
},
|
|
84
|
+
multiSelect: true,
|
|
85
|
+
preserveCursorByKey: true,
|
|
86
|
+
},
|
|
87
|
+
);
|
|
67
88
|
|
|
68
89
|
const { activeIndex } = document;
|
|
69
90
|
// `setCursor` rather than `selectRow`: the latter stands down while a modal
|
|
@@ -178,35 +199,6 @@ export const DiffSubview = function DiffSubview({
|
|
|
178
199
|
title: "Go to file",
|
|
179
200
|
});
|
|
180
201
|
|
|
181
|
-
const activeRow = activeIndex === null ? undefined : model.rows[activeIndex];
|
|
182
|
-
const statusItems = useMemo(
|
|
183
|
-
() => [
|
|
184
|
-
{ label: "Format:", value: content.format },
|
|
185
|
-
{ label: "Files:", value: String(model.files.length) },
|
|
186
|
-
{ label: "Changes:", value: `+${model.stats.additions} -${model.stats.deletions}` },
|
|
187
|
-
{ label: "Layout:", value: layout },
|
|
188
|
-
...(activeRow
|
|
189
|
-
? [
|
|
190
|
-
{
|
|
191
|
-
label: "At:",
|
|
192
|
-
value:
|
|
193
|
-
activeRow.hunkIndex >= 0
|
|
194
|
-
? `${activeRow.parent.path ?? activeRow.fileId}:${activeRow.hunkIndex + 1}`
|
|
195
|
-
: (activeRow.parent.path ?? activeRow.fileId),
|
|
196
|
-
},
|
|
197
|
-
]
|
|
198
|
-
: []),
|
|
199
|
-
],
|
|
200
|
-
[
|
|
201
|
-
activeRow,
|
|
202
|
-
content.format,
|
|
203
|
-
layout,
|
|
204
|
-
model.files.length,
|
|
205
|
-
model.stats.additions,
|
|
206
|
-
model.stats.deletions,
|
|
207
|
-
],
|
|
208
|
-
);
|
|
209
|
-
|
|
210
202
|
return (
|
|
211
203
|
<ViewScreen
|
|
212
204
|
content={content}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { sourceLineAdapter, sourceLines } from "@tooee/renderers";
|
|
3
|
-
import { useDocumentController } from "@tooee/shell";
|
|
4
3
|
import type { SourceLineRow } from "@tooee/renderers";
|
|
5
4
|
import type { ImageContent } from "../../types.js";
|
|
6
5
|
import { getTextContent } from "../../types.js";
|
|
7
|
-
import {
|
|
6
|
+
import { useContentDocument } from "../../hooks/use-content-document.js";
|
|
8
7
|
import { ViewScreen } from "../view-screen.js";
|
|
9
8
|
import type { SubviewProps } from "./types.js";
|
|
9
|
+
import type { ReactNode } from "react";
|
|
10
10
|
|
|
11
11
|
interface ImageSubviewProps extends SubviewProps {
|
|
12
12
|
content: ImageContent;
|
|
@@ -17,7 +17,7 @@ export const ImageSubview = function ImageSubview({
|
|
|
17
17
|
decorations,
|
|
18
18
|
actions,
|
|
19
19
|
...screen
|
|
20
|
-
}: ImageSubviewProps):
|
|
20
|
+
}: ImageSubviewProps): ReactNode {
|
|
21
21
|
const textContent = getTextContent(content);
|
|
22
22
|
const rows = useMemo(() => sourceLines(textContent), [textContent]);
|
|
23
23
|
const [error, setError] = useState(false);
|
|
@@ -26,21 +26,22 @@ export const ImageSubview = function ImageSubview({
|
|
|
26
26
|
setError(false);
|
|
27
27
|
}, [content.src]);
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
const document = useDocumentController<SourceLineRow>({
|
|
31
|
-
adapter: sourceLineAdapter,
|
|
32
|
-
contextMenu: actions,
|
|
33
|
-
decorations,
|
|
34
|
-
multiSelect: false,
|
|
29
|
+
const { document, statusItems } = useContentDocument<SourceLineRow>(
|
|
35
30
|
rows,
|
|
36
|
-
|
|
31
|
+
sourceLineAdapter,
|
|
32
|
+
{ actions, content, decorations, textContent },
|
|
33
|
+
{
|
|
34
|
+
multiSelect: false,
|
|
35
|
+
statusItems: [{ label: "Format:", value: content.format }],
|
|
36
|
+
},
|
|
37
|
+
);
|
|
37
38
|
|
|
38
39
|
return (
|
|
39
40
|
<ViewScreen
|
|
40
41
|
content={content}
|
|
41
42
|
controller={document}
|
|
42
43
|
actions={actions}
|
|
43
|
-
statusItems={
|
|
44
|
+
statusItems={statusItems}
|
|
44
45
|
{...screen}
|
|
45
46
|
>
|
|
46
47
|
<box style={{ flexDirection: "column", flexGrow: 1 }}>
|
|
@@ -3,12 +3,12 @@ import type { TextBufferRenderable } from "@opentui/core";
|
|
|
3
3
|
import { MarkdownView, flattenMarkdown, getFlatBlockText } from "@tooee/renderers";
|
|
4
4
|
import type { CodeBlockRenderer, FlatBlock } from "@tooee/renderers";
|
|
5
5
|
import { useBuildCommandContext, useCommand } from "@tooee/commands";
|
|
6
|
-
import { useDocumentController } from "@tooee/shell";
|
|
7
6
|
import type { DocumentRowAdapter } from "@tooee/shell";
|
|
8
7
|
import type { MarkdownContent, MarkdownLinkActivateHandler } from "../../types.js";
|
|
9
|
-
import {
|
|
8
|
+
import { useContentDocument } from "../../hooks/use-content-document.js";
|
|
10
9
|
import { ViewScreen } from "../view-screen.js";
|
|
11
10
|
import type { SubviewProps } from "./types.js";
|
|
11
|
+
import type { ReactNode } from "react";
|
|
12
12
|
|
|
13
13
|
interface MarkdownSubviewProps extends SubviewProps {
|
|
14
14
|
content: MarkdownContent;
|
|
@@ -36,26 +36,28 @@ export const MarkdownSubview = function MarkdownSubview({
|
|
|
36
36
|
decorations,
|
|
37
37
|
actions,
|
|
38
38
|
...screen
|
|
39
|
-
}: MarkdownSubviewProps):
|
|
39
|
+
}: MarkdownSubviewProps): ReactNode {
|
|
40
40
|
const textContent = content.markdown;
|
|
41
41
|
const lineCount = useMemo(() => textContent.split("\n").length, [textContent]);
|
|
42
42
|
const blocks = useMemo(() => flattenMarkdown(content.markdown), [content.markdown]);
|
|
43
43
|
|
|
44
|
-
const { showLineNumbers } =
|
|
44
|
+
const { document, showLineNumbers, statusItems } = useContentDocument<FlatBlock>(
|
|
45
|
+
blocks,
|
|
46
|
+
MARKDOWN_BLOCK_ADAPTER,
|
|
47
|
+
{ actions, content, decorations, textContent },
|
|
48
|
+
{
|
|
49
|
+
multiSelect: true,
|
|
50
|
+
statusItems: [
|
|
51
|
+
{ label: "Format:", value: content.format },
|
|
52
|
+
{ label: "Lines:", value: String(lineCount) },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
);
|
|
45
56
|
const buildCommandContext = useBuildCommandContext();
|
|
46
57
|
const handleLinkActivate = onLinkActivate
|
|
47
|
-
? (href: string)
|
|
58
|
+
? (href: string) => onLinkActivate(href, buildCommandContext())
|
|
48
59
|
: undefined;
|
|
49
60
|
|
|
50
|
-
const document = useDocumentController<FlatBlock>({
|
|
51
|
-
adapter: MARKDOWN_BLOCK_ADAPTER,
|
|
52
|
-
// The controller projects the screen's actions onto menu entries at open time.
|
|
53
|
-
contextMenu: actions,
|
|
54
|
-
decorations,
|
|
55
|
-
multiSelect: true,
|
|
56
|
-
rows: blocks,
|
|
57
|
-
});
|
|
58
|
-
|
|
59
61
|
const hScrollableBlocksRef = useRef<Map<number, TextBufferRenderable>>(new Map());
|
|
60
62
|
const cursorScrollable = () =>
|
|
61
63
|
document.activeIndex === null
|
|
@@ -88,14 +90,6 @@ export const MarkdownSubview = function MarkdownSubview({
|
|
|
88
90
|
when: () => cursorScrollable() !== undefined,
|
|
89
91
|
});
|
|
90
92
|
|
|
91
|
-
const statusItems = useMemo(
|
|
92
|
-
() => [
|
|
93
|
-
{ label: "Format:", value: content.format },
|
|
94
|
-
{ label: "Lines:", value: String(lineCount) },
|
|
95
|
-
],
|
|
96
|
-
[content.format, lineCount],
|
|
97
|
-
);
|
|
98
|
-
|
|
99
93
|
return (
|
|
100
94
|
<ViewScreen
|
|
101
95
|
content={content}
|
|
@@ -1,84 +1,44 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
-
import { Table } from "@tooee/renderers";
|
|
2
|
+
import { Table, formatTableCell } from "@tooee/renderers";
|
|
3
3
|
import type { TableRow } from "@tooee/renderers";
|
|
4
|
-
import { useDocumentController } from "@tooee/shell";
|
|
5
4
|
import { getTextContent } from "../../types.js";
|
|
6
5
|
import type { TableContent } from "../../types.js";
|
|
7
|
-
import {
|
|
6
|
+
import { useContentDocument } from "../../hooks/use-content-document.js";
|
|
8
7
|
import { ViewScreen } from "../view-screen.js";
|
|
9
8
|
import type { SubviewProps } from "./types.js";
|
|
9
|
+
import type { ReactNode } from "react";
|
|
10
10
|
|
|
11
11
|
interface TableSubviewProps extends SubviewProps {
|
|
12
12
|
content: TableContent;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const stringifyRowCell = function stringifyRowCell(value: unknown): string {
|
|
16
|
-
if (value === null || value === undefined) {
|
|
17
|
-
return "";
|
|
18
|
-
}
|
|
19
|
-
if (typeof value === "string") {
|
|
20
|
-
return value;
|
|
21
|
-
}
|
|
22
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
23
|
-
return String(value);
|
|
24
|
-
}
|
|
25
|
-
if (typeof value === "string") {
|
|
26
|
-
return value;
|
|
27
|
-
}
|
|
28
|
-
if (
|
|
29
|
-
typeof value === "number" ||
|
|
30
|
-
typeof value === "bigint" ||
|
|
31
|
-
typeof value === "boolean" ||
|
|
32
|
-
typeof value === "symbol"
|
|
33
|
-
) {
|
|
34
|
-
return value.toString();
|
|
35
|
-
}
|
|
36
|
-
if (value === null || value === undefined) {
|
|
37
|
-
return String(value);
|
|
38
|
-
}
|
|
39
|
-
if (typeof value === "function") {
|
|
40
|
-
return Function.prototype.toString.call(value);
|
|
41
|
-
}
|
|
42
|
-
try {
|
|
43
|
-
return JSON.stringify(value);
|
|
44
|
-
} catch {
|
|
45
|
-
return Array.isArray(value) ? value.join(",") : Object.prototype.toString.call(value);
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
|
|
49
15
|
export const TableSubview = function TableSubview({
|
|
50
16
|
content,
|
|
51
17
|
decorations,
|
|
52
18
|
actions,
|
|
53
19
|
...screen
|
|
54
|
-
}: TableSubviewProps):
|
|
20
|
+
}: TableSubviewProps): ReactNode {
|
|
55
21
|
const textContent = useMemo(() => getTextContent(content), [content]);
|
|
56
|
-
const { showLineNumbers } = useContentCommands({ content, textContent });
|
|
57
|
-
|
|
58
22
|
const { columns, rows } = content;
|
|
59
23
|
const adapter = useMemo(
|
|
60
24
|
() => ({
|
|
61
25
|
getText: (row: TableRow) =>
|
|
62
|
-
columns.map((column) =>
|
|
26
|
+
columns.map((column) => formatTableCell(row[column.key])).join("\t"),
|
|
63
27
|
}),
|
|
64
28
|
[columns],
|
|
65
29
|
);
|
|
66
|
-
const document =
|
|
67
|
-
adapter,
|
|
68
|
-
// The controller projects the screen's actions onto menu entries at open time.
|
|
69
|
-
contextMenu: actions,
|
|
70
|
-
decorations,
|
|
71
|
-
multiSelect: true,
|
|
30
|
+
const { document, showLineNumbers, statusItems } = useContentDocument<TableRow>(
|
|
72
31
|
rows,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
32
|
+
adapter,
|
|
33
|
+
{ actions, content, decorations, textContent },
|
|
34
|
+
{
|
|
35
|
+
multiSelect: true,
|
|
36
|
+
statusItems: [
|
|
37
|
+
{ label: "Format:", value: content.format },
|
|
38
|
+
{ label: "Rows:", value: String(rows.length) },
|
|
39
|
+
{ label: "Cols:", value: String(columns.length) },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
82
42
|
);
|
|
83
43
|
|
|
84
44
|
return (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useQuitCommand } from "@tooee/shell";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
2
3
|
|
|
3
4
|
interface ViewStateProps {
|
|
4
5
|
message: string;
|
|
@@ -6,7 +7,7 @@ interface ViewStateProps {
|
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
/** A non-document view state that preserves the viewer's normal quit command. */
|
|
9
|
-
export const ViewState = function ViewState({ message, color }: ViewStateProps):
|
|
10
|
+
export const ViewState = function ViewState({ message, color }: ViewStateProps): ReactNode {
|
|
10
11
|
useQuitCommand();
|
|
11
12
|
|
|
12
13
|
return (
|
|
@@ -2,6 +2,8 @@ import { createStore } from "@xstate/store";
|
|
|
2
2
|
import type { MarkSet } from "@tooee/marks";
|
|
3
3
|
import type { AnyContent, Content, ContentChunk, ContentFormat } from "./types.js";
|
|
4
4
|
|
|
5
|
+
type ContentProviderResult = AnyContent | Promise<AnyContent> | AsyncIterable<ContentChunk>;
|
|
6
|
+
|
|
5
7
|
export type ContentLoaderStatus = "idle" | "loading" | "streaming" | "ready" | "error";
|
|
6
8
|
|
|
7
9
|
export interface ContentLoaderContext {
|
|
@@ -30,11 +32,12 @@ export type ContentLoaderEvents = {
|
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
export const isAsyncIterable = function isAsyncIterable(
|
|
33
|
-
value:
|
|
35
|
+
value: ContentProviderResult,
|
|
34
36
|
): value is AsyncIterable<ContentChunk> {
|
|
35
37
|
return (
|
|
36
38
|
value !== null &&
|
|
37
39
|
value !== undefined &&
|
|
40
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- provider result boundary distinguishes the documented async-iterable source from content and promises
|
|
38
41
|
typeof value === "object" &&
|
|
39
42
|
Symbol.asyncIterator in value
|
|
40
43
|
);
|
|
@@ -69,11 +72,11 @@ const ensureContentFormat = function ensureContentFormat<F extends ContentFormat
|
|
|
69
72
|
title?: string,
|
|
70
73
|
): Extract<Content, { format: F }> {
|
|
71
74
|
if (!current || current.format !== format) {
|
|
72
|
-
//
|
|
75
|
+
// SAFETY: createEmptyContent returns the built-in content variant selected by the same format.
|
|
73
76
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- format-dependent content is trusted at this provider boundary
|
|
74
77
|
return createEmptyContent(format, title) as Extract<Content, { format: F }>;
|
|
75
78
|
}
|
|
76
|
-
//
|
|
79
|
+
// SAFETY: The equality guard narrows current to the built-in content variant selected by format.
|
|
77
80
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the provider contract supplies the matching format
|
|
78
81
|
return current as Extract<Content, { format: F }>;
|
|
79
82
|
};
|
|
@@ -117,8 +120,8 @@ export const applyContentChunk = function applyContentChunk(
|
|
|
117
120
|
}
|
|
118
121
|
};
|
|
119
122
|
|
|
120
|
-
export const normalizeError = function normalizeError(
|
|
121
|
-
return
|
|
123
|
+
export const normalizeError = function normalizeError(cause: unknown): string {
|
|
124
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
122
125
|
};
|
|
123
126
|
|
|
124
127
|
export const createContentLoaderStore = function createContentLoaderStore() {
|
package/src/default-provider.ts
CHANGED
|
@@ -7,10 +7,12 @@ export interface CreateProviderOptions {
|
|
|
7
7
|
renderer?: ContentFormat;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
interface DetectedFileFormat {
|
|
11
11
|
format: ContentFormat;
|
|
12
12
|
language?: string;
|
|
13
|
-
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const detectFormat = function detectFormat(filePath: string): DetectedFileFormat {
|
|
14
16
|
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
15
17
|
if (ext === undefined || ext === "") {
|
|
16
18
|
return { format: "text" };
|
|
@@ -36,36 +38,36 @@ const detectFormat = function detectFormat(filePath: string): {
|
|
|
36
38
|
return { format: "markdown" };
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
const codeExts
|
|
40
|
-
bash
|
|
41
|
-
c
|
|
42
|
-
cpp
|
|
43
|
-
css
|
|
44
|
-
go
|
|
45
|
-
h
|
|
46
|
-
hpp
|
|
47
|
-
html
|
|
48
|
-
java
|
|
49
|
-
js
|
|
50
|
-
json
|
|
51
|
-
jsx
|
|
52
|
-
kt
|
|
53
|
-
py
|
|
54
|
-
rb
|
|
55
|
-
rs
|
|
56
|
-
sh
|
|
57
|
-
sql
|
|
58
|
-
swift
|
|
59
|
-
toml
|
|
60
|
-
ts
|
|
61
|
-
tsx
|
|
62
|
-
yaml
|
|
63
|
-
yml
|
|
64
|
-
zsh
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const language = codeExts
|
|
68
|
-
if (
|
|
41
|
+
const codeExts = new Map<string, string>([
|
|
42
|
+
["bash", "bash"],
|
|
43
|
+
["c", "c"],
|
|
44
|
+
["cpp", "cpp"],
|
|
45
|
+
["css", "css"],
|
|
46
|
+
["go", "go"],
|
|
47
|
+
["h", "c"],
|
|
48
|
+
["hpp", "cpp"],
|
|
49
|
+
["html", "html"],
|
|
50
|
+
["java", "java"],
|
|
51
|
+
["js", "javascript"],
|
|
52
|
+
["json", "json"],
|
|
53
|
+
["jsx", "javascript"],
|
|
54
|
+
["kt", "kotlin"],
|
|
55
|
+
["py", "python"],
|
|
56
|
+
["rb", "ruby"],
|
|
57
|
+
["rs", "rust"],
|
|
58
|
+
["sh", "bash"],
|
|
59
|
+
["sql", "sql"],
|
|
60
|
+
["swift", "swift"],
|
|
61
|
+
["toml", "toml"],
|
|
62
|
+
["ts", "typescript"],
|
|
63
|
+
["tsx", "typescript"],
|
|
64
|
+
["yaml", "yaml"],
|
|
65
|
+
["yml", "yaml"],
|
|
66
|
+
["zsh", "zsh"],
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
const language = codeExts.get(ext);
|
|
70
|
+
if (language !== undefined) {
|
|
69
71
|
return { format: "code", language };
|
|
70
72
|
}
|
|
71
73
|
|
package/src/directory-view.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import type { DirectoryEntry } from "./directory-provider.js";
|
|
|
7
7
|
import type { AnyContent, ContentProvider } from "./types.js";
|
|
8
8
|
import { createFileProvider } from "./default-provider.js";
|
|
9
9
|
import { ViewState } from "./components/view-state.js";
|
|
10
|
+
import type { ReactNode } from "react";
|
|
10
11
|
|
|
11
12
|
const createDirectoryFileProvider = function createDirectoryFileProvider(
|
|
12
13
|
entry: DirectoryEntry,
|
|
@@ -17,7 +18,8 @@ const createDirectoryFileProvider = function createDirectoryFileProvider(
|
|
|
17
18
|
return {
|
|
18
19
|
async load(): Promise<AnyContent> {
|
|
19
20
|
const result = inner.load();
|
|
20
|
-
//
|
|
21
|
+
// SAFETY: createFileProvider owns this provider and its load method returns Content directly
|
|
22
|
+
// or through a Promise; it never returns the ContentProvider async-iterable arm.
|
|
21
23
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- file providers return external content at this boundary
|
|
22
24
|
const content = result instanceof Promise ? await result : (result as AnyContent);
|
|
23
25
|
return {
|
|
@@ -36,7 +38,7 @@ interface DirectoryViewProps {
|
|
|
36
38
|
export const DirectoryView = function DirectoryView({
|
|
37
39
|
dirPath,
|
|
38
40
|
actions,
|
|
39
|
-
}: DirectoryViewProps):
|
|
41
|
+
}: DirectoryViewProps): ReactNode {
|
|
40
42
|
const files = useMemo(() => listDirectoryFiles(dirPath), [dirPath]);
|
|
41
43
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
42
44
|
|