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/keymap.js
CHANGED
|
@@ -18,7 +18,7 @@ import { commandRegistry } from './commands.js';
|
|
|
18
18
|
*/
|
|
19
19
|
export const defaultKeybindings = {
|
|
20
20
|
// Execution
|
|
21
|
-
'Mod-Enter': '
|
|
21
|
+
'Mod-Enter': 'runCellOrInsertPageBreak',
|
|
22
22
|
'Shift-Enter': 'runCellAndAdvance',
|
|
23
23
|
'Mod-Shift-Enter': 'runAllCells',
|
|
24
24
|
|
|
@@ -42,6 +42,12 @@ export const defaultKeybindings = {
|
|
|
42
42
|
// Code intelligence
|
|
43
43
|
'F12': 'viewSource',
|
|
44
44
|
|
|
45
|
+
// Grammar
|
|
46
|
+
'Alt-Enter': 'applyFirstGrammarSuggestion',
|
|
47
|
+
|
|
48
|
+
// View modes
|
|
49
|
+
'Mod-Alt-Shift-w': 'toggleWysiwygMode',
|
|
50
|
+
|
|
45
51
|
// Enter accepts completion if active, otherwise inserts newline
|
|
46
52
|
'Enter': 'acceptCompletionOrNewline',
|
|
47
53
|
};
|
|
@@ -84,7 +90,7 @@ function acceptCompletionOrNewline(editor) {
|
|
|
84
90
|
*
|
|
85
91
|
* // Custom bindings
|
|
86
92
|
* createKeymap(editor, {
|
|
87
|
-
* 'Mod-Enter': '
|
|
93
|
+
* 'Mod-Enter': 'runCellOrInsertPageBreak',
|
|
88
94
|
* 'Shift-Enter': 'runCellAndAdvance',
|
|
89
95
|
* 'F5': 'runAllCells',
|
|
90
96
|
* })
|
|
@@ -163,6 +169,7 @@ export function mergeKeybindings(userBindings, defaults = defaultKeybindings) {
|
|
|
163
169
|
export function listCommands() {
|
|
164
170
|
return [
|
|
165
171
|
{ name: 'runCell', description: 'Run current cell and stay in place' },
|
|
172
|
+
{ name: 'runCellOrInsertPageBreak', description: 'Run current cell, or insert a page break when editing prose' },
|
|
166
173
|
{ name: 'runCellAndAdvance', description: 'Run current cell and move to next (create if needed)' },
|
|
167
174
|
{ name: 'runAllCells', description: 'Run all cells in document' },
|
|
168
175
|
{ name: 'runAllAbove', description: 'Run all cells above and including current' },
|
|
@@ -177,5 +184,7 @@ export function listCommands() {
|
|
|
177
184
|
{ name: 'indent', description: 'Indent current line or selection' },
|
|
178
185
|
{ name: 'dedent', description: 'Dedent current line or selection' },
|
|
179
186
|
{ name: 'viewSource', description: 'View source code for symbol under cursor' },
|
|
187
|
+
{ name: 'applyFirstGrammarSuggestion', description: 'Apply the first grammar suggestion near the cursor' },
|
|
188
|
+
{ name: 'insertFrontmatterTemplate', description: 'Insert or augment document frontmatter with a scholarly template' },
|
|
180
189
|
];
|
|
181
190
|
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { StateField } from '@codemirror/state';
|
|
19
19
|
import { EditorView, Decoration, WidgetType, ViewPlugin } from '@codemirror/view';
|
|
20
20
|
import { syntaxTree } from '@codemirror/language';
|
|
21
|
+
import { sourceModeFacet, wysiwygModeFacet } from './facets.js';
|
|
21
22
|
|
|
22
23
|
// =============================================================================
|
|
23
24
|
// Line Height Tracking for Accurate Spacing
|
|
@@ -67,6 +68,20 @@ import {
|
|
|
67
68
|
import {
|
|
68
69
|
FrontmatterWidget,
|
|
69
70
|
} from './widgets/frontmatter.js';
|
|
71
|
+
import {
|
|
72
|
+
LinkedTableWidget,
|
|
73
|
+
} from '../tables/widgets/linked-table-widget.js';
|
|
74
|
+
import {
|
|
75
|
+
LinkedTableSourceBannerWidget,
|
|
76
|
+
} from '../tables/widgets/linked-table-source-banner.js';
|
|
77
|
+
import {
|
|
78
|
+
findLinkedTableBlocksInState,
|
|
79
|
+
getLinkedTableBlockRange,
|
|
80
|
+
isRangeInsideLinkedTable,
|
|
81
|
+
} from '../tables/parsing/linked-table-blocks.js';
|
|
82
|
+
import {
|
|
83
|
+
linkedTableMarkdownState,
|
|
84
|
+
} from '../tables/state/linked-table-state.js';
|
|
70
85
|
|
|
71
86
|
// =============================================================================
|
|
72
87
|
// Height Cache for Stable Layout
|
|
@@ -195,6 +210,35 @@ class TableWidgetWithHeightCache extends TableWidget {
|
|
|
195
210
|
}
|
|
196
211
|
}
|
|
197
212
|
|
|
213
|
+
/**
|
|
214
|
+
* LinkedTableWidget wrapper that caches its rendered height for stable layout.
|
|
215
|
+
*/
|
|
216
|
+
class LinkedTableWidgetWithHeightCache extends LinkedTableWidget {
|
|
217
|
+
constructor(block, parsedTable, contentHash, options = {}) {
|
|
218
|
+
super(block, parsedTable, contentHash, options);
|
|
219
|
+
this.contentHash = contentHash;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
eq(other) {
|
|
223
|
+
return super.eq(other) && other.contentHash === this.contentHash;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
toDOM(view) {
|
|
227
|
+
const dom = super.toDOM(view);
|
|
228
|
+
const contentHash = this.contentHash;
|
|
229
|
+
|
|
230
|
+
requestAnimationFrame(() => {
|
|
231
|
+
const line = dom.closest('.cm-line');
|
|
232
|
+
const height = line ? line.offsetHeight : dom.offsetHeight;
|
|
233
|
+
if (height > 0) {
|
|
234
|
+
cacheWidgetHeight(contentHash, height);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return dom;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
198
242
|
/**
|
|
199
243
|
* DisplayMathWidget wrapper that caches its rendered height for stable layout.
|
|
200
244
|
*/
|
|
@@ -440,11 +484,70 @@ function buildBlockDecorations(state) {
|
|
|
440
484
|
const cursorLine = doc.lineAt(cursorPos).number;
|
|
441
485
|
const decorations = [];
|
|
442
486
|
|
|
443
|
-
//
|
|
487
|
+
// Mode flags
|
|
488
|
+
const isSourceMode = state.facet(sourceModeFacet);
|
|
489
|
+
const isWysiwygMode = state.facet(wysiwygModeFacet);
|
|
490
|
+
const revealedLinkedTables = state.field(linkedTableMarkdownState, false) || new Set();
|
|
491
|
+
|
|
492
|
+
// Find and process linked tables first
|
|
493
|
+
const linkedTableBlocks = findLinkedTableBlocksInState(state);
|
|
494
|
+
|
|
495
|
+
for (const block of linkedTableBlocks) {
|
|
496
|
+
const blockRange = getLinkedTableBlockRange(block);
|
|
497
|
+
const contentHash = 'linked-table-' + hashContent(doc.sliceString(blockRange.from, blockRange.to));
|
|
498
|
+
const showLinkedSource = isSourceMode || revealedLinkedTables.has(block.spec.id);
|
|
499
|
+
|
|
500
|
+
if (!showLinkedSource) {
|
|
501
|
+
const parsed = parseTable(block.tableLines || []);
|
|
502
|
+
if (parsed && parsed.rows.length > 0) {
|
|
503
|
+
decorations.push(
|
|
504
|
+
Decoration.replace({
|
|
505
|
+
widget: new LinkedTableWidgetWithHeightCache(block, parsed, contentHash),
|
|
506
|
+
}).range(blockRange.from, blockRange.to)
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
if (!isSourceMode && revealedLinkedTables.has(block.spec.id)) {
|
|
511
|
+
decorations.push(
|
|
512
|
+
Decoration.widget({
|
|
513
|
+
widget: new LinkedTableSourceBannerWidget(block),
|
|
514
|
+
side: -1,
|
|
515
|
+
block: true,
|
|
516
|
+
}).range(block.headerFrom)
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const cachedHeight = getCachedHeight(contentHash);
|
|
521
|
+
if (cachedHeight) {
|
|
522
|
+
const lineCount = block.endLine - block.startLine + 1;
|
|
523
|
+
const lineHeight = getLineHeight();
|
|
524
|
+
const rawHeight = lineCount * lineHeight;
|
|
525
|
+
const padding = cachedHeight - rawHeight;
|
|
526
|
+
|
|
527
|
+
if (padding > 0) {
|
|
528
|
+
const lastLine = doc.line(block.endLine);
|
|
529
|
+
decorations.push(
|
|
530
|
+
Decoration.line({
|
|
531
|
+
attributes: {
|
|
532
|
+
class: 'cm-block-spacer-line',
|
|
533
|
+
style: `padding-bottom: ${padding}px`
|
|
534
|
+
}
|
|
535
|
+
}).range(lastLine.from)
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Find and process plain tables
|
|
444
543
|
const tableRanges = findTableRanges(state);
|
|
445
544
|
|
|
446
545
|
for (const range of tableRanges) {
|
|
447
|
-
|
|
546
|
+
if (isRangeInsideLinkedTable(range, linkedTableBlocks)) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const cursorInTable = isSourceMode || (!isWysiwygMode && cursorLine >= range.startLine && cursorLine <= range.endLine);
|
|
448
551
|
|
|
449
552
|
// Collect lines for both rendering and height calculation
|
|
450
553
|
const lines = [];
|
|
@@ -496,7 +599,7 @@ function buildBlockDecorations(state) {
|
|
|
496
599
|
const mathRanges = findDisplayMathRanges(state);
|
|
497
600
|
|
|
498
601
|
for (const range of mathRanges) {
|
|
499
|
-
const cursorInMath = cursorLine >= range.startLine && cursorLine <= range.endLine;
|
|
602
|
+
const cursorInMath = isSourceMode || (!isWysiwygMode && cursorLine >= range.startLine && cursorLine <= range.endLine);
|
|
500
603
|
const contentHash = 'math-' + hashContent(range.content);
|
|
501
604
|
|
|
502
605
|
if (!cursorInMath) {
|
|
@@ -536,7 +639,7 @@ function buildBlockDecorations(state) {
|
|
|
536
639
|
const fmRange = findFrontmatterRange(state);
|
|
537
640
|
|
|
538
641
|
if (fmRange) {
|
|
539
|
-
const cursorInFrontmatter = cursorLine >= fmRange.startLine && cursorLine <= fmRange.endLine;
|
|
642
|
+
const cursorInFrontmatter = isSourceMode || (!isWysiwygMode && cursorLine >= fmRange.startLine && cursorLine <= fmRange.endLine);
|
|
540
643
|
const contentHash = 'fm-' + hashContent(fmRange.content);
|
|
541
644
|
|
|
542
645
|
if (!cursorInFrontmatter) {
|
|
@@ -592,7 +695,7 @@ export const blockDecorations = StateField.define({
|
|
|
592
695
|
// Rebuild on any change that could affect block elements
|
|
593
696
|
// For efficiency, we could map positions and only rebuild affected ranges,
|
|
594
697
|
// but for now, full rebuild is acceptable
|
|
595
|
-
if (tr.docChanged || tr.selection) {
|
|
698
|
+
if (tr.docChanged || tr.selection || tr.reconfigured) {
|
|
596
699
|
return buildBlockDecorations(tr.state);
|
|
597
700
|
}
|
|
598
701
|
return decorations;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown Rendering Facets
|
|
3
|
+
*
|
|
4
|
+
* Shared facets used by both the ViewPlugin (renderer.js) and
|
|
5
|
+
* the StateField (block-decorations.js). Kept in a separate file
|
|
6
|
+
* to avoid circular dependencies between those modules.
|
|
7
|
+
*
|
|
8
|
+
* @module markdown/facets
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Facet } from '@codemirror/state';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Facet to toggle "source mode" — when true, all markdown syntax is shown
|
|
15
|
+
* as if the cursor were on every line. No rendering/hiding of markers,
|
|
16
|
+
* no widget replacement of syntax.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* sourceModeFacet.of(true) // enable source mode
|
|
20
|
+
* sourceModeFacet.of(false) // normal rendering mode
|
|
21
|
+
*
|
|
22
|
+
* @type {Facet<boolean, boolean>}
|
|
23
|
+
*/
|
|
24
|
+
export const sourceModeFacet = Facet.define({
|
|
25
|
+
combine: (values) => values.some(v => v),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Facet to toggle WYSIWYG mode — when true, markdown is rendered everywhere,
|
|
30
|
+
* including the active line/block, and editing is routed through a protected,
|
|
31
|
+
* syntax-safe interaction layer.
|
|
32
|
+
*
|
|
33
|
+
* @type {Facet<boolean, boolean>}
|
|
34
|
+
*/
|
|
35
|
+
export const wysiwygModeFacet = Facet.define({
|
|
36
|
+
combine: (values) => values.some(v => v),
|
|
37
|
+
});
|
|
@@ -16,8 +16,12 @@ import { WidgetType } from '@codemirror/view';
|
|
|
16
16
|
/**
|
|
17
17
|
* Regex to match HTML tags (opening, closing, self-closing, and comments)
|
|
18
18
|
* Matches: <tag>, </tag>, <tag />, <tag attr="value">, <!-- comment -->
|
|
19
|
+
*
|
|
20
|
+
* MRMD special comments use <!--! ... !--> and are intentionally excluded
|
|
21
|
+
* here so they can be handled by the comment-syntax extension instead of
|
|
22
|
+
* being rendered away as invisible HTML comments.
|
|
19
23
|
*/
|
|
20
|
-
const HTML_TAG_REGEX = /<\/?[a-zA-Z][a-zA-Z0-9]*(?:\s+[^>]*)?\/?>|<!--[\s\S]*?-->/g;
|
|
24
|
+
const HTML_TAG_REGEX = /<\/?[a-zA-Z][a-zA-Z0-9]*(?:\s+[^>]*)?\/?>|<!--(?!\!)[\s\S]*?-->/g;
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
27
|
* Regex to match complete HTML elements (opening + content + closing)
|
|
@@ -26,8 +30,8 @@ const HTML_TAG_REGEX = /<\/?[a-zA-Z][a-zA-Z0-9]*(?:\s+[^>]*)?\/?>|<!--[\s\S]*?--
|
|
|
26
30
|
const HTML_ELEMENT_PATTERNS = [
|
|
27
31
|
// Self-closing tags
|
|
28
32
|
/<(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)(?:\s+[^>]*)?\/?\s*>/gi,
|
|
29
|
-
// HTML comments
|
|
30
|
-
/<!--[\s\S]*?-->/g,
|
|
33
|
+
// HTML comments (excluding MRMD special comments: <!--! ... !-->)
|
|
34
|
+
/<!--(?!\!)[\s\S]*?-->/g,
|
|
31
35
|
// Inline elements with content (non-greedy, handles simple nesting)
|
|
32
36
|
/<(span|strong|em|b|i|u|s|mark|small|sub|sup|kbd|code|abbr|cite|dfn|q|time|var|samp|data|ruby|rt|rp|bdi|bdo|ins|del)(?:\s+[^>]*)?>[\s\S]*?<\/\1>/gi,
|
|
33
37
|
];
|
|
@@ -87,8 +91,8 @@ export function extractHtmlElements(text) {
|
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
// Match HTML comments
|
|
91
|
-
const comments = /<!--[\s\S]*?-->/g;
|
|
94
|
+
// Match HTML comments, but leave MRMD special comments to comment-syntax
|
|
95
|
+
const comments = /<!--(?!\!)[\s\S]*?-->/g;
|
|
92
96
|
while ((match = comments.exec(text)) !== null) {
|
|
93
97
|
const key = `${match.index}-${match.index + match[0].length}`;
|
|
94
98
|
if (!seen.has(key)) {
|
package/src/markdown/index.js
CHANGED
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
|
|
29
29
|
import { markdownRenderer } from './renderer.js';
|
|
30
30
|
import { blockDecorations, lineHeightTracker } from './block-decorations.js';
|
|
31
|
+
import { createWysiwygExtensions } from './wysiwyg.js';
|
|
32
|
+
import { createInlineEditingExtensions } from './inline-state.js';
|
|
31
33
|
import { markdownStyles, injectMarkdownStyles } from './styles.js';
|
|
32
34
|
|
|
33
35
|
/**
|
|
@@ -49,20 +51,28 @@ export function markdown() {
|
|
|
49
51
|
console.log('[Markdown] Creating extensions, blockDecorations:', blockDecorations, 'markdownRenderer:', markdownRenderer);
|
|
50
52
|
|
|
51
53
|
return [
|
|
52
|
-
lineHeightTracker,
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
lineHeightTracker, // ViewPlugin: updates line height cache (must come first!)
|
|
55
|
+
...createInlineEditingExtensions(),
|
|
56
|
+
blockDecorations, // StateField: tables, display math
|
|
57
|
+
markdownRenderer, // ViewPlugin: everything else
|
|
58
|
+
...createWysiwygExtensions(),
|
|
55
59
|
];
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
// Export individual pieces for advanced use
|
|
59
63
|
export { markdownRenderer, assetResolverFacet } from './renderer.js';
|
|
64
|
+
export { sourceModeFacet, wysiwygModeFacet } from './facets.js';
|
|
65
|
+
export { createWysiwygExtensions, toggleInlineFormat, findDelimitedRange, findFencedCodeAt } from './wysiwyg.js';
|
|
66
|
+
export { createInlineEditingExtensions, getPendingInlineSplit } from './inline-state.js';
|
|
67
|
+
export { toggleInlineMark, toggleInlineMarkFromSyntax, getActiveInlineMarks, getSelectionFormattingState } from './inline-commands.js';
|
|
68
|
+
export { getLineInlineModel, getCaretInlineContext, getSelectionInlineContext, inlineClassForMark, syntaxToMark, markToSyntax } from './inline-model.js';
|
|
60
69
|
export { blockDecorations, lineHeightTracker, cacheWidgetHeight, getCachedHeight, clearHeightCache } from './block-decorations.js';
|
|
61
70
|
export { markdownStyles, injectMarkdownStyles } from './styles.js';
|
|
62
71
|
|
|
63
72
|
// Widget exports
|
|
64
73
|
export {
|
|
65
74
|
TaskCheckboxWidget,
|
|
75
|
+
ListMarkerWidget,
|
|
66
76
|
ImageWidget,
|
|
67
77
|
ImagePlaceholder,
|
|
68
78
|
parseImageMarkdown,
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic inline formatting commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ChangeSet } from '@codemirror/state';
|
|
6
|
+
import {
|
|
7
|
+
cloneMarkSet,
|
|
8
|
+
findInnermostSpanContaining,
|
|
9
|
+
getCaretInlineContext,
|
|
10
|
+
getSelectionInlineContext,
|
|
11
|
+
markToSyntax,
|
|
12
|
+
normalizeWhitespaceSegments,
|
|
13
|
+
serializeSegments,
|
|
14
|
+
splitSegmentsAtPositions,
|
|
15
|
+
syntaxToMark,
|
|
16
|
+
} from './inline-model.js';
|
|
17
|
+
import {
|
|
18
|
+
clearPendingInlineSplitEffect,
|
|
19
|
+
getPendingInlineSplit,
|
|
20
|
+
setPendingInlineSplitEffect,
|
|
21
|
+
} from './inline-state.js';
|
|
22
|
+
|
|
23
|
+
function clearPendingEffects(state) {
|
|
24
|
+
return getPendingInlineSplit(state) ? [clearPendingInlineSplitEffect.of(null)] : [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function addMarkToSet(marks, mark) {
|
|
28
|
+
const next = cloneMarkSet(marks);
|
|
29
|
+
next.add(mark);
|
|
30
|
+
return next;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function removeMarkFromSet(marks, mark) {
|
|
34
|
+
const next = cloneMarkSet(marks);
|
|
35
|
+
next.delete(mark);
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function toggleMarkAtCaret(view, mark) {
|
|
40
|
+
const state = view.state;
|
|
41
|
+
const pos = state.selection.main.head;
|
|
42
|
+
const pending = getPendingInlineSplit(state);
|
|
43
|
+
|
|
44
|
+
if (pending && pending.pos === pos && pending.mark === mark) {
|
|
45
|
+
view.dispatch({
|
|
46
|
+
effects: clearPendingInlineSplitEffect.of(null),
|
|
47
|
+
userEvent: 'input.inline.pending-cancel',
|
|
48
|
+
});
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ctx = getCaretInlineContext(state, pos);
|
|
53
|
+
const sameSpan = findInnermostSpanContaining(ctx.model.spans, pos, mark);
|
|
54
|
+
const effects = clearPendingEffects(state);
|
|
55
|
+
|
|
56
|
+
if (ctx.insideCode && mark !== 'code') {
|
|
57
|
+
if (effects.length > 0) {
|
|
58
|
+
view.dispatch({ effects, userEvent: 'input.inline.noop' });
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (sameSpan) {
|
|
64
|
+
if (sameSpan.contentFrom === sameSpan.contentTo && pos === sameSpan.contentFrom) {
|
|
65
|
+
view.dispatch({
|
|
66
|
+
changes: { from: sameSpan.from, to: sameSpan.to, insert: '' },
|
|
67
|
+
selection: { anchor: sameSpan.from },
|
|
68
|
+
effects,
|
|
69
|
+
userEvent: 'input.inline.remove-empty',
|
|
70
|
+
scrollIntoView: true,
|
|
71
|
+
});
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (pos === sameSpan.contentFrom) {
|
|
76
|
+
view.dispatch({
|
|
77
|
+
selection: { anchor: sameSpan.from },
|
|
78
|
+
effects,
|
|
79
|
+
userEvent: 'input.inline.exit-left',
|
|
80
|
+
});
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (pos === sameSpan.contentTo) {
|
|
85
|
+
view.dispatch({
|
|
86
|
+
selection: { anchor: sameSpan.to },
|
|
87
|
+
effects,
|
|
88
|
+
userEvent: 'input.inline.exit-right',
|
|
89
|
+
});
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
view.dispatch({
|
|
94
|
+
selection: { anchor: pos },
|
|
95
|
+
effects: [
|
|
96
|
+
...effects,
|
|
97
|
+
setPendingInlineSplitEffect.of({
|
|
98
|
+
pos,
|
|
99
|
+
mark,
|
|
100
|
+
marks: Array.from(removeMarkFromSet(ctx.marksAtCaret, mark)),
|
|
101
|
+
}),
|
|
102
|
+
],
|
|
103
|
+
userEvent: 'input.inline.pending-split',
|
|
104
|
+
});
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const syntax = markToSyntax(mark);
|
|
109
|
+
if (!syntax) return false;
|
|
110
|
+
|
|
111
|
+
view.dispatch({
|
|
112
|
+
changes: { from: pos, insert: syntax.open + syntax.close },
|
|
113
|
+
selection: { anchor: pos + syntax.open.length },
|
|
114
|
+
effects,
|
|
115
|
+
userEvent: 'input.inline.start',
|
|
116
|
+
scrollIntoView: true,
|
|
117
|
+
});
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function applyMarkToSegment(segment, mark, action) {
|
|
122
|
+
if (mark !== 'code' && segment.marks.has('code')) return segment;
|
|
123
|
+
|
|
124
|
+
const next = {
|
|
125
|
+
...segment,
|
|
126
|
+
marks: cloneMarkSet(segment.marks),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (action === 'add') {
|
|
130
|
+
next.marks.add(mark);
|
|
131
|
+
} else {
|
|
132
|
+
next.marks.delete(mark);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return next;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function toggleMarkOnSelection(view, mark) {
|
|
139
|
+
const state = view.state;
|
|
140
|
+
const selection = state.selection.main;
|
|
141
|
+
const ctx = getSelectionInlineContext(state, selection.from, selection.to);
|
|
142
|
+
if (ctx.empty || ctx.selectedTextLength === 0) {
|
|
143
|
+
return toggleMarkAtCaret(view, mark);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const action = ctx.fullyCoveredBy.has(mark) ? 'remove' : 'add';
|
|
147
|
+
const changes = [];
|
|
148
|
+
const trackedOffsets = new Map();
|
|
149
|
+
const originalAnchor = selection.anchor;
|
|
150
|
+
const originalHead = selection.head;
|
|
151
|
+
|
|
152
|
+
for (const info of ctx.lines) {
|
|
153
|
+
if (info.from === info.to) continue;
|
|
154
|
+
|
|
155
|
+
let segments = splitSegmentsAtPositions(info.model.segments, [info.from, info.to]);
|
|
156
|
+
let changed = false;
|
|
157
|
+
|
|
158
|
+
segments = segments.map((segment) => {
|
|
159
|
+
if (segment.to <= info.from || segment.from >= info.to) return segment;
|
|
160
|
+
const next = applyMarkToSegment(segment, mark, action);
|
|
161
|
+
if (next !== segment) changed = true;
|
|
162
|
+
return next;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const normalized = normalizeWhitespaceSegments(segments);
|
|
166
|
+
const trackedPositions = [];
|
|
167
|
+
if (originalAnchor >= info.line.from && originalAnchor <= info.line.to) trackedPositions.push(originalAnchor);
|
|
168
|
+
if (originalHead >= info.line.from && originalHead <= info.line.to) trackedPositions.push(originalHead);
|
|
169
|
+
const rendered = serializeSegments(normalized, trackedPositions);
|
|
170
|
+
|
|
171
|
+
for (const [oldPos, newPos] of rendered.tracked.entries()) {
|
|
172
|
+
trackedOffsets.set(oldPos, { lineFrom: info.line.from, offset: newPos });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (changed || rendered.text !== info.line.text) {
|
|
176
|
+
changes.push({ from: info.line.from, to: info.line.to, insert: rendered.text });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (changes.length === 0) {
|
|
181
|
+
const effects = clearPendingEffects(state);
|
|
182
|
+
if (effects.length > 0) view.dispatch({ effects, userEvent: 'input.inline.noop' });
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const changeSet = ChangeSet.of(changes, state.doc.length);
|
|
187
|
+
const mappedAnchor = trackedOffsets.has(originalAnchor)
|
|
188
|
+
? changeSet.mapPos(trackedOffsets.get(originalAnchor).lineFrom, 1) + trackedOffsets.get(originalAnchor).offset
|
|
189
|
+
: changeSet.mapPos(originalAnchor, -1);
|
|
190
|
+
const mappedHead = trackedOffsets.has(originalHead)
|
|
191
|
+
? changeSet.mapPos(trackedOffsets.get(originalHead).lineFrom, 1) + trackedOffsets.get(originalHead).offset
|
|
192
|
+
: changeSet.mapPos(originalHead, 1);
|
|
193
|
+
|
|
194
|
+
view.dispatch({
|
|
195
|
+
changes,
|
|
196
|
+
selection: { anchor: mappedAnchor, head: mappedHead },
|
|
197
|
+
effects: clearPendingEffects(state),
|
|
198
|
+
userEvent: `input.inline.${action}`,
|
|
199
|
+
scrollIntoView: true,
|
|
200
|
+
});
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function toggleInlineMark(view, mark) {
|
|
205
|
+
if (!view?.state) return false;
|
|
206
|
+
const selection = view.state.selection.main;
|
|
207
|
+
if (selection.empty) return toggleMarkAtCaret(view, mark);
|
|
208
|
+
return toggleMarkOnSelection(view, mark);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function toggleInlineMarkFromSyntax(view, open, close = open) {
|
|
212
|
+
const mark = syntaxToMark(open, close);
|
|
213
|
+
if (!mark) return false;
|
|
214
|
+
return toggleInlineMark(view, mark);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function getActiveInlineMarks(view) {
|
|
218
|
+
const state = view.state;
|
|
219
|
+
const pending = getPendingInlineSplit(state);
|
|
220
|
+
const selection = state.selection.main;
|
|
221
|
+
|
|
222
|
+
if (selection.empty && pending && pending.pos === selection.head) {
|
|
223
|
+
return new Set(pending.marks);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (selection.empty) {
|
|
227
|
+
return cloneMarkSet(getCaretInlineContext(state, selection.head).marksAtCaret);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return cloneMarkSet(getSelectionInlineContext(state, selection.from, selection.to).fullyCoveredBy);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function getSelectionFormattingState(view) {
|
|
234
|
+
const active = getActiveInlineMarks(view);
|
|
235
|
+
const selection = view.state.selection.main;
|
|
236
|
+
const mixed = selection.empty
|
|
237
|
+
? new Set()
|
|
238
|
+
: getSelectionInlineContext(view.state, selection.from, selection.to).mixedMarks;
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
bold: active.has('bold'),
|
|
242
|
+
italic: active.has('italic'),
|
|
243
|
+
underline: active.has('underline'),
|
|
244
|
+
strike: active.has('strike'),
|
|
245
|
+
strikethrough: active.has('strike'),
|
|
246
|
+
code: active.has('code'),
|
|
247
|
+
mixed: {
|
|
248
|
+
bold: mixed.has('bold'),
|
|
249
|
+
italic: mixed.has('italic'),
|
|
250
|
+
underline: mixed.has('underline'),
|
|
251
|
+
strike: mixed.has('strike'),
|
|
252
|
+
strikethrough: mixed.has('strike'),
|
|
253
|
+
code: mixed.has('code'),
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|