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
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section Controls Commands
|
|
3
|
+
*
|
|
4
|
+
* Formatting commands + AI shortcuts for the focused section.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { syntaxTree } from '@codemirror/language';
|
|
8
|
+
import { findCodeBlockAtPosition } from '../cells.js';
|
|
9
|
+
import { toggleInlineMark, toggleInlineMarkFromSyntax } from '../markdown/inline-commands.js';
|
|
10
|
+
import { executeAiOperation, getAiContext } from '../ai-integration.js';
|
|
11
|
+
import { ctrlKConfigFacet } from '../ctrl-k-modal.js';
|
|
12
|
+
import { applyFrontmatterTemplate } from '../frontmatter-updater.js';
|
|
13
|
+
import {
|
|
14
|
+
insertComment,
|
|
15
|
+
addressAllComments,
|
|
16
|
+
addressNearbyComment,
|
|
17
|
+
} from '../comment-syntax.js';
|
|
18
|
+
|
|
19
|
+
// ===========================================================================
|
|
20
|
+
// Formatting Commands
|
|
21
|
+
// ===========================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Toggle markdown formatting around selection.
|
|
25
|
+
*
|
|
26
|
+
* @param {import('@codemirror/view').EditorView} view
|
|
27
|
+
* @param {string} marker
|
|
28
|
+
* @param {string} [endMarker]
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
export function toggleMarkdownFormat(view, marker, endMarker = marker) {
|
|
32
|
+
if (toggleInlineMarkFromSyntax(view, marker, endMarker)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sel = view.state.selection.main;
|
|
37
|
+
const { from, to } = sel;
|
|
38
|
+
const selected = view.state.doc.sliceString(from, to);
|
|
39
|
+
|
|
40
|
+
if (sel.empty) {
|
|
41
|
+
view.dispatch({
|
|
42
|
+
changes: { from, insert: marker + endMarker },
|
|
43
|
+
selection: { anchor: from + marker.length },
|
|
44
|
+
userEvent: 'input.format.add',
|
|
45
|
+
});
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const hasWrapper = selected.startsWith(marker) && selected.endsWith(endMarker);
|
|
50
|
+
|
|
51
|
+
if (hasWrapper) {
|
|
52
|
+
const unwrapped = selected.slice(marker.length, selected.length - endMarker.length);
|
|
53
|
+
view.dispatch({
|
|
54
|
+
changes: { from, to, insert: unwrapped },
|
|
55
|
+
selection: { anchor: from, head: from + unwrapped.length },
|
|
56
|
+
userEvent: 'input.format.remove',
|
|
57
|
+
});
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
view.dispatch({
|
|
62
|
+
changes: { from, to, insert: marker + selected + endMarker },
|
|
63
|
+
selection: {
|
|
64
|
+
anchor: from + marker.length,
|
|
65
|
+
head: from + marker.length + selected.length,
|
|
66
|
+
},
|
|
67
|
+
userEvent: 'input.format.add',
|
|
68
|
+
});
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const toggleBold = (view) => toggleInlineMark(view, 'bold');
|
|
73
|
+
export const toggleItalic = (view) => toggleInlineMark(view, 'italic');
|
|
74
|
+
export const toggleUnderline = (view) => toggleInlineMark(view, 'underline');
|
|
75
|
+
export const toggleStrikethrough = (view) => toggleInlineMark(view, 'strike');
|
|
76
|
+
export const toggleInlineCode = (view) => toggleInlineMark(view, 'code');
|
|
77
|
+
|
|
78
|
+
function toTitleCase(text) {
|
|
79
|
+
return String(text || '').replace(/\p{L}[\p{L}\p{M}'’\-]*/gu, (word) => {
|
|
80
|
+
const [first = '', ...rest] = Array.from(word);
|
|
81
|
+
return first.toLocaleUpperCase() + rest.join('').toLocaleLowerCase();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function transformSelectionCase(view, mode) {
|
|
86
|
+
const sel = view?.state?.selection?.main;
|
|
87
|
+
if (!sel || sel.empty) return false;
|
|
88
|
+
|
|
89
|
+
const from = Math.min(sel.from, sel.to);
|
|
90
|
+
const to = Math.max(sel.from, sel.to);
|
|
91
|
+
const selected = view.state.doc.sliceString(from, to);
|
|
92
|
+
|
|
93
|
+
let next = selected;
|
|
94
|
+
if (mode === 'uppercase') next = selected.toLocaleUpperCase();
|
|
95
|
+
else if (mode === 'lowercase') next = selected.toLocaleLowerCase();
|
|
96
|
+
else if (mode === 'titlecase') next = toTitleCase(selected);
|
|
97
|
+
else return false;
|
|
98
|
+
|
|
99
|
+
const forward = sel.anchor <= sel.head;
|
|
100
|
+
view.dispatch({
|
|
101
|
+
changes: { from, to, insert: next },
|
|
102
|
+
selection: forward
|
|
103
|
+
? { anchor: from, head: from + next.length }
|
|
104
|
+
: { anchor: from + next.length, head: from },
|
|
105
|
+
userEvent: `input.case.${mode}`,
|
|
106
|
+
scrollIntoView: true,
|
|
107
|
+
});
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const transformSelectionUppercase = (view) => transformSelectionCase(view, 'uppercase');
|
|
112
|
+
export const transformSelectionLowercase = (view) => transformSelectionCase(view, 'lowercase');
|
|
113
|
+
export const transformSelectionTitlecase = (view) => transformSelectionCase(view, 'titlecase');
|
|
114
|
+
|
|
115
|
+
function getLineRangeForSelection(view) {
|
|
116
|
+
const sel = view.state.selection.main;
|
|
117
|
+
const fromLine = view.state.doc.lineAt(sel.from);
|
|
118
|
+
const toLine = view.state.doc.lineAt(sel.to);
|
|
119
|
+
return {
|
|
120
|
+
from: fromLine.from,
|
|
121
|
+
to: toLine.to,
|
|
122
|
+
text: view.state.doc.sliceString(fromLine.from, toLine.to),
|
|
123
|
+
selection: sel,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function prefixSelectedLines(view, prefix) {
|
|
128
|
+
const range = getLineRangeForSelection(view);
|
|
129
|
+
const lines = range.text.split('\n');
|
|
130
|
+
const prefixed = lines.map((line) => `${prefix}${line}`).join('\n');
|
|
131
|
+
|
|
132
|
+
const { selection } = range;
|
|
133
|
+
const anchor = selection.anchor + prefix.length;
|
|
134
|
+
const head = selection.head + prefix.length;
|
|
135
|
+
|
|
136
|
+
view.dispatch({
|
|
137
|
+
changes: { from: range.from, to: range.to, insert: prefixed },
|
|
138
|
+
selection: { anchor, head },
|
|
139
|
+
userEvent: 'input.format.add',
|
|
140
|
+
});
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function insertTemplate(view, template) {
|
|
145
|
+
const sel = view.state.selection.main;
|
|
146
|
+
const marker = '{{cursor}}';
|
|
147
|
+
const selectionMarker = '{{selection}}';
|
|
148
|
+
const selectedText = view.state.doc.sliceString(sel.from, sel.to);
|
|
149
|
+
|
|
150
|
+
let text = template.includes(selectionMarker)
|
|
151
|
+
? template.replace(selectionMarker, selectedText)
|
|
152
|
+
: template;
|
|
153
|
+
|
|
154
|
+
const markerPos = text.indexOf(marker);
|
|
155
|
+
if (markerPos >= 0) {
|
|
156
|
+
text = text.replace(marker, '');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const cursorPos = markerPos >= 0 ? sel.from + markerPos : sel.from + text.length;
|
|
160
|
+
|
|
161
|
+
view.dispatch({
|
|
162
|
+
changes: { from: sel.from, to: sel.to, insert: text },
|
|
163
|
+
selection: { anchor: cursorPos },
|
|
164
|
+
userEvent: 'input.format.add',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function insertBlockQuote(view) {
|
|
171
|
+
if (!view.state.selection.main.empty) {
|
|
172
|
+
return prefixSelectedLines(view, '> ');
|
|
173
|
+
}
|
|
174
|
+
return insertTemplate(view, '> {{cursor}}');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function insertTableTemplate(view) {
|
|
178
|
+
return insertTemplate(
|
|
179
|
+
view,
|
|
180
|
+
'| Column 1 | Column 2 | Column 3 |\n| --- | --- | --- |\n| {{cursor}} | | |\n| | | |'
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function insertCodeCellTemplate(view, language = 'python') {
|
|
185
|
+
const line = view.state.doc.lineAt(view.state.selection.main.from);
|
|
186
|
+
const prefixNewline = line.text.trim().length > 0 ? '\n' : '';
|
|
187
|
+
return insertTemplate(view, `${prefixNewline}\`\`\`${language}\n{{cursor}}\n\`\`\``);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function insertBulletList(view) {
|
|
191
|
+
if (!view.state.selection.main.empty) {
|
|
192
|
+
return prefixSelectedLines(view, '- ');
|
|
193
|
+
}
|
|
194
|
+
return insertTemplate(view, '- {{cursor}}');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function insertNumberedList(view) {
|
|
198
|
+
if (!view.state.selection.main.empty) {
|
|
199
|
+
return prefixSelectedLines(view, '1. ');
|
|
200
|
+
}
|
|
201
|
+
return insertTemplate(view, '1. {{cursor}}');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function insertTaskList(view) {
|
|
205
|
+
if (!view.state.selection.main.empty) {
|
|
206
|
+
return prefixSelectedLines(view, '- [ ] ');
|
|
207
|
+
}
|
|
208
|
+
return insertTemplate(view, '- [ ] {{cursor}}');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function insertHeading(view, level = 2) {
|
|
212
|
+
const prefix = '#'.repeat(Math.max(1, Math.min(level, 6))) + ' ';
|
|
213
|
+
return insertTemplate(view, `${prefix}{{cursor}}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function insertHorizontalRule(view) {
|
|
217
|
+
return insertTemplate(view, '---\n{{cursor}}');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function insertPageBreak(view) {
|
|
221
|
+
const sel = view.state.selection.main;
|
|
222
|
+
const doc = view.state.doc;
|
|
223
|
+
const before = doc.sliceString(Math.max(0, sel.from - 2), sel.from);
|
|
224
|
+
const after = doc.sliceString(sel.to, Math.min(doc.length, sel.to + 2));
|
|
225
|
+
|
|
226
|
+
const prefix = sel.from === 0
|
|
227
|
+
? ''
|
|
228
|
+
: before.endsWith('\n\n')
|
|
229
|
+
? ''
|
|
230
|
+
: before.endsWith('\n')
|
|
231
|
+
? '\n'
|
|
232
|
+
: '\n\n';
|
|
233
|
+
|
|
234
|
+
const suffix = sel.to === doc.length
|
|
235
|
+
? '\n\n'
|
|
236
|
+
: after.startsWith('\n\n')
|
|
237
|
+
? ''
|
|
238
|
+
: after.startsWith('\n')
|
|
239
|
+
? '\n'
|
|
240
|
+
: '\n\n';
|
|
241
|
+
|
|
242
|
+
const trailingGap = after.startsWith('\n\n')
|
|
243
|
+
? 2
|
|
244
|
+
: after.startsWith('\n')
|
|
245
|
+
? 1
|
|
246
|
+
: 0;
|
|
247
|
+
|
|
248
|
+
const insert = `${prefix}\\pagebreak${suffix}`;
|
|
249
|
+
|
|
250
|
+
view.dispatch({
|
|
251
|
+
changes: { from: sel.from, to: sel.to, insert },
|
|
252
|
+
selection: { anchor: sel.from + insert.length + trailingGap },
|
|
253
|
+
userEvent: 'input.format.add',
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function insertFrontmatterTemplate(view) {
|
|
260
|
+
const result = applyFrontmatterTemplate(view.state.doc.toString());
|
|
261
|
+
|
|
262
|
+
if (!result) {
|
|
263
|
+
console.warn('[frontmatter] Cannot apply template to invalid frontmatter. Fix YAML first.');
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const spec = {
|
|
268
|
+
changes: result.changes,
|
|
269
|
+
userEvent: 'input.format.add',
|
|
270
|
+
scrollIntoView: true,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
if (result.selection) {
|
|
274
|
+
spec.selection = {
|
|
275
|
+
anchor: result.selection.from,
|
|
276
|
+
head: result.selection.to,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
view.dispatch(spec);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function insertCommentCommand(view) {
|
|
285
|
+
return insertComment(view);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export const FORMATTING_COMMAND_DEFINITIONS = [
|
|
289
|
+
{ id: 'bold', label: 'Bold', shortcut: 'Mod-B', icon: 'format' },
|
|
290
|
+
{ id: 'italic', label: 'Italic', shortcut: 'Mod-I', icon: 'format' },
|
|
291
|
+
{ id: 'underline', label: 'Underline', shortcut: 'Mod-U', icon: 'format' },
|
|
292
|
+
{ id: 'strikethrough', label: 'Strikethrough', shortcut: '', icon: 'format' },
|
|
293
|
+
{ id: 'inline-code', label: 'Inline Code', shortcut: 'Mod-`', icon: 'code' },
|
|
294
|
+
{ id: 'uppercase', label: 'Make Uppercase', shortcut: '', icon: 'type' },
|
|
295
|
+
{ id: 'lowercase', label: 'Make Lowercase', shortcut: '', icon: 'type' },
|
|
296
|
+
{ id: 'titlecase', label: 'Make Title Case', shortcut: '', icon: 'type' },
|
|
297
|
+
{ id: 'comment', label: 'Insert Comment', shortcut: 'Mod-Shift-M', icon: 'comment' },
|
|
298
|
+
{ id: 'frontmatter-template', label: 'Insert Frontmatter Template', shortcut: '', icon: 'doc' },
|
|
299
|
+
{ id: 'blockquote', label: 'Block Quote', shortcut: '', icon: 'quote' },
|
|
300
|
+
{ id: 'table', label: 'Insert Table Template', shortcut: '', icon: 'table' },
|
|
301
|
+
{ id: 'code-cell', label: 'Insert Code Cell', shortcut: '', icon: 'code' },
|
|
302
|
+
{ id: 'bullet-list', label: 'Bullet List', shortcut: '', icon: 'list' },
|
|
303
|
+
{ id: 'numbered-list', label: 'Numbered List', shortcut: '', icon: 'list-number' },
|
|
304
|
+
{ id: 'task-list', label: 'Task List', shortcut: '', icon: 'checklist' },
|
|
305
|
+
{ id: 'heading-2', label: 'Heading (H2)', shortcut: '', icon: 'heading' },
|
|
306
|
+
{ id: 'horizontal-rule', label: 'Horizontal Rule', shortcut: '', icon: 'minus' },
|
|
307
|
+
{ id: 'page-break', label: 'Page Break', shortcut: 'Mod-Enter', icon: 'minus' },
|
|
308
|
+
];
|
|
309
|
+
|
|
310
|
+
export function executeFormattingDefinition(view, def) {
|
|
311
|
+
switch (def.id) {
|
|
312
|
+
case 'bold':
|
|
313
|
+
return toggleBold(view);
|
|
314
|
+
case 'italic':
|
|
315
|
+
return toggleItalic(view);
|
|
316
|
+
case 'underline':
|
|
317
|
+
return toggleUnderline(view);
|
|
318
|
+
case 'strikethrough':
|
|
319
|
+
return toggleStrikethrough(view);
|
|
320
|
+
case 'inline-code':
|
|
321
|
+
return toggleInlineCode(view);
|
|
322
|
+
case 'uppercase':
|
|
323
|
+
return transformSelectionUppercase(view);
|
|
324
|
+
case 'lowercase':
|
|
325
|
+
return transformSelectionLowercase(view);
|
|
326
|
+
case 'titlecase':
|
|
327
|
+
return transformSelectionTitlecase(view);
|
|
328
|
+
case 'comment':
|
|
329
|
+
return insertCommentCommand(view);
|
|
330
|
+
case 'frontmatter-template':
|
|
331
|
+
return insertFrontmatterTemplate(view);
|
|
332
|
+
case 'blockquote':
|
|
333
|
+
return insertBlockQuote(view);
|
|
334
|
+
case 'table':
|
|
335
|
+
return insertTableTemplate(view);
|
|
336
|
+
case 'code-cell':
|
|
337
|
+
return insertCodeCellTemplate(view, 'python');
|
|
338
|
+
case 'bullet-list':
|
|
339
|
+
return insertBulletList(view);
|
|
340
|
+
case 'numbered-list':
|
|
341
|
+
return insertNumberedList(view);
|
|
342
|
+
case 'task-list':
|
|
343
|
+
return insertTaskList(view);
|
|
344
|
+
case 'heading-2':
|
|
345
|
+
return insertHeading(view, 2);
|
|
346
|
+
case 'horizontal-rule':
|
|
347
|
+
return insertHorizontalRule(view);
|
|
348
|
+
case 'page-break':
|
|
349
|
+
return insertPageBreak(view);
|
|
350
|
+
default:
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ===========================================================================
|
|
356
|
+
// AI Helpers
|
|
357
|
+
// ===========================================================================
|
|
358
|
+
|
|
359
|
+
function getAiClient(view) {
|
|
360
|
+
const cfg = view.state.facet(ctrlKConfigFacet);
|
|
361
|
+
return cfg?.aiClient || null;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function getFocusedSectionRange(view) {
|
|
365
|
+
const sel = view.state.selection.main;
|
|
366
|
+
if (!sel.empty) {
|
|
367
|
+
return {
|
|
368
|
+
from: sel.from,
|
|
369
|
+
to: sel.to,
|
|
370
|
+
text: view.state.doc.sliceString(sel.from, sel.to),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const tree = syntaxTree(view.state);
|
|
375
|
+
let node = tree.resolveInner(sel.head, 1);
|
|
376
|
+
|
|
377
|
+
while (node?.parent && node.parent.name !== 'Document') {
|
|
378
|
+
node = node.parent;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (!node || node.name === 'Document') {
|
|
382
|
+
const line = view.state.doc.lineAt(sel.head);
|
|
383
|
+
return {
|
|
384
|
+
from: line.from,
|
|
385
|
+
to: line.to,
|
|
386
|
+
text: line.text,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
from: node.from,
|
|
392
|
+
to: node.to,
|
|
393
|
+
text: view.state.doc.sliceString(node.from, node.to),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function getCodeContextAtCursor(view) {
|
|
398
|
+
const cursor = view.state.selection.main.head;
|
|
399
|
+
const sel = view.state.selection.main;
|
|
400
|
+
const content = view.state.doc.toString();
|
|
401
|
+
const block = findCodeBlockAtPosition(content, cursor);
|
|
402
|
+
if (!block) return null;
|
|
403
|
+
|
|
404
|
+
const hasCodeSelection = !sel.empty && sel.from >= block.codeStart && sel.to <= block.codeEnd;
|
|
405
|
+
|
|
406
|
+
const selectedCode = hasCodeSelection
|
|
407
|
+
? view.state.doc.sliceString(sel.from, sel.to)
|
|
408
|
+
: block.code;
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
language: block.baseLanguage || block.language || 'text',
|
|
412
|
+
localContext: block.code,
|
|
413
|
+
codeBeforeCursor: view.state.doc.sliceString(block.codeStart, Math.min(cursor, block.codeEnd)),
|
|
414
|
+
replaceFrom: hasCodeSelection ? sel.from : block.codeStart,
|
|
415
|
+
replaceTo: hasCodeSelection ? sel.to : block.codeEnd,
|
|
416
|
+
selectedCode,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async function runAi(view, program, params, operation) {
|
|
421
|
+
const aiClient = getAiClient(view);
|
|
422
|
+
if (!aiClient) {
|
|
423
|
+
console.warn('[SectionControls] AI client not available. Ensure Ctrl-K AI extension is configured.');
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
await executeAiOperation(view, aiClient, {
|
|
428
|
+
program,
|
|
429
|
+
params,
|
|
430
|
+
type: operation.type,
|
|
431
|
+
from: operation.from,
|
|
432
|
+
to: operation.to,
|
|
433
|
+
resultField: operation.resultField,
|
|
434
|
+
juiceLevel: aiClient.juiceLevel,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ===========================================================================
|
|
439
|
+
// AI Command Definitions (for expanded menu)
|
|
440
|
+
// ===========================================================================
|
|
441
|
+
|
|
442
|
+
export const AI_COMMAND_DEFINITIONS = [
|
|
443
|
+
{ id: 'finish-sentence', label: 'Complete Sentence', shortcut: 'Mod-L', icon: 'line', program: 'FinishSentencePredict', type: 'insert', resultField: 'completion' },
|
|
444
|
+
{ id: 'finish-paragraph', label: 'Complete Paragraph', shortcut: 'Mod-O', icon: 'section', program: 'FinishParagraphPredict', type: 'insert', resultField: 'completion' },
|
|
445
|
+
{ id: 'continue-document', label: 'Continue Document', shortcut: '', icon: 'doc', action: 'continue-document' },
|
|
446
|
+
{ id: 'fix-grammar', label: 'Fix Grammar', shortcut: 'Mod-G', icon: 'grammar', program: 'FixGrammarPredict', type: 'replace', resultField: 'fixed_text' },
|
|
447
|
+
{ id: 'fix-transcription', label: 'Fix Transcription', shortcut: '', icon: 'wand', program: 'FixTranscriptionPredict', type: 'replace', resultField: 'fixed_text' },
|
|
448
|
+
{ id: 'correct-finish-line', label: 'Correct + Finish Line', shortcut: '', icon: 'line', program: 'CorrectAndFinishLinePredict', type: 'replace', resultField: 'corrected_completion' },
|
|
449
|
+
{ id: 'correct-finish-section', label: 'Correct + Finish Section', shortcut: '', icon: 'section', program: 'CorrectAndFinishSectionPredict', type: 'replace', resultField: 'corrected_completion' },
|
|
450
|
+
{ id: 'reformat-markdown', label: 'Reformat Markdown', shortcut: '', icon: 'format', program: 'ReformatMarkdownPredict', type: 'replace', resultField: 'reformatted_text' },
|
|
451
|
+
{ id: 'address-nearby-comment', label: 'Address Nearby Comment', shortcut: '', icon: 'comment', action: 'address-nearby-comment', requiresNearbyComment: true },
|
|
452
|
+
{ id: 'address-all-comments', label: 'Address All Comments', shortcut: '', icon: 'comment', action: 'address-all-comments', requiresComments: true },
|
|
453
|
+
|
|
454
|
+
// Code-focused
|
|
455
|
+
{ id: 'document-code', label: 'Add Documentation to Code', shortcut: '', icon: 'doc', program: 'DocumentCodePredict', type: 'replace', resultField: 'documented_code', codeOnly: true },
|
|
456
|
+
{ id: 'complete-code', label: 'Complete Code', shortcut: '', icon: 'code', program: 'CompleteCodePredict', type: 'replace', resultField: 'completion', codeOnly: true },
|
|
457
|
+
{ id: 'add-type-hints', label: 'Add Type Hints', shortcut: '', icon: 'type', program: 'AddTypeHintsPredict', type: 'replace', resultField: 'typed_code', codeOnly: true },
|
|
458
|
+
{ id: 'improve-names', label: 'Improve Names', shortcut: '', icon: 'rename', program: 'ImproveNamesPredict', type: 'replace', resultField: 'improved_code', codeOnly: true },
|
|
459
|
+
{ id: 'explain-code', label: 'Explain Code', shortcut: '', icon: 'comment', program: 'ExplainCodePredict', type: 'replace', resultField: 'explained_code', codeOnly: true },
|
|
460
|
+
{ id: 'refactor-code', label: 'Refactor Code', shortcut: '', icon: 'refactor', program: 'RefactorCodePredict', type: 'replace', resultField: 'refactored_code', codeOnly: true },
|
|
461
|
+
{ id: 'format-code', label: 'Format Code', shortcut: '', icon: 'format', program: 'FormatCodePredict', type: 'replace', resultField: 'formatted_code', codeOnly: true },
|
|
462
|
+
];
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Execute one menu AI definition.
|
|
466
|
+
* @param {import('@codemirror/view').EditorView} view
|
|
467
|
+
* @param {Object} editor
|
|
468
|
+
* @param {Object} def
|
|
469
|
+
*/
|
|
470
|
+
export async function executeAiDefinition(view, editor, def) {
|
|
471
|
+
if (def.action === 'continue-document') {
|
|
472
|
+
const aiClient = getAiClient(view);
|
|
473
|
+
if (!aiClient) {
|
|
474
|
+
console.warn('[SectionControls] AI client not available. Ensure Ctrl-K AI extension is configured.');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const content = view.state.doc.toString();
|
|
479
|
+
const docEnd = view.state.doc.length;
|
|
480
|
+
|
|
481
|
+
await executeAiOperation(view, aiClient, {
|
|
482
|
+
program: 'DocumentResponsePredict',
|
|
483
|
+
params: { document: content },
|
|
484
|
+
type: 'insert',
|
|
485
|
+
from: docEnd,
|
|
486
|
+
to: docEnd,
|
|
487
|
+
resultField: 'response',
|
|
488
|
+
juiceLevel: aiClient.juiceLevel,
|
|
489
|
+
});
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (def.action === 'address-nearby-comment') {
|
|
494
|
+
await addressNearbyComment(view);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (def.action === 'address-all-comments') {
|
|
499
|
+
await addressAllComments(view);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const ctx = getAiContext(view);
|
|
504
|
+
const section = getFocusedSectionRange(view);
|
|
505
|
+
const code = getCodeContextAtCursor(view);
|
|
506
|
+
const sel = view.state.selection.main;
|
|
507
|
+
|
|
508
|
+
if (def.codeOnly && !code) {
|
|
509
|
+
console.warn(`[SectionControls] ${def.label} requires cursor in a code block.`);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const isCodeFinish = def.program === 'FinishCodeLinePredict' || def.program === 'FinishCodeSectionPredict';
|
|
514
|
+
|
|
515
|
+
// Operation target
|
|
516
|
+
let from;
|
|
517
|
+
let to;
|
|
518
|
+
if (def.type === 'insert') {
|
|
519
|
+
from = ctx.cursorPos;
|
|
520
|
+
to = ctx.cursorPos;
|
|
521
|
+
} else if (def.codeOnly && code) {
|
|
522
|
+
from = code.replaceFrom;
|
|
523
|
+
to = code.replaceTo;
|
|
524
|
+
} else {
|
|
525
|
+
from = sel.empty ? section.from : sel.from;
|
|
526
|
+
to = sel.empty ? section.to : sel.to;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Build params by command family
|
|
530
|
+
let params = {};
|
|
531
|
+
|
|
532
|
+
if (def.program.startsWith('Finish')) {
|
|
533
|
+
if (isCodeFinish || (def.codeOnly && code)) {
|
|
534
|
+
params = {
|
|
535
|
+
code_before_cursor: code?.codeBeforeCursor || '',
|
|
536
|
+
language: code?.language || 'text',
|
|
537
|
+
local_context: code?.localContext || '',
|
|
538
|
+
document_context: ctx.documentContext,
|
|
539
|
+
};
|
|
540
|
+
} else {
|
|
541
|
+
params = {
|
|
542
|
+
text_before_cursor: ctx.textBeforeCursor,
|
|
543
|
+
local_context: ctx.localContext,
|
|
544
|
+
document_context: ctx.documentContext,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
} else if (def.program.startsWith('Fix')) {
|
|
548
|
+
params = {
|
|
549
|
+
text_to_fix: view.state.doc.sliceString(from, to),
|
|
550
|
+
local_context: ctx.localContext,
|
|
551
|
+
document_context: ctx.documentContext,
|
|
552
|
+
};
|
|
553
|
+
} else if (def.program.startsWith('CorrectAndFinish')) {
|
|
554
|
+
params = {
|
|
555
|
+
text_to_fix: view.state.doc.sliceString(from, to),
|
|
556
|
+
content_type: code ? 'code' : 'text',
|
|
557
|
+
local_context: code ? code.localContext : ctx.localContext,
|
|
558
|
+
document_context: ctx.documentContext,
|
|
559
|
+
};
|
|
560
|
+
} else if (def.program === 'ReformatMarkdownPredict') {
|
|
561
|
+
params = {
|
|
562
|
+
text: view.state.doc.sliceString(from, to),
|
|
563
|
+
local_context: ctx.localContext,
|
|
564
|
+
document_context: ctx.documentContext,
|
|
565
|
+
};
|
|
566
|
+
} else if (def.program.endsWith('CodePredict')) {
|
|
567
|
+
params = {
|
|
568
|
+
code: code?.selectedCode || view.state.doc.sliceString(from, to),
|
|
569
|
+
language: code?.language || 'text',
|
|
570
|
+
local_context: code?.localContext || ctx.localContext,
|
|
571
|
+
document_context: ctx.documentContext,
|
|
572
|
+
};
|
|
573
|
+
} else {
|
|
574
|
+
params = {
|
|
575
|
+
text_to_fix: view.state.doc.sliceString(from, to),
|
|
576
|
+
local_context: ctx.localContext,
|
|
577
|
+
document_context: ctx.documentContext,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
await runAi(view, def.program, params, {
|
|
582
|
+
type: def.type,
|
|
583
|
+
from,
|
|
584
|
+
to,
|
|
585
|
+
resultField: def.resultField,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ===========================================================================
|
|
590
|
+
// AI Quick Commands
|
|
591
|
+
// ===========================================================================
|
|
592
|
+
|
|
593
|
+
export const fixGrammar = (editor) => (view) => {
|
|
594
|
+
const def = AI_COMMAND_DEFINITIONS.find(d => d.id === 'fix-grammar');
|
|
595
|
+
if (def) void executeAiDefinition(view, editor, def);
|
|
596
|
+
return true;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
export const finishLine = (editor) => (view) => {
|
|
600
|
+
const code = getCodeContextAtCursor(view);
|
|
601
|
+
const def = code
|
|
602
|
+
? { id: 'finish-code-line', label: 'Complete Code Line', program: 'FinishCodeLinePredict', type: 'insert', resultField: 'completion', codeOnly: true }
|
|
603
|
+
: AI_COMMAND_DEFINITIONS.find(d => d.id === 'finish-sentence');
|
|
604
|
+
|
|
605
|
+
if (def) void executeAiDefinition(view, editor, def);
|
|
606
|
+
return true;
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
export const finishSection = (editor) => (view) => {
|
|
610
|
+
const code = getCodeContextAtCursor(view);
|
|
611
|
+
const def = code
|
|
612
|
+
? { id: 'finish-code-section', label: 'Complete Code Section', program: 'FinishCodeSectionPredict', type: 'insert', resultField: 'completion', codeOnly: true }
|
|
613
|
+
: AI_COMMAND_DEFINITIONS.find(d => d.id === 'finish-paragraph');
|
|
614
|
+
|
|
615
|
+
if (def) void executeAiDefinition(view, editor, def);
|
|
616
|
+
return true;
|
|
617
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section Controls Module
|
|
3
|
+
*
|
|
4
|
+
* Provides AI and formatting controls that appear next to the focused section.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { sectionControlsFacet, createSectionControlsPlugin } from './plugin.js';
|
|
8
|
+
import { keymap, EditorView } from '@codemirror/view';
|
|
9
|
+
import { Prec } from '@codemirror/state';
|
|
10
|
+
import * as commands from './commands.js';
|
|
11
|
+
import { openSectionControlsMenu } from './widgets.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create section controls extensions
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} editor - Editor API instance
|
|
17
|
+
* @param {Object} [options] - Configuration
|
|
18
|
+
* @returns {Array} CodeMirror extensions
|
|
19
|
+
*/
|
|
20
|
+
export function sectionControls(editor, options = {}) {
|
|
21
|
+
const config = {
|
|
22
|
+
enabled: options.enabled !== false,
|
|
23
|
+
showAi: options.showAi !== false,
|
|
24
|
+
showFormatting: options.showFormatting !== false,
|
|
25
|
+
...options,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (!config.enabled) return [];
|
|
29
|
+
|
|
30
|
+
const keybindings = [
|
|
31
|
+
{ key: 'Mod-b', run: (view) => commands.toggleBold(view) },
|
|
32
|
+
{ key: 'Mod-i', run: (view) => commands.toggleItalic(view) },
|
|
33
|
+
{ key: 'Mod-u', run: (view) => commands.toggleUnderline(view) },
|
|
34
|
+
{ key: 'Mod-`', run: (view) => commands.toggleInlineCode(view) },
|
|
35
|
+
{ key: 'Mod-g', run: (view) => commands.fixGrammar(editor)(view) },
|
|
36
|
+
{ key: 'Mod-l', run: (view) => commands.finishLine(editor)(view) },
|
|
37
|
+
{ key: 'Mod-o', run: (view) => commands.finishSection(editor)(view) },
|
|
38
|
+
{ key: "Mod-'", run: (view) => openSectionControlsMenu(view, editor) },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const shortcutFallback = EditorView.domEventHandlers({
|
|
42
|
+
keydown(event, view) {
|
|
43
|
+
// Robust fallback for international keyboard layouts:
|
|
44
|
+
// use physical Quote key (preferred) and keep Period as secondary.
|
|
45
|
+
const isPrimary = event.ctrlKey || event.metaKey;
|
|
46
|
+
if (isPrimary && !event.altKey && (event.code === 'Quote' || event.code === 'Period')) {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
event.stopPropagation();
|
|
49
|
+
return openSectionControlsMenu(view, editor);
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return [
|
|
56
|
+
sectionControlsFacet.of(config),
|
|
57
|
+
createSectionControlsPlugin(editor),
|
|
58
|
+
keybindings.length > 0 ? Prec.high(keymap.of(keybindings)) : [],
|
|
59
|
+
Prec.high(shortcutFallback),
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export * from './commands.js';
|