mrmd-editor 0.7.1 → 0.8.1
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/package.json +7 -3
- package/src/commands.js +112 -4
- package/src/comment-syntax.js +364 -39
- package/src/config/handlers.js +1 -2
- package/src/config/schema.js +46 -4
- package/src/document-template.js +2236 -0
- package/src/frontmatter-updater.js +204 -74
- package/src/grammar.js +758 -0
- package/src/index.js +1074 -55
- package/src/keymap.js +11 -2
- package/src/markdown/block-decorations.js +108 -5
- package/src/markdown/facets.js +37 -0
- package/src/markdown/html-inline.js +9 -5
- package/src/markdown/index.js +13 -3
- package/src/markdown/inline-commands.js +256 -0
- package/src/markdown/inline-model.js +578 -0
- package/src/markdown/inline-state.js +103 -0
- package/src/markdown/renderer.js +219 -12
- package/src/markdown/styles.js +290 -3
- package/src/markdown/widgets/alert-title.js +10 -8
- package/src/markdown/widgets/frontmatter.js +0 -6
- package/src/markdown/widgets/index.js +1 -0
- package/src/markdown/widgets/list-marker.js +29 -0
- package/src/markdown/wysiwyg.js +1158 -0
- package/src/mrp-types.js +2 -0
- package/src/output-widget.js +567 -27
- package/src/page-view-pagination.js +127 -0
- package/src/runtime-lsp.js +1757 -150
- package/src/section-controls/commands.js +617 -0
- package/src/section-controls/index.js +63 -0
- package/src/section-controls/plugin.js +165 -0
- package/src/section-controls/widgets.js +936 -0
- package/src/shell/ai-menu.js +11 -0
- package/src/shell/components/context-panel.js +572 -0
- package/src/shell/components/status-bar.js +10 -2
- package/src/shell/layouts/studio.js +206 -14
- package/src/shell/orchestrator-client.js +69 -0
- package/src/spellcheck.js +166 -0
- package/src/tables/README.md +97 -0
- package/src/tables/commands/insert-linked-table.js +122 -0
- package/src/tables/commands/open-table-workspace.js +43 -0
- package/src/tables/index.js +24 -0
- package/src/tables/jobs/client.js +158 -0
- package/src/tables/parsing/anchors.js +82 -0
- package/src/tables/parsing/linked-table-blocks.js +61 -0
- package/src/tables/state/linked-table-state.js +68 -0
- package/src/tables/widgets/linked-table-source-banner.js +77 -0
- package/src/tables/widgets/linked-table-widget.js +256 -0
- package/src/tables/workspace/controller.js +616 -0
- package/src/term-pty-client.js +51 -2
- package/src/term-widget.js +43 -3
- package/src/widgets/theme-utils.js +24 -16
- package/src/widgets/theme.js +1015 -1
- package/src/runtime-codelens/detector.js +0 -279
- package/src/runtime-codelens/index.js +0 -76
- package/src/runtime-codelens/plugin.js +0 -142
- package/src/runtime-codelens/styles.js +0 -184
- package/src/runtime-codelens/widgets.js +0 -216
package/src/output-widget.js
CHANGED
|
@@ -33,6 +33,28 @@ import { terminalToHtml, hasAnsi, stripAnsi, ansiStyles, parseAnsiDecorations }
|
|
|
33
33
|
// Regex to match ANSI escape sequences (same as in terminal.js)
|
|
34
34
|
const ANSI_ESCAPE_REGEX = /\x1b\[[0-9;]*[a-zA-Z]/g;
|
|
35
35
|
|
|
36
|
+
const LONG_OUTPUT_WIDGET_LINE_THRESHOLD = 15;
|
|
37
|
+
const JSON_OUTPUT_WIDGET_SETTING_KEY = 'mrmd-json-output-widget-enabled';
|
|
38
|
+
const LONG_OUTPUT_WIDGET_SETTING_KEY = 'mrmd-long-output-widget-enabled';
|
|
39
|
+
|
|
40
|
+
function readBooleanSetting(key, defaultValue = true) {
|
|
41
|
+
try {
|
|
42
|
+
if (typeof window === 'undefined' || !window.localStorage) return defaultValue;
|
|
43
|
+
const raw = window.localStorage.getItem(key);
|
|
44
|
+
if (raw == null) return defaultValue;
|
|
45
|
+
return !['0', 'false', 'off', 'no'].includes(String(raw).trim().toLowerCase());
|
|
46
|
+
} catch {
|
|
47
|
+
return defaultValue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getOutputWidgetSettings() {
|
|
52
|
+
return {
|
|
53
|
+
jsonEnabled: readBooleanSetting(JSON_OUTPUT_WIDGET_SETTING_KEY, true),
|
|
54
|
+
longOutputEnabled: readBooleanSetting(LONG_OUTPUT_WIDGET_SETTING_KEY, true),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
/**
|
|
37
59
|
* Zero-width widget used to completely hide ANSI escape sequences.
|
|
38
60
|
* Using Decoration.replace with this widget removes the escape codes
|
|
@@ -64,22 +86,303 @@ function escapeHtml(text) {
|
|
|
64
86
|
.replace(/'/g, ''');
|
|
65
87
|
}
|
|
66
88
|
|
|
89
|
+
function removeTrailingCommasOutsideStrings(input) {
|
|
90
|
+
let output = '';
|
|
91
|
+
let inDouble = false;
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < input.length; i++) {
|
|
94
|
+
const ch = input[i];
|
|
95
|
+
|
|
96
|
+
if (inDouble) {
|
|
97
|
+
output += ch;
|
|
98
|
+
if (ch === '\\' && i + 1 < input.length) {
|
|
99
|
+
output += input[i + 1];
|
|
100
|
+
i++;
|
|
101
|
+
} else if (ch === '"') {
|
|
102
|
+
inDouble = false;
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (ch === '"') {
|
|
108
|
+
inDouble = true;
|
|
109
|
+
output += ch;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (ch === ',') {
|
|
114
|
+
let lookahead = i + 1;
|
|
115
|
+
while (lookahead < input.length && /\s/.test(input[lookahead])) lookahead++;
|
|
116
|
+
if (lookahead < input.length && (input[lookahead] === '}' || input[lookahead] === ']')) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
output += ch;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return output;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function replacePythonLiteralsOutsideStrings(input) {
|
|
128
|
+
const replacements = {
|
|
129
|
+
True: 'true',
|
|
130
|
+
False: 'false',
|
|
131
|
+
None: 'null',
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
let output = '';
|
|
135
|
+
let token = '';
|
|
136
|
+
let inDouble = false;
|
|
137
|
+
|
|
138
|
+
const flushToken = () => {
|
|
139
|
+
if (!token) return;
|
|
140
|
+
output += replacements[token] ?? token;
|
|
141
|
+
token = '';
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < input.length; i++) {
|
|
145
|
+
const ch = input[i];
|
|
146
|
+
|
|
147
|
+
if (inDouble) {
|
|
148
|
+
flushToken();
|
|
149
|
+
output += ch;
|
|
150
|
+
if (ch === '\\' && i + 1 < input.length) {
|
|
151
|
+
output += input[i + 1];
|
|
152
|
+
i++;
|
|
153
|
+
} else if (ch === '"') {
|
|
154
|
+
inDouble = false;
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (ch === '"') {
|
|
160
|
+
flushToken();
|
|
161
|
+
inDouble = true;
|
|
162
|
+
output += ch;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (/[A-Za-z_]/.test(ch)) {
|
|
167
|
+
token += ch;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
flushToken();
|
|
172
|
+
output += ch;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
flushToken();
|
|
176
|
+
return output;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeJsonLikeOutput(input) {
|
|
180
|
+
let output = '';
|
|
181
|
+
let inSingle = false;
|
|
182
|
+
let inDouble = false;
|
|
183
|
+
|
|
184
|
+
for (let i = 0; i < input.length; i++) {
|
|
185
|
+
const ch = input[i];
|
|
186
|
+
|
|
187
|
+
if (inSingle) {
|
|
188
|
+
if (ch === '\\') {
|
|
189
|
+
const next = input[i + 1];
|
|
190
|
+
if (next === undefined) {
|
|
191
|
+
output += '\\\\';
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (next === "'") {
|
|
196
|
+
output += "'";
|
|
197
|
+
i++;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (next === '"') {
|
|
201
|
+
output += '\\"';
|
|
202
|
+
i++;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (next === '\\') {
|
|
206
|
+
output += '\\\\';
|
|
207
|
+
i++;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (next === 'x' && /^[0-9A-Fa-f]{2}$/.test(input.slice(i + 2, i + 4))) {
|
|
211
|
+
output += `\\u00${input.slice(i + 2, i + 4)}`;
|
|
212
|
+
i += 3;
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (next === 'u' && /^[0-9A-Fa-f]{4}$/.test(input.slice(i + 2, i + 6))) {
|
|
216
|
+
output += `\\u${input.slice(i + 2, i + 6)}`;
|
|
217
|
+
i += 5;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if ('bfnrt/'.includes(next)) {
|
|
221
|
+
output += `\\${next}`;
|
|
222
|
+
i++;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
output += `\\${next}`;
|
|
227
|
+
i++;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (ch === "'") {
|
|
232
|
+
inSingle = false;
|
|
233
|
+
output += '"';
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (ch === '"') {
|
|
238
|
+
output += '\\"';
|
|
239
|
+
} else if (ch === '\n') {
|
|
240
|
+
output += '\\n';
|
|
241
|
+
} else if (ch === '\r') {
|
|
242
|
+
output += '\\r';
|
|
243
|
+
} else {
|
|
244
|
+
output += ch;
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (inDouble) {
|
|
250
|
+
output += ch;
|
|
251
|
+
if (ch === '\\' && i + 1 < input.length) {
|
|
252
|
+
output += input[i + 1];
|
|
253
|
+
i++;
|
|
254
|
+
} else if (ch === '"') {
|
|
255
|
+
inDouble = false;
|
|
256
|
+
}
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (ch === "'") {
|
|
261
|
+
inSingle = true;
|
|
262
|
+
output += '"';
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (ch === '"') {
|
|
267
|
+
inDouble = true;
|
|
268
|
+
output += ch;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
output += ch;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (inSingle || inDouble) return null;
|
|
276
|
+
|
|
277
|
+
const withoutTrailingCommas = removeTrailingCommasOutsideStrings(output);
|
|
278
|
+
return replacePythonLiteralsOutsideStrings(withoutTrailingCommas);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function isJsonContainerString(value) {
|
|
282
|
+
return (
|
|
283
|
+
(value.startsWith('{') && value.endsWith('}')) ||
|
|
284
|
+
(value.startsWith('[') && value.endsWith(']'))
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function isJsonContainerValue(value) {
|
|
289
|
+
return value !== null && (Array.isArray(value) || typeof value === 'object');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function tryParseJsonLikeContainer(value) {
|
|
293
|
+
const trimmed = String(value ?? '').trim();
|
|
294
|
+
if (!trimmed || !isJsonContainerString(trimmed)) return null;
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const parsed = JSON.parse(trimmed);
|
|
298
|
+
return isJsonContainerValue(parsed) ? parsed : null;
|
|
299
|
+
} catch {
|
|
300
|
+
const normalized = normalizeJsonLikeOutput(trimmed);
|
|
301
|
+
if (!normalized) return null;
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const parsed = JSON.parse(normalized);
|
|
305
|
+
return isJsonContainerValue(parsed) ? parsed : null;
|
|
306
|
+
} catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function parseOutLabelLine(line) {
|
|
313
|
+
const match = /^\s*out\[(\d+)\]:\s*(.*)$/i.exec(line);
|
|
314
|
+
if (!match) return null;
|
|
315
|
+
return {
|
|
316
|
+
label: `Out[${match[1]}]`,
|
|
317
|
+
remainder: match[2] ?? '',
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function tryParseOutLabeledJsonOutput(input) {
|
|
322
|
+
const lines = String(input ?? '').split(/\r?\n/);
|
|
323
|
+
const labels = [];
|
|
324
|
+
const values = [];
|
|
325
|
+
|
|
326
|
+
let i = 0;
|
|
327
|
+
while (i < lines.length) {
|
|
328
|
+
while (i < lines.length && !lines[i].trim()) i++;
|
|
329
|
+
if (i >= lines.length) break;
|
|
330
|
+
|
|
331
|
+
const parsedLabel = parseOutLabelLine(lines[i]);
|
|
332
|
+
if (!parsedLabel) return null;
|
|
333
|
+
|
|
334
|
+
labels.push(parsedLabel.label);
|
|
335
|
+
i++;
|
|
336
|
+
|
|
337
|
+
const sectionLines = [];
|
|
338
|
+
if (parsedLabel.remainder.trim()) {
|
|
339
|
+
sectionLines.push(parsedLabel.remainder);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
while (i < lines.length) {
|
|
343
|
+
if (parseOutLabelLine(lines[i])) break;
|
|
344
|
+
sectionLines.push(lines[i]);
|
|
345
|
+
i++;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const sectionText = sectionLines.join('\n').trim();
|
|
349
|
+
const parsedValue = tryParseJsonLikeContainer(sectionText);
|
|
350
|
+
if (parsedValue === null) return null;
|
|
351
|
+
values.push(parsedValue);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (values.length === 0) return null;
|
|
355
|
+
return {
|
|
356
|
+
value: values.length === 1 ? values[0] : values,
|
|
357
|
+
labels,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
67
361
|
function tryParseJsonOutput(content) {
|
|
68
362
|
if (!content || hasAnsi(content)) return null;
|
|
69
363
|
if (content.length > 250_000) return null; // Guard large payloads
|
|
70
364
|
|
|
71
365
|
const trimmed = content.trim();
|
|
72
366
|
if (!trimmed) return null;
|
|
73
|
-
const looksLikeObjectOrArray =
|
|
74
|
-
(trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
75
|
-
(trimmed.startsWith('[') && trimmed.endsWith(']'));
|
|
76
|
-
if (!looksLikeObjectOrArray) return null;
|
|
77
367
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
368
|
+
const direct = tryParseJsonLikeContainer(trimmed);
|
|
369
|
+
if (direct !== null) {
|
|
370
|
+
return {
|
|
371
|
+
value: direct,
|
|
372
|
+
labels: [],
|
|
373
|
+
};
|
|
82
374
|
}
|
|
375
|
+
|
|
376
|
+
return tryParseOutLabeledJsonOutput(trimmed);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function countOutputLines(content) {
|
|
380
|
+
const normalized = String(content ?? '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
381
|
+
const lines = normalized.split('\n');
|
|
382
|
+
while (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
383
|
+
lines.pop();
|
|
384
|
+
}
|
|
385
|
+
return lines.length;
|
|
83
386
|
}
|
|
84
387
|
|
|
85
388
|
function jsonType(value) {
|
|
@@ -381,7 +684,7 @@ class OutputWidget extends WidgetType {
|
|
|
381
684
|
}
|
|
382
685
|
|
|
383
686
|
ignoreEvent() {
|
|
384
|
-
return
|
|
687
|
+
return true;
|
|
385
688
|
}
|
|
386
689
|
}
|
|
387
690
|
|
|
@@ -541,7 +844,7 @@ class HtmlOutputWidget extends WidgetType {
|
|
|
541
844
|
}
|
|
542
845
|
|
|
543
846
|
ignoreEvent() {
|
|
544
|
-
return
|
|
847
|
+
return true;
|
|
545
848
|
}
|
|
546
849
|
}
|
|
547
850
|
|
|
@@ -734,7 +1037,98 @@ class CssOutputWidget extends WidgetType {
|
|
|
734
1037
|
}
|
|
735
1038
|
|
|
736
1039
|
ignoreEvent() {
|
|
737
|
-
return
|
|
1040
|
+
return true;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
/**
|
|
1045
|
+
* Widget for long plain output blocks (line-threshold based).
|
|
1046
|
+
* Keeps output scrollable so very long results don't expand notebook height.
|
|
1047
|
+
*/
|
|
1048
|
+
class ScrollableOutputWidget extends WidgetType {
|
|
1049
|
+
/**
|
|
1050
|
+
* @param {string} content - Output content
|
|
1051
|
+
* @param {boolean} hidden - Whether widget should be hidden
|
|
1052
|
+
* @param {number} blockStart - Document position where this output block starts
|
|
1053
|
+
* @param {string|null} execId - Execution ID for this output block
|
|
1054
|
+
* @param {number} lineCount - Number of output lines
|
|
1055
|
+
*/
|
|
1056
|
+
constructor(content, hidden = false, blockStart = 0, execId = null, lineCount = 0) {
|
|
1057
|
+
super();
|
|
1058
|
+
this.content = content;
|
|
1059
|
+
this.hidden = hidden;
|
|
1060
|
+
this.blockStart = blockStart;
|
|
1061
|
+
this.execId = execId;
|
|
1062
|
+
this.lineCount = lineCount;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
eq(other) {
|
|
1066
|
+
return other.content === this.content &&
|
|
1067
|
+
other.hidden === this.hidden &&
|
|
1068
|
+
other.blockStart === this.blockStart &&
|
|
1069
|
+
other.execId === this.execId &&
|
|
1070
|
+
other.lineCount === this.lineCount;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
toDOM() {
|
|
1074
|
+
const container = document.createElement('div');
|
|
1075
|
+
container.className = 'cm-scroll-output-widget' + (this.hidden ? ' cm-output-widget-hidden' : '');
|
|
1076
|
+
container.dataset.outputBlockStart = String(this.blockStart);
|
|
1077
|
+
if (this.execId) {
|
|
1078
|
+
container.dataset.execId = this.execId;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
const header = document.createElement('div');
|
|
1082
|
+
header.className = 'cm-scroll-output-header';
|
|
1083
|
+
header.innerHTML = `
|
|
1084
|
+
<span class="cm-scroll-output-badge">Output</span>
|
|
1085
|
+
<span class="cm-scroll-output-lines">${escapeHtml(String(this.lineCount))} lines</span>
|
|
1086
|
+
<div class="cm-scroll-output-actions">
|
|
1087
|
+
<button type="button" class="cm-scroll-output-action" data-action="expand">Expand</button>
|
|
1088
|
+
<button type="button" class="cm-scroll-output-action" data-action="collapse">Collapse</button>
|
|
1089
|
+
<button type="button" class="cm-scroll-output-action" data-action="copy">Copy</button>
|
|
1090
|
+
</div>
|
|
1091
|
+
`;
|
|
1092
|
+
container.appendChild(header);
|
|
1093
|
+
|
|
1094
|
+
const contentWrap = document.createElement('div');
|
|
1095
|
+
contentWrap.className = 'cm-scroll-output-body';
|
|
1096
|
+
|
|
1097
|
+
const pre = document.createElement('pre');
|
|
1098
|
+
pre.className = 'cm-scroll-output-content';
|
|
1099
|
+
pre.innerHTML = terminalToHtml(this.content);
|
|
1100
|
+
|
|
1101
|
+
contentWrap.appendChild(pre);
|
|
1102
|
+
container.appendChild(contentWrap);
|
|
1103
|
+
|
|
1104
|
+
const actionButtons = header.querySelectorAll('.cm-scroll-output-action');
|
|
1105
|
+
actionButtons.forEach((btn) => {
|
|
1106
|
+
btn.addEventListener('click', (e) => {
|
|
1107
|
+
e.preventDefault();
|
|
1108
|
+
e.stopPropagation();
|
|
1109
|
+
|
|
1110
|
+
const action = btn.getAttribute('data-action');
|
|
1111
|
+
if (action === 'expand') {
|
|
1112
|
+
contentWrap.classList.add('cm-scroll-output-body-expanded');
|
|
1113
|
+
} else if (action === 'collapse') {
|
|
1114
|
+
contentWrap.classList.remove('cm-scroll-output-body-expanded');
|
|
1115
|
+
} else if (action === 'copy') {
|
|
1116
|
+
navigator.clipboard.writeText(stripAnsi(this.content)).then(() => {
|
|
1117
|
+
const previous = btn.textContent;
|
|
1118
|
+
btn.textContent = 'Copied';
|
|
1119
|
+
setTimeout(() => {
|
|
1120
|
+
btn.textContent = previous;
|
|
1121
|
+
}, 1200);
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
return container;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
ignoreEvent() {
|
|
1131
|
+
return true;
|
|
738
1132
|
}
|
|
739
1133
|
}
|
|
740
1134
|
|
|
@@ -771,17 +1165,23 @@ class JsonOutputWidget extends WidgetType {
|
|
|
771
1165
|
container.dataset.execId = this.execId;
|
|
772
1166
|
}
|
|
773
1167
|
|
|
774
|
-
const
|
|
775
|
-
if (
|
|
1168
|
+
const parsedResult = tryParseJsonOutput(this.content);
|
|
1169
|
+
if (parsedResult === null) {
|
|
776
1170
|
container.innerHTML = `<pre class="cm-json-fallback">${escapeHtml(this.content)}</pre>`;
|
|
777
1171
|
return container;
|
|
778
1172
|
}
|
|
779
1173
|
|
|
1174
|
+
const parsedValue = parsedResult.value;
|
|
1175
|
+
const originLabel = parsedResult.labels.length > 0
|
|
1176
|
+
? (parsedResult.labels.length === 1 ? parsedResult.labels[0] : parsedResult.labels.join(', '))
|
|
1177
|
+
: null;
|
|
1178
|
+
|
|
780
1179
|
const header = document.createElement('div');
|
|
781
1180
|
header.className = 'cm-json-header';
|
|
782
1181
|
header.innerHTML = `
|
|
783
1182
|
<span class="cm-json-badge">JSON</span>
|
|
784
|
-
|
|
1183
|
+
${originLabel ? `<span class="cm-json-origin">${escapeHtml(originLabel)}</span>` : ''}
|
|
1184
|
+
<span class="cm-json-summary">${escapeHtml(summarizeJson(parsedValue))}</span>
|
|
785
1185
|
<div class="cm-json-actions">
|
|
786
1186
|
<button type="button" class="cm-json-action" data-action="expand">Expand</button>
|
|
787
1187
|
<button type="button" class="cm-json-action" data-action="collapse">Collapse</button>
|
|
@@ -792,7 +1192,7 @@ class JsonOutputWidget extends WidgetType {
|
|
|
792
1192
|
|
|
793
1193
|
const tree = document.createElement('div');
|
|
794
1194
|
tree.className = 'cm-json-tree';
|
|
795
|
-
tree.appendChild(buildJsonTreeNode(null,
|
|
1195
|
+
tree.appendChild(buildJsonTreeNode(null, parsedValue));
|
|
796
1196
|
container.appendChild(tree);
|
|
797
1197
|
|
|
798
1198
|
const actionButtons = header.querySelectorAll('.cm-json-action');
|
|
@@ -814,7 +1214,7 @@ class JsonOutputWidget extends WidgetType {
|
|
|
814
1214
|
}
|
|
815
1215
|
});
|
|
816
1216
|
} else if (action === 'copy') {
|
|
817
|
-
navigator.clipboard.writeText(JSON.stringify(
|
|
1217
|
+
navigator.clipboard.writeText(JSON.stringify(parsedValue, null, 2)).then(() => {
|
|
818
1218
|
const previous = btn.textContent;
|
|
819
1219
|
btn.textContent = 'Copied';
|
|
820
1220
|
setTimeout(() => {
|
|
@@ -829,7 +1229,7 @@ class JsonOutputWidget extends WidgetType {
|
|
|
829
1229
|
}
|
|
830
1230
|
|
|
831
1231
|
ignoreEvent() {
|
|
832
|
-
return
|
|
1232
|
+
return true;
|
|
833
1233
|
}
|
|
834
1234
|
}
|
|
835
1235
|
|
|
@@ -978,6 +1378,7 @@ function buildDecorations(view, awarenessSystem) {
|
|
|
978
1378
|
const cursorPos = view.state.selection.main.head;
|
|
979
1379
|
const cursorLine = doc.lineAt(cursorPos).number;
|
|
980
1380
|
const text = doc.toString();
|
|
1381
|
+
const outputWidgetSettings = getOutputWidgetSettings();
|
|
981
1382
|
|
|
982
1383
|
// Find ```output or ```output:execId blocks (supports 3+ backticks for nesting)
|
|
983
1384
|
// Group 1: backticks, Group 2: optional execId, Group 3: content
|
|
@@ -1046,10 +1447,17 @@ function buildDecorations(view, awarenessSystem) {
|
|
|
1046
1447
|
// Check if output is empty (just whitespace)
|
|
1047
1448
|
const trimmedContent = content.trim();
|
|
1048
1449
|
const isEmpty = trimmedContent.length === 0;
|
|
1049
|
-
const parsedJson = !isEmpty && (outputType === null || outputType === 'json')
|
|
1450
|
+
const parsedJson = !isEmpty && outputWidgetSettings.jsonEnabled && (outputType === null || outputType === 'json')
|
|
1050
1451
|
? tryParseJsonOutput(trimmedContent)
|
|
1051
1452
|
: null;
|
|
1052
1453
|
const shouldRenderJson = parsedJson !== null;
|
|
1454
|
+
const outputLineCount = isEmpty ? 0 : countOutputLines(content);
|
|
1455
|
+
const shouldRenderScrollableOutput =
|
|
1456
|
+
outputWidgetSettings.longOutputEnabled &&
|
|
1457
|
+
!isEmpty &&
|
|
1458
|
+
outputType === null &&
|
|
1459
|
+
!shouldRenderJson &&
|
|
1460
|
+
outputLineCount > LONG_OUTPUT_WIDGET_LINE_THRESHOLD;
|
|
1053
1461
|
|
|
1054
1462
|
if (anyCollaboratorFocused) {
|
|
1055
1463
|
// EDITING MODE: Keep ANSI colors rendered, but make escape sequences
|
|
@@ -1116,7 +1524,7 @@ function buildDecorations(view, awarenessSystem) {
|
|
|
1116
1524
|
// Style the fence lines (opening and closing fences).
|
|
1117
1525
|
// Rich output widgets (HTML/CSS, including Mermaid rendered as HTML) are
|
|
1118
1526
|
// attached to the opening fence line, so that line must remain unclipped.
|
|
1119
|
-
const richOutput = outputType === 'html' || outputType === 'css' || shouldRenderJson;
|
|
1527
|
+
const richOutput = outputType === 'html' || outputType === 'css' || shouldRenderJson || shouldRenderScrollableOutput;
|
|
1120
1528
|
const startFenceClass = richOutput
|
|
1121
1529
|
? 'cm-output-fence-line cm-output-fence-start cm-output-fence-rich-start'
|
|
1122
1530
|
: 'cm-output-fence-line cm-output-fence-start';
|
|
@@ -1189,6 +1597,23 @@ function buildDecorations(view, awarenessSystem) {
|
|
|
1189
1597
|
side: 1,
|
|
1190
1598
|
}).range(startLine.to)
|
|
1191
1599
|
);
|
|
1600
|
+
} else if (shouldRenderScrollableOutput) {
|
|
1601
|
+
// LONG OUTPUT: Hide raw lines and show a scrollable output widget
|
|
1602
|
+
for (let i = startLine.number + 1; i < endLine.number; i++) {
|
|
1603
|
+
const line = doc.line(i);
|
|
1604
|
+
decorations.push(
|
|
1605
|
+
Decoration.line({
|
|
1606
|
+
class: 'cm-output-content-line cm-rich-output-hidden',
|
|
1607
|
+
}).range(line.from)
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
decorations.push(
|
|
1612
|
+
Decoration.widget({
|
|
1613
|
+
widget: new ScrollableOutputWidget(content, false, blockStart, execId, outputLineCount),
|
|
1614
|
+
side: 1,
|
|
1615
|
+
}).range(startLine.to)
|
|
1616
|
+
);
|
|
1192
1617
|
} else {
|
|
1193
1618
|
// REGULAR OUTPUT: Show with ANSI styling
|
|
1194
1619
|
// Style content lines with output block background
|
|
@@ -1433,7 +1858,7 @@ export const outputWidgetStyles = `
|
|
|
1433
1858
|
/* Widget is absolutely positioned - overlays on transparent text lines, doesn't add to flow */
|
|
1434
1859
|
.cm-output-widget {
|
|
1435
1860
|
position: absolute;
|
|
1436
|
-
left: var(--widget-inset-left,
|
|
1861
|
+
left: var(--widget-inset-left, 24px);
|
|
1437
1862
|
right: 0;
|
|
1438
1863
|
top: var(--widget-offset-top, 0); /* Can be negative to pull widget up closer to code block */
|
|
1439
1864
|
z-index: 1;
|
|
@@ -1471,22 +1896,37 @@ export const outputWidgetStyles = `
|
|
|
1471
1896
|
This approach works with CM6 viewport virtualization.
|
|
1472
1897
|
========================================================================== */
|
|
1473
1898
|
|
|
1474
|
-
/* Fence lines (opening and closing) - hidden in viewing mode
|
|
1899
|
+
/* Fence lines (opening and closing) - visually hidden in viewing mode.
|
|
1900
|
+
* Uses font-size:1px (not 0) so CodeMirror's posAtCoordsInline can
|
|
1901
|
+
* still find a child with a non-zero bounding rect. With font-size:0,
|
|
1902
|
+
* all text rects are zero-sized, point widgets are skipped, and CM
|
|
1903
|
+
* throws "Invalid child in posBefore". */
|
|
1475
1904
|
.cm-output-fence-line {
|
|
1476
|
-
font-size:
|
|
1905
|
+
font-size: 1px !important;
|
|
1477
1906
|
line-height: 0 !important;
|
|
1478
1907
|
height: 0 !important;
|
|
1479
1908
|
overflow: hidden !important;
|
|
1480
1909
|
padding: 0 !important;
|
|
1481
1910
|
margin: 0 !important;
|
|
1911
|
+
color: transparent !important;
|
|
1482
1912
|
}
|
|
1483
1913
|
|
|
1484
1914
|
/* Rich output widgets (HTML/CSS/Mermaid->HTML) are mounted on the opening
|
|
1485
|
-
* fence line. Keep that line unclipped so the inline widget can paint.
|
|
1915
|
+
* fence line. Keep that line unclipped so the inline widget can paint.
|
|
1916
|
+
*
|
|
1917
|
+
* The text span children on this line must have non-zero bounding rects
|
|
1918
|
+
* so CodeMirror's posAtCoordsInline can find a measurable child.
|
|
1919
|
+
* Without this, clicking/hovering on the widget area crashes with
|
|
1920
|
+
* "Invalid child in posBefore" because CM skips point widgets during
|
|
1921
|
+
* coordinate mapping and finds no other child with height > 0. */
|
|
1922
|
+
/* Rich output widgets are mounted on the opening fence line.
|
|
1923
|
+
* The fence text stays at font-size:1px (inherited from .cm-output-fence-line)
|
|
1924
|
+
* so CodeMirror can measure it for position mapping.
|
|
1925
|
+
* The widget paints via overflow:visible beyond the line's layout height. */
|
|
1486
1926
|
.cm-output-fence-rich-start {
|
|
1487
1927
|
height: auto !important;
|
|
1488
1928
|
overflow: visible !important;
|
|
1489
|
-
line-height:
|
|
1929
|
+
line-height: 0 !important;
|
|
1490
1930
|
}
|
|
1491
1931
|
|
|
1492
1932
|
/* Hide CodeMirror's special character rendering (escape symbols) in output blocks */
|
|
@@ -1544,7 +1984,7 @@ export const outputWidgetStyles = `
|
|
|
1544
1984
|
*/
|
|
1545
1985
|
.cm-output-content-line {
|
|
1546
1986
|
background: color-mix(in srgb, var(--widget-surface, rgba(0, 0, 0, 0.35)) 85%, transparent);
|
|
1547
|
-
margin-left: var(--widget-inset-left,
|
|
1987
|
+
margin-left: var(--widget-inset-left, 24px);
|
|
1548
1988
|
padding-left: var(--widget-padding-x, 16px);
|
|
1549
1989
|
padding-right: var(--widget-padding-x, 16px);
|
|
1550
1990
|
font-family: var(--widget-font-mono, 'Roboto Mono', 'SF Mono', Monaco, Consolas, monospace);
|
|
@@ -1599,7 +2039,7 @@ export const outputWidgetStyles = `
|
|
|
1599
2039
|
font-size: 0.65em;
|
|
1600
2040
|
color: var(--widget-text-muted, rgba(255, 255, 255, 0.3));
|
|
1601
2041
|
padding-left: var(--widget-padding-x, 16px);
|
|
1602
|
-
margin-left: var(--widget-inset-left,
|
|
2042
|
+
margin-left: var(--widget-inset-left, 24px);
|
|
1603
2043
|
opacity: 0.7;
|
|
1604
2044
|
}
|
|
1605
2045
|
|
|
@@ -1805,8 +2245,11 @@ export const outputWidgetStyles = `
|
|
|
1805
2245
|
========================================================================== */
|
|
1806
2246
|
|
|
1807
2247
|
/* Hide content lines for rich output (HTML/CSS) */
|
|
2248
|
+
/* Hidden content lines for rich output (HTML/CSS/JSON).
|
|
2249
|
+
* Use clip-path instead of height:0 so CodeMirror can still
|
|
2250
|
+
* resolve positions (prevents "Invalid child in posBefore"). */
|
|
1808
2251
|
.cm-rich-output-hidden {
|
|
1809
|
-
font-size:
|
|
2252
|
+
font-size: 1px !important;
|
|
1810
2253
|
line-height: 0 !important;
|
|
1811
2254
|
height: 0 !important;
|
|
1812
2255
|
overflow: hidden !important;
|
|
@@ -1825,6 +2268,8 @@ export const outputWidgetStyles = `
|
|
|
1825
2268
|
border-radius: var(--widget-border-radius, 6px);
|
|
1826
2269
|
overflow: hidden;
|
|
1827
2270
|
border: 1px solid var(--widget-border, rgba(255, 255, 255, 0.1));
|
|
2271
|
+
line-height: normal; /* Override parent's collapsed line-height */
|
|
2272
|
+
font-size: var(--mrmd-ui-font-size, 13px);
|
|
1828
2273
|
}
|
|
1829
2274
|
|
|
1830
2275
|
.cm-html-output-widget::before {
|
|
@@ -1862,6 +2307,8 @@ export const outputWidgetStyles = `
|
|
|
1862
2307
|
border-radius: var(--widget-border-radius, 6px);
|
|
1863
2308
|
border: 1px solid var(--widget-border, rgba(255, 255, 255, 0.08));
|
|
1864
2309
|
border-left: 2px solid var(--widget-accent-css, #64b5f6);
|
|
2310
|
+
line-height: normal; /* Override parent's collapsed line-height */
|
|
2311
|
+
font-size: var(--mrmd-ui-font-size, 13px);
|
|
1865
2312
|
}
|
|
1866
2313
|
|
|
1867
2314
|
.cm-css-header {
|
|
@@ -1978,6 +2425,90 @@ export const outputWidgetStyles = `
|
|
|
1978
2425
|
font-family: var(--widget-font-mono, monospace);
|
|
1979
2426
|
}
|
|
1980
2427
|
|
|
2428
|
+
/* Scrollable plain output widget (for long outputs) */
|
|
2429
|
+
.cm-scroll-output-widget {
|
|
2430
|
+
position: relative;
|
|
2431
|
+
margin: 8px 0;
|
|
2432
|
+
background: var(--widget-surface, rgba(0, 0, 0, 0.35));
|
|
2433
|
+
border: 1px solid var(--widget-border, rgba(255, 255, 255, 0.1));
|
|
2434
|
+
border-left: 3px solid var(--widget-border-accent, rgba(100, 149, 237, 0.6));
|
|
2435
|
+
border-radius: var(--widget-border-radius, 6px);
|
|
2436
|
+
overflow: hidden;
|
|
2437
|
+
line-height: normal; /* Override parent's collapsed line-height */
|
|
2438
|
+
font-size: var(--mrmd-ui-font-size, 13px);
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
.cm-scroll-output-header {
|
|
2442
|
+
display: flex;
|
|
2443
|
+
align-items: center;
|
|
2444
|
+
gap: 8px;
|
|
2445
|
+
padding: 8px 10px;
|
|
2446
|
+
border-bottom: 1px solid var(--widget-border, rgba(255, 255, 255, 0.08));
|
|
2447
|
+
background: var(--widget-surface-elevated, rgba(255, 255, 255, 0.02));
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
.cm-scroll-output-badge {
|
|
2451
|
+
font-size: 10px;
|
|
2452
|
+
color: var(--widget-text-accent, #8cc0ff);
|
|
2453
|
+
background: color-mix(in srgb, var(--widget-text-accent, #8cc0ff) 16%, transparent);
|
|
2454
|
+
border: 1px solid color-mix(in srgb, var(--widget-text-accent, #8cc0ff) 35%, transparent);
|
|
2455
|
+
border-radius: 3px;
|
|
2456
|
+
padding: 2px 6px;
|
|
2457
|
+
letter-spacing: 0.4px;
|
|
2458
|
+
text-transform: uppercase;
|
|
2459
|
+
font-family: var(--widget-font-mono, monospace);
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
.cm-scroll-output-lines {
|
|
2463
|
+
color: var(--widget-text-muted, rgba(255, 255, 255, 0.65));
|
|
2464
|
+
font-size: 11px;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
.cm-scroll-output-actions {
|
|
2468
|
+
margin-left: auto;
|
|
2469
|
+
display: flex;
|
|
2470
|
+
gap: 6px;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
.cm-scroll-output-action {
|
|
2474
|
+
background: var(--widget-surface-inset, rgba(255, 255, 255, 0.04));
|
|
2475
|
+
border: 1px solid var(--widget-border, rgba(255, 255, 255, 0.14));
|
|
2476
|
+
color: var(--widget-text-muted, rgba(255, 255, 255, 0.75));
|
|
2477
|
+
border-radius: 4px;
|
|
2478
|
+
padding: 2px 7px;
|
|
2479
|
+
font-size: 11px;
|
|
2480
|
+
cursor: pointer;
|
|
2481
|
+
font-family: var(--widget-font-mono, monospace);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
.cm-scroll-output-action:hover {
|
|
2485
|
+
background: var(--widget-surface-hover, rgba(255, 255, 255, 0.08));
|
|
2486
|
+
color: var(--widget-text, #e0e0e0);
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
.cm-scroll-output-body {
|
|
2490
|
+
max-height: 320px;
|
|
2491
|
+
overflow: auto;
|
|
2492
|
+
padding: 8px 10px 10px 10px;
|
|
2493
|
+
user-select: text;
|
|
2494
|
+
cursor: text;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
.cm-scroll-output-body.cm-scroll-output-body-expanded {
|
|
2498
|
+
max-height: none;
|
|
2499
|
+
overflow: visible;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
.cm-scroll-output-content {
|
|
2503
|
+
margin: 0;
|
|
2504
|
+
white-space: var(--widget-white-space, pre-wrap);
|
|
2505
|
+
word-break: var(--widget-word-break, break-word);
|
|
2506
|
+
color: var(--widget-text, #e0e0e0);
|
|
2507
|
+
font-size: 12px;
|
|
2508
|
+
line-height: 1.45;
|
|
2509
|
+
user-select: text;
|
|
2510
|
+
}
|
|
2511
|
+
|
|
1981
2512
|
/* JSON Output Widget - expandable tree view */
|
|
1982
2513
|
.cm-json-output-widget {
|
|
1983
2514
|
position: relative;
|
|
@@ -1987,6 +2518,8 @@ export const outputWidgetStyles = `
|
|
|
1987
2518
|
border-left: 3px solid var(--widget-accent-json, #8cc0ff);
|
|
1988
2519
|
border-radius: var(--widget-border-radius, 6px);
|
|
1989
2520
|
overflow: hidden;
|
|
2521
|
+
line-height: normal; /* Override parent's collapsed line-height */
|
|
2522
|
+
font-size: var(--mrmd-ui-font-size, 13px);
|
|
1990
2523
|
}
|
|
1991
2524
|
|
|
1992
2525
|
.cm-json-header {
|
|
@@ -2015,6 +2548,13 @@ export const outputWidgetStyles = `
|
|
|
2015
2548
|
font-size: 11px;
|
|
2016
2549
|
}
|
|
2017
2550
|
|
|
2551
|
+
.cm-json-origin {
|
|
2552
|
+
color: var(--widget-text, #e0e0e0);
|
|
2553
|
+
font-size: 11px;
|
|
2554
|
+
font-family: var(--widget-font-mono, monospace);
|
|
2555
|
+
opacity: 0.9;
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2018
2558
|
.cm-json-actions {
|
|
2019
2559
|
margin-left: auto;
|
|
2020
2560
|
display: flex;
|