mrmd-editor 0.7.1 → 0.8.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/package.json +3 -1
- 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 +532 -18
- 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/mrp-types.js
CHANGED
|
@@ -299,6 +299,8 @@
|
|
|
299
299
|
* @property {string} [type] - Type string
|
|
300
300
|
* @property {string} [value] - Short value repr
|
|
301
301
|
* @property {string} [signature] - Function signature
|
|
302
|
+
* @property {string} [docstring] - Documentation string
|
|
303
|
+
* @property {string} [documentation] - Documentation (normalized alias)
|
|
302
304
|
*/
|
|
303
305
|
|
|
304
306
|
// #endregion INTROSPECTION
|
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
|
+
};
|
|
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();
|
|
82
384
|
}
|
|
385
|
+
return lines.length;
|
|
83
386
|
}
|
|
84
387
|
|
|
85
388
|
function jsonType(value) {
|
|
@@ -738,6 +1041,97 @@ class CssOutputWidget extends WidgetType {
|
|
|
738
1041
|
}
|
|
739
1042
|
}
|
|
740
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 false;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
741
1135
|
/**
|
|
742
1136
|
* Widget for rendering JSON output with an expandable tree.
|
|
743
1137
|
*/
|
|
@@ -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(() => {
|
|
@@ -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;
|
|
@@ -1544,7 +1969,7 @@ export const outputWidgetStyles = `
|
|
|
1544
1969
|
*/
|
|
1545
1970
|
.cm-output-content-line {
|
|
1546
1971
|
background: color-mix(in srgb, var(--widget-surface, rgba(0, 0, 0, 0.35)) 85%, transparent);
|
|
1547
|
-
margin-left: var(--widget-inset-left,
|
|
1972
|
+
margin-left: var(--widget-inset-left, 24px);
|
|
1548
1973
|
padding-left: var(--widget-padding-x, 16px);
|
|
1549
1974
|
padding-right: var(--widget-padding-x, 16px);
|
|
1550
1975
|
font-family: var(--widget-font-mono, 'Roboto Mono', 'SF Mono', Monaco, Consolas, monospace);
|
|
@@ -1599,7 +2024,7 @@ export const outputWidgetStyles = `
|
|
|
1599
2024
|
font-size: 0.65em;
|
|
1600
2025
|
color: var(--widget-text-muted, rgba(255, 255, 255, 0.3));
|
|
1601
2026
|
padding-left: var(--widget-padding-x, 16px);
|
|
1602
|
-
margin-left: var(--widget-inset-left,
|
|
2027
|
+
margin-left: var(--widget-inset-left, 24px);
|
|
1603
2028
|
opacity: 0.7;
|
|
1604
2029
|
}
|
|
1605
2030
|
|
|
@@ -1978,6 +2403,88 @@ export const outputWidgetStyles = `
|
|
|
1978
2403
|
font-family: var(--widget-font-mono, monospace);
|
|
1979
2404
|
}
|
|
1980
2405
|
|
|
2406
|
+
/* Scrollable plain output widget (for long outputs) */
|
|
2407
|
+
.cm-scroll-output-widget {
|
|
2408
|
+
position: relative;
|
|
2409
|
+
margin: 8px 0;
|
|
2410
|
+
background: var(--widget-surface, rgba(0, 0, 0, 0.35));
|
|
2411
|
+
border: 1px solid var(--widget-border, rgba(255, 255, 255, 0.1));
|
|
2412
|
+
border-left: 3px solid var(--widget-border-accent, rgba(100, 149, 237, 0.6));
|
|
2413
|
+
border-radius: var(--widget-border-radius, 6px);
|
|
2414
|
+
overflow: hidden;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
.cm-scroll-output-header {
|
|
2418
|
+
display: flex;
|
|
2419
|
+
align-items: center;
|
|
2420
|
+
gap: 8px;
|
|
2421
|
+
padding: 8px 10px;
|
|
2422
|
+
border-bottom: 1px solid var(--widget-border, rgba(255, 255, 255, 0.08));
|
|
2423
|
+
background: var(--widget-surface-elevated, rgba(255, 255, 255, 0.02));
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
.cm-scroll-output-badge {
|
|
2427
|
+
font-size: 10px;
|
|
2428
|
+
color: var(--widget-text-accent, #8cc0ff);
|
|
2429
|
+
background: color-mix(in srgb, var(--widget-text-accent, #8cc0ff) 16%, transparent);
|
|
2430
|
+
border: 1px solid color-mix(in srgb, var(--widget-text-accent, #8cc0ff) 35%, transparent);
|
|
2431
|
+
border-radius: 3px;
|
|
2432
|
+
padding: 2px 6px;
|
|
2433
|
+
letter-spacing: 0.4px;
|
|
2434
|
+
text-transform: uppercase;
|
|
2435
|
+
font-family: var(--widget-font-mono, monospace);
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
.cm-scroll-output-lines {
|
|
2439
|
+
color: var(--widget-text-muted, rgba(255, 255, 255, 0.65));
|
|
2440
|
+
font-size: 11px;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
.cm-scroll-output-actions {
|
|
2444
|
+
margin-left: auto;
|
|
2445
|
+
display: flex;
|
|
2446
|
+
gap: 6px;
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
.cm-scroll-output-action {
|
|
2450
|
+
background: var(--widget-surface-inset, rgba(255, 255, 255, 0.04));
|
|
2451
|
+
border: 1px solid var(--widget-border, rgba(255, 255, 255, 0.14));
|
|
2452
|
+
color: var(--widget-text-muted, rgba(255, 255, 255, 0.75));
|
|
2453
|
+
border-radius: 4px;
|
|
2454
|
+
padding: 2px 7px;
|
|
2455
|
+
font-size: 11px;
|
|
2456
|
+
cursor: pointer;
|
|
2457
|
+
font-family: var(--widget-font-mono, monospace);
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
.cm-scroll-output-action:hover {
|
|
2461
|
+
background: var(--widget-surface-hover, rgba(255, 255, 255, 0.08));
|
|
2462
|
+
color: var(--widget-text, #e0e0e0);
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
.cm-scroll-output-body {
|
|
2466
|
+
max-height: 320px;
|
|
2467
|
+
overflow: auto;
|
|
2468
|
+
padding: 8px 10px 10px 10px;
|
|
2469
|
+
user-select: text;
|
|
2470
|
+
cursor: text;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
.cm-scroll-output-body.cm-scroll-output-body-expanded {
|
|
2474
|
+
max-height: none;
|
|
2475
|
+
overflow: visible;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
.cm-scroll-output-content {
|
|
2479
|
+
margin: 0;
|
|
2480
|
+
white-space: var(--widget-white-space, pre-wrap);
|
|
2481
|
+
word-break: var(--widget-word-break, break-word);
|
|
2482
|
+
color: var(--widget-text, #e0e0e0);
|
|
2483
|
+
font-size: 12px;
|
|
2484
|
+
line-height: 1.45;
|
|
2485
|
+
user-select: text;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
1981
2488
|
/* JSON Output Widget - expandable tree view */
|
|
1982
2489
|
.cm-json-output-widget {
|
|
1983
2490
|
position: relative;
|
|
@@ -2015,6 +2522,13 @@ export const outputWidgetStyles = `
|
|
|
2015
2522
|
font-size: 11px;
|
|
2016
2523
|
}
|
|
2017
2524
|
|
|
2525
|
+
.cm-json-origin {
|
|
2526
|
+
color: var(--widget-text, #e0e0e0);
|
|
2527
|
+
font-size: 11px;
|
|
2528
|
+
font-family: var(--widget-font-mono, monospace);
|
|
2529
|
+
opacity: 0.9;
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2018
2532
|
.cm-json-actions {
|
|
2019
2533
|
margin-left: auto;
|
|
2020
2534
|
display: flex;
|