@involvex/fresh-editor 0.1.76 → 0.1.78
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/bin/CHANGELOG.md +1017 -0
- package/bin/LICENSE +117 -0
- package/bin/README.md +248 -0
- package/bin/fresh.exe +0 -0
- package/bin/plugins/README.md +71 -0
- package/bin/plugins/audit_mode.i18n.json +821 -0
- package/bin/plugins/audit_mode.ts +1810 -0
- package/bin/plugins/buffer_modified.i18n.json +67 -0
- package/bin/plugins/buffer_modified.ts +281 -0
- package/bin/plugins/calculator.i18n.json +93 -0
- package/bin/plugins/calculator.ts +770 -0
- package/bin/plugins/clangd-lsp.ts +168 -0
- package/bin/plugins/clangd_support.i18n.json +223 -0
- package/bin/plugins/clangd_support.md +20 -0
- package/bin/plugins/clangd_support.ts +325 -0
- package/bin/plugins/color_highlighter.i18n.json +145 -0
- package/bin/plugins/color_highlighter.ts +304 -0
- package/bin/plugins/config-schema.json +768 -0
- package/bin/plugins/csharp-lsp.ts +147 -0
- package/bin/plugins/csharp_support.i18n.json +80 -0
- package/bin/plugins/csharp_support.ts +170 -0
- package/bin/plugins/css-lsp.ts +143 -0
- package/bin/plugins/diagnostics_panel.i18n.json +236 -0
- package/bin/plugins/diagnostics_panel.ts +642 -0
- package/bin/plugins/examples/README.md +85 -0
- package/bin/plugins/examples/async_demo.ts +165 -0
- package/bin/plugins/examples/bookmarks.ts +329 -0
- package/bin/plugins/examples/buffer_query_demo.ts +110 -0
- package/bin/plugins/examples/git_grep.ts +262 -0
- package/bin/plugins/examples/hello_world.ts +93 -0
- package/bin/plugins/examples/virtual_buffer_demo.ts +116 -0
- package/bin/plugins/find_references.i18n.json +275 -0
- package/bin/plugins/find_references.ts +359 -0
- package/bin/plugins/git_blame.i18n.json +496 -0
- package/bin/plugins/git_blame.ts +707 -0
- package/bin/plugins/git_find_file.i18n.json +314 -0
- package/bin/plugins/git_find_file.ts +300 -0
- package/bin/plugins/git_grep.i18n.json +171 -0
- package/bin/plugins/git_grep.ts +191 -0
- package/bin/plugins/git_gutter.i18n.json +93 -0
- package/bin/plugins/git_gutter.ts +477 -0
- package/bin/plugins/git_log.i18n.json +481 -0
- package/bin/plugins/git_log.ts +1285 -0
- package/bin/plugins/go-lsp.ts +143 -0
- package/bin/plugins/html-lsp.ts +145 -0
- package/bin/plugins/json-lsp.ts +145 -0
- package/bin/plugins/lib/fresh.d.ts +1321 -0
- package/bin/plugins/lib/index.ts +24 -0
- package/bin/plugins/lib/navigation-controller.ts +214 -0
- package/bin/plugins/lib/panel-manager.ts +220 -0
- package/bin/plugins/lib/types.ts +72 -0
- package/bin/plugins/lib/virtual-buffer-factory.ts +130 -0
- package/bin/plugins/live_grep.i18n.json +171 -0
- package/bin/plugins/live_grep.ts +422 -0
- package/bin/plugins/markdown_compose.i18n.json +223 -0
- package/bin/plugins/markdown_compose.ts +630 -0
- package/bin/plugins/merge_conflict.i18n.json +821 -0
- package/bin/plugins/merge_conflict.ts +1810 -0
- package/bin/plugins/path_complete.i18n.json +80 -0
- package/bin/plugins/path_complete.ts +165 -0
- package/bin/plugins/python-lsp.ts +162 -0
- package/bin/plugins/rust-lsp.ts +166 -0
- package/bin/plugins/search_replace.i18n.json +405 -0
- package/bin/plugins/search_replace.ts +484 -0
- package/bin/plugins/test_i18n.i18n.json +67 -0
- package/bin/plugins/test_i18n.ts +18 -0
- package/bin/plugins/theme_editor.i18n.json +3746 -0
- package/bin/plugins/theme_editor.ts +2063 -0
- package/bin/plugins/todo_highlighter.i18n.json +184 -0
- package/bin/plugins/todo_highlighter.ts +206 -0
- package/bin/plugins/typescript-lsp.ts +167 -0
- package/bin/plugins/vi_mode.i18n.json +1549 -0
- package/bin/plugins/vi_mode.ts +2747 -0
- package/bin/plugins/welcome.i18n.json +236 -0
- package/bin/plugins/welcome.ts +76 -0
- package/bin/themes/dark.json +102 -0
- package/bin/themes/dracula.json +62 -0
- package/bin/themes/high-contrast.json +102 -0
- package/bin/themes/light.json +102 -0
- package/bin/themes/nord.json +62 -0
- package/bin/themes/nostalgia.json +102 -0
- package/bin/themes/solarized-dark.json +62 -0
- package/binary-install.js +1 -1
- package/dist/bin/fresh.js +9 -0
- package/dist/binary-install.js +149 -0
- package/dist/binary.js +30 -0
- package/dist/fresh-6yhknp07.exe +0 -0
- package/dist/install.js +158 -0
- package/dist/run-fresh.js +43 -0
- package/package.json +7 -2
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
// Markdown Compose Mode Plugin
|
|
2
|
+
// Provides compose mode for Markdown documents with:
|
|
3
|
+
// - Soft wrapping at a configurable width
|
|
4
|
+
// - Hanging indents for lists and block quotes
|
|
5
|
+
// - Centered margins
|
|
6
|
+
//
|
|
7
|
+
// Syntax highlighting is handled by the TextMate grammar (built-in to the editor)
|
|
8
|
+
// This plugin only adds the compose mode layout features.
|
|
9
|
+
const editor = getEditor();
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
interface MarkdownConfig {
|
|
13
|
+
composeWidth: number;
|
|
14
|
+
maxWidth: number;
|
|
15
|
+
hideLineNumbers: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const config: MarkdownConfig = {
|
|
19
|
+
composeWidth: 80,
|
|
20
|
+
maxWidth: 100,
|
|
21
|
+
hideLineNumbers: true,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Track buffers in compose mode (explicit toggle)
|
|
25
|
+
const composeBuffers = new Set<number>();
|
|
26
|
+
|
|
27
|
+
// Types match the Rust ViewTokenWire structure
|
|
28
|
+
interface ViewTokenWire {
|
|
29
|
+
source_offset: number | null;
|
|
30
|
+
kind: ViewTokenWireKind;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type ViewTokenWireKind =
|
|
34
|
+
| { Text: string }
|
|
35
|
+
| "Newline"
|
|
36
|
+
| "Space"
|
|
37
|
+
| "Break";
|
|
38
|
+
|
|
39
|
+
interface LayoutHints {
|
|
40
|
+
compose_width?: number | null;
|
|
41
|
+
column_guides?: number[] | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Block-based parser for hanging indent support
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
interface ParsedBlock {
|
|
49
|
+
type: 'paragraph' | 'list-item' | 'ordered-list' | 'checkbox' | 'blockquote' |
|
|
50
|
+
'heading' | 'code-fence' | 'code-content' | 'hr' | 'empty' | 'image';
|
|
51
|
+
startByte: number; // First byte of the line
|
|
52
|
+
endByte: number; // Byte after last char (before newline)
|
|
53
|
+
leadingIndent: number; // Spaces before marker/content
|
|
54
|
+
marker: string; // "- ", "1. ", "> ", "## ", etc.
|
|
55
|
+
markerStartByte: number; // Where marker begins
|
|
56
|
+
contentStartByte: number; // Where content begins (after marker)
|
|
57
|
+
content: string; // The actual text content (after marker)
|
|
58
|
+
hangingIndent: number; // Continuation indent for wrapped lines
|
|
59
|
+
forceHardBreak: boolean; // Should this block end with hard newline?
|
|
60
|
+
headingLevel?: number; // For headings (1-6)
|
|
61
|
+
checked?: boolean; // For checkboxes
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse a markdown document into blocks with structure info for wrapping
|
|
66
|
+
*/
|
|
67
|
+
function parseMarkdownBlocks(text: string): ParsedBlock[] {
|
|
68
|
+
const blocks: ParsedBlock[] = [];
|
|
69
|
+
const lines = text.split('\n');
|
|
70
|
+
let byteOffset = 0;
|
|
71
|
+
let inCodeBlock = false;
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < lines.length; i++) {
|
|
74
|
+
const line = lines[i];
|
|
75
|
+
const lineStart = byteOffset;
|
|
76
|
+
const lineEnd = byteOffset + line.length;
|
|
77
|
+
|
|
78
|
+
// Code block detection
|
|
79
|
+
const trimmed = line.trim();
|
|
80
|
+
if (trimmed.startsWith('```')) {
|
|
81
|
+
inCodeBlock = !inCodeBlock;
|
|
82
|
+
blocks.push({
|
|
83
|
+
type: 'code-fence',
|
|
84
|
+
startByte: lineStart,
|
|
85
|
+
endByte: lineEnd,
|
|
86
|
+
leadingIndent: line.length - line.trimStart().length,
|
|
87
|
+
marker: '',
|
|
88
|
+
markerStartByte: lineStart,
|
|
89
|
+
contentStartByte: lineStart,
|
|
90
|
+
content: line,
|
|
91
|
+
hangingIndent: 0,
|
|
92
|
+
forceHardBreak: true,
|
|
93
|
+
});
|
|
94
|
+
byteOffset = lineEnd + 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (inCodeBlock) {
|
|
99
|
+
blocks.push({
|
|
100
|
+
type: 'code-content',
|
|
101
|
+
startByte: lineStart,
|
|
102
|
+
endByte: lineEnd,
|
|
103
|
+
leadingIndent: 0,
|
|
104
|
+
marker: '',
|
|
105
|
+
markerStartByte: lineStart,
|
|
106
|
+
contentStartByte: lineStart,
|
|
107
|
+
content: line,
|
|
108
|
+
hangingIndent: 0,
|
|
109
|
+
forceHardBreak: true,
|
|
110
|
+
});
|
|
111
|
+
byteOffset = lineEnd + 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Empty line
|
|
116
|
+
if (trimmed.length === 0) {
|
|
117
|
+
blocks.push({
|
|
118
|
+
type: 'empty',
|
|
119
|
+
startByte: lineStart,
|
|
120
|
+
endByte: lineEnd,
|
|
121
|
+
leadingIndent: 0,
|
|
122
|
+
marker: '',
|
|
123
|
+
markerStartByte: lineStart,
|
|
124
|
+
contentStartByte: lineStart,
|
|
125
|
+
content: '',
|
|
126
|
+
hangingIndent: 0,
|
|
127
|
+
forceHardBreak: true,
|
|
128
|
+
});
|
|
129
|
+
byteOffset = lineEnd + 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Headers: # Heading
|
|
134
|
+
const headerMatch = line.match(/^(\s*)(#{1,6})\s+(.*)$/);
|
|
135
|
+
if (headerMatch) {
|
|
136
|
+
const leadingIndent = headerMatch[1].length;
|
|
137
|
+
const marker = headerMatch[2] + ' ';
|
|
138
|
+
const content = headerMatch[3];
|
|
139
|
+
blocks.push({
|
|
140
|
+
type: 'heading',
|
|
141
|
+
startByte: lineStart,
|
|
142
|
+
endByte: lineEnd,
|
|
143
|
+
leadingIndent,
|
|
144
|
+
marker,
|
|
145
|
+
markerStartByte: lineStart + leadingIndent,
|
|
146
|
+
contentStartByte: lineStart + leadingIndent + marker.length,
|
|
147
|
+
content,
|
|
148
|
+
hangingIndent: 0,
|
|
149
|
+
forceHardBreak: true,
|
|
150
|
+
headingLevel: headerMatch[2].length,
|
|
151
|
+
});
|
|
152
|
+
byteOffset = lineEnd + 1;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Horizontal rule
|
|
157
|
+
if (trimmed.match(/^(-{3,}|\*{3,}|_{3,})$/)) {
|
|
158
|
+
blocks.push({
|
|
159
|
+
type: 'hr',
|
|
160
|
+
startByte: lineStart,
|
|
161
|
+
endByte: lineEnd,
|
|
162
|
+
leadingIndent: line.length - line.trimStart().length,
|
|
163
|
+
marker: '',
|
|
164
|
+
markerStartByte: lineStart,
|
|
165
|
+
contentStartByte: lineStart,
|
|
166
|
+
content: line,
|
|
167
|
+
hangingIndent: 0,
|
|
168
|
+
forceHardBreak: true,
|
|
169
|
+
});
|
|
170
|
+
byteOffset = lineEnd + 1;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Checkbox: - [ ] or - [x]
|
|
175
|
+
const checkboxMatch = line.match(/^(\s*)([-*+])\s+(\[[ x]\])\s+(.*)$/);
|
|
176
|
+
if (checkboxMatch) {
|
|
177
|
+
const leadingIndent = checkboxMatch[1].length;
|
|
178
|
+
const bullet = checkboxMatch[2];
|
|
179
|
+
const checkbox = checkboxMatch[3];
|
|
180
|
+
const marker = bullet + ' ' + checkbox + ' ';
|
|
181
|
+
const content = checkboxMatch[4];
|
|
182
|
+
const checked = checkbox === '[x]';
|
|
183
|
+
blocks.push({
|
|
184
|
+
type: 'checkbox',
|
|
185
|
+
startByte: lineStart,
|
|
186
|
+
endByte: lineEnd,
|
|
187
|
+
leadingIndent,
|
|
188
|
+
marker,
|
|
189
|
+
markerStartByte: lineStart + leadingIndent,
|
|
190
|
+
contentStartByte: lineStart + leadingIndent + marker.length,
|
|
191
|
+
content,
|
|
192
|
+
hangingIndent: leadingIndent + marker.length,
|
|
193
|
+
forceHardBreak: true,
|
|
194
|
+
checked,
|
|
195
|
+
});
|
|
196
|
+
byteOffset = lineEnd + 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Unordered list: - item or * item or + item
|
|
201
|
+
const bulletMatch = line.match(/^(\s*)([-*+])\s+(.*)$/);
|
|
202
|
+
if (bulletMatch) {
|
|
203
|
+
const leadingIndent = bulletMatch[1].length;
|
|
204
|
+
const bullet = bulletMatch[2];
|
|
205
|
+
const marker = bullet + ' ';
|
|
206
|
+
const content = bulletMatch[3];
|
|
207
|
+
blocks.push({
|
|
208
|
+
type: 'list-item',
|
|
209
|
+
startByte: lineStart,
|
|
210
|
+
endByte: lineEnd,
|
|
211
|
+
leadingIndent,
|
|
212
|
+
marker,
|
|
213
|
+
markerStartByte: lineStart + leadingIndent,
|
|
214
|
+
contentStartByte: lineStart + leadingIndent + marker.length,
|
|
215
|
+
content,
|
|
216
|
+
hangingIndent: leadingIndent + marker.length,
|
|
217
|
+
forceHardBreak: true,
|
|
218
|
+
});
|
|
219
|
+
byteOffset = lineEnd + 1;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Ordered list: 1. item
|
|
224
|
+
const orderedMatch = line.match(/^(\s*)(\d+\.)\s+(.*)$/);
|
|
225
|
+
if (orderedMatch) {
|
|
226
|
+
const leadingIndent = orderedMatch[1].length;
|
|
227
|
+
const number = orderedMatch[2];
|
|
228
|
+
const marker = number + ' ';
|
|
229
|
+
const content = orderedMatch[3];
|
|
230
|
+
blocks.push({
|
|
231
|
+
type: 'ordered-list',
|
|
232
|
+
startByte: lineStart,
|
|
233
|
+
endByte: lineEnd,
|
|
234
|
+
leadingIndent,
|
|
235
|
+
marker,
|
|
236
|
+
markerStartByte: lineStart + leadingIndent,
|
|
237
|
+
contentStartByte: lineStart + leadingIndent + marker.length,
|
|
238
|
+
content,
|
|
239
|
+
hangingIndent: leadingIndent + marker.length,
|
|
240
|
+
forceHardBreak: true,
|
|
241
|
+
});
|
|
242
|
+
byteOffset = lineEnd + 1;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Block quote: > text
|
|
247
|
+
const quoteMatch = line.match(/^(\s*)(>)\s*(.*)$/);
|
|
248
|
+
if (quoteMatch) {
|
|
249
|
+
const leadingIndent = quoteMatch[1].length;
|
|
250
|
+
const marker = '> ';
|
|
251
|
+
const content = quoteMatch[3];
|
|
252
|
+
blocks.push({
|
|
253
|
+
type: 'blockquote',
|
|
254
|
+
startByte: lineStart,
|
|
255
|
+
endByte: lineEnd,
|
|
256
|
+
leadingIndent,
|
|
257
|
+
marker,
|
|
258
|
+
markerStartByte: lineStart + leadingIndent,
|
|
259
|
+
contentStartByte: lineStart + leadingIndent + 2, // "> " is 2 chars
|
|
260
|
+
content,
|
|
261
|
+
hangingIndent: leadingIndent + 2,
|
|
262
|
+
forceHardBreak: true,
|
|
263
|
+
});
|
|
264
|
+
byteOffset = lineEnd + 1;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Image: 
|
|
269
|
+
if (trimmed.match(/^!\[.*\]\(.*\)$/)) {
|
|
270
|
+
blocks.push({
|
|
271
|
+
type: 'image',
|
|
272
|
+
startByte: lineStart,
|
|
273
|
+
endByte: lineEnd,
|
|
274
|
+
leadingIndent: line.length - line.trimStart().length,
|
|
275
|
+
marker: '',
|
|
276
|
+
markerStartByte: lineStart,
|
|
277
|
+
contentStartByte: lineStart,
|
|
278
|
+
content: line,
|
|
279
|
+
hangingIndent: 0,
|
|
280
|
+
forceHardBreak: true,
|
|
281
|
+
});
|
|
282
|
+
byteOffset = lineEnd + 1;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Hard break (trailing spaces or backslash)
|
|
287
|
+
const hasHardBreak = line.endsWith(' ') || line.endsWith('\\');
|
|
288
|
+
|
|
289
|
+
// Default: paragraph
|
|
290
|
+
const leadingIndent = line.length - line.trimStart().length;
|
|
291
|
+
blocks.push({
|
|
292
|
+
type: 'paragraph',
|
|
293
|
+
startByte: lineStart,
|
|
294
|
+
endByte: lineEnd,
|
|
295
|
+
leadingIndent,
|
|
296
|
+
marker: '',
|
|
297
|
+
markerStartByte: lineStart + leadingIndent,
|
|
298
|
+
contentStartByte: lineStart + leadingIndent,
|
|
299
|
+
content: trimmed,
|
|
300
|
+
hangingIndent: leadingIndent, // Paragraph continuation aligns with first line
|
|
301
|
+
forceHardBreak: hasHardBreak,
|
|
302
|
+
});
|
|
303
|
+
byteOffset = lineEnd + 1;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return blocks;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Check if a file is a markdown file
|
|
310
|
+
function isMarkdownFile(path: string): boolean {
|
|
311
|
+
return path.endsWith('.md') || path.endsWith('.markdown');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Process a buffer in compose mode - just enables compose mode
|
|
315
|
+
// The actual transform happens via view_transform_request hook
|
|
316
|
+
function processBuffer(bufferId: number, _splitId?: number): void {
|
|
317
|
+
if (!composeBuffers.has(bufferId)) return;
|
|
318
|
+
|
|
319
|
+
const info = editor.getBufferInfo(bufferId);
|
|
320
|
+
if (!info || !isMarkdownFile(info.path)) return;
|
|
321
|
+
|
|
322
|
+
editor.debug(`processBuffer: enabling compose mode for ${info.path}, buffer_id=${bufferId}`);
|
|
323
|
+
|
|
324
|
+
// Trigger a refresh to get the view_transform_request hook called
|
|
325
|
+
editor.refreshLines(bufferId);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Enable full compose mode for a buffer (explicit toggle)
|
|
329
|
+
function enableMarkdownCompose(bufferId: number): void {
|
|
330
|
+
const info = editor.getBufferInfo(bufferId);
|
|
331
|
+
if (!info || !isMarkdownFile(info.path)) return;
|
|
332
|
+
|
|
333
|
+
if (!composeBuffers.has(bufferId)) {
|
|
334
|
+
composeBuffers.add(bufferId);
|
|
335
|
+
|
|
336
|
+
// Hide line numbers in compose mode
|
|
337
|
+
editor.setLineNumbers(bufferId, false);
|
|
338
|
+
|
|
339
|
+
processBuffer(bufferId);
|
|
340
|
+
editor.debug(`Markdown compose enabled for buffer ${bufferId}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Disable compose mode for a buffer
|
|
345
|
+
function disableMarkdownCompose(bufferId: number): void {
|
|
346
|
+
if (composeBuffers.has(bufferId)) {
|
|
347
|
+
composeBuffers.delete(bufferId);
|
|
348
|
+
|
|
349
|
+
// Re-enable line numbers
|
|
350
|
+
editor.setLineNumbers(bufferId, true);
|
|
351
|
+
|
|
352
|
+
// Clear view transform to return to normal rendering
|
|
353
|
+
editor.clearViewTransform(bufferId);
|
|
354
|
+
|
|
355
|
+
editor.refreshLines(bufferId);
|
|
356
|
+
editor.debug(`Markdown compose disabled for buffer ${bufferId}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Toggle markdown compose mode for current buffer
|
|
361
|
+
globalThis.markdownToggleCompose = function(): void {
|
|
362
|
+
const bufferId = editor.getActiveBufferId();
|
|
363
|
+
const info = editor.getBufferInfo(bufferId);
|
|
364
|
+
|
|
365
|
+
if (!info) return;
|
|
366
|
+
|
|
367
|
+
// Only work with markdown files
|
|
368
|
+
if (!info.path.endsWith('.md') && !info.path.endsWith('.markdown')) {
|
|
369
|
+
editor.setStatus(editor.t("status.not_markdown_file"));
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (composeBuffers.has(bufferId)) {
|
|
374
|
+
disableMarkdownCompose(bufferId);
|
|
375
|
+
editor.setStatus(editor.t("status.compose_off"));
|
|
376
|
+
} else {
|
|
377
|
+
enableMarkdownCompose(bufferId);
|
|
378
|
+
// Trigger a re-render to apply the transform
|
|
379
|
+
editor.refreshLines(bufferId);
|
|
380
|
+
editor.setStatus(editor.t("status.compose_on"));
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Extract text content from incoming tokens
|
|
386
|
+
* Reconstructs the source text from ViewTokenWire tokens
|
|
387
|
+
*/
|
|
388
|
+
function extractTextFromTokens(tokens: ViewTokenWire[]): string {
|
|
389
|
+
let text = '';
|
|
390
|
+
for (const token of tokens) {
|
|
391
|
+
const kind = token.kind;
|
|
392
|
+
if (kind === "Newline") {
|
|
393
|
+
text += '\n';
|
|
394
|
+
} else if (kind === "Space") {
|
|
395
|
+
text += ' ';
|
|
396
|
+
} else if (kind === "Break") {
|
|
397
|
+
// Soft break, ignore for text extraction
|
|
398
|
+
} else if (typeof kind === 'object' && 'Text' in kind) {
|
|
399
|
+
text += kind.Text;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return text;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Transform tokens for markdown compose mode with hanging indents
|
|
407
|
+
*
|
|
408
|
+
* Strategy: Parse the source text to identify block structure, then walk through
|
|
409
|
+
* incoming tokens and emit transformed tokens with soft wraps and hanging indents.
|
|
410
|
+
*/
|
|
411
|
+
function transformMarkdownTokens(
|
|
412
|
+
inputTokens: ViewTokenWire[],
|
|
413
|
+
width: number,
|
|
414
|
+
viewportStart: number
|
|
415
|
+
): ViewTokenWire[] {
|
|
416
|
+
// First, extract text to understand block structure
|
|
417
|
+
const text = extractTextFromTokens(inputTokens);
|
|
418
|
+
const blocks = parseMarkdownBlocks(text);
|
|
419
|
+
|
|
420
|
+
// Build a map of source_offset -> block info for quick lookup
|
|
421
|
+
// Block byte positions are 0-based within extracted text
|
|
422
|
+
// Source offsets are actual buffer positions (viewportStart + position_in_text)
|
|
423
|
+
const offsetToBlock = new Map<number, ParsedBlock>();
|
|
424
|
+
for (const block of blocks) {
|
|
425
|
+
// Map byte positions that fall within this block to the block
|
|
426
|
+
// contentStartByte and endByte are positions within extracted text (0-based)
|
|
427
|
+
// source_offset = viewportStart + position_in_extracted_text
|
|
428
|
+
for (let textPos = block.startByte; textPos < block.endByte; textPos++) {
|
|
429
|
+
const sourceOffset = viewportStart + textPos;
|
|
430
|
+
offsetToBlock.set(sourceOffset, block);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const outputTokens: ViewTokenWire[] = [];
|
|
435
|
+
let column = 0; // Current column position
|
|
436
|
+
let currentBlock: ParsedBlock | null = null;
|
|
437
|
+
let lineStarted = false; // Have we output anything on current line?
|
|
438
|
+
|
|
439
|
+
for (let i = 0; i < inputTokens.length; i++) {
|
|
440
|
+
const token = inputTokens[i];
|
|
441
|
+
const kind = token.kind;
|
|
442
|
+
const sourceOffset = token.source_offset;
|
|
443
|
+
|
|
444
|
+
// Track which block we're in based on source offset
|
|
445
|
+
if (sourceOffset !== null) {
|
|
446
|
+
const block = offsetToBlock.get(sourceOffset);
|
|
447
|
+
if (block) {
|
|
448
|
+
currentBlock = block;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Get hanging indent for current block (default 0)
|
|
453
|
+
const hangingIndent = currentBlock?.hangingIndent ?? 0;
|
|
454
|
+
|
|
455
|
+
// Handle different token types
|
|
456
|
+
if (kind === "Newline") {
|
|
457
|
+
// Real newlines pass through - they end a block
|
|
458
|
+
outputTokens.push(token);
|
|
459
|
+
column = 0;
|
|
460
|
+
lineStarted = false;
|
|
461
|
+
currentBlock = null; // Reset at line boundary
|
|
462
|
+
} else if (kind === "Space") {
|
|
463
|
+
// Space handling - potentially wrap before space + next word
|
|
464
|
+
if (!lineStarted) {
|
|
465
|
+
// Leading space on a line - preserve it
|
|
466
|
+
outputTokens.push(token);
|
|
467
|
+
column++;
|
|
468
|
+
lineStarted = true;
|
|
469
|
+
} else {
|
|
470
|
+
// Mid-line space - look ahead to see if we need to wrap
|
|
471
|
+
// Find next non-space token to check word length
|
|
472
|
+
let nextWordLen = 0;
|
|
473
|
+
for (let j = i + 1; j < inputTokens.length; j++) {
|
|
474
|
+
const nextKind = inputTokens[j].kind;
|
|
475
|
+
if (nextKind === "Space" || nextKind === "Newline" || nextKind === "Break") {
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
if (typeof nextKind === 'object' && 'Text' in nextKind) {
|
|
479
|
+
nextWordLen += nextKind.Text.length;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Check if space + next word would exceed width
|
|
484
|
+
if (column + 1 + nextWordLen > width && nextWordLen > 0) {
|
|
485
|
+
// Wrap: emit soft newline + hanging indent instead of space
|
|
486
|
+
outputTokens.push({ source_offset: null, kind: "Newline" });
|
|
487
|
+
for (let j = 0; j < hangingIndent; j++) {
|
|
488
|
+
outputTokens.push({ source_offset: null, kind: "Space" });
|
|
489
|
+
}
|
|
490
|
+
column = hangingIndent;
|
|
491
|
+
// Don't emit the space - we wrapped instead
|
|
492
|
+
} else {
|
|
493
|
+
// No wrap needed - emit the space normally
|
|
494
|
+
outputTokens.push(token);
|
|
495
|
+
column++;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
} else if (kind === "Break") {
|
|
499
|
+
// Existing soft breaks - we're replacing wrapping logic, so skip these
|
|
500
|
+
// and handle wrapping ourselves
|
|
501
|
+
} else if (typeof kind === 'object' && 'Text' in kind) {
|
|
502
|
+
const text = kind.Text;
|
|
503
|
+
|
|
504
|
+
if (!lineStarted) {
|
|
505
|
+
lineStarted = true;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Check if this word alone would exceed width (need to wrap)
|
|
509
|
+
if (column > hangingIndent && column + text.length > width) {
|
|
510
|
+
// Wrap before this word
|
|
511
|
+
outputTokens.push({ source_offset: null, kind: "Newline" });
|
|
512
|
+
for (let j = 0; j < hangingIndent; j++) {
|
|
513
|
+
outputTokens.push({ source_offset: null, kind: "Space" });
|
|
514
|
+
}
|
|
515
|
+
column = hangingIndent;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Emit the text token
|
|
519
|
+
outputTokens.push(token);
|
|
520
|
+
column += text.length;
|
|
521
|
+
} else {
|
|
522
|
+
// Unknown token type - pass through
|
|
523
|
+
outputTokens.push(token);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return outputTokens;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Handle view transform request - receives tokens from core for transformation
|
|
531
|
+
// Only applies transforms when in compose mode
|
|
532
|
+
globalThis.onMarkdownViewTransform = function(data: {
|
|
533
|
+
buffer_id: number;
|
|
534
|
+
split_id: number;
|
|
535
|
+
viewport_start: number;
|
|
536
|
+
viewport_end: number;
|
|
537
|
+
tokens: ViewTokenWire[];
|
|
538
|
+
}): void {
|
|
539
|
+
// Only transform when in compose mode
|
|
540
|
+
if (!composeBuffers.has(data.buffer_id)) return;
|
|
541
|
+
|
|
542
|
+
const info = editor.getBufferInfo(data.buffer_id);
|
|
543
|
+
if (!info || !isMarkdownFile(info.path)) return;
|
|
544
|
+
|
|
545
|
+
editor.debug(`onMarkdownViewTransform: buffer=${data.buffer_id}, split=${data.split_id}, tokens=${data.tokens.length}`);
|
|
546
|
+
|
|
547
|
+
// Transform the incoming tokens with markdown-aware wrapping
|
|
548
|
+
const transformedTokens = transformMarkdownTokens(
|
|
549
|
+
data.tokens,
|
|
550
|
+
config.composeWidth,
|
|
551
|
+
data.viewport_start
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
// Submit the transformed tokens - keep compose_width for margins/centering
|
|
555
|
+
const layoutHints: LayoutHints = {
|
|
556
|
+
compose_width: config.composeWidth,
|
|
557
|
+
column_guides: null,
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
editor.submitViewTransform(
|
|
561
|
+
data.buffer_id,
|
|
562
|
+
data.split_id,
|
|
563
|
+
data.viewport_start,
|
|
564
|
+
data.viewport_end,
|
|
565
|
+
transformedTokens,
|
|
566
|
+
layoutHints
|
|
567
|
+
);
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// Handle buffer close events - clean up compose mode tracking
|
|
571
|
+
globalThis.onMarkdownBufferClosed = function(data: { buffer_id: number }): void {
|
|
572
|
+
composeBuffers.delete(data.buffer_id);
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// Register hooks
|
|
576
|
+
editor.on("view_transform_request", "onMarkdownViewTransform");
|
|
577
|
+
editor.on("buffer_closed", "onMarkdownBufferClosed");
|
|
578
|
+
editor.on("prompt_confirmed", "onMarkdownComposeWidthConfirmed");
|
|
579
|
+
|
|
580
|
+
// Set compose width command - starts interactive prompt
|
|
581
|
+
globalThis.markdownSetComposeWidth = function(): void {
|
|
582
|
+
editor.startPrompt(editor.t("prompt.compose_width"), "markdown-compose-width");
|
|
583
|
+
editor.setPromptSuggestions([
|
|
584
|
+
{ text: "60", description: editor.t("suggestion.narrow") },
|
|
585
|
+
{ text: "72", description: editor.t("suggestion.classic") },
|
|
586
|
+
{ text: "80", description: editor.t("suggestion.standard") },
|
|
587
|
+
{ text: "100", description: editor.t("suggestion.wide") },
|
|
588
|
+
]);
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
// Handle compose width prompt confirmation
|
|
592
|
+
globalThis.onMarkdownComposeWidthConfirmed = function(args: {
|
|
593
|
+
prompt_type: string;
|
|
594
|
+
text: string;
|
|
595
|
+
}): void {
|
|
596
|
+
if (args.prompt_type !== "markdown-compose-width") return;
|
|
597
|
+
|
|
598
|
+
const width = parseInt(args.text, 10);
|
|
599
|
+
if (!isNaN(width) && width > 20 && width < 300) {
|
|
600
|
+
config.composeWidth = width;
|
|
601
|
+
editor.setStatus(editor.t("status.width_set", { width: String(width) }));
|
|
602
|
+
|
|
603
|
+
// Re-process active buffer if in compose mode
|
|
604
|
+
const bufferId = editor.getActiveBufferId();
|
|
605
|
+
if (composeBuffers.has(bufferId)) {
|
|
606
|
+
editor.refreshLines(bufferId); // Trigger re-transform
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
editor.setStatus(editor.t("status.invalid_width"));
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// Register commands
|
|
614
|
+
editor.registerCommand(
|
|
615
|
+
"%cmd.toggle_compose",
|
|
616
|
+
"%cmd.toggle_compose_desc",
|
|
617
|
+
"markdownToggleCompose",
|
|
618
|
+
"normal"
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
editor.registerCommand(
|
|
622
|
+
"%cmd.set_compose_width",
|
|
623
|
+
"%cmd.set_compose_width_desc",
|
|
624
|
+
"markdownSetComposeWidth",
|
|
625
|
+
"normal"
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
// Initialization
|
|
629
|
+
editor.debug("Markdown Compose plugin loaded - use 'Markdown: Toggle Compose' command");
|
|
630
|
+
editor.setStatus(editor.t("status.plugin_ready"));
|