@tooee/view 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/subviews/diff-subview.d.ts.map +1 -1
- package/dist/components/subviews/diff-subview.js +16 -4
- package/dist/components/subviews/diff-subview.js.map +1 -1
- package/dist/components/subviews/image-subview.d.ts.map +1 -1
- package/dist/components/subviews/image-subview.js +2 -1
- package/dist/components/subviews/image-subview.js.map +1 -1
- package/dist/components/subviews/index.d.ts.map +1 -1
- package/dist/components/subviews/index.js.map +1 -1
- package/dist/components/subviews/markdown-subview.d.ts.map +1 -1
- package/dist/components/subviews/markdown-subview.js +3 -2
- package/dist/components/subviews/markdown-subview.js.map +1 -1
- package/dist/components/subviews/table-subview.d.ts.map +1 -1
- package/dist/components/subviews/table-subview.js +4 -3
- package/dist/components/subviews/table-subview.js.map +1 -1
- package/dist/components/view-screen.d.ts.map +1 -1
- package/dist/components/view-screen.js +11 -1
- package/dist/components/view-screen.js.map +1 -1
- package/dist/content-loader-store.d.ts.map +1 -1
- package/dist/content-loader-store.js.map +1 -1
- package/dist/default-provider.d.ts.map +1 -1
- package/dist/default-provider.js.map +1 -1
- package/dist/directory-provider.d.ts.map +1 -1
- package/dist/directory-provider.js.map +1 -1
- package/dist/directory-view.d.ts.map +1 -1
- package/dist/directory-view.js +5 -2
- package/dist/directory-view.js.map +1 -1
- package/dist/folds/fold-decorations.d.ts.map +1 -1
- package/dist/folds/fold-decorations.js.map +1 -1
- package/dist/folds/fold-model.d.ts.map +1 -1
- package/dist/folds/fold-model.js +3 -1
- package/dist/folds/fold-model.js.map +1 -1
- package/dist/folds/markdown-heading-folds.d.ts.map +1 -1
- package/dist/folds/markdown-heading-folds.js.map +1 -1
- package/dist/folds/use-folds.d.ts.map +1 -1
- package/dist/folds/use-folds.js.map +1 -1
- package/dist/hooks/use-content-document.d.ts.map +1 -1
- package/dist/hooks/use-content-document.js +3 -2
- package/dist/hooks/use-content-document.js.map +1 -1
- package/dist/hooks/use-content-loader.d.ts.map +1 -1
- package/dist/hooks/use-content-loader.js.map +1 -1
- package/dist/hooks/use-view-command-context.d.ts.map +1 -1
- package/dist/hooks/use-view-command-context.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/launch.d.ts +7 -1
- package/dist/launch.d.ts.map +1 -1
- package/dist/launch.js.map +1 -1
- package/dist/link-handlers.d.ts.map +1 -1
- package/dist/link-handlers.js.map +1 -1
- package/dist/markdown-links.d.ts.map +1 -1
- package/dist/markdown-links.js.map +1 -1
- package/dist/outline/markdown-outline.d.ts.map +1 -1
- package/dist/outline/markdown-outline.js.map +1 -1
- package/dist/outline/outline-panel.d.ts.map +1 -1
- package/dist/outline/outline-panel.js.map +1 -1
- package/dist/outline/use-outline.d.ts.map +1 -1
- package/dist/outline/use-outline.js.map +1 -1
- package/dist/standalone-router.d.ts.map +1 -1
- package/dist/standalone-router.js +3 -0
- package/dist/standalone-router.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js.map +1 -1
- package/package.json +14 -14
- package/src/components/subviews/diff-subview.tsx +21 -4
- package/src/components/subviews/image-subview.tsx +2 -1
- package/src/components/subviews/index.ts +6 -0
- package/src/components/subviews/markdown-subview.tsx +12 -2
- package/src/components/subviews/table-subview.tsx +6 -3
- package/src/components/view-screen.tsx +11 -1
- package/src/content-loader-store.ts +23 -0
- package/src/default-provider.ts +18 -0
- package/src/directory-provider.ts +7 -0
- package/src/directory-view.tsx +7 -2
- package/src/folds/fold-decorations.ts +4 -0
- package/src/folds/fold-model.ts +8 -1
- package/src/folds/markdown-heading-folds.ts +7 -0
- package/src/folds/use-folds.ts +18 -0
- package/src/hooks/use-content-document.ts +5 -2
- package/src/hooks/use-content-loader.ts +7 -0
- package/src/hooks/use-view-command-context.ts +1 -0
- package/src/index.ts +17 -0
- package/src/launch.tsx +9 -1
- package/src/link-handlers.ts +10 -0
- package/src/markdown-links.ts +16 -0
- package/src/outline/markdown-outline.ts +3 -0
- package/src/outline/outline-panel.tsx +13 -0
- package/src/outline/use-outline.ts +24 -0
- package/src/standalone-router.tsx +23 -2
- package/src/types.ts +11 -0
- package/src/view.tsx +6 -0
|
@@ -52,15 +52,19 @@ export const createEmptyContent = function createEmptyContent(
|
|
|
52
52
|
case "markdown": {
|
|
53
53
|
return { format, markdown: "", title };
|
|
54
54
|
}
|
|
55
|
+
|
|
55
56
|
case "code": {
|
|
56
57
|
return { code: "", format, title };
|
|
57
58
|
}
|
|
59
|
+
|
|
58
60
|
case "text": {
|
|
59
61
|
return { format, text: "", title };
|
|
60
62
|
}
|
|
63
|
+
|
|
61
64
|
case "table": {
|
|
62
65
|
return { columns: [], format, rows: [], title };
|
|
63
66
|
}
|
|
67
|
+
|
|
64
68
|
default: {
|
|
65
69
|
return { data: undefined, format, title };
|
|
66
70
|
}
|
|
@@ -77,6 +81,7 @@ const ensureContentFormat = function ensureContentFormat<F extends ContentFormat
|
|
|
77
81
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- format-dependent content is trusted at this provider boundary
|
|
78
82
|
return createEmptyContent(format, title) as Extract<Content, { format: F }>;
|
|
79
83
|
}
|
|
84
|
+
|
|
80
85
|
// SAFETY: The equality guard narrows current to the built-in content variant selected by format.
|
|
81
86
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- the provider contract supplies the matching format
|
|
82
87
|
return current as Extract<Content, { format: F }>;
|
|
@@ -91,30 +96,39 @@ export const applyContentChunk = function applyContentChunk(
|
|
|
91
96
|
case "replace": {
|
|
92
97
|
return chunk.content;
|
|
93
98
|
}
|
|
99
|
+
|
|
94
100
|
case "append": {
|
|
95
101
|
if (chunk.format === "markdown") {
|
|
96
102
|
const target = ensureContentFormat(current, "markdown", title);
|
|
103
|
+
|
|
97
104
|
return { ...target, markdown: target.markdown + chunk.data };
|
|
98
105
|
}
|
|
106
|
+
|
|
99
107
|
if (chunk.format === "code") {
|
|
100
108
|
const target = ensureContentFormat(current, "code", title);
|
|
109
|
+
|
|
101
110
|
return {
|
|
102
111
|
...target,
|
|
103
112
|
code: target.code + chunk.data,
|
|
104
113
|
language: chunk.language ?? target.language,
|
|
105
114
|
};
|
|
106
115
|
}
|
|
116
|
+
|
|
107
117
|
{
|
|
108
118
|
const target = ensureContentFormat(current, "text", title);
|
|
119
|
+
|
|
109
120
|
return { ...target, text: target.text + chunk.data };
|
|
110
121
|
}
|
|
111
122
|
}
|
|
123
|
+
|
|
112
124
|
case "patch": {
|
|
113
125
|
return chunk.apply(current);
|
|
114
126
|
}
|
|
127
|
+
|
|
115
128
|
case "marks": {
|
|
116
129
|
return current ?? createEmptyContent("markdown", title);
|
|
117
130
|
}
|
|
131
|
+
|
|
118
132
|
default: {
|
|
119
133
|
return current ?? createEmptyContent("markdown", title);
|
|
120
134
|
}
|
|
@@ -141,14 +155,18 @@ export const createContentLoaderStore = function createContentLoaderStore() {
|
|
|
141
155
|
if (event.requestId !== ctx.requestId) {
|
|
142
156
|
return ctx;
|
|
143
157
|
}
|
|
158
|
+
|
|
144
159
|
if (event.chunk.type === "marks") {
|
|
145
160
|
const markSet = event.chunk.set;
|
|
161
|
+
|
|
146
162
|
const providerMarks = [
|
|
147
163
|
...ctx.providerMarks.filter((set) => set.namespace !== markSet.namespace),
|
|
148
164
|
markSet,
|
|
149
165
|
];
|
|
166
|
+
|
|
150
167
|
return { ...ctx, providerMarks };
|
|
151
168
|
}
|
|
169
|
+
|
|
152
170
|
return { ...ctx, content: applyContentChunk(ctx.content, event.chunk, ctx.title) };
|
|
153
171
|
},
|
|
154
172
|
loadCancelled: (ctx, event) =>
|
|
@@ -183,8 +201,13 @@ export const createContentLoaderStore = function createContentLoaderStore() {
|
|
|
183
201
|
};
|
|
184
202
|
|
|
185
203
|
export const selectContent = (ctx: ContentLoaderContext) => ctx.content;
|
|
204
|
+
|
|
186
205
|
export const selectStatus = (ctx: ContentLoaderContext) => ctx.status;
|
|
206
|
+
|
|
187
207
|
export const selectStreaming = (ctx: ContentLoaderContext) => ctx.status === "streaming";
|
|
208
|
+
|
|
188
209
|
export const selectError = (ctx: ContentLoaderContext) => ctx.error;
|
|
210
|
+
|
|
189
211
|
export const selectProviderMarks = (ctx: ContentLoaderContext) => ctx.providerMarks;
|
|
212
|
+
|
|
190
213
|
export const selectLoadSeq = (ctx: ContentLoaderContext) => ctx.loadSeq;
|
package/src/default-provider.ts
CHANGED
|
@@ -16,26 +16,31 @@ interface DetectedFileFormat {
|
|
|
16
16
|
|
|
17
17
|
const detectFormat = function detectFormat(filePath: string): DetectedFileFormat {
|
|
18
18
|
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
19
|
+
|
|
19
20
|
if (ext === undefined || ext === "") {
|
|
20
21
|
return { format: "text" };
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const imageExts = new Set(["png", "jpg", "jpeg", "gif", "webp"]);
|
|
25
|
+
|
|
24
26
|
if (imageExts.has(ext)) {
|
|
25
27
|
return { format: "image" };
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
const tableExts = new Set(["csv", "tsv"]);
|
|
31
|
+
|
|
29
32
|
if (tableExts.has(ext)) {
|
|
30
33
|
return { format: "table" };
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
const diffExts = new Set(["diff", "patch"]);
|
|
37
|
+
|
|
34
38
|
if (diffExts.has(ext)) {
|
|
35
39
|
return { format: "diff" };
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
const markdownExts = new Set(["md", "mdx", "markdown"]);
|
|
43
|
+
|
|
39
44
|
if (markdownExts.has(ext)) {
|
|
40
45
|
return { format: "markdown" };
|
|
41
46
|
}
|
|
@@ -69,6 +74,7 @@ const detectFormat = function detectFormat(filePath: string): DetectedFileFormat
|
|
|
69
74
|
]);
|
|
70
75
|
|
|
71
76
|
const language = codeExts.get(ext);
|
|
77
|
+
|
|
72
78
|
if (language !== undefined) {
|
|
73
79
|
return { format: "code", language };
|
|
74
80
|
}
|
|
@@ -86,22 +92,29 @@ const contentFromText = function contentFromText(
|
|
|
86
92
|
case "markdown": {
|
|
87
93
|
return { format: "markdown", markdown: text, title };
|
|
88
94
|
}
|
|
95
|
+
|
|
89
96
|
case "code": {
|
|
90
97
|
return { code: text, format: "code", language, title };
|
|
91
98
|
}
|
|
99
|
+
|
|
92
100
|
case "diff": {
|
|
93
101
|
return { format: "diff", patch: text, title };
|
|
94
102
|
}
|
|
103
|
+
|
|
95
104
|
case "table": {
|
|
96
105
|
const parsed = parseAuto(text);
|
|
106
|
+
|
|
97
107
|
return { columns: parsed.columns, format: "table", rows: parsed.rows, title };
|
|
98
108
|
}
|
|
109
|
+
|
|
99
110
|
case "text": {
|
|
100
111
|
return { format: "text", text, title };
|
|
101
112
|
}
|
|
113
|
+
|
|
102
114
|
case "image": {
|
|
103
115
|
return { format: "image", src: text.trim(), title };
|
|
104
116
|
}
|
|
117
|
+
|
|
105
118
|
default: {
|
|
106
119
|
return { format: "text", text, title };
|
|
107
120
|
}
|
|
@@ -124,15 +137,19 @@ export const createFileProvider = function createFileProvider(
|
|
|
124
137
|
|
|
125
138
|
const file = Bun.file(filePath);
|
|
126
139
|
const text = await file.text();
|
|
140
|
+
|
|
127
141
|
// Patches are routinely written to files with no telling extension.
|
|
128
142
|
const resolved =
|
|
129
143
|
options.renderer === undefined && detected.format === "text" && isDiffPatch(text)
|
|
130
144
|
? "diff"
|
|
131
145
|
: format;
|
|
146
|
+
|
|
132
147
|
const content = contentFromText(text, resolved, title, detected.language);
|
|
148
|
+
|
|
133
149
|
if (content.format === "markdown") {
|
|
134
150
|
content.imageBasePath = path.dirname(path.resolve(filePath));
|
|
135
151
|
}
|
|
152
|
+
|
|
136
153
|
return content;
|
|
137
154
|
},
|
|
138
155
|
};
|
|
@@ -145,6 +162,7 @@ export const createStdinProvider = function createStdinProvider(
|
|
|
145
162
|
async load(): Promise<Content> {
|
|
146
163
|
const text = await new Response(Bun.stdin.stream()).text();
|
|
147
164
|
const format = options.renderer ?? (isDiffPatch(text) ? "diff" : "markdown");
|
|
165
|
+
|
|
148
166
|
return contentFromText(text, format, "stdin");
|
|
149
167
|
},
|
|
150
168
|
};
|
|
@@ -58,23 +58,30 @@ export const listDirectoryFiles = function listDirectoryFiles(dirPath: string):
|
|
|
58
58
|
if (entry.startsWith(".")) {
|
|
59
59
|
continue;
|
|
60
60
|
}
|
|
61
|
+
|
|
61
62
|
const fullPath = path.join(dirPath, entry);
|
|
63
|
+
|
|
62
64
|
try {
|
|
63
65
|
const stat = statSync(fullPath);
|
|
66
|
+
|
|
64
67
|
if (!stat.isFile()) {
|
|
65
68
|
continue;
|
|
66
69
|
}
|
|
67
70
|
} catch {
|
|
68
71
|
continue;
|
|
69
72
|
}
|
|
73
|
+
|
|
70
74
|
const ext = entry.split(".").pop()?.toLowerCase();
|
|
75
|
+
|
|
71
76
|
if (ext === undefined || ext === "" || !SUPPORTED_EXTENSIONS.has(ext)) {
|
|
72
77
|
continue;
|
|
73
78
|
}
|
|
79
|
+
|
|
74
80
|
files.push({ name: entry, path: fullPath });
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
files.sort((a, b) => a.name.localeCompare(b.name));
|
|
84
|
+
|
|
78
85
|
return files;
|
|
79
86
|
};
|
|
80
87
|
|
package/src/directory-view.tsx
CHANGED
|
@@ -16,6 +16,7 @@ const createDirectoryFileProvider = function createDirectoryFileProvider(
|
|
|
16
16
|
total: number
|
|
17
17
|
): ContentProvider {
|
|
18
18
|
const inner = createFileProvider(entry.path);
|
|
19
|
+
|
|
19
20
|
return {
|
|
20
21
|
async load(): Promise<AnyContent> {
|
|
21
22
|
const result = inner.load();
|
|
@@ -23,6 +24,7 @@ const createDirectoryFileProvider = function createDirectoryFileProvider(
|
|
|
23
24
|
// or through a Promise; it never returns the ContentProvider async-iterable arm.
|
|
24
25
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- file providers return external content at this boundary
|
|
25
26
|
const content = result instanceof Promise ? await result : (result as AnyContent);
|
|
27
|
+
|
|
26
28
|
return {
|
|
27
29
|
...content,
|
|
28
30
|
title: `${entry.name} (${index + 1}/${total})`,
|
|
@@ -51,11 +53,14 @@ export const DirectoryView = function DirectoryView({
|
|
|
51
53
|
[files, currentIndex]
|
|
52
54
|
);
|
|
53
55
|
|
|
56
|
+
// File keys use a `g` sequence. `h` and `l` belong to horizontal scroll in
|
|
57
|
+
// the markdown and diff subviews, and two commands on one key let mount
|
|
58
|
+
// order pick the winner.
|
|
54
59
|
useCommand({
|
|
55
60
|
handler: () => {
|
|
56
61
|
setCurrentIndex((i) => Math.min(i + 1, files.length - 1));
|
|
57
62
|
},
|
|
58
|
-
hotkey: "
|
|
63
|
+
hotkey: "g n",
|
|
59
64
|
id: "dir.next-file",
|
|
60
65
|
modes: ["cursor"],
|
|
61
66
|
title: "Next file",
|
|
@@ -65,7 +70,7 @@ export const DirectoryView = function DirectoryView({
|
|
|
65
70
|
handler: () => {
|
|
66
71
|
setCurrentIndex((i) => Math.max(i - 1, 0));
|
|
67
72
|
},
|
|
68
|
-
hotkey: "
|
|
73
|
+
hotkey: "g p",
|
|
69
74
|
id: "dir.prev-file",
|
|
70
75
|
modes: ["cursor"],
|
|
71
76
|
title: "Previous file",
|
|
@@ -11,15 +11,19 @@ export const projectDecorationLayers = function projectDecorationLayers(
|
|
|
11
11
|
view: FoldView
|
|
12
12
|
): DecorationLayer[] {
|
|
13
13
|
const { visibleToSource, sourceToVisible } = view;
|
|
14
|
+
|
|
14
15
|
return layers.map((layer): DecorationLayer => ({
|
|
15
16
|
*forVisibleRows(from: number, to: number): Generator<RowDecoration> {
|
|
16
17
|
const sourceFrom = visibleToSource[from];
|
|
17
18
|
const sourceTo = visibleToSource[Math.min(to, visibleToSource.length - 1)];
|
|
19
|
+
|
|
18
20
|
if (sourceFrom === undefined || sourceTo === undefined) {
|
|
19
21
|
return;
|
|
20
22
|
}
|
|
23
|
+
|
|
21
24
|
for (const decoration of layer.forVisibleRows(sourceFrom, sourceTo)) {
|
|
22
25
|
const row = sourceToVisible[decoration.row] ?? -1;
|
|
26
|
+
|
|
23
27
|
if (row !== -1) {
|
|
24
28
|
yield { ...decoration, row };
|
|
25
29
|
}
|
package/src/folds/fold-model.ts
CHANGED
|
@@ -33,6 +33,7 @@ export const computeFoldView = function computeFoldView(
|
|
|
33
33
|
closedKeys: ReadonlySet<Key>
|
|
34
34
|
): FoldView {
|
|
35
35
|
const closedEnds = new Map<number, number>();
|
|
36
|
+
|
|
36
37
|
for (const range of ranges) {
|
|
37
38
|
if (range.end > range.start && closedKeys.has(range.key)) {
|
|
38
39
|
closedEnds.set(range.start, Math.max(closedEnds.get(range.start) ?? range.end, range.end));
|
|
@@ -43,14 +44,17 @@ export const computeFoldView = function computeFoldView(
|
|
|
43
44
|
const sourceToVisible: number[] = Array.from({ length: rowCount }, () => -1);
|
|
44
45
|
const hiddenCounts = new Map<number, number>();
|
|
45
46
|
let hiddenUntil = -1;
|
|
47
|
+
|
|
46
48
|
for (let index = 0; index < rowCount; index += 1) {
|
|
47
49
|
if (index <= hiddenUntil) {
|
|
48
50
|
continue;
|
|
49
51
|
}
|
|
52
|
+
|
|
50
53
|
const visibleIndex = visibleToSource.length;
|
|
51
54
|
visibleToSource.push(index);
|
|
52
55
|
sourceToVisible[index] = visibleIndex;
|
|
53
56
|
const end = closedEnds.get(index);
|
|
57
|
+
|
|
54
58
|
if (end !== undefined) {
|
|
55
59
|
hiddenUntil = Math.min(end, rowCount - 1);
|
|
56
60
|
hiddenCounts.set(visibleIndex, hiddenUntil - index);
|
|
@@ -80,8 +84,10 @@ export const closedFoldsHiding = function closedFoldsHiding(
|
|
|
80
84
|
row: number
|
|
81
85
|
): Key[] {
|
|
82
86
|
return ranges
|
|
87
|
+
.values()
|
|
83
88
|
.filter((range) => closedKeys.has(range.key) && range.start < row && row <= range.end)
|
|
84
|
-
.map((range) => range.key)
|
|
89
|
+
.map((range) => range.key)
|
|
90
|
+
.toArray();
|
|
85
91
|
};
|
|
86
92
|
|
|
87
93
|
/**
|
|
@@ -95,5 +101,6 @@ export const visibleAnchorRow = function visibleAnchorRow(view: FoldView, row: n
|
|
|
95
101
|
return index;
|
|
96
102
|
}
|
|
97
103
|
}
|
|
104
|
+
|
|
98
105
|
return row;
|
|
99
106
|
};
|
|
@@ -8,6 +8,7 @@ export const headingToken = function headingToken(block: FlatBlock): Tokens.Head
|
|
|
8
8
|
if (block.token.type !== "heading") {
|
|
9
9
|
return null;
|
|
10
10
|
}
|
|
11
|
+
|
|
11
12
|
// SAFETY: Marked creates a Heading token for the checked "heading" discriminator.
|
|
12
13
|
// oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Marked's Generic fallback prevents discriminator narrowing
|
|
13
14
|
return block.token as Tokens.Heading;
|
|
@@ -28,6 +29,7 @@ export const markdownHeadingFoldRanges = function markdownHeadingFoldRanges(
|
|
|
28
29
|
): FoldRange[] {
|
|
29
30
|
const ranges: FoldRange[] = [];
|
|
30
31
|
const open: { depth: number; start: number }[] = [];
|
|
32
|
+
|
|
31
33
|
const close = (start: number, end: number) => {
|
|
32
34
|
if (end > start) {
|
|
33
35
|
ranges.push({ end, key: start, start });
|
|
@@ -36,17 +38,22 @@ export const markdownHeadingFoldRanges = function markdownHeadingFoldRanges(
|
|
|
36
38
|
|
|
37
39
|
for (const [index, block] of blocks.entries()) {
|
|
38
40
|
const depth = headingDepth(block);
|
|
41
|
+
|
|
39
42
|
if (depth === null) {
|
|
40
43
|
continue;
|
|
41
44
|
}
|
|
45
|
+
|
|
42
46
|
while (open.length > 0 && (open.at(-1)?.depth ?? 0) >= depth) {
|
|
43
47
|
const finished = open.pop();
|
|
48
|
+
|
|
44
49
|
if (finished) {
|
|
45
50
|
close(finished.start, index - 1);
|
|
46
51
|
}
|
|
47
52
|
}
|
|
53
|
+
|
|
48
54
|
open.push({ depth, start: index });
|
|
49
55
|
}
|
|
56
|
+
|
|
50
57
|
for (const finished of open) {
|
|
51
58
|
close(finished.start, blocks.length - 1);
|
|
52
59
|
}
|
package/src/folds/use-folds.ts
CHANGED
|
@@ -39,10 +39,12 @@ export const useFoldState = function useFoldState<T>(
|
|
|
39
39
|
decorations: DecorationLayer[]
|
|
40
40
|
): FoldState<T> {
|
|
41
41
|
const [closedKeys, setClosedKeys] = useState<ReadonlySet<Key>>(EMPTY_KEYS);
|
|
42
|
+
|
|
42
43
|
const view = useMemo(
|
|
43
44
|
() => computeFoldView(rows.length, ranges, closedKeys),
|
|
44
45
|
[rows.length, ranges, closedKeys]
|
|
45
46
|
);
|
|
47
|
+
|
|
46
48
|
const folded = view.visibleToSource.length < rows.length;
|
|
47
49
|
|
|
48
50
|
const visibleRows = useMemo(
|
|
@@ -50,15 +52,18 @@ export const useFoldState = function useFoldState<T>(
|
|
|
50
52
|
folded
|
|
51
53
|
? view.visibleToSource.flatMap((index) => {
|
|
52
54
|
const row = rows[index];
|
|
55
|
+
|
|
53
56
|
return row === undefined ? [] : [row];
|
|
54
57
|
})
|
|
55
58
|
: rows,
|
|
56
59
|
[folded, rows, view]
|
|
57
60
|
);
|
|
61
|
+
|
|
58
62
|
const visibleDecorations = useMemo(
|
|
59
63
|
() => (folded ? projectDecorationLayers(decorations, view) : decorations),
|
|
60
64
|
[folded, decorations, view]
|
|
61
65
|
);
|
|
66
|
+
|
|
62
67
|
const rowNumbers = useMemo(
|
|
63
68
|
() => (folded ? view.visibleToSource.map((index) => index + 1) : undefined),
|
|
64
69
|
[folded, view]
|
|
@@ -109,27 +114,36 @@ export const useFoldActions = function useFoldActions<T>(
|
|
|
109
114
|
if (cursorRow !== undefined) {
|
|
110
115
|
const anchor = visibleAnchorRow(computeFoldView(rowCount, ranges, next), cursorRow);
|
|
111
116
|
const anchorIndex = view.sourceToVisible[anchor] ?? -1;
|
|
117
|
+
|
|
112
118
|
if (anchor !== cursorRow && anchorIndex !== -1) {
|
|
113
119
|
setCursor(anchorIndex);
|
|
114
120
|
}
|
|
115
121
|
}
|
|
122
|
+
|
|
116
123
|
setClosedKeys(next);
|
|
117
124
|
};
|
|
125
|
+
|
|
118
126
|
const withKey = (key: Key | undefined, closed: boolean): boolean => {
|
|
119
127
|
if (key === undefined) {
|
|
120
128
|
return false;
|
|
121
129
|
}
|
|
130
|
+
|
|
122
131
|
const next = new Set(closedKeys);
|
|
132
|
+
|
|
123
133
|
if (closed) {
|
|
124
134
|
next.add(key);
|
|
125
135
|
} else {
|
|
126
136
|
next.delete(key);
|
|
127
137
|
}
|
|
138
|
+
|
|
128
139
|
apply(next);
|
|
140
|
+
|
|
129
141
|
return true;
|
|
130
142
|
};
|
|
143
|
+
|
|
131
144
|
const close = (row: number) =>
|
|
132
145
|
withKey(foldsAt(ranges, row).find((range) => !closedKeys.has(range.key))?.key, true);
|
|
146
|
+
|
|
133
147
|
const open = (row: number) =>
|
|
134
148
|
withKey(foldsAt(ranges, row).find((range) => closedKeys.has(range.key))?.key, false);
|
|
135
149
|
|
|
@@ -144,6 +158,7 @@ export const useFoldActions = function useFoldActions<T>(
|
|
|
144
158
|
},
|
|
145
159
|
toggle: (row) => {
|
|
146
160
|
const [innermost] = foldsAt(ranges, row);
|
|
161
|
+
|
|
147
162
|
return withKey(innermost?.key, innermost !== undefined && !closedKeys.has(innermost.key));
|
|
148
163
|
},
|
|
149
164
|
toggleBlock: (row) =>
|
|
@@ -168,10 +183,12 @@ export const foldActionDefinitions = function foldActionDefinitions(
|
|
|
168
183
|
(run: (row: number) => boolean) =>
|
|
169
184
|
(ctx: CommandContext): void => {
|
|
170
185
|
const row = targetRow();
|
|
186
|
+
|
|
171
187
|
if (row === undefined || !run(row)) {
|
|
172
188
|
ctx.toast?.toast({ level: "warning", message: noFoldMessage });
|
|
173
189
|
}
|
|
174
190
|
};
|
|
191
|
+
|
|
175
192
|
const definitions: [hotkey: string, id: string, title: string, handler: CommandHandler][] = [
|
|
176
193
|
["z a", "toggle", "Toggle fold", atTarget(actions.toggle)],
|
|
177
194
|
["z c", "close", "Close fold", atTarget(actions.close)],
|
|
@@ -180,6 +197,7 @@ export const foldActionDefinitions = function foldActionDefinitions(
|
|
|
180
197
|
["z shift+m", "close-all", "Close all folds", actions.closeAll],
|
|
181
198
|
["z shift+r", "open-all", "Open all folds", actions.openAll],
|
|
182
199
|
];
|
|
200
|
+
|
|
183
201
|
return definitions.map(([hotkey, id, title, handler]) => ({
|
|
184
202
|
category: "Fold",
|
|
185
203
|
handler,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ActionDefinition } from "@tooee/commands";
|
|
2
|
+
import { StatusPriority } from "@tooee/layout";
|
|
2
3
|
import type { StatusBarItem } from "@tooee/layout";
|
|
3
4
|
import type { DecorationLayer } from "@tooee/renderers";
|
|
4
5
|
import { useDocumentController } from "@tooee/shell";
|
|
@@ -37,6 +38,7 @@ export const useContentDocument = function useContentDocument<T>(
|
|
|
37
38
|
options: ContentDocumentOptions<T>
|
|
38
39
|
): ContentDocumentResult<T> {
|
|
39
40
|
const { showLineNumbers } = useContentCommands({ content, textContent });
|
|
41
|
+
|
|
40
42
|
const document = useDocumentController<T>({
|
|
41
43
|
adapter,
|
|
42
44
|
contextMenu: options.contextMenu === false ? false : actions,
|
|
@@ -45,10 +47,11 @@ export const useContentDocument = function useContentDocument<T>(
|
|
|
45
47
|
preserveCursorByKey: options.preserveCursorByKey,
|
|
46
48
|
rows,
|
|
47
49
|
});
|
|
50
|
+
|
|
48
51
|
const statusItems = options.buildStatusItems?.(document) ??
|
|
49
52
|
options.statusItems ?? [
|
|
50
|
-
{ label: "Format:", value: content.format },
|
|
51
|
-
{ label: "Lines:", value: String(rows.length) },
|
|
53
|
+
{ label: "Format:", priority: StatusPriority.content, value: content.format },
|
|
54
|
+
{ label: "Lines:", priority: StatusPriority.content, value: String(rows.length) },
|
|
52
55
|
];
|
|
53
56
|
|
|
54
57
|
return { document, showLineNumbers, statusItems };
|
|
@@ -39,6 +39,7 @@ export const useContentLoader = function useContentLoader(
|
|
|
39
39
|
});
|
|
40
40
|
const { requestId } = store.getSnapshot().context;
|
|
41
41
|
const loaded = contentProvider.load();
|
|
42
|
+
|
|
42
43
|
let cleanup: () => void = () => {
|
|
43
44
|
// Synchronous providers have no pending work to cancel.
|
|
44
45
|
};
|
|
@@ -55,16 +56,20 @@ export const useContentLoader = function useContentLoader(
|
|
|
55
56
|
// preserve sequential stream consumption for ordered backpressure.
|
|
56
57
|
// oxlint-disable-next-line no-await-in-loop -- each chunk must be consumed in iterator order
|
|
57
58
|
const result = await iterator.next();
|
|
59
|
+
|
|
58
60
|
if (result.done === true) {
|
|
59
61
|
break;
|
|
60
62
|
}
|
|
63
|
+
|
|
61
64
|
store.trigger.chunkReceived({ chunk: result.value, requestId });
|
|
62
65
|
}
|
|
66
|
+
|
|
63
67
|
store.trigger.streamEnded({ requestId });
|
|
64
68
|
} catch (error) {
|
|
65
69
|
store.trigger.loadFailed({ error: normalizeError(error), requestId });
|
|
66
70
|
}
|
|
67
71
|
})();
|
|
72
|
+
|
|
68
73
|
cleanup = () => {
|
|
69
74
|
store.trigger.loadCancelled({ requestId });
|
|
70
75
|
void (async () => {
|
|
@@ -84,6 +89,7 @@ export const useContentLoader = function useContentLoader(
|
|
|
84
89
|
store.trigger.loadFailed({ error: normalizeError(error), requestId });
|
|
85
90
|
}
|
|
86
91
|
})();
|
|
92
|
+
|
|
87
93
|
cleanup = () => {
|
|
88
94
|
store.trigger.loadCancelled({ requestId });
|
|
89
95
|
};
|
|
@@ -99,6 +105,7 @@ export const useContentLoader = function useContentLoader(
|
|
|
99
105
|
const error = useSelector(store, (snapshot) => selectError(snapshot.context));
|
|
100
106
|
const providerMarks = useSelector(store, (snapshot) => selectProviderMarks(snapshot.context));
|
|
101
107
|
const status = useSelector(store, (snapshot) => selectStatus(snapshot.context));
|
|
108
|
+
|
|
102
109
|
const reload = useCallback(() => {
|
|
103
110
|
store.trigger.reloadRequested({});
|
|
104
111
|
}, [store]);
|
|
@@ -72,6 +72,7 @@ export const createViewCommandContext = function createViewCommandContext({
|
|
|
72
72
|
marks,
|
|
73
73
|
}: CreateViewCommandContextOptions): ViewCommandContext {
|
|
74
74
|
const resolvedFormat = format ?? content?.format ?? "custom";
|
|
75
|
+
|
|
75
76
|
const resolvedContent: AnyContent =
|
|
76
77
|
content ??
|
|
77
78
|
({
|
package/src/index.ts
CHANGED
|
@@ -1,27 +1,39 @@
|
|
|
1
1
|
export { View } from "./view.js";
|
|
2
|
+
|
|
2
3
|
export { DirectoryView } from "./directory-view.js";
|
|
4
|
+
|
|
3
5
|
export { launch, launchDirectory } from "./launch.js";
|
|
6
|
+
|
|
4
7
|
export type { ViewLaunchOptions } from "./launch.js";
|
|
8
|
+
|
|
5
9
|
export {
|
|
6
10
|
createFileProvider,
|
|
7
11
|
createStdinProvider,
|
|
8
12
|
createTableFileProvider,
|
|
9
13
|
createTableStdinProvider,
|
|
10
14
|
} from "./default-provider.js";
|
|
15
|
+
|
|
11
16
|
export type { CreateProviderOptions } from "./default-provider.js";
|
|
17
|
+
|
|
12
18
|
export { listDirectoryFiles } from "./directory-provider.js";
|
|
19
|
+
|
|
13
20
|
export type { DirectoryEntry } from "./directory-provider.js";
|
|
21
|
+
|
|
14
22
|
export {
|
|
15
23
|
createViewCommandContext,
|
|
16
24
|
useProvideViewCommandContext,
|
|
17
25
|
} from "./hooks/use-view-command-context.js";
|
|
26
|
+
|
|
18
27
|
export type {
|
|
19
28
|
CreateViewCommandContextOptions,
|
|
20
29
|
ProvideViewCommandContextOptions,
|
|
21
30
|
ViewCommandContext,
|
|
22
31
|
} from "./hooks/use-view-command-context.js";
|
|
32
|
+
|
|
23
33
|
export type { ContentLoaderState } from "./hooks/use-content-loader.js";
|
|
34
|
+
|
|
24
35
|
export type { ContentCommandsResult } from "./hooks/use-content-commands.js";
|
|
36
|
+
|
|
25
37
|
export type {
|
|
26
38
|
AnyContent,
|
|
27
39
|
Content,
|
|
@@ -43,8 +55,13 @@ export type {
|
|
|
43
55
|
ColumnDef,
|
|
44
56
|
TableRow,
|
|
45
57
|
} from "./types.js";
|
|
58
|
+
|
|
46
59
|
export { getTextContent, isBuiltinContent, isCustomContent } from "./types.js";
|
|
60
|
+
|
|
47
61
|
export { CodeBlockChrome } from "@tooee/renderers";
|
|
62
|
+
|
|
48
63
|
export type { CodeBlockRenderer, CodeBlockRendererProps, CodeBlockHScroll } from "@tooee/renderers";
|
|
64
|
+
|
|
49
65
|
export { localFileLinkHandler } from "./link-handlers.js";
|
|
66
|
+
|
|
50
67
|
export type { LinkHandler, LinkHandlerContext, LinkTarget } from "./link-handlers.js";
|
package/src/launch.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import type { ReactNode } from "react";
|
|
|
7
7
|
import { DirectoryView } from "./directory-view.js";
|
|
8
8
|
import type { LinkHandler } from "./link-handlers.js";
|
|
9
9
|
import { createStandaloneRouter } from "./standalone-router.js";
|
|
10
|
-
import type { ContentProvider, ContentRenderer } from "./types.js";
|
|
10
|
+
import type { ContentProvider, ContentRenderer, MarkdownLinkActivateHandler } from "./types.js";
|
|
11
11
|
|
|
12
12
|
export interface ViewLaunchOptions {
|
|
13
13
|
contentProvider: ContentProvider;
|
|
@@ -15,6 +15,12 @@ export interface ViewLaunchOptions {
|
|
|
15
15
|
filePath?: string;
|
|
16
16
|
/** Synchronous handlers tried before local file navigation. */
|
|
17
17
|
linkHandlers?: readonly LinkHandler[];
|
|
18
|
+
/**
|
|
19
|
+
* Handles activation of a Markdown link, by mouse or by Enter on the cursor
|
|
20
|
+
* line. When set, it replaces the default handling: `linkHandlers`, local
|
|
21
|
+
* file navigation, and the stdin "Links need a source file" toast.
|
|
22
|
+
*/
|
|
23
|
+
onMarkdownLinkActivate?: MarkdownLinkActivateHandler;
|
|
18
24
|
actions?: ActionDefinition[];
|
|
19
25
|
renderers?: Record<string, ContentRenderer>;
|
|
20
26
|
/**
|
|
@@ -35,9 +41,11 @@ export interface DirectoryLaunchOptions {
|
|
|
35
41
|
export const launch = async function launch(options: ViewLaunchOptions): Promise<void> {
|
|
36
42
|
const { router } = createStandaloneRouter(options);
|
|
37
43
|
const startup = await router.start();
|
|
44
|
+
|
|
38
45
|
if (startup.status !== "committed") {
|
|
39
46
|
throw new Error(`View router startup ${startup.status}`);
|
|
40
47
|
}
|
|
48
|
+
|
|
41
49
|
await runCliSession<undefined>(
|
|
42
50
|
(): ReactNode => (
|
|
43
51
|
<RouterProvider router={router}>
|
package/src/link-handlers.ts
CHANGED
|
@@ -24,22 +24,29 @@ export type LinkHandler = (link: LinkTarget, context: LinkHandlerContext) => boo
|
|
|
24
24
|
|
|
25
25
|
export const localFileLinkHandler: LinkHandler = function localFileLinkHandler(link, context) {
|
|
26
26
|
const result = resolveMarkdownLink(link.href, link.baseDir, link.currentPath);
|
|
27
|
+
|
|
27
28
|
if (result.status === "unsupported") {
|
|
28
29
|
return false;
|
|
29
30
|
}
|
|
31
|
+
|
|
30
32
|
if (result.status === "file") {
|
|
31
33
|
void context.navigate.push(context.documentRoute, { path: result.path });
|
|
34
|
+
|
|
32
35
|
return true;
|
|
33
36
|
}
|
|
37
|
+
|
|
34
38
|
const relative = path.relative(link.baseDir, result.path);
|
|
39
|
+
|
|
35
40
|
const target =
|
|
36
41
|
relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)
|
|
37
42
|
? result.path
|
|
38
43
|
: relative || ".";
|
|
44
|
+
|
|
39
45
|
context.command.toast?.toast({
|
|
40
46
|
level: "warning",
|
|
41
47
|
message: `${result.status === "missing" ? "File not found" : "Not a file"}: ${target}`,
|
|
42
48
|
});
|
|
49
|
+
|
|
43
50
|
return true;
|
|
44
51
|
};
|
|
45
52
|
|
|
@@ -54,9 +61,12 @@ export const runLinkHandlers = function runLinkHandlers(
|
|
|
54
61
|
return true;
|
|
55
62
|
}
|
|
56
63
|
}
|
|
64
|
+
|
|
57
65
|
if (localFileLinkHandler(link, context)) {
|
|
58
66
|
return true;
|
|
59
67
|
}
|
|
68
|
+
|
|
60
69
|
context.command.toast?.toast({ level: "warning", message: `Unsupported link: ${link.href}` });
|
|
70
|
+
|
|
61
71
|
return true;
|
|
62
72
|
};
|