@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.
Files changed (90) hide show
  1. package/bin/CHANGELOG.md +1017 -0
  2. package/bin/LICENSE +117 -0
  3. package/bin/README.md +248 -0
  4. package/bin/fresh.exe +0 -0
  5. package/bin/plugins/README.md +71 -0
  6. package/bin/plugins/audit_mode.i18n.json +821 -0
  7. package/bin/plugins/audit_mode.ts +1810 -0
  8. package/bin/plugins/buffer_modified.i18n.json +67 -0
  9. package/bin/plugins/buffer_modified.ts +281 -0
  10. package/bin/plugins/calculator.i18n.json +93 -0
  11. package/bin/plugins/calculator.ts +770 -0
  12. package/bin/plugins/clangd-lsp.ts +168 -0
  13. package/bin/plugins/clangd_support.i18n.json +223 -0
  14. package/bin/plugins/clangd_support.md +20 -0
  15. package/bin/plugins/clangd_support.ts +325 -0
  16. package/bin/plugins/color_highlighter.i18n.json +145 -0
  17. package/bin/plugins/color_highlighter.ts +304 -0
  18. package/bin/plugins/config-schema.json +768 -0
  19. package/bin/plugins/csharp-lsp.ts +147 -0
  20. package/bin/plugins/csharp_support.i18n.json +80 -0
  21. package/bin/plugins/csharp_support.ts +170 -0
  22. package/bin/plugins/css-lsp.ts +143 -0
  23. package/bin/plugins/diagnostics_panel.i18n.json +236 -0
  24. package/bin/plugins/diagnostics_panel.ts +642 -0
  25. package/bin/plugins/examples/README.md +85 -0
  26. package/bin/plugins/examples/async_demo.ts +165 -0
  27. package/bin/plugins/examples/bookmarks.ts +329 -0
  28. package/bin/plugins/examples/buffer_query_demo.ts +110 -0
  29. package/bin/plugins/examples/git_grep.ts +262 -0
  30. package/bin/plugins/examples/hello_world.ts +93 -0
  31. package/bin/plugins/examples/virtual_buffer_demo.ts +116 -0
  32. package/bin/plugins/find_references.i18n.json +275 -0
  33. package/bin/plugins/find_references.ts +359 -0
  34. package/bin/plugins/git_blame.i18n.json +496 -0
  35. package/bin/plugins/git_blame.ts +707 -0
  36. package/bin/plugins/git_find_file.i18n.json +314 -0
  37. package/bin/plugins/git_find_file.ts +300 -0
  38. package/bin/plugins/git_grep.i18n.json +171 -0
  39. package/bin/plugins/git_grep.ts +191 -0
  40. package/bin/plugins/git_gutter.i18n.json +93 -0
  41. package/bin/plugins/git_gutter.ts +477 -0
  42. package/bin/plugins/git_log.i18n.json +481 -0
  43. package/bin/plugins/git_log.ts +1285 -0
  44. package/bin/plugins/go-lsp.ts +143 -0
  45. package/bin/plugins/html-lsp.ts +145 -0
  46. package/bin/plugins/json-lsp.ts +145 -0
  47. package/bin/plugins/lib/fresh.d.ts +1321 -0
  48. package/bin/plugins/lib/index.ts +24 -0
  49. package/bin/plugins/lib/navigation-controller.ts +214 -0
  50. package/bin/plugins/lib/panel-manager.ts +220 -0
  51. package/bin/plugins/lib/types.ts +72 -0
  52. package/bin/plugins/lib/virtual-buffer-factory.ts +130 -0
  53. package/bin/plugins/live_grep.i18n.json +171 -0
  54. package/bin/plugins/live_grep.ts +422 -0
  55. package/bin/plugins/markdown_compose.i18n.json +223 -0
  56. package/bin/plugins/markdown_compose.ts +630 -0
  57. package/bin/plugins/merge_conflict.i18n.json +821 -0
  58. package/bin/plugins/merge_conflict.ts +1810 -0
  59. package/bin/plugins/path_complete.i18n.json +80 -0
  60. package/bin/plugins/path_complete.ts +165 -0
  61. package/bin/plugins/python-lsp.ts +162 -0
  62. package/bin/plugins/rust-lsp.ts +166 -0
  63. package/bin/plugins/search_replace.i18n.json +405 -0
  64. package/bin/plugins/search_replace.ts +484 -0
  65. package/bin/plugins/test_i18n.i18n.json +67 -0
  66. package/bin/plugins/test_i18n.ts +18 -0
  67. package/bin/plugins/theme_editor.i18n.json +3746 -0
  68. package/bin/plugins/theme_editor.ts +2063 -0
  69. package/bin/plugins/todo_highlighter.i18n.json +184 -0
  70. package/bin/plugins/todo_highlighter.ts +206 -0
  71. package/bin/plugins/typescript-lsp.ts +167 -0
  72. package/bin/plugins/vi_mode.i18n.json +1549 -0
  73. package/bin/plugins/vi_mode.ts +2747 -0
  74. package/bin/plugins/welcome.i18n.json +236 -0
  75. package/bin/plugins/welcome.ts +76 -0
  76. package/bin/themes/dark.json +102 -0
  77. package/bin/themes/dracula.json +62 -0
  78. package/bin/themes/high-contrast.json +102 -0
  79. package/bin/themes/light.json +102 -0
  80. package/bin/themes/nord.json +62 -0
  81. package/bin/themes/nostalgia.json +102 -0
  82. package/bin/themes/solarized-dark.json +62 -0
  83. package/binary-install.js +1 -1
  84. package/dist/bin/fresh.js +9 -0
  85. package/dist/binary-install.js +149 -0
  86. package/dist/binary.js +30 -0
  87. package/dist/fresh-6yhknp07.exe +0 -0
  88. package/dist/install.js +158 -0
  89. package/dist/run-fresh.js +43 -0
  90. 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: ![alt](url)
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"));