@tooee/view 0.2.3 → 0.3.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/README.md +15 -15
- package/dist/components/subviews/{CodeSubview.d.ts → code-subview.d.ts} +2 -2
- package/dist/components/subviews/code-subview.d.ts.map +1 -0
- package/dist/components/subviews/{CodeSubview.js → code-subview.js} +8 -8
- package/dist/components/subviews/code-subview.js.map +1 -0
- package/dist/components/subviews/custom-subview.d.ts +9 -0
- package/dist/components/subviews/custom-subview.d.ts.map +1 -0
- package/dist/components/subviews/{CustomSubview.js → custom-subview.js} +7 -7
- package/dist/components/subviews/custom-subview.js.map +1 -0
- package/dist/components/subviews/index.d.ts +4 -4
- package/dist/components/subviews/index.d.ts.map +1 -1
- package/dist/components/subviews/index.js +4 -4
- package/dist/components/subviews/index.js.map +1 -1
- package/dist/components/subviews/markdown-subview.d.ts +11 -0
- package/dist/components/subviews/markdown-subview.d.ts.map +1 -0
- package/dist/components/subviews/{MarkdownSubview.js → markdown-subview.js} +28 -26
- package/dist/components/subviews/markdown-subview.js.map +1 -0
- package/dist/components/subviews/table-subview.d.ts +8 -0
- package/dist/components/subviews/table-subview.d.ts.map +1 -0
- package/dist/components/subviews/{TableSubview.js → table-subview.js} +40 -22
- package/dist/components/subviews/table-subview.js.map +1 -0
- package/dist/components/subviews/types.d.ts.map +1 -1
- package/dist/components/{ViewScreen.d.ts → view-screen.d.ts} +3 -3
- package/dist/components/view-screen.d.ts.map +1 -0
- package/dist/components/{ViewScreen.js → view-screen.js} +11 -11
- package/dist/components/view-screen.js.map +1 -0
- package/dist/content-loader-store.d.ts +11 -7
- package/dist/content-loader-store.d.ts.map +1 -1
- package/dist/content-loader-store.js +70 -50
- package/dist/content-loader-store.js.map +1 -1
- package/dist/default-provider.d.ts +4 -4
- package/dist/default-provider.d.ts.map +1 -1
- package/dist/default-provider.js +48 -39
- package/dist/default-provider.js.map +1 -1
- package/dist/directory-provider.d.ts +2 -2
- package/dist/directory-provider.d.ts.map +1 -1
- package/dist/directory-provider.js +13 -10
- package/dist/directory-provider.js.map +1 -1
- package/dist/directory-view.d.ts +8 -0
- package/dist/directory-view.d.ts.map +1 -0
- package/dist/{DirectoryView.js → directory-view.js} +18 -12
- package/dist/directory-view.js.map +1 -0
- package/dist/hooks/index.d.ts +4 -4
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +3 -3
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/{useContentCommands.d.ts → use-content-commands.d.ts} +2 -2
- package/dist/hooks/use-content-commands.d.ts.map +1 -0
- package/dist/hooks/{useContentCommands.js → use-content-commands.js} +6 -4
- package/dist/hooks/use-content-commands.js.map +1 -0
- package/dist/hooks/{useContentLoader.d.ts → use-content-loader.d.ts} +4 -4
- package/dist/hooks/use-content-loader.d.ts.map +1 -0
- package/dist/hooks/use-content-loader.js +81 -0
- package/dist/hooks/use-content-loader.js.map +1 -0
- package/dist/hooks/{useViewCommandContext.d.ts → use-view-command-context.d.ts} +3 -3
- package/dist/hooks/use-view-command-context.d.ts.map +1 -0
- package/dist/hooks/{useViewCommandContext.js → use-view-command-context.js} +15 -13
- package/dist/hooks/use-view-command-context.js.map +1 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/launch.d.ts +2 -2
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js +6 -6
- package/dist/launch.js.map +1 -1
- package/dist/types.d.ts +4 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +46 -24
- package/dist/types.js.map +1 -1
- package/dist/view.d.ts +18 -0
- package/dist/view.d.ts.map +1 -0
- package/dist/{View.js → view.js} +20 -14
- package/dist/view.js.map +1 -0
- package/package.json +10 -10
- package/src/components/subviews/{CodeSubview.tsx → code-subview.tsx} +25 -19
- package/src/components/subviews/{CustomSubview.tsx → custom-subview.tsx} +23 -21
- package/src/components/subviews/index.ts +5 -5
- package/src/components/subviews/{MarkdownSubview.tsx → markdown-subview.tsx} +55 -52
- package/src/components/subviews/table-subview.tsx +95 -0
- package/src/components/subviews/types.ts +14 -14
- package/src/components/{ViewScreen.tsx → view-screen.tsx} +33 -32
- package/src/content-loader-store.ts +131 -99
- package/src/default-provider.ts +84 -64
- package/src/directory-provider.ts +32 -24
- package/src/directory-view.tsx +79 -0
- package/src/hooks/index.ts +7 -4
- package/src/hooks/{useContentCommands.ts → use-content-commands.ts} +19 -14
- package/src/hooks/use-content-loader.ts +92 -0
- package/src/hooks/{useViewCommandContext.ts → use-view-command-context.ts} +40 -38
- package/src/index.ts +14 -14
- package/src/launch.tsx +20 -18
- package/src/types.ts +107 -78
- package/src/view.tsx +114 -0
- package/dist/DirectoryView.d.ts +0 -7
- package/dist/DirectoryView.d.ts.map +0 -1
- package/dist/DirectoryView.js.map +0 -1
- package/dist/View.d.ts +0 -16
- package/dist/View.d.ts.map +0 -1
- package/dist/View.js.map +0 -1
- package/dist/components/ViewScreen.d.ts.map +0 -1
- package/dist/components/ViewScreen.js.map +0 -1
- package/dist/components/subviews/CodeSubview.d.ts.map +0 -1
- package/dist/components/subviews/CodeSubview.js.map +0 -1
- package/dist/components/subviews/CustomSubview.d.ts +0 -9
- package/dist/components/subviews/CustomSubview.d.ts.map +0 -1
- package/dist/components/subviews/CustomSubview.js.map +0 -1
- package/dist/components/subviews/MarkdownSubview.d.ts +0 -10
- package/dist/components/subviews/MarkdownSubview.d.ts.map +0 -1
- package/dist/components/subviews/MarkdownSubview.js.map +0 -1
- package/dist/components/subviews/TableSubview.d.ts +0 -8
- package/dist/components/subviews/TableSubview.d.ts.map +0 -1
- package/dist/components/subviews/TableSubview.js.map +0 -1
- package/dist/hooks/useContentCommands.d.ts.map +0 -1
- package/dist/hooks/useContentCommands.js.map +0 -1
- package/dist/hooks/useContentLoader.d.ts.map +0 -1
- package/dist/hooks/useContentLoader.js +0 -55
- package/dist/hooks/useContentLoader.js.map +0 -1
- package/dist/hooks/useViewCommandContext.d.ts.map +0 -1
- package/dist/hooks/useViewCommandContext.js.map +0 -1
- package/src/DirectoryView.tsx +0 -68
- package/src/View.tsx +0 -94
- package/src/components/subviews/TableSubview.tsx +0 -65
- package/src/hooks/useContentLoader.ts +0 -72
|
@@ -1,154 +1,186 @@
|
|
|
1
|
-
import { createStore } from "@xstate/store"
|
|
2
|
-
import type { MarkSet } from "@tooee/marks"
|
|
3
|
-
import type { AnyContent, Content, ContentChunk, ContentFormat
|
|
1
|
+
import { createStore } from "@xstate/store";
|
|
2
|
+
import type { MarkSet } from "@tooee/marks";
|
|
3
|
+
import type { AnyContent, Content, ContentChunk, ContentFormat } from "./types.js";
|
|
4
4
|
|
|
5
|
-
export type ContentLoaderStatus = "idle" | "loading" | "streaming" | "ready" | "error"
|
|
5
|
+
export type ContentLoaderStatus = "idle" | "loading" | "streaming" | "ready" | "error";
|
|
6
6
|
|
|
7
7
|
export interface ContentLoaderContext {
|
|
8
|
-
status: ContentLoaderStatus
|
|
9
|
-
requestId: number
|
|
10
|
-
loadSeq: number
|
|
11
|
-
content: AnyContent | null
|
|
12
|
-
error: string | null
|
|
13
|
-
providerMarks: MarkSet[]
|
|
14
|
-
title: string | undefined
|
|
8
|
+
status: ContentLoaderStatus;
|
|
9
|
+
requestId: number;
|
|
10
|
+
loadSeq: number;
|
|
11
|
+
content: AnyContent | null;
|
|
12
|
+
error: string | null;
|
|
13
|
+
providerMarks: MarkSet[];
|
|
14
|
+
title: string | undefined;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
loadStarted: { marks: MarkSet[]; title?: string }
|
|
19
|
-
streamStarted: { requestId: number; format: string }
|
|
20
|
-
chunkReceived: { requestId: number; chunk: ContentChunk }
|
|
21
|
-
loaded: { requestId: number; content: AnyContent }
|
|
22
|
-
streamEnded: { requestId: number }
|
|
23
|
-
loadFailed: { requestId: number; error: string }
|
|
24
|
-
loadCancelled: { requestId: number }
|
|
25
|
-
reloadRequested:
|
|
17
|
+
interface ContentLoaderEventPayloads {
|
|
18
|
+
loadStarted: { marks: MarkSet[]; title?: string };
|
|
19
|
+
streamStarted: { requestId: number; format: string };
|
|
20
|
+
chunkReceived: { requestId: number; chunk: ContentChunk };
|
|
21
|
+
loaded: { requestId: number; content: AnyContent };
|
|
22
|
+
streamEnded: { requestId: number };
|
|
23
|
+
loadFailed: { requestId: number; error: string };
|
|
24
|
+
loadCancelled: { requestId: number };
|
|
25
|
+
reloadRequested: Record<PropertyKey, never>;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
}
|
|
28
|
+
export type ContentLoaderEvents = {
|
|
29
|
+
[Event in keyof ContentLoaderEventPayloads]: ContentLoaderEventPayloads[Event];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const isAsyncIterable = function isAsyncIterable(
|
|
33
|
+
value: unknown,
|
|
34
|
+
): value is AsyncIterable<ContentChunk> {
|
|
35
|
+
return (
|
|
36
|
+
value !== null &&
|
|
37
|
+
value !== undefined &&
|
|
38
|
+
typeof value === "object" &&
|
|
39
|
+
Symbol.asyncIterator in value
|
|
40
|
+
);
|
|
41
|
+
};
|
|
31
42
|
|
|
32
|
-
export
|
|
43
|
+
export const createEmptyContent = function createEmptyContent(
|
|
44
|
+
format: string,
|
|
45
|
+
title?: string,
|
|
46
|
+
): AnyContent {
|
|
33
47
|
switch (format) {
|
|
34
|
-
case "markdown":
|
|
35
|
-
return { format, markdown: "", title }
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
case "
|
|
41
|
-
return { format,
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
case "markdown": {
|
|
49
|
+
return { format, markdown: "", title };
|
|
50
|
+
}
|
|
51
|
+
case "code": {
|
|
52
|
+
return { code: "", format, title };
|
|
53
|
+
}
|
|
54
|
+
case "text": {
|
|
55
|
+
return { format, text: "", title };
|
|
56
|
+
}
|
|
57
|
+
case "table": {
|
|
58
|
+
return { columns: [], format, rows: [], title };
|
|
59
|
+
}
|
|
60
|
+
default: {
|
|
61
|
+
return { data: undefined, format, title };
|
|
62
|
+
}
|
|
44
63
|
}
|
|
45
|
-
}
|
|
64
|
+
};
|
|
46
65
|
|
|
47
|
-
function ensureContentFormat<F extends ContentFormat>(
|
|
66
|
+
const ensureContentFormat = function ensureContentFormat<F extends ContentFormat>(
|
|
48
67
|
current: AnyContent | null,
|
|
49
68
|
format: F,
|
|
50
69
|
title?: string,
|
|
51
70
|
): Extract<Content, { format: F }> {
|
|
52
71
|
if (!current || current.format !== format) {
|
|
53
|
-
|
|
72
|
+
// Deferred(lint-sweep): add schema-based validation for custom and streamed content formats.
|
|
73
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- format-dependent content is trusted at this provider boundary
|
|
74
|
+
return createEmptyContent(format, title) as Extract<Content, { format: F }>;
|
|
54
75
|
}
|
|
55
|
-
|
|
56
|
-
|
|
76
|
+
// Deferred(lint-sweep): add schema-based validation for custom and streamed content formats.
|
|
77
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the provider contract supplies the matching format
|
|
78
|
+
return current as Extract<Content, { format: F }>;
|
|
79
|
+
};
|
|
57
80
|
|
|
58
|
-
export function applyContentChunk(
|
|
81
|
+
export const applyContentChunk = function applyContentChunk(
|
|
59
82
|
current: AnyContent | null,
|
|
60
83
|
chunk: ContentChunk,
|
|
61
84
|
title?: string,
|
|
62
85
|
): AnyContent {
|
|
63
86
|
switch (chunk.type) {
|
|
64
|
-
case "replace":
|
|
65
|
-
return chunk.content
|
|
66
|
-
|
|
87
|
+
case "replace": {
|
|
88
|
+
return chunk.content;
|
|
89
|
+
}
|
|
90
|
+
case "append": {
|
|
67
91
|
if (chunk.format === "markdown") {
|
|
68
|
-
const target = ensureContentFormat(current, "markdown", title)
|
|
69
|
-
return { ...target, markdown: target.markdown + chunk.data }
|
|
92
|
+
const target = ensureContentFormat(current, "markdown", title);
|
|
93
|
+
return { ...target, markdown: target.markdown + chunk.data };
|
|
70
94
|
}
|
|
71
95
|
if (chunk.format === "code") {
|
|
72
|
-
const target = ensureContentFormat(current, "code", title)
|
|
96
|
+
const target = ensureContentFormat(current, "code", title);
|
|
73
97
|
return {
|
|
74
98
|
...target,
|
|
75
99
|
code: target.code + chunk.data,
|
|
76
100
|
language: chunk.language ?? target.language,
|
|
77
|
-
}
|
|
101
|
+
};
|
|
78
102
|
}
|
|
79
103
|
{
|
|
80
|
-
const target = ensureContentFormat(current, "text", title)
|
|
81
|
-
return { ...target, text: target.text + chunk.data }
|
|
104
|
+
const target = ensureContentFormat(current, "text", title);
|
|
105
|
+
return { ...target, text: target.text + chunk.data };
|
|
82
106
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
107
|
+
}
|
|
108
|
+
case "patch": {
|
|
109
|
+
return chunk.apply(current);
|
|
110
|
+
}
|
|
111
|
+
case "marks": {
|
|
112
|
+
return current ?? createEmptyContent("markdown", title);
|
|
113
|
+
}
|
|
114
|
+
default: {
|
|
115
|
+
return current ?? createEmptyContent("markdown", title);
|
|
116
|
+
}
|
|
87
117
|
}
|
|
88
|
-
}
|
|
118
|
+
};
|
|
89
119
|
|
|
90
|
-
export function normalizeError(error: unknown): string {
|
|
91
|
-
return error instanceof Error ? error.message : String(error)
|
|
92
|
-
}
|
|
120
|
+
export const normalizeError = function normalizeError(error: unknown): string {
|
|
121
|
+
return error instanceof Error ? error.message : String(error);
|
|
122
|
+
};
|
|
93
123
|
|
|
94
|
-
export function createContentLoaderStore() {
|
|
124
|
+
export const createContentLoaderStore = function createContentLoaderStore() {
|
|
95
125
|
return createStore<ContentLoaderContext, ContentLoaderEvents>({
|
|
96
126
|
context: {
|
|
97
|
-
status: "idle",
|
|
98
|
-
requestId: 0,
|
|
99
|
-
loadSeq: 0,
|
|
100
127
|
content: null,
|
|
101
128
|
error: null,
|
|
129
|
+
loadSeq: 0,
|
|
102
130
|
providerMarks: [],
|
|
131
|
+
requestId: 0,
|
|
132
|
+
status: "idle",
|
|
103
133
|
title: undefined,
|
|
104
134
|
},
|
|
105
135
|
on: {
|
|
106
|
-
loadStarted: (ctx, event) => ({
|
|
107
|
-
...ctx,
|
|
108
|
-
status: "loading",
|
|
109
|
-
requestId: ctx.requestId + 1,
|
|
110
|
-
error: null,
|
|
111
|
-
providerMarks: event.marks,
|
|
112
|
-
title: event.title,
|
|
113
|
-
}),
|
|
114
|
-
streamStarted: (ctx, event) =>
|
|
115
|
-
event.requestId !== ctx.requestId
|
|
116
|
-
? ctx
|
|
117
|
-
: {
|
|
118
|
-
...ctx,
|
|
119
|
-
status: "streaming",
|
|
120
|
-
content: createEmptyContent(event.format, ctx.title),
|
|
121
|
-
},
|
|
122
136
|
chunkReceived: (ctx, event) => {
|
|
123
|
-
if (event.requestId !== ctx.requestId)
|
|
137
|
+
if (event.requestId !== ctx.requestId) {
|
|
138
|
+
return ctx;
|
|
139
|
+
}
|
|
124
140
|
if (event.chunk.type === "marks") {
|
|
125
|
-
const markSet = event.chunk.set
|
|
141
|
+
const markSet = event.chunk.set;
|
|
126
142
|
const providerMarks = [
|
|
127
143
|
...ctx.providerMarks.filter((set) => set.namespace !== markSet.namespace),
|
|
128
144
|
markSet,
|
|
129
|
-
]
|
|
130
|
-
return { ...ctx, providerMarks }
|
|
145
|
+
];
|
|
146
|
+
return { ...ctx, providerMarks };
|
|
131
147
|
}
|
|
132
|
-
return { ...ctx, content: applyContentChunk(ctx.content, event.chunk, ctx.title) }
|
|
148
|
+
return { ...ctx, content: applyContentChunk(ctx.content, event.chunk, ctx.title) };
|
|
133
149
|
},
|
|
134
|
-
loaded: (ctx, event) =>
|
|
135
|
-
event.requestId !== ctx.requestId
|
|
136
|
-
? ctx
|
|
137
|
-
: { ...ctx, status: "ready", content: event.content },
|
|
138
|
-
streamEnded: (ctx, event) =>
|
|
139
|
-
event.requestId !== ctx.requestId ? ctx : { ...ctx, status: "ready" },
|
|
140
|
-
loadFailed: (ctx, event) =>
|
|
141
|
-
event.requestId !== ctx.requestId ? ctx : { ...ctx, status: "error", error: event.error },
|
|
142
150
|
loadCancelled: (ctx, event) =>
|
|
143
|
-
event.requestId
|
|
151
|
+
event.requestId === ctx.requestId ? { ...ctx, requestId: ctx.requestId + 1 } : ctx,
|
|
152
|
+
loadFailed: (ctx, event) =>
|
|
153
|
+
event.requestId === ctx.requestId ? { ...ctx, error: event.error, status: "error" } : ctx,
|
|
154
|
+
loadStarted: (ctx, event) => ({
|
|
155
|
+
...ctx,
|
|
156
|
+
error: null,
|
|
157
|
+
providerMarks: event.marks,
|
|
158
|
+
requestId: ctx.requestId + 1,
|
|
159
|
+
status: "loading",
|
|
160
|
+
title: event.title,
|
|
161
|
+
}),
|
|
162
|
+
loaded: (ctx, event) =>
|
|
163
|
+
event.requestId === ctx.requestId
|
|
164
|
+
? { ...ctx, content: event.content, status: "ready" }
|
|
165
|
+
: ctx,
|
|
144
166
|
reloadRequested: (ctx) => ({ ...ctx, loadSeq: ctx.loadSeq + 1 }),
|
|
167
|
+
streamEnded: (ctx, event) =>
|
|
168
|
+
event.requestId === ctx.requestId ? { ...ctx, status: "ready" } : ctx,
|
|
169
|
+
streamStarted: (ctx, event) =>
|
|
170
|
+
event.requestId === ctx.requestId
|
|
171
|
+
? {
|
|
172
|
+
...ctx,
|
|
173
|
+
content: createEmptyContent(event.format, ctx.title),
|
|
174
|
+
status: "streaming",
|
|
175
|
+
}
|
|
176
|
+
: ctx,
|
|
145
177
|
},
|
|
146
|
-
})
|
|
147
|
-
}
|
|
178
|
+
});
|
|
179
|
+
};
|
|
148
180
|
|
|
149
|
-
export const selectContent = (ctx: ContentLoaderContext) => ctx.content
|
|
150
|
-
export const selectStatus = (ctx: ContentLoaderContext) => ctx.status
|
|
151
|
-
export const selectStreaming = (ctx: ContentLoaderContext) => ctx.status === "streaming"
|
|
152
|
-
export const selectError = (ctx: ContentLoaderContext) => ctx.error
|
|
153
|
-
export const selectProviderMarks = (ctx: ContentLoaderContext) => ctx.providerMarks
|
|
154
|
-
export const selectLoadSeq = (ctx: ContentLoaderContext) => ctx.loadSeq
|
|
181
|
+
export const selectContent = (ctx: ContentLoaderContext) => ctx.content;
|
|
182
|
+
export const selectStatus = (ctx: ContentLoaderContext) => ctx.status;
|
|
183
|
+
export const selectStreaming = (ctx: ContentLoaderContext) => ctx.status === "streaming";
|
|
184
|
+
export const selectError = (ctx: ContentLoaderContext) => ctx.error;
|
|
185
|
+
export const selectProviderMarks = (ctx: ContentLoaderContext) => ctx.providerMarks;
|
|
186
|
+
export const selectLoadSeq = (ctx: ContentLoaderContext) => ctx.loadSeq;
|
package/src/default-provider.ts
CHANGED
|
@@ -1,106 +1,126 @@
|
|
|
1
|
-
import { parseAuto } from "@tooee/renderers"
|
|
2
|
-
import type { Content, ContentFormat, ContentProvider } from "./types.js"
|
|
1
|
+
import { parseAuto } from "@tooee/renderers";
|
|
2
|
+
import type { Content, ContentFormat, ContentProvider } from "./types.js";
|
|
3
3
|
|
|
4
4
|
export interface CreateProviderOptions {
|
|
5
|
-
renderer?: ContentFormat
|
|
5
|
+
renderer?: ContentFormat;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
function detectFormat(filePath: string): {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
const detectFormat = function detectFormat(filePath: string): {
|
|
9
|
+
format: ContentFormat;
|
|
10
|
+
language?: string;
|
|
11
|
+
} {
|
|
12
|
+
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
13
|
+
if (ext === undefined || ext === "") {
|
|
14
|
+
return { format: "text" };
|
|
15
|
+
}
|
|
11
16
|
|
|
12
|
-
const tableExts = new Set(["csv", "tsv"])
|
|
13
|
-
if (tableExts.has(ext))
|
|
17
|
+
const tableExts = new Set(["csv", "tsv"]);
|
|
18
|
+
if (tableExts.has(ext)) {
|
|
19
|
+
return { format: "table" };
|
|
20
|
+
}
|
|
14
21
|
|
|
15
|
-
const markdownExts = new Set(["md", "mdx", "markdown"])
|
|
16
|
-
if (markdownExts.has(ext))
|
|
22
|
+
const markdownExts = new Set(["md", "mdx", "markdown"]);
|
|
23
|
+
if (markdownExts.has(ext)) {
|
|
24
|
+
return { format: "markdown" };
|
|
25
|
+
}
|
|
17
26
|
|
|
18
27
|
const codeExts: Record<string, string> = {
|
|
19
|
-
ts: "typescript",
|
|
20
|
-
tsx: "typescript",
|
|
21
|
-
js: "javascript",
|
|
22
|
-
jsx: "javascript",
|
|
23
|
-
py: "python",
|
|
24
|
-
rs: "rust",
|
|
25
|
-
go: "go",
|
|
26
|
-
rb: "ruby",
|
|
27
|
-
sh: "bash",
|
|
28
28
|
bash: "bash",
|
|
29
|
-
zsh: "zsh",
|
|
30
|
-
json: "json",
|
|
31
|
-
yaml: "yaml",
|
|
32
|
-
yml: "yaml",
|
|
33
|
-
toml: "toml",
|
|
34
|
-
css: "css",
|
|
35
|
-
html: "html",
|
|
36
|
-
sql: "sql",
|
|
37
29
|
c: "c",
|
|
38
30
|
cpp: "cpp",
|
|
31
|
+
css: "css",
|
|
32
|
+
go: "go",
|
|
39
33
|
h: "c",
|
|
40
34
|
hpp: "cpp",
|
|
35
|
+
html: "html",
|
|
41
36
|
java: "java",
|
|
37
|
+
js: "javascript",
|
|
38
|
+
json: "json",
|
|
39
|
+
jsx: "javascript",
|
|
42
40
|
kt: "kotlin",
|
|
41
|
+
py: "python",
|
|
42
|
+
rb: "ruby",
|
|
43
|
+
rs: "rust",
|
|
44
|
+
sh: "bash",
|
|
45
|
+
sql: "sql",
|
|
43
46
|
swift: "swift",
|
|
44
|
-
|
|
47
|
+
toml: "toml",
|
|
48
|
+
ts: "typescript",
|
|
49
|
+
tsx: "typescript",
|
|
50
|
+
yaml: "yaml",
|
|
51
|
+
yml: "yaml",
|
|
52
|
+
zsh: "zsh",
|
|
53
|
+
};
|
|
45
54
|
|
|
46
|
-
const language = codeExts[ext]
|
|
47
|
-
if (language
|
|
55
|
+
const language = codeExts[ext];
|
|
56
|
+
if (typeof language === "string") {
|
|
57
|
+
return { format: "code", language };
|
|
58
|
+
}
|
|
48
59
|
|
|
49
|
-
return { format: "text" }
|
|
50
|
-
}
|
|
60
|
+
return { format: "text" };
|
|
61
|
+
};
|
|
51
62
|
|
|
52
|
-
function contentFromText(
|
|
63
|
+
const contentFromText = function contentFromText(
|
|
53
64
|
text: string,
|
|
54
65
|
format: ContentFormat,
|
|
55
66
|
title: string | undefined,
|
|
56
67
|
language?: string,
|
|
57
68
|
): Content {
|
|
58
69
|
switch (format) {
|
|
59
|
-
case "markdown":
|
|
60
|
-
return { format: "markdown", markdown: text, title }
|
|
61
|
-
|
|
62
|
-
|
|
70
|
+
case "markdown": {
|
|
71
|
+
return { format: "markdown", markdown: text, title };
|
|
72
|
+
}
|
|
73
|
+
case "code": {
|
|
74
|
+
return { code: text, format: "code", language, title };
|
|
75
|
+
}
|
|
63
76
|
case "table": {
|
|
64
|
-
const parsed = parseAuto(text)
|
|
65
|
-
return { format: "table",
|
|
77
|
+
const parsed = parseAuto(text);
|
|
78
|
+
return { columns: parsed.columns, format: "table", rows: parsed.rows, title };
|
|
79
|
+
}
|
|
80
|
+
case "text": {
|
|
81
|
+
return { format: "text", text, title };
|
|
82
|
+
}
|
|
83
|
+
default: {
|
|
84
|
+
return { format: "text", text, title };
|
|
66
85
|
}
|
|
67
|
-
case "text":
|
|
68
|
-
default:
|
|
69
|
-
return { format: "text", text, title }
|
|
70
86
|
}
|
|
71
|
-
}
|
|
87
|
+
};
|
|
72
88
|
|
|
73
|
-
export function createFileProvider(
|
|
89
|
+
export const createFileProvider = function createFileProvider(
|
|
74
90
|
filePath: string,
|
|
75
91
|
options: CreateProviderOptions = {},
|
|
76
92
|
): ContentProvider {
|
|
77
93
|
return {
|
|
78
94
|
async load(): Promise<Content> {
|
|
79
|
-
const detected = detectFormat(filePath)
|
|
80
|
-
const format = options.renderer ?? detected.format
|
|
81
|
-
const title = filePath.split("/").pop()
|
|
95
|
+
const detected = detectFormat(filePath);
|
|
96
|
+
const format = options.renderer ?? detected.format;
|
|
97
|
+
const title = filePath.split("/").pop();
|
|
82
98
|
|
|
83
|
-
const file = Bun.file(filePath)
|
|
84
|
-
const text = await file.text()
|
|
85
|
-
return contentFromText(text, format, title, detected.language)
|
|
99
|
+
const file = Bun.file(filePath);
|
|
100
|
+
const text = await file.text();
|
|
101
|
+
return contentFromText(text, format, title, detected.language);
|
|
86
102
|
},
|
|
87
|
-
}
|
|
88
|
-
}
|
|
103
|
+
};
|
|
104
|
+
};
|
|
89
105
|
|
|
90
|
-
export
|
|
106
|
+
export const createStdinProvider = function createStdinProvider(
|
|
107
|
+
options: CreateProviderOptions = {},
|
|
108
|
+
): ContentProvider {
|
|
91
109
|
return {
|
|
92
110
|
async load(): Promise<Content> {
|
|
93
|
-
const text = await new Response(Bun.stdin.stream()
|
|
94
|
-
const format = options.renderer ?? "markdown"
|
|
95
|
-
return contentFromText(text, format, "stdin")
|
|
111
|
+
const text = await new Response(Bun.stdin.stream()).text();
|
|
112
|
+
const format = options.renderer ?? "markdown";
|
|
113
|
+
return contentFromText(text, format, "stdin");
|
|
96
114
|
},
|
|
97
|
-
}
|
|
98
|
-
}
|
|
115
|
+
};
|
|
116
|
+
};
|
|
99
117
|
|
|
100
|
-
export
|
|
101
|
-
|
|
102
|
-
|
|
118
|
+
export const createTableFileProvider = function createTableFileProvider(
|
|
119
|
+
filePath: string,
|
|
120
|
+
): ContentProvider {
|
|
121
|
+
return createFileProvider(filePath, { renderer: "table" });
|
|
122
|
+
};
|
|
103
123
|
|
|
104
|
-
export function createTableStdinProvider(): ContentProvider {
|
|
105
|
-
return createStdinProvider({ renderer: "table" })
|
|
106
|
-
}
|
|
124
|
+
export const createTableStdinProvider = function createTableStdinProvider(): ContentProvider {
|
|
125
|
+
return createStdinProvider({ renderer: "table" });
|
|
126
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { readdirSync, statSync } from "fs"
|
|
2
|
-
import
|
|
3
|
-
import type { ContentProvider } from "./types.js"
|
|
4
|
-
import { createFileProvider } from "./default-provider.js"
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { ContentProvider } from "./types.js";
|
|
4
|
+
import { createFileProvider } from "./default-provider.js";
|
|
5
5
|
|
|
6
6
|
const SUPPORTED_EXTENSIONS = new Set([
|
|
7
7
|
"md",
|
|
@@ -33,35 +33,43 @@ const SUPPORTED_EXTENSIONS = new Set([
|
|
|
33
33
|
"kt",
|
|
34
34
|
"swift",
|
|
35
35
|
"sql",
|
|
36
|
-
])
|
|
36
|
+
]);
|
|
37
37
|
|
|
38
38
|
export interface DirectoryEntry {
|
|
39
|
-
name: string
|
|
40
|
-
path: string
|
|
39
|
+
name: string;
|
|
40
|
+
path: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function listDirectoryFiles(dirPath: string): DirectoryEntry[] {
|
|
44
|
-
const entries = readdirSync(dirPath)
|
|
45
|
-
const files: DirectoryEntry[] = []
|
|
43
|
+
export const listDirectoryFiles = function listDirectoryFiles(dirPath: string): DirectoryEntry[] {
|
|
44
|
+
const entries = readdirSync(dirPath);
|
|
45
|
+
const files: DirectoryEntry[] = [];
|
|
46
46
|
|
|
47
47
|
for (const entry of entries) {
|
|
48
|
-
if (entry.startsWith("."))
|
|
49
|
-
|
|
48
|
+
if (entry.startsWith(".")) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const fullPath = path.join(dirPath, entry);
|
|
50
52
|
try {
|
|
51
|
-
const stat = statSync(fullPath)
|
|
52
|
-
if (!stat.isFile())
|
|
53
|
+
const stat = statSync(fullPath);
|
|
54
|
+
if (!stat.isFile()) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
53
57
|
} catch {
|
|
54
|
-
continue
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const ext = entry.split(".").pop()?.toLowerCase();
|
|
61
|
+
if (ext === undefined || ext === "" || !SUPPORTED_EXTENSIONS.has(ext)) {
|
|
62
|
+
continue;
|
|
55
63
|
}
|
|
56
|
-
|
|
57
|
-
if (!ext || !SUPPORTED_EXTENSIONS.has(ext)) continue
|
|
58
|
-
files.push({ name: entry, path: fullPath })
|
|
64
|
+
files.push({ name: entry, path: fullPath });
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
files.sort((a, b) => a.name.localeCompare(b.name))
|
|
62
|
-
return files
|
|
63
|
-
}
|
|
67
|
+
files.sort((a, b) => a.name.localeCompare(b.name));
|
|
68
|
+
return files;
|
|
69
|
+
};
|
|
64
70
|
|
|
65
|
-
export
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
export const createDirectoryFileProvider = function createDirectoryFileProvider(
|
|
72
|
+
filePath: string,
|
|
73
|
+
): ContentProvider {
|
|
74
|
+
return createFileProvider(filePath);
|
|
75
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useState, useMemo } from "react";
|
|
2
|
+
import { useCommand } from "@tooee/commands";
|
|
3
|
+
import type { ActionDefinition } from "@tooee/commands";
|
|
4
|
+
import { View } from "./view.js";
|
|
5
|
+
import { listDirectoryFiles } from "./directory-provider.js";
|
|
6
|
+
import type { DirectoryEntry } from "./directory-provider.js";
|
|
7
|
+
import type { AnyContent, ContentProvider } from "./types.js";
|
|
8
|
+
import { createFileProvider } from "./default-provider.js";
|
|
9
|
+
|
|
10
|
+
const createDirectoryFileProvider = function createDirectoryFileProvider(
|
|
11
|
+
entry: DirectoryEntry,
|
|
12
|
+
index: number,
|
|
13
|
+
total: number,
|
|
14
|
+
): ContentProvider {
|
|
15
|
+
const inner = createFileProvider(entry.path);
|
|
16
|
+
return {
|
|
17
|
+
async load(): Promise<AnyContent> {
|
|
18
|
+
const result = inner.load();
|
|
19
|
+
// Deferred(lint-sweep): add schema-based validation for custom and streamed content formats.
|
|
20
|
+
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- file providers return external content at this boundary
|
|
21
|
+
const content = result instanceof Promise ? await result : (result as AnyContent);
|
|
22
|
+
return {
|
|
23
|
+
...content,
|
|
24
|
+
title: `${entry.name} (${index + 1}/${total})`,
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
interface DirectoryViewProps {
|
|
31
|
+
dirPath: string;
|
|
32
|
+
actions?: ActionDefinition[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const DirectoryView = function DirectoryView({
|
|
36
|
+
dirPath,
|
|
37
|
+
actions,
|
|
38
|
+
}: DirectoryViewProps): React.ReactNode {
|
|
39
|
+
const files = useMemo(() => listDirectoryFiles(dirPath), [dirPath]);
|
|
40
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
41
|
+
|
|
42
|
+
const contentProvider = useMemo(
|
|
43
|
+
() =>
|
|
44
|
+
files.length > 0
|
|
45
|
+
? createDirectoryFileProvider(files[currentIndex], currentIndex, files.length)
|
|
46
|
+
: null,
|
|
47
|
+
[files, currentIndex],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
useCommand({
|
|
51
|
+
handler: () => {
|
|
52
|
+
setCurrentIndex((i) => Math.min(i + 1, files.length - 1));
|
|
53
|
+
},
|
|
54
|
+
hotkey: "l",
|
|
55
|
+
id: "dir.next-file",
|
|
56
|
+
modes: ["cursor"],
|
|
57
|
+
title: "Next file",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
useCommand({
|
|
61
|
+
handler: () => {
|
|
62
|
+
setCurrentIndex((i) => Math.max(i - 1, 0));
|
|
63
|
+
},
|
|
64
|
+
hotkey: "h",
|
|
65
|
+
id: "dir.prev-file",
|
|
66
|
+
modes: ["cursor"],
|
|
67
|
+
title: "Previous file",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (files.length === 0 || !contentProvider) {
|
|
71
|
+
return (
|
|
72
|
+
<box>
|
|
73
|
+
<text content="No viewable files in directory" />
|
|
74
|
+
</box>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return <View contentProvider={contentProvider} actions={actions} />;
|
|
79
|
+
};
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
export { useContentLoader } from "./
|
|
2
|
-
export { useContentCommands } from "./
|
|
3
|
-
export {
|
|
1
|
+
export { useContentLoader } from "./use-content-loader.js";
|
|
2
|
+
export { useContentCommands } from "./use-content-commands.js";
|
|
3
|
+
export {
|
|
4
|
+
createViewCommandContext,
|
|
5
|
+
useProvideViewCommandContext,
|
|
6
|
+
} from "./use-view-command-context.js";
|
|
4
7
|
export type {
|
|
5
8
|
CreateViewCommandContextOptions,
|
|
6
9
|
ProvideViewCommandContextOptions,
|
|
7
10
|
ViewCommandContext,
|
|
8
|
-
} from "./
|
|
11
|
+
} from "./use-view-command-context.js";
|