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/comment-syntax.js
CHANGED
|
@@ -82,9 +82,225 @@ export const commentBubbleState = StateField.define({
|
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
// ===========================================================================
|
|
85
|
-
// Comment Extraction
|
|
85
|
+
// Comment Extraction / Thread Grouping
|
|
86
86
|
// ===========================================================================
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Parse a comment body into a simple multi-turn thread.
|
|
90
|
+
*
|
|
91
|
+
* Thread format:
|
|
92
|
+
*
|
|
93
|
+
* @comment
|
|
94
|
+
* First message
|
|
95
|
+
*
|
|
96
|
+
* @reply
|
|
97
|
+
* Follow-up message
|
|
98
|
+
*
|
|
99
|
+
* If no @role headers are present, the whole body is treated as a single
|
|
100
|
+
* `comment` message for backward compatibility.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} content
|
|
103
|
+
* @returns {Array<{role: string, text: string}>}
|
|
104
|
+
*/
|
|
105
|
+
export function parseCommentThread(content) {
|
|
106
|
+
const text = String(content || '').trim();
|
|
107
|
+
if (!text) return [];
|
|
108
|
+
|
|
109
|
+
const lines = text.split('\n');
|
|
110
|
+
const messages = [];
|
|
111
|
+
let current = null;
|
|
112
|
+
|
|
113
|
+
const pushCurrent = () => {
|
|
114
|
+
if (!current) return;
|
|
115
|
+
const body = current.lines.join('\n').trim();
|
|
116
|
+
if (!body) return;
|
|
117
|
+
messages.push({
|
|
118
|
+
role: sanitizeCommentRole(current.role),
|
|
119
|
+
text: body,
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
for (const line of lines) {
|
|
124
|
+
const header = line.trim().match(/^@([a-zA-Z][\w-]*)\s*$/);
|
|
125
|
+
if (header) {
|
|
126
|
+
pushCurrent();
|
|
127
|
+
current = { role: header[1].toLowerCase(), lines: [] };
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!current) {
|
|
132
|
+
current = { role: 'comment', lines: [] };
|
|
133
|
+
}
|
|
134
|
+
current.lines.push(line);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
pushCurrent();
|
|
138
|
+
|
|
139
|
+
if (messages.length === 0 && text) {
|
|
140
|
+
return [{ role: 'comment', text }];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return messages;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sanitizeCommentRole(role) {
|
|
147
|
+
const normalized = String(role || 'comment').toLowerCase().trim();
|
|
148
|
+
return normalized.match(/^[a-z][\w-]*$/) ? normalized : 'comment';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normalizeCommentContent(content) {
|
|
152
|
+
return String(content || '').replace(/[\r\n]+/g, ' ').replace(/[ \t]+/g, ' ').trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Serialize a comment thread back to comment body text.
|
|
157
|
+
*
|
|
158
|
+
* Keeps legacy single-message comments as plain text. Multi-turn threads use
|
|
159
|
+
* the @role stanza format described above.
|
|
160
|
+
*
|
|
161
|
+
* @param {Array<{role: string, text: string}>} messages
|
|
162
|
+
* @returns {string}
|
|
163
|
+
*/
|
|
164
|
+
export function serializeCommentThread(messages) {
|
|
165
|
+
const cleaned = (messages || [])
|
|
166
|
+
.map((message) => ({
|
|
167
|
+
role: sanitizeCommentRole(message?.role),
|
|
168
|
+
text: String(message?.text || '').trim(),
|
|
169
|
+
}))
|
|
170
|
+
.filter((message) => message.text.length > 0);
|
|
171
|
+
|
|
172
|
+
if (cleaned.length === 0) return '';
|
|
173
|
+
if (cleaned.length === 1 && cleaned[0].role === 'comment') {
|
|
174
|
+
return cleaned[0].text;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return cleaned
|
|
178
|
+
.map((message) => `@${message.role}\n${message.text}`)
|
|
179
|
+
.join('\n\n');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Build raw <!--! !--> syntax for a comment body.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} content
|
|
186
|
+
* @returns {string}
|
|
187
|
+
*/
|
|
188
|
+
export function buildCommentRaw(content) {
|
|
189
|
+
const text = normalizeCommentContent(content);
|
|
190
|
+
return `<!--! ${text} !-->`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Append a reply to an existing comment body.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} content
|
|
197
|
+
* @param {string} replyText
|
|
198
|
+
* @param {string} [role='reply']
|
|
199
|
+
* @returns {string}
|
|
200
|
+
*/
|
|
201
|
+
export function appendCommentReply(content, replyText, role = 'reply') {
|
|
202
|
+
const reply = String(replyText || '').trim();
|
|
203
|
+
if (!reply) return String(content || '').trim();
|
|
204
|
+
|
|
205
|
+
const thread = parseCommentThread(content);
|
|
206
|
+
thread.push({ role: sanitizeCommentRole(role), text: reply });
|
|
207
|
+
return serializeCommentThread(thread);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get a human-friendly one-line preview for a comment/thread.
|
|
212
|
+
*
|
|
213
|
+
* @param {string} content
|
|
214
|
+
* @returns {string}
|
|
215
|
+
*/
|
|
216
|
+
export function getCommentPreview(content) {
|
|
217
|
+
const messages = parseCommentThread(content);
|
|
218
|
+
const text = messages[0]?.text || String(content || '').trim();
|
|
219
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Group adjacent one-line comment markers into visual threads.
|
|
224
|
+
*
|
|
225
|
+
* A thread is a run of comment markers separated only by spaces/tabs on the
|
|
226
|
+
* same physical line. This keeps the document representation single-line and
|
|
227
|
+
* lets the UI treat immediately-adjacent markers as replies.
|
|
228
|
+
*
|
|
229
|
+
* @param {string} text
|
|
230
|
+
* @param {Array<{start:number,end:number,content:string,raw:string,thread?:Array<{role:string,text:string}>}>} [comments]
|
|
231
|
+
* @returns {Array<{start:number,end:number,raw:string,preview:string,count:number,comments:Array, messages:Array<{role:string,text:string}>}>}
|
|
232
|
+
*/
|
|
233
|
+
export function groupAdjacentComments(text, comments = null) {
|
|
234
|
+
const sourceText = String(text || '');
|
|
235
|
+
const items = Array.isArray(comments) ? comments : extractComments(sourceText);
|
|
236
|
+
if (items.length === 0) return [];
|
|
237
|
+
|
|
238
|
+
const threads = [];
|
|
239
|
+
let current = null;
|
|
240
|
+
|
|
241
|
+
const startThread = (comment) => {
|
|
242
|
+
current = {
|
|
243
|
+
start: comment.start,
|
|
244
|
+
end: comment.end,
|
|
245
|
+
raw: '',
|
|
246
|
+
preview: '',
|
|
247
|
+
count: 0,
|
|
248
|
+
comments: [comment],
|
|
249
|
+
messages: [],
|
|
250
|
+
};
|
|
251
|
+
appendCommentMessages(comment);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const appendCommentMessages = (comment) => {
|
|
255
|
+
const parsed = Array.isArray(comment.thread) && comment.thread.length > 0
|
|
256
|
+
? comment.thread
|
|
257
|
+
: [{ role: 'comment', text: comment.content }];
|
|
258
|
+
const preserveRoles = parsed.length > 1;
|
|
259
|
+
|
|
260
|
+
for (const message of parsed) {
|
|
261
|
+
const text = normalizeCommentContent(message?.text || '');
|
|
262
|
+
if (!text) continue;
|
|
263
|
+
current.messages.push({
|
|
264
|
+
role: preserveRoles
|
|
265
|
+
? sanitizeCommentRole(message?.role)
|
|
266
|
+
: (current.messages.length === 0 ? 'comment' : 'reply'),
|
|
267
|
+
text,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const finishThread = () => {
|
|
273
|
+
if (!current) return;
|
|
274
|
+
current.raw = sourceText.slice(current.start, current.end);
|
|
275
|
+
current.count = current.messages.length;
|
|
276
|
+
current.preview = current.messages[0]?.text || current.comments[0]?.preview || '';
|
|
277
|
+
threads.push(current);
|
|
278
|
+
current = null;
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
for (const comment of items) {
|
|
282
|
+
if (!current) {
|
|
283
|
+
startThread(comment);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const separator = sourceText.slice(current.end, comment.start);
|
|
288
|
+
const isAdjacent = /^[ \t]*$/.test(separator);
|
|
289
|
+
|
|
290
|
+
if (isAdjacent) {
|
|
291
|
+
current.end = comment.end;
|
|
292
|
+
current.comments.push(comment);
|
|
293
|
+
appendCommentMessages(comment);
|
|
294
|
+
} else {
|
|
295
|
+
finishThread();
|
|
296
|
+
startThread(comment);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
finishThread();
|
|
301
|
+
return threads;
|
|
302
|
+
}
|
|
303
|
+
|
|
88
304
|
/**
|
|
89
305
|
* Extract all comments from a document
|
|
90
306
|
*
|
|
@@ -97,11 +313,14 @@ export function extractComments(text) {
|
|
|
97
313
|
const regex = new RegExp(COMMENT_REGEX.source, 'g');
|
|
98
314
|
|
|
99
315
|
while ((match = regex.exec(text)) !== null) {
|
|
316
|
+
const content = match[1].trim();
|
|
100
317
|
comments.push({
|
|
101
318
|
start: match.index,
|
|
102
319
|
end: match.index + match[0].length,
|
|
103
|
-
content
|
|
320
|
+
content,
|
|
104
321
|
raw: match[0],
|
|
322
|
+
preview: getCommentPreview(content),
|
|
323
|
+
thread: parseCommentThread(content),
|
|
105
324
|
});
|
|
106
325
|
}
|
|
107
326
|
|
|
@@ -146,48 +365,80 @@ class CommentMarkerWidget extends WidgetType {
|
|
|
146
365
|
* @param {number} from - Start position
|
|
147
366
|
* @param {number} to - End position
|
|
148
367
|
* @param {string} raw - Raw comment text
|
|
368
|
+
* @param {{count?: number, preview?: string, threadStart?: number, threadEnd?: number, threadRaw?: string}} [options]
|
|
149
369
|
*/
|
|
150
|
-
constructor(content, from, to, raw) {
|
|
370
|
+
constructor(content, from, to, raw, options = {}) {
|
|
151
371
|
super();
|
|
152
372
|
this.content = content;
|
|
153
373
|
this.from = from;
|
|
154
374
|
this.to = to;
|
|
155
375
|
this.raw = raw;
|
|
376
|
+
this.count = Math.max(1, options.count || 1);
|
|
377
|
+
this.preview = options.preview || getCommentPreview(content);
|
|
378
|
+
this.threadStart = Number.isFinite(options.threadStart) ? options.threadStart : from;
|
|
379
|
+
this.threadEnd = Number.isFinite(options.threadEnd) ? options.threadEnd : to;
|
|
380
|
+
this.threadRaw = options.threadRaw || raw;
|
|
156
381
|
}
|
|
157
382
|
|
|
158
383
|
eq(other) {
|
|
159
384
|
return (
|
|
160
385
|
other instanceof CommentMarkerWidget &&
|
|
161
386
|
other.content === this.content &&
|
|
162
|
-
other.from === this.from
|
|
387
|
+
other.from === this.from &&
|
|
388
|
+
other.to === this.to &&
|
|
389
|
+
other.count === this.count &&
|
|
390
|
+
other.preview === this.preview &&
|
|
391
|
+
other.threadStart === this.threadStart &&
|
|
392
|
+
other.threadEnd === this.threadEnd
|
|
163
393
|
);
|
|
164
394
|
}
|
|
165
395
|
|
|
166
396
|
toDOM(view) {
|
|
167
397
|
const marker = document.createElement('span');
|
|
398
|
+
const previewText = this.preview;
|
|
399
|
+
const label = this.count > 1
|
|
400
|
+
? `Comment thread with ${this.count} messages: ${previewText.slice(0, 50)}`
|
|
401
|
+
: `Comment: ${previewText.slice(0, 50)}`;
|
|
402
|
+
|
|
168
403
|
marker.className = 'cm-comment-marker';
|
|
169
|
-
marker.setAttribute('aria-label',
|
|
404
|
+
marker.setAttribute('aria-label', label);
|
|
405
|
+
marker.setAttribute('title', label);
|
|
170
406
|
marker.setAttribute('role', 'button');
|
|
171
407
|
marker.setAttribute('tabindex', '0');
|
|
172
408
|
|
|
173
|
-
// Icon
|
|
174
409
|
const icon = document.createElement('span');
|
|
175
410
|
icon.className = 'cm-comment-marker-icon';
|
|
176
|
-
icon.textContent = '
|
|
411
|
+
icon.textContent = '💬';
|
|
177
412
|
marker.appendChild(icon);
|
|
178
413
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
marker.appendChild(preview);
|
|
414
|
+
if (this.count > 1) {
|
|
415
|
+
const count = document.createElement('span');
|
|
416
|
+
count.className = 'cm-comment-marker-count';
|
|
417
|
+
count.textContent = String(this.count);
|
|
418
|
+
marker.appendChild(count);
|
|
185
419
|
}
|
|
186
420
|
|
|
187
|
-
// Click handler
|
|
188
421
|
marker.addEventListener('click', (e) => {
|
|
189
422
|
e.preventDefault();
|
|
190
423
|
e.stopPropagation();
|
|
424
|
+
|
|
425
|
+
let handledBySidebar = false;
|
|
426
|
+
if (typeof window !== 'undefined') {
|
|
427
|
+
const event = new CustomEvent('mrmd-comment-thread-open', {
|
|
428
|
+
cancelable: true,
|
|
429
|
+
detail: {
|
|
430
|
+
from: this.threadStart,
|
|
431
|
+
to: this.threadEnd,
|
|
432
|
+
raw: this.threadRaw,
|
|
433
|
+
preview: this.preview,
|
|
434
|
+
count: this.count,
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
handledBySidebar = window.dispatchEvent(event) === false || event.defaultPrevented;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (handledBySidebar) return;
|
|
441
|
+
|
|
191
442
|
view.dispatch({
|
|
192
443
|
effects: showCommentBubble.of({
|
|
193
444
|
from: this.from,
|
|
@@ -198,7 +449,6 @@ class CommentMarkerWidget extends WidgetType {
|
|
|
198
449
|
});
|
|
199
450
|
});
|
|
200
451
|
|
|
201
|
-
// Keyboard handler
|
|
202
452
|
marker.addEventListener('keydown', (e) => {
|
|
203
453
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
204
454
|
e.preventDefault();
|
|
@@ -214,6 +464,32 @@ class CommentMarkerWidget extends WidgetType {
|
|
|
214
464
|
}
|
|
215
465
|
}
|
|
216
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Zero-width anchor used in sidebar mode so comment syntax disappears from the
|
|
469
|
+
* document flow while the discussion lives in the side panel.
|
|
470
|
+
*/
|
|
471
|
+
class CommentAnchorWidget extends WidgetType {
|
|
472
|
+
constructor(from) {
|
|
473
|
+
super();
|
|
474
|
+
this.from = from;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
eq(other) {
|
|
478
|
+
return other instanceof CommentAnchorWidget && other.from === this.from;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
toDOM() {
|
|
482
|
+
const anchor = document.createElement('span');
|
|
483
|
+
anchor.className = 'cm-comment-anchor';
|
|
484
|
+
anchor.setAttribute('aria-hidden', 'true');
|
|
485
|
+
return anchor;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
ignoreEvent() {
|
|
489
|
+
return true;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
217
493
|
// ===========================================================================
|
|
218
494
|
// Decoration Builder
|
|
219
495
|
// ===========================================================================
|
|
@@ -234,33 +510,48 @@ function buildDecorations(view) {
|
|
|
234
510
|
// Get the line the cursor is on
|
|
235
511
|
const cursorLine = doc.lineAt(cursorPos);
|
|
236
512
|
|
|
237
|
-
// Extract
|
|
513
|
+
// Extract and group comments into same-line threads
|
|
238
514
|
const comments = extractComments(text);
|
|
515
|
+
const threads = groupAdjacentComments(text, comments);
|
|
239
516
|
|
|
240
|
-
for (const
|
|
241
|
-
const
|
|
517
|
+
for (const thread of threads) {
|
|
518
|
+
const startLine = doc.lineAt(thread.start);
|
|
519
|
+
const endLine = doc.lineAt(Math.max(thread.start, thread.end - 1));
|
|
520
|
+
const isMultiline = thread.raw.includes('\n');
|
|
242
521
|
|
|
243
|
-
//
|
|
244
|
-
const isActiveLine = cursorLine.number
|
|
522
|
+
// Show raw syntax whenever the cursor is on a line touched by the thread.
|
|
523
|
+
const isActiveLine = cursorLine.number >= startLine.number && cursorLine.number <= endLine.number;
|
|
524
|
+
const primaryComment = thread.comments[0];
|
|
245
525
|
|
|
246
526
|
if (isActiveLine) {
|
|
247
|
-
// Show raw syntax with styling
|
|
248
527
|
decorations.push(
|
|
249
528
|
Decoration.mark({
|
|
250
529
|
class: 'cm-comment-syntax-active',
|
|
251
|
-
}).range(
|
|
530
|
+
}).range(thread.start, thread.end)
|
|
531
|
+
);
|
|
532
|
+
} else if (isMultiline) {
|
|
533
|
+
decorations.push(
|
|
534
|
+
Decoration.mark({
|
|
535
|
+
class: 'cm-comment-syntax-thread',
|
|
536
|
+
}).range(thread.start, thread.end)
|
|
252
537
|
);
|
|
253
538
|
} else {
|
|
254
|
-
// Replace with marker widget
|
|
255
539
|
decorations.push(
|
|
256
540
|
Decoration.replace({
|
|
257
541
|
widget: new CommentMarkerWidget(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
542
|
+
primaryComment.content,
|
|
543
|
+
primaryComment.start,
|
|
544
|
+
primaryComment.end,
|
|
545
|
+
primaryComment.raw,
|
|
546
|
+
{
|
|
547
|
+
count: thread.count,
|
|
548
|
+
preview: thread.preview,
|
|
549
|
+
threadStart: thread.start,
|
|
550
|
+
threadEnd: thread.end,
|
|
551
|
+
threadRaw: thread.raw,
|
|
552
|
+
}
|
|
262
553
|
),
|
|
263
|
-
}).range(
|
|
554
|
+
}).range(thread.start, thread.end)
|
|
264
555
|
);
|
|
265
556
|
}
|
|
266
557
|
}
|
|
@@ -414,7 +705,7 @@ function showBubble(view, comment) {
|
|
|
414
705
|
const newContent = textarea.value.trim();
|
|
415
706
|
if (newContent !== comment.content) {
|
|
416
707
|
// Update the comment in the document
|
|
417
|
-
const newRaw =
|
|
708
|
+
const newRaw = buildCommentRaw(newContent);
|
|
418
709
|
view.dispatch({
|
|
419
710
|
changes: { from: comment.from, to: comment.to, insert: newRaw },
|
|
420
711
|
effects: hideCommentBubble.of(null),
|
|
@@ -457,7 +748,7 @@ function showBubble(view, comment) {
|
|
|
457
748
|
// Save changes before closing
|
|
458
749
|
const newContent = textarea.value.trim();
|
|
459
750
|
if (newContent !== comment.content) {
|
|
460
|
-
const newRaw =
|
|
751
|
+
const newRaw = buildCommentRaw(newContent);
|
|
461
752
|
view.dispatch({
|
|
462
753
|
changes: { from: comment.from, to: comment.to, insert: newRaw },
|
|
463
754
|
effects: hideCommentBubble.of(null),
|
|
@@ -670,16 +961,17 @@ const commentStyles = EditorView.baseTheme({
|
|
|
670
961
|
alignItems: 'center',
|
|
671
962
|
gap: '4px',
|
|
672
963
|
padding: '1px 6px',
|
|
673
|
-
background: 'var(--bg-comment, rgba(255, 203, 107, 0.
|
|
674
|
-
border: '1px solid var(--border-comment, rgba(255, 203, 107, 0.
|
|
675
|
-
borderRadius: '
|
|
964
|
+
background: 'var(--bg-comment, rgba(255, 203, 107, 0.12))',
|
|
965
|
+
border: '1px solid var(--border-comment, rgba(255, 203, 107, 0.24))',
|
|
966
|
+
borderRadius: '999px',
|
|
676
967
|
cursor: 'pointer',
|
|
677
|
-
fontSize: '
|
|
968
|
+
fontSize: '11px',
|
|
969
|
+
lineHeight: '1.2',
|
|
678
970
|
verticalAlign: 'baseline',
|
|
679
971
|
transition: 'background 0.15s, border-color 0.15s',
|
|
680
972
|
'&:hover': {
|
|
681
|
-
background: 'var(--bg-comment-hover, rgba(255, 203, 107, 0.
|
|
682
|
-
borderColor: 'var(--border-comment-hover, rgba(255, 203, 107, 0.
|
|
973
|
+
background: 'var(--bg-comment-hover, rgba(255, 203, 107, 0.2))',
|
|
974
|
+
borderColor: 'var(--border-comment-hover, rgba(255, 203, 107, 0.4))',
|
|
683
975
|
},
|
|
684
976
|
},
|
|
685
977
|
|
|
@@ -687,6 +979,20 @@ const commentStyles = EditorView.baseTheme({
|
|
|
687
979
|
fontSize: '11px',
|
|
688
980
|
},
|
|
689
981
|
|
|
982
|
+
'.cm-comment-marker-count': {
|
|
983
|
+
minWidth: '14px',
|
|
984
|
+
height: '14px',
|
|
985
|
+
padding: '0 4px',
|
|
986
|
+
borderRadius: '999px',
|
|
987
|
+
background: 'color-mix(in srgb, var(--accent, #58a6ff) 14%, transparent)',
|
|
988
|
+
color: 'var(--text, inherit)',
|
|
989
|
+
display: 'inline-flex',
|
|
990
|
+
alignItems: 'center',
|
|
991
|
+
justifyContent: 'center',
|
|
992
|
+
fontSize: '10px',
|
|
993
|
+
fontWeight: '600',
|
|
994
|
+
},
|
|
995
|
+
|
|
690
996
|
'.cm-comment-marker-preview': {
|
|
691
997
|
color: 'var(--text-comment, #ffcb6b)',
|
|
692
998
|
maxWidth: '150px',
|
|
@@ -700,6 +1006,25 @@ const commentStyles = EditorView.baseTheme({
|
|
|
700
1006
|
borderRadius: '2px',
|
|
701
1007
|
},
|
|
702
1008
|
|
|
1009
|
+
'.cm-comment-syntax-thread': {
|
|
1010
|
+
background: 'var(--bg-comment-thread, rgba(255, 203, 107, 0.08))',
|
|
1011
|
+
borderRadius: '4px',
|
|
1012
|
+
boxDecorationBreak: 'clone',
|
|
1013
|
+
WebkitBoxDecorationBreak: 'clone',
|
|
1014
|
+
},
|
|
1015
|
+
|
|
1016
|
+
'.cm-comment-anchor': {
|
|
1017
|
+
display: 'inline-block',
|
|
1018
|
+
width: '0',
|
|
1019
|
+
height: '0',
|
|
1020
|
+
overflow: 'hidden',
|
|
1021
|
+
verticalAlign: 'baseline',
|
|
1022
|
+
},
|
|
1023
|
+
|
|
1024
|
+
'.cm-comment-sidebar-hidden': {
|
|
1025
|
+
display: 'none',
|
|
1026
|
+
},
|
|
1027
|
+
|
|
703
1028
|
'.cm-comment-bubble': {
|
|
704
1029
|
background: 'var(--bg-secondary, #1e1e1e)',
|
|
705
1030
|
border: '1px solid var(--border, #3c3c3c)',
|
|
@@ -819,13 +1144,13 @@ function insertComment(view) {
|
|
|
819
1144
|
let cursorPos;
|
|
820
1145
|
|
|
821
1146
|
if (hasSelection) {
|
|
822
|
-
// Wrap selection in comment
|
|
1147
|
+
// Wrap selection in a single-line comment token
|
|
823
1148
|
const selectedText = state.sliceDoc(from, to);
|
|
824
|
-
insert =
|
|
1149
|
+
insert = buildCommentRaw(selectedText);
|
|
825
1150
|
cursorPos = from + 6; // After "<!--! "
|
|
826
1151
|
} else {
|
|
827
1152
|
// Insert empty comment and place cursor inside
|
|
828
|
-
insert =
|
|
1153
|
+
insert = buildCommentRaw('');
|
|
829
1154
|
cursorPos = from + 6; // After "<!--! "
|
|
830
1155
|
}
|
|
831
1156
|
|
package/src/config/handlers.js
CHANGED
package/src/config/schema.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* @property {ExecutionConfig} [execution] - Execution settings
|
|
21
21
|
* @property {CellControlsConfig} [cellControls] - Cell controls (buttons, status, queue)
|
|
22
22
|
* @property {AIConfig} [ai] - AI service endpoints
|
|
23
|
+
* @property {SectionControlsConfig} [sectionControls] - Section controls (AI/formatting)
|
|
23
24
|
* @property {AwarenessConfig} [awareness] - Collaboration UI settings
|
|
24
25
|
* @property {boolean | DevPanelConfig} [devPanel] - Developer panel
|
|
25
26
|
*/
|
|
@@ -50,18 +51,22 @@
|
|
|
50
51
|
* Visual appearance configuration
|
|
51
52
|
* @typedef {Object} AppearanceConfig
|
|
52
53
|
* @property {boolean | null} [dark] - Dark mode: true=dark, false=light, null=system
|
|
53
|
-
* @property {string | null} [theme] - Theme name: 'midnight', 'daylight', '
|
|
54
|
-
* If null,
|
|
54
|
+
* @property {string | null} [theme] - Theme name: 'midnight', 'daylight', 'plain-light', or custom.
|
|
55
|
+
* If null, defaults to 'plain-light'.
|
|
55
56
|
* @property {boolean} [readonly] - View-only mode
|
|
56
57
|
* @property {string} [placeholder] - Placeholder text when empty
|
|
58
|
+
* @property {boolean} [spellcheck] - Enable browser-native spellcheck on prose
|
|
59
|
+
* (automatically disabled inside fenced code blocks).
|
|
60
|
+
* Works in browsers and Electron (Chromium's OS-level spellchecker).
|
|
57
61
|
*/
|
|
58
62
|
|
|
59
63
|
/** @type {AppearanceConfig} */
|
|
60
64
|
export const DEFAULT_APPEARANCE = {
|
|
61
65
|
dark: null,
|
|
62
|
-
theme: null, //
|
|
66
|
+
theme: null, // Defaults to plain-light
|
|
63
67
|
readonly: false,
|
|
64
|
-
placeholder: 'Start typing...'
|
|
68
|
+
placeholder: 'Start typing...',
|
|
69
|
+
spellcheck: true,
|
|
65
70
|
};
|
|
66
71
|
|
|
67
72
|
// =============================================================================
|
|
@@ -270,6 +275,25 @@ export const DEFAULT_CELL_CONTROLS = {
|
|
|
270
275
|
queue: { ...DEFAULT_CELL_QUEUE }
|
|
271
276
|
};
|
|
272
277
|
|
|
278
|
+
/**
|
|
279
|
+
* Section controls configuration - AI and formatting buttons next to sections
|
|
280
|
+
* @typedef {Object} SectionControlsConfig
|
|
281
|
+
* @property {boolean} [enabled] - Master switch for section controls
|
|
282
|
+
* @property {boolean} [showAi] - Show AI command buttons
|
|
283
|
+
* @property {boolean} [showFormatting] - Show markdown formatting buttons
|
|
284
|
+
* @property {'full' | 'dots-hover' | 'dots-click'} [mode] - Display mode:
|
|
285
|
+
* 'full' = always show toolbar, 'dots-hover' = dots that expand on hover,
|
|
286
|
+
* 'dots-click' = dots that open command palette on click
|
|
287
|
+
*/
|
|
288
|
+
|
|
289
|
+
/** @type {SectionControlsConfig} */
|
|
290
|
+
export const DEFAULT_SECTION_CONTROLS = {
|
|
291
|
+
enabled: true,
|
|
292
|
+
showAi: true,
|
|
293
|
+
showFormatting: true,
|
|
294
|
+
mode: 'dots-click'
|
|
295
|
+
};
|
|
296
|
+
|
|
273
297
|
// =============================================================================
|
|
274
298
|
// DEV PANEL
|
|
275
299
|
// =============================================================================
|
|
@@ -321,6 +345,7 @@ export function getDefaultConfig() {
|
|
|
321
345
|
endpoints: [],
|
|
322
346
|
default: null
|
|
323
347
|
},
|
|
348
|
+
sectionControls: { ...DEFAULT_SECTION_CONTROLS },
|
|
324
349
|
awareness: { ...DEFAULT_AWARENESS },
|
|
325
350
|
devPanel: { ...DEFAULT_DEV_PANEL }
|
|
326
351
|
};
|
|
@@ -358,6 +383,9 @@ export function normalizeOptions(options = {}) {
|
|
|
358
383
|
if (options.placeholder !== undefined) {
|
|
359
384
|
config.appearance.placeholder = options.placeholder;
|
|
360
385
|
}
|
|
386
|
+
if (options.spellcheck !== undefined) {
|
|
387
|
+
config.appearance.spellcheck = options.spellcheck;
|
|
388
|
+
}
|
|
361
389
|
|
|
362
390
|
// User
|
|
363
391
|
if (options.userName !== undefined) {
|
|
@@ -421,6 +449,20 @@ export function normalizeOptions(options = {}) {
|
|
|
421
449
|
config.execution.autoRefreshVariables = options.autoRefreshVariables;
|
|
422
450
|
}
|
|
423
451
|
|
|
452
|
+
// Section controls
|
|
453
|
+
if (options.sectionControls !== undefined) {
|
|
454
|
+
if (options.sectionControls === true) {
|
|
455
|
+
config.sectionControls.enabled = true;
|
|
456
|
+
} else if (options.sectionControls === false) {
|
|
457
|
+
config.sectionControls.enabled = false;
|
|
458
|
+
} else if (typeof options.sectionControls === 'object') {
|
|
459
|
+
config.sectionControls = {
|
|
460
|
+
...config.sectionControls,
|
|
461
|
+
...options.sectionControls
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
424
466
|
// Cell controls
|
|
425
467
|
if (options.cellControls !== undefined) {
|
|
426
468
|
if (options.cellControls === true) {
|