mrmd-editor 0.7.0 → 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/execution.js +69 -15
- package/src/frontmatter-updater.js +204 -74
- package/src/grammar.js +758 -0
- package/src/index.js +1120 -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 +218 -8
- package/src/shell/dialogs/file-picker.js +211 -0
- package/src/shell/layouts/studio.js +229 -14
- package/src/shell/orchestrator-client.js +114 -0
- package/src/shell/styles.js +62 -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 +111 -7
- package/src/term-widget.js +43 -3
- package/src/widgets/theme-utils.js +24 -16
- package/src/widgets/theme.js +1535 -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/index.js
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
// #region IMPORTS
|
|
32
32
|
import { EditorView, basicSetup } from 'codemirror';
|
|
33
33
|
import { EditorState, StateEffect, Compartment, Text, Transaction } from '@codemirror/state';
|
|
34
|
-
import { keymap, Decoration, ViewPlugin, WidgetType, placeholder } from '@codemirror/view';
|
|
34
|
+
import { keymap, Decoration, ViewPlugin, WidgetType, placeholder, highlightWhitespace } from '@codemirror/view';
|
|
35
35
|
import { StreamLanguage, syntaxTree, syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
|
|
36
36
|
import { createCodemirrorTheme } from './widgets/codemirror-theme.js';
|
|
37
37
|
|
|
@@ -64,8 +64,44 @@ import { WebsocketProvider } from 'y-websocket';
|
|
|
64
64
|
// Internal modules
|
|
65
65
|
import { findCells, getCellAtCursor, countCells, findTerminalBlocks, isTerminalLanguage } from './cells.js';
|
|
66
66
|
import { RuntimeRegistry, createRuntimeRegistry } from './runtime.js';
|
|
67
|
+
import {
|
|
68
|
+
defaultDocumentTemplate,
|
|
69
|
+
documentTemplatePresets,
|
|
70
|
+
normalizeDocumentTemplate,
|
|
71
|
+
cloneDocumentTemplate,
|
|
72
|
+
createDocumentTemplateExtension,
|
|
73
|
+
compileDocumentTemplateCSS,
|
|
74
|
+
serializeDocumentTemplateToCss,
|
|
75
|
+
findDocumentTemplatePreset,
|
|
76
|
+
resolveFontForExport,
|
|
77
|
+
serializeDocumentTemplateToPandocMeta,
|
|
78
|
+
serializeDocumentTemplateToPandocYaml,
|
|
79
|
+
serializeDocumentTemplateToLatexPreamble,
|
|
80
|
+
serializeDocumentTemplateToHtml,
|
|
81
|
+
serializeDocumentTemplateToWordStyleMap,
|
|
82
|
+
buildPandocCommand,
|
|
83
|
+
} from './document-template.js';
|
|
84
|
+
import { parseFrontmatter, readFrontmatterValue, updateFrontmatterField } from './frontmatter-updater.js';
|
|
67
85
|
import { ExecutionManager, createExecutionManager } from './execution.js';
|
|
68
86
|
import { MonitorCoordination, EXECUTION_STATUS, createMonitorCoordination } from './monitor-coordination.js';
|
|
87
|
+
import * as linkedTables from './tables/index.js';
|
|
88
|
+
import {
|
|
89
|
+
LINKED_TABLE_EVENT,
|
|
90
|
+
dispatchLinkedTableAction,
|
|
91
|
+
openLinkedTableWorkspace,
|
|
92
|
+
canImportLinkedTableFromHost,
|
|
93
|
+
normalizeLinkedTableBlockInsertion,
|
|
94
|
+
insertLinkedTableBlock,
|
|
95
|
+
importLinkedTableFromHost,
|
|
96
|
+
TableJobsClient,
|
|
97
|
+
TABLE_JOB_STATUS,
|
|
98
|
+
createTableJobsClient,
|
|
99
|
+
createLinkedTableController,
|
|
100
|
+
LinkedTableController,
|
|
101
|
+
createLinkedTableBlockAnchor,
|
|
102
|
+
resolveLinkedTableBlockAnchor,
|
|
103
|
+
linkedTableMarkdownState,
|
|
104
|
+
} from './tables/index.js';
|
|
69
105
|
import { MRPClient } from './mrp-client.js';
|
|
70
106
|
|
|
71
107
|
// Shell (status bar, file management, studio layout)
|
|
@@ -83,20 +119,8 @@ import * as commentSyntaxModule from './comment-syntax.js';
|
|
|
83
119
|
// Cell controls (run buttons, queue, status)
|
|
84
120
|
import { createCellControls, CellControlsSystem } from './cell-controls/index.js';
|
|
85
121
|
|
|
86
|
-
//
|
|
87
|
-
import {
|
|
88
|
-
createRuntimeCodeLensExtensions,
|
|
89
|
-
rebuildRuntimeCodeLens,
|
|
90
|
-
runtimeCodeLensFacet,
|
|
91
|
-
runtimeCodeLensPlugin,
|
|
92
|
-
rebuildRuntimeCodeLensEffect,
|
|
93
|
-
injectRuntimeCodeLensStyles,
|
|
94
|
-
removeRuntimeCodeLensStyles,
|
|
95
|
-
findRuntimeBlocks,
|
|
96
|
-
findYamlConfigBlocks,
|
|
97
|
-
findSessionFrontmatter,
|
|
98
|
-
RuntimeCodeLensWidget,
|
|
99
|
-
} from './runtime-codelens/index.js';
|
|
122
|
+
// Section controls (AI/formatting next to focused line)
|
|
123
|
+
import { sectionControls } from './section-controls/index.js';
|
|
100
124
|
|
|
101
125
|
// Commands and keymap
|
|
102
126
|
import { commandRegistry } from './commands.js';
|
|
@@ -108,10 +132,24 @@ import {
|
|
|
108
132
|
adaptMRPClient,
|
|
109
133
|
createRuntimeHoverExtension,
|
|
110
134
|
createRuntimeCompletionExtension,
|
|
135
|
+
createRuntimeSignatureHelpExtension,
|
|
111
136
|
createVariableExplorer,
|
|
112
137
|
injectRuntimeLspStyles,
|
|
113
138
|
} from './runtime-lsp.js';
|
|
114
139
|
|
|
140
|
+
// Spellcheck (prose-only, disabled in code blocks)
|
|
141
|
+
import { createSpellcheckExtensions } from './spellcheck.js';
|
|
142
|
+
// Grammar diagnostics (LanguageTool host integration)
|
|
143
|
+
import {
|
|
144
|
+
createLanguageToolDiagnosticsExtension,
|
|
145
|
+
collectVisibleProseFragments,
|
|
146
|
+
forceLanguageToolRefresh,
|
|
147
|
+
refreshLanguageToolDiagnostics,
|
|
148
|
+
applyFirstLanguageToolSuggestion,
|
|
149
|
+
getLanguageToolSuggestionMenu,
|
|
150
|
+
applyLanguageToolSuggestionAt,
|
|
151
|
+
} from './grammar.js';
|
|
152
|
+
|
|
115
153
|
// Wiki-link completion ([[internal-links]])
|
|
116
154
|
import {
|
|
117
155
|
projectFilesFacet,
|
|
@@ -157,6 +195,14 @@ import {
|
|
|
157
195
|
markdown as markdownRendering,
|
|
158
196
|
markdownRenderer,
|
|
159
197
|
assetResolverFacet, // Facet for resolving asset URLs in Electron/desktop apps
|
|
198
|
+
sourceModeFacet, // Facet to toggle source/raw markdown view
|
|
199
|
+
wysiwygModeFacet, // Facet to toggle protected WYSIWYG rendering
|
|
200
|
+
createWysiwygExtensions,
|
|
201
|
+
createInlineEditingExtensions,
|
|
202
|
+
toggleInlineFormat,
|
|
203
|
+
toggleInlineMark,
|
|
204
|
+
getSelectionFormattingState,
|
|
205
|
+
findFencedCodeAt,
|
|
160
206
|
blockDecorations, // StateField for tables, display math (multi-line replace)
|
|
161
207
|
lineHeightTracker, // ViewPlugin for accurate line height tracking
|
|
162
208
|
markdownStyles,
|
|
@@ -174,6 +220,9 @@ import {
|
|
|
174
220
|
AlertTitleWidget,
|
|
175
221
|
} from './markdown/index.js';
|
|
176
222
|
|
|
223
|
+
// Page view pagination (spacer-based page breaks)
|
|
224
|
+
import { pageViewPagination } from './page-view-pagination.js';
|
|
225
|
+
|
|
177
226
|
// Awareness system
|
|
178
227
|
import {
|
|
179
228
|
createAwareness,
|
|
@@ -681,6 +730,12 @@ const codeBlockBackground = ViewPlugin.fromClass(class {
|
|
|
681
730
|
const firstLine = view.state.doc.lineAt(from);
|
|
682
731
|
const lastLine = view.state.doc.lineAt(to);
|
|
683
732
|
|
|
733
|
+
// Extract language from the opening fence line
|
|
734
|
+
const fenceText = firstLine.text;
|
|
735
|
+
const langMatch = fenceText.match(/^(\s*`{3,}|~{3,})\s*(\S*)/);
|
|
736
|
+
const rawLang = langMatch?.[2] || '';
|
|
737
|
+
const language = normalizeCodeLanguage(rawLang);
|
|
738
|
+
|
|
684
739
|
// Iterate through each line in the code block
|
|
685
740
|
for (let pos = from; pos < to;) {
|
|
686
741
|
const line = view.state.doc.lineAt(pos);
|
|
@@ -690,12 +745,18 @@ const codeBlockBackground = ViewPlugin.fromClass(class {
|
|
|
690
745
|
if (isFirstLine || isLastLine) {
|
|
691
746
|
// Fence lines - subtle styling
|
|
692
747
|
decorations.push(
|
|
693
|
-
Decoration.line({
|
|
748
|
+
Decoration.line({
|
|
749
|
+
class: 'cm-codeblock-fence',
|
|
750
|
+
attributes: language ? { 'data-lang': language } : undefined,
|
|
751
|
+
}).range(line.from)
|
|
694
752
|
);
|
|
695
753
|
} else {
|
|
696
754
|
// Content lines - normal code block styling
|
|
697
755
|
decorations.push(
|
|
698
|
-
Decoration.line({
|
|
756
|
+
Decoration.line({
|
|
757
|
+
class: 'cm-codeblock-line',
|
|
758
|
+
attributes: language ? { 'data-lang': language } : undefined,
|
|
759
|
+
}).range(line.from)
|
|
699
760
|
);
|
|
700
761
|
}
|
|
701
762
|
pos = line.to + 1;
|
|
@@ -754,6 +815,99 @@ const codeBlockStyles = EditorView.theme({
|
|
|
754
815
|
});
|
|
755
816
|
// #endregion CODE_BLOCK_BACKGROUND
|
|
756
817
|
|
|
818
|
+
// #region INVISIBLE_CHARACTERS
|
|
819
|
+
/**
|
|
820
|
+
* Extension that shows newline markers (¶) at the end of each line.
|
|
821
|
+
* Used in combination with CM6's built-in highlightWhitespace() for
|
|
822
|
+
* spaces and tabs. Together they provide full invisible character rendering.
|
|
823
|
+
*/
|
|
824
|
+
class NewlineMarkerWidget extends WidgetType {
|
|
825
|
+
eq() { return true; }
|
|
826
|
+
toDOM() {
|
|
827
|
+
const span = document.createElement('span');
|
|
828
|
+
span.className = 'cm-newline-marker';
|
|
829
|
+
span.textContent = '¶';
|
|
830
|
+
span.setAttribute('aria-hidden', 'true');
|
|
831
|
+
return span;
|
|
832
|
+
}
|
|
833
|
+
ignoreEvent() { return true; }
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const newlineMarkerPlugin = ViewPlugin.fromClass(
|
|
837
|
+
class {
|
|
838
|
+
constructor(view) {
|
|
839
|
+
this.decorations = this.buildDecorations(view);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
update(update) {
|
|
843
|
+
if (update.docChanged || update.viewportChanged) {
|
|
844
|
+
this.decorations = this.buildDecorations(update.view);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
buildDecorations(view) {
|
|
849
|
+
const decorations = [];
|
|
850
|
+
const doc = view.state.doc;
|
|
851
|
+
|
|
852
|
+
for (let i = doc.lineAt(view.viewport.from).number; i <= doc.lineAt(view.viewport.to).number; i++) {
|
|
853
|
+
const line = doc.line(i);
|
|
854
|
+
// Add newline marker at the end of each line (except the last line if it has no trailing newline)
|
|
855
|
+
if (i < doc.lines) {
|
|
856
|
+
decorations.push(
|
|
857
|
+
Decoration.widget({
|
|
858
|
+
widget: new NewlineMarkerWidget(),
|
|
859
|
+
side: 1, // After the line content
|
|
860
|
+
}).range(line.to)
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
return Decoration.set(decorations, true);
|
|
866
|
+
}
|
|
867
|
+
},
|
|
868
|
+
{ decorations: (v) => v.decorations }
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Styles for invisible character markers.
|
|
873
|
+
* Overrides CM6's default whitespace styling with more visible symbols.
|
|
874
|
+
*/
|
|
875
|
+
const invisibleCharStyles = EditorView.theme({
|
|
876
|
+
// Newline markers (¶)
|
|
877
|
+
'.cm-newline-marker': {
|
|
878
|
+
color: 'var(--md-marker-color, #aaa)',
|
|
879
|
+
opacity: '0.5',
|
|
880
|
+
fontSize: '0.8em',
|
|
881
|
+
userSelect: 'none',
|
|
882
|
+
pointerEvents: 'none',
|
|
883
|
+
},
|
|
884
|
+
// Override CM6's default space dots - make them subtler
|
|
885
|
+
'.cm-highlightSpace': {
|
|
886
|
+
backgroundImage: 'radial-gradient(circle at 50% 55%, var(--md-marker-color, #aaa) 20%, transparent 5%)',
|
|
887
|
+
opacity: '0.4',
|
|
888
|
+
},
|
|
889
|
+
// Override CM6's tab arrows
|
|
890
|
+
'.cm-highlightTab': {
|
|
891
|
+
backgroundImage: `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="20"><path stroke="%23aaa" stroke-width="1" fill="none" d="M1 10H196L190 5M190 15L196 10M197 4L197 16"/></svg>')`,
|
|
892
|
+
opacity: '0.5',
|
|
893
|
+
},
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Create the invisibles extension bundle.
|
|
898
|
+
* Includes whitespace highlighting + newline markers + styles.
|
|
899
|
+
*
|
|
900
|
+
* @returns {import('@codemirror/state').Extension}
|
|
901
|
+
*/
|
|
902
|
+
function createInvisiblesExtension() {
|
|
903
|
+
return [
|
|
904
|
+
highlightWhitespace(),
|
|
905
|
+
newlineMarkerPlugin,
|
|
906
|
+
invisibleCharStyles,
|
|
907
|
+
];
|
|
908
|
+
}
|
|
909
|
+
// #endregion INVISIBLE_CHARACTERS
|
|
910
|
+
|
|
757
911
|
// #region WRITER
|
|
758
912
|
/**
|
|
759
913
|
* Writer for streaming content into the editor.
|
|
@@ -791,6 +945,457 @@ class Writer {
|
|
|
791
945
|
}
|
|
792
946
|
// #endregion WRITER
|
|
793
947
|
|
|
948
|
+
// #region INITIAL_CURSOR
|
|
949
|
+
/**
|
|
950
|
+
* Find the ideal initial cursor position for a markdown document.
|
|
951
|
+
*
|
|
952
|
+
* When opening a file, placing the cursor at position 0 shows raw frontmatter
|
|
953
|
+
* YAML which looks ugly. Instead, we find the first empty line after any
|
|
954
|
+
* frontmatter block — this causes the frontmatter to render as a nice widget
|
|
955
|
+
* and gives a clean first impression.
|
|
956
|
+
*
|
|
957
|
+
* @param {string} content - Document content
|
|
958
|
+
* @returns {number} Character position for the cursor
|
|
959
|
+
*/
|
|
960
|
+
function findInitialCursorPosition(content) {
|
|
961
|
+
if (!content) return 0;
|
|
962
|
+
|
|
963
|
+
const lines = content.split('\n');
|
|
964
|
+
let i = 0;
|
|
965
|
+
|
|
966
|
+
// Skip YAML frontmatter if present (--- ... ---)
|
|
967
|
+
if (lines[0]?.trim() === '---') {
|
|
968
|
+
i = 1;
|
|
969
|
+
while (i < lines.length && lines[i]?.trim() !== '---') {
|
|
970
|
+
i++;
|
|
971
|
+
}
|
|
972
|
+
if (i < lines.length) i++; // skip closing ---
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Find first empty line from current position
|
|
976
|
+
while (i < lines.length) {
|
|
977
|
+
if (lines[i]?.trim() === '') {
|
|
978
|
+
// Calculate character position (start of this empty line)
|
|
979
|
+
let pos = 0;
|
|
980
|
+
for (let j = 0; j < i; j++) {
|
|
981
|
+
pos += lines[j].length + 1; // +1 for \n
|
|
982
|
+
}
|
|
983
|
+
return pos;
|
|
984
|
+
}
|
|
985
|
+
i++;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
return 0; // fallback to start
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function wrapSelectionsWith(view, open, close = open, userEvent = 'input.wysiwyg.format') {
|
|
992
|
+
const state = view.state;
|
|
993
|
+
const changes = [];
|
|
994
|
+
const ranges = [];
|
|
995
|
+
let delta = 0;
|
|
996
|
+
|
|
997
|
+
for (const range of state.selection.ranges) {
|
|
998
|
+
if (range.empty) {
|
|
999
|
+
changes.push({ from: range.from, insert: open + close });
|
|
1000
|
+
const pos = range.from + open.length + delta;
|
|
1001
|
+
ranges.push({ anchor: pos, head: pos });
|
|
1002
|
+
delta += open.length + close.length;
|
|
1003
|
+
} else {
|
|
1004
|
+
changes.push({ from: range.from, insert: open });
|
|
1005
|
+
changes.push({ from: range.to, insert: close });
|
|
1006
|
+
ranges.push({ anchor: range.from + open.length + delta, head: range.to + open.length + delta });
|
|
1007
|
+
delta += open.length + close.length;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
view.dispatch({
|
|
1012
|
+
changes,
|
|
1013
|
+
selection: { ranges, mainIndex: state.selection.mainIndex },
|
|
1014
|
+
userEvent,
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function currentLineStructuralPrefix(lineText) {
|
|
1019
|
+
const heading = lineText.match(/^\s{0,3}(#{1,6})\s+/);
|
|
1020
|
+
if (heading) return heading[0];
|
|
1021
|
+
const quote = lineText.match(/^(\s*>\s?)+/);
|
|
1022
|
+
if (quote) return quote[0];
|
|
1023
|
+
const list = lineText.match(/^(\s*)(?:[-+*]|\d+\.)\s+/);
|
|
1024
|
+
if (list) return list[0];
|
|
1025
|
+
return '';
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function getCurrentBlockTypeInfo(view) {
|
|
1029
|
+
const pos = view.state.selection.main.head;
|
|
1030
|
+
const line = view.state.doc.lineAt(pos);
|
|
1031
|
+
const text = line.text;
|
|
1032
|
+
|
|
1033
|
+
// ── Check if inside a fenced code block ──
|
|
1034
|
+
const tree = syntaxTree(view.state);
|
|
1035
|
+
let fencedCode = null;
|
|
1036
|
+
tree.iterate({
|
|
1037
|
+
from: Math.max(0, pos - 1),
|
|
1038
|
+
to: pos + 1,
|
|
1039
|
+
enter: (node) => {
|
|
1040
|
+
if (node.name === 'FencedCode' && node.from <= pos && node.to >= pos) {
|
|
1041
|
+
fencedCode = node;
|
|
1042
|
+
}
|
|
1043
|
+
},
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
if (fencedCode) {
|
|
1047
|
+
// Extract language from the opening fence line (```python, ```r, etc.)
|
|
1048
|
+
const fenceLine = view.state.doc.lineAt(fencedCode.from);
|
|
1049
|
+
const fenceText = fenceLine.text;
|
|
1050
|
+
const langMatch = fenceText.match(/^(\s*`{3,}|~{3,})\s*(\S*)/);
|
|
1051
|
+
const rawLang = langMatch?.[2] || '';
|
|
1052
|
+
const language = normalizeCodeLanguage(rawLang);
|
|
1053
|
+
|
|
1054
|
+
// Determine if cursor is on the fence line itself or inside code content
|
|
1055
|
+
const isOnFence = line.number === fenceLine.number ||
|
|
1056
|
+
line.number === view.state.doc.lineAt(fencedCode.to).number;
|
|
1057
|
+
|
|
1058
|
+
// Detect the syntax token under the cursor
|
|
1059
|
+
const syntaxToken = isOnFence ? null : getSyntaxTokenAtPos(view, pos);
|
|
1060
|
+
|
|
1061
|
+
return {
|
|
1062
|
+
type: 'codeblock',
|
|
1063
|
+
level: 0,
|
|
1064
|
+
label: language ? `codeblock-${language}` : 'codeblock',
|
|
1065
|
+
language,
|
|
1066
|
+
isOnFence,
|
|
1067
|
+
syntaxToken,
|
|
1068
|
+
fenceFrom: fencedCode.from,
|
|
1069
|
+
fenceTo: fencedCode.to,
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const heading = text.match(/^\s{0,3}(#{1,6})\s+/);
|
|
1074
|
+
if (heading) {
|
|
1075
|
+
return { type: 'heading', level: heading[1].length, label: `h${heading[1].length}` };
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (/^(\s*>\s?)+/.test(text)) {
|
|
1079
|
+
return { type: 'blockquote', level: 1, label: 'blockquote' };
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
if (/^(\s*)(?:[-+*])\s+/.test(text)) {
|
|
1083
|
+
return { type: 'unordered-list', level: 1, label: 'unordered-list' };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (/^(\s*)(?:\d+\.)\s+/.test(text)) {
|
|
1087
|
+
return { type: 'ordered-list', level: 1, label: 'ordered-list' };
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
return { type: 'paragraph', level: 0, label: 'paragraph' };
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Normalize code language aliases to canonical names matching template keys.
|
|
1095
|
+
*/
|
|
1096
|
+
function normalizeCodeLanguage(raw) {
|
|
1097
|
+
if (!raw) return '';
|
|
1098
|
+
const lang = raw.toLowerCase().trim();
|
|
1099
|
+
const map = {
|
|
1100
|
+
'js': 'javascript', 'node': 'javascript', 'ecmascript': 'javascript',
|
|
1101
|
+
'ts': 'typescript',
|
|
1102
|
+
'py': 'python', 'python3': 'python',
|
|
1103
|
+
'rb': 'ruby',
|
|
1104
|
+
'rs': 'rust',
|
|
1105
|
+
'sh': 'shell', 'bash': 'shell', 'zsh': 'shell', 'fish': 'shell',
|
|
1106
|
+
'ps1': 'powershell', 'pwsh': 'powershell',
|
|
1107
|
+
'yml': 'yaml',
|
|
1108
|
+
'htm': 'html',
|
|
1109
|
+
'c': 'cpp', 'c++': 'cpp', 'cxx': 'cpp', 'h': 'cpp', 'hpp': 'cpp',
|
|
1110
|
+
'golang': 'go',
|
|
1111
|
+
'jl': 'julia',
|
|
1112
|
+
'rlang': 'r',
|
|
1113
|
+
'mysql': 'sql', 'postgresql': 'sql', 'postgres': 'sql', 'sqlite': 'sql',
|
|
1114
|
+
'jsonc': 'json',
|
|
1115
|
+
'jsx': 'javascript', 'tsx': 'typescript',
|
|
1116
|
+
'xml': 'html', 'svg': 'html',
|
|
1117
|
+
};
|
|
1118
|
+
return map[lang] || lang;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Get the semantic syntax token type at a position within a code block.
|
|
1123
|
+
* Maps CodeMirror/Lezer highlight tags to our template token names.
|
|
1124
|
+
*
|
|
1125
|
+
* @param {EditorView} view
|
|
1126
|
+
* @param {number} pos
|
|
1127
|
+
* @returns {string|null} Token name like 'keyword', 'string', 'comment', etc.
|
|
1128
|
+
*/
|
|
1129
|
+
function getSyntaxTokenAtPos(view, pos) {
|
|
1130
|
+
const tree = syntaxTree(view.state);
|
|
1131
|
+
let bestNode = null;
|
|
1132
|
+
let bestSize = Infinity;
|
|
1133
|
+
|
|
1134
|
+
// Find the most specific (smallest) node at pos
|
|
1135
|
+
tree.iterate({
|
|
1136
|
+
from: pos,
|
|
1137
|
+
to: pos + 1,
|
|
1138
|
+
enter: (node) => {
|
|
1139
|
+
const size = node.to - node.from;
|
|
1140
|
+
if (size < bestSize && node.from <= pos && node.to > pos) {
|
|
1141
|
+
bestNode = node;
|
|
1142
|
+
bestSize = size;
|
|
1143
|
+
}
|
|
1144
|
+
},
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
if (!bestNode) return null;
|
|
1148
|
+
const name = bestNode.name;
|
|
1149
|
+
|
|
1150
|
+
// Map Lezer tree node names to our semantic token names.
|
|
1151
|
+
// These are the node names from @lezer/highlight and language parsers.
|
|
1152
|
+
const tokenMap = {
|
|
1153
|
+
// Keywords
|
|
1154
|
+
'Keyword': 'keyword',
|
|
1155
|
+
'keyword': 'keyword',
|
|
1156
|
+
'ControlKeyword': 'controlKeyword',
|
|
1157
|
+
'controlKeyword': 'controlKeyword',
|
|
1158
|
+
'for': 'controlKeyword',
|
|
1159
|
+
'if': 'controlKeyword',
|
|
1160
|
+
'else': 'controlKeyword',
|
|
1161
|
+
'while': 'controlKeyword',
|
|
1162
|
+
'return': 'keyword',
|
|
1163
|
+
'def': 'keyword',
|
|
1164
|
+
'class': 'keyword',
|
|
1165
|
+
'import': 'keyword',
|
|
1166
|
+
'from': 'keyword',
|
|
1167
|
+
'as': 'keyword',
|
|
1168
|
+
'in': 'keyword',
|
|
1169
|
+
'not': 'keyword',
|
|
1170
|
+
'and': 'keyword',
|
|
1171
|
+
'or': 'keyword',
|
|
1172
|
+
'is': 'keyword',
|
|
1173
|
+
'with': 'keyword',
|
|
1174
|
+
'try': 'controlKeyword',
|
|
1175
|
+
'except': 'controlKeyword',
|
|
1176
|
+
'finally': 'controlKeyword',
|
|
1177
|
+
'raise': 'keyword',
|
|
1178
|
+
'yield': 'keyword',
|
|
1179
|
+
'lambda': 'keyword',
|
|
1180
|
+
'pass': 'keyword',
|
|
1181
|
+
'break': 'controlKeyword',
|
|
1182
|
+
'continue': 'controlKeyword',
|
|
1183
|
+
'del': 'keyword',
|
|
1184
|
+
'global': 'keyword',
|
|
1185
|
+
'nonlocal': 'keyword',
|
|
1186
|
+
'assert': 'keyword',
|
|
1187
|
+
'async': 'keyword',
|
|
1188
|
+
'await': 'keyword',
|
|
1189
|
+
'let': 'keyword',
|
|
1190
|
+
'const': 'keyword',
|
|
1191
|
+
'var': 'keyword',
|
|
1192
|
+
'function': 'keyword',
|
|
1193
|
+
'switch': 'controlKeyword',
|
|
1194
|
+
'case': 'controlKeyword',
|
|
1195
|
+
'default': 'controlKeyword',
|
|
1196
|
+
'do': 'controlKeyword',
|
|
1197
|
+
'throw': 'keyword',
|
|
1198
|
+
'catch': 'controlKeyword',
|
|
1199
|
+
'new': 'keyword',
|
|
1200
|
+
'this': 'keyword',
|
|
1201
|
+
'super': 'keyword',
|
|
1202
|
+
'extends': 'keyword',
|
|
1203
|
+
'implements': 'keyword',
|
|
1204
|
+
'interface': 'keyword',
|
|
1205
|
+
'enum': 'keyword',
|
|
1206
|
+
'export': 'keyword',
|
|
1207
|
+
'typeof': 'keyword',
|
|
1208
|
+
'instanceof': 'keyword',
|
|
1209
|
+
'void': 'keyword',
|
|
1210
|
+
'delete': 'keyword',
|
|
1211
|
+
|
|
1212
|
+
// Strings
|
|
1213
|
+
'String': 'string',
|
|
1214
|
+
'string': 'string',
|
|
1215
|
+
'TemplateString': 'string',
|
|
1216
|
+
'FormatString': 'string',
|
|
1217
|
+
'Character': 'string',
|
|
1218
|
+
|
|
1219
|
+
// Numbers
|
|
1220
|
+
'Number': 'number',
|
|
1221
|
+
'number': 'number',
|
|
1222
|
+
'Integer': 'number',
|
|
1223
|
+
'Float': 'number',
|
|
1224
|
+
|
|
1225
|
+
// Comments
|
|
1226
|
+
'Comment': 'comment',
|
|
1227
|
+
'comment': 'comment',
|
|
1228
|
+
'LineComment': 'comment',
|
|
1229
|
+
'BlockComment': 'comment',
|
|
1230
|
+
|
|
1231
|
+
// Functions
|
|
1232
|
+
'FunctionDefinition': 'function',
|
|
1233
|
+
'FunctionDeclaration': 'function',
|
|
1234
|
+
'CallExpression': 'function',
|
|
1235
|
+
|
|
1236
|
+
// Variables
|
|
1237
|
+
'VariableName': 'variable',
|
|
1238
|
+
'VariableDefinition': 'variable',
|
|
1239
|
+
|
|
1240
|
+
// Types
|
|
1241
|
+
'TypeName': 'type',
|
|
1242
|
+
'TypeDefinition': 'type',
|
|
1243
|
+
'ClassName': 'type',
|
|
1244
|
+
'ClassDefinition': 'type',
|
|
1245
|
+
|
|
1246
|
+
// Operators
|
|
1247
|
+
'ArithOp': 'operator',
|
|
1248
|
+
'LogicOp': 'operator',
|
|
1249
|
+
'BitOp': 'operator',
|
|
1250
|
+
'CompareOp': 'operator',
|
|
1251
|
+
'AssignOp': 'operator',
|
|
1252
|
+
'Equals': 'operator',
|
|
1253
|
+
|
|
1254
|
+
// Punctuation
|
|
1255
|
+
'Punctuation': 'punctuation',
|
|
1256
|
+
'(': 'punctuation',
|
|
1257
|
+
')': 'punctuation',
|
|
1258
|
+
'[': 'punctuation',
|
|
1259
|
+
']': 'punctuation',
|
|
1260
|
+
'{': 'punctuation',
|
|
1261
|
+
'}': 'punctuation',
|
|
1262
|
+
'.': 'punctuation',
|
|
1263
|
+
',': 'punctuation',
|
|
1264
|
+
';': 'punctuation',
|
|
1265
|
+
':': 'punctuation',
|
|
1266
|
+
|
|
1267
|
+
// Properties
|
|
1268
|
+
'PropertyName': 'property',
|
|
1269
|
+
'PropertyDefinition': 'property',
|
|
1270
|
+
|
|
1271
|
+
// Constants
|
|
1272
|
+
'BooleanLiteral': 'constant',
|
|
1273
|
+
'Boolean': 'constant',
|
|
1274
|
+
'True': 'constant',
|
|
1275
|
+
'False': 'constant',
|
|
1276
|
+
'None': 'constant',
|
|
1277
|
+
'Null': 'constant',
|
|
1278
|
+
'null': 'constant',
|
|
1279
|
+
'undefined': 'constant',
|
|
1280
|
+
|
|
1281
|
+
// Regex
|
|
1282
|
+
'RegExp': 'regexp',
|
|
1283
|
+
|
|
1284
|
+
// Escape
|
|
1285
|
+
'Escape': 'escape',
|
|
1286
|
+
'EscapeSequence': 'escape',
|
|
1287
|
+
|
|
1288
|
+
// Tags (HTML/XML)
|
|
1289
|
+
'TagName': 'tag',
|
|
1290
|
+
'StartTag': 'tag',
|
|
1291
|
+
'EndTag': 'tag',
|
|
1292
|
+
'SelfClosingTag': 'tag',
|
|
1293
|
+
|
|
1294
|
+
// Attributes
|
|
1295
|
+
'AttributeName': 'attribute',
|
|
1296
|
+
'AttributeValue': 'attributeValue',
|
|
1297
|
+
|
|
1298
|
+
// Meta / decorators
|
|
1299
|
+
'Decorator': 'meta',
|
|
1300
|
+
'Annotation': 'meta',
|
|
1301
|
+
'Meta': 'meta',
|
|
1302
|
+
'ProcessingInstruction': 'meta',
|
|
1303
|
+
};
|
|
1304
|
+
|
|
1305
|
+
// Direct match
|
|
1306
|
+
if (tokenMap[name]) return tokenMap[name];
|
|
1307
|
+
|
|
1308
|
+
// Try resolving from the tree directly and walking up parent nodes
|
|
1309
|
+
try {
|
|
1310
|
+
let node = tree.resolveInner(pos, 1);
|
|
1311
|
+
let depth = 0;
|
|
1312
|
+
while (node && depth < 8) {
|
|
1313
|
+
if (tokenMap[node.name]) return tokenMap[node.name];
|
|
1314
|
+
node = node.parent;
|
|
1315
|
+
depth++;
|
|
1316
|
+
}
|
|
1317
|
+
} catch (e) { /* ignore tree resolution errors */ }
|
|
1318
|
+
|
|
1319
|
+
return null;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function getSelectionFormattingInfo(view) {
|
|
1323
|
+
return getSelectionFormattingState(view);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
function setCurrentBlockType(view, type, options = {}) {
|
|
1327
|
+
const state = view.state;
|
|
1328
|
+
const pos = state.selection.main.head;
|
|
1329
|
+
const line = state.doc.lineAt(pos);
|
|
1330
|
+
const prefix = currentLineStructuralPrefix(line.text);
|
|
1331
|
+
const contentStart = line.from + prefix.length;
|
|
1332
|
+
let newPrefix = '';
|
|
1333
|
+
|
|
1334
|
+
if (type === 'paragraph') {
|
|
1335
|
+
newPrefix = '';
|
|
1336
|
+
} else if (type === 'heading') {
|
|
1337
|
+
const level = Math.max(1, Math.min(6, Number(options.level) || 1));
|
|
1338
|
+
newPrefix = '#'.repeat(level) + ' ';
|
|
1339
|
+
} else if (type === 'blockquote') {
|
|
1340
|
+
newPrefix = '> ';
|
|
1341
|
+
} else if (type === 'unordered-list') {
|
|
1342
|
+
newPrefix = '- ';
|
|
1343
|
+
} else if (type === 'ordered-list') {
|
|
1344
|
+
newPrefix = '1. ';
|
|
1345
|
+
} else {
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
view.dispatch({
|
|
1350
|
+
changes: { from: line.from, to: contentStart, insert: newPrefix },
|
|
1351
|
+
selection: { anchor: line.from + newPrefix.length + Math.max(0, pos - contentStart) },
|
|
1352
|
+
userEvent: 'input.wysiwyg.blocktype',
|
|
1353
|
+
});
|
|
1354
|
+
return true;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function insertLinkAtSelection(view, url, text = null) {
|
|
1358
|
+
const state = view.state;
|
|
1359
|
+
const range = state.selection.main;
|
|
1360
|
+
const selected = state.sliceDoc(range.from, range.to);
|
|
1361
|
+
const label = text ?? selected ?? 'link';
|
|
1362
|
+
const link = `[${label}](${url})`;
|
|
1363
|
+
const insertFrom = range.from;
|
|
1364
|
+
const insertTo = range.to;
|
|
1365
|
+
view.dispatch({
|
|
1366
|
+
changes: { from: insertFrom, to: insertTo, insert: link },
|
|
1367
|
+
selection: { anchor: insertFrom + 1, head: insertFrom + 1 + label.length },
|
|
1368
|
+
userEvent: 'input.wysiwyg.link',
|
|
1369
|
+
});
|
|
1370
|
+
return true;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function insertImageAtSelection(view, url, alt = 'image') {
|
|
1374
|
+
const pos = view.state.selection.main.head;
|
|
1375
|
+
const image = ``;
|
|
1376
|
+
view.dispatch({
|
|
1377
|
+
changes: { from: pos, insert: image },
|
|
1378
|
+
selection: { anchor: pos + 2, head: pos + 2 + alt.length },
|
|
1379
|
+
userEvent: 'input.wysiwyg.image',
|
|
1380
|
+
});
|
|
1381
|
+
return true;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function insertCodeBlockAtCursor(view, language = '') {
|
|
1385
|
+
const pos = view.state.selection.main.head;
|
|
1386
|
+
const lang = language ? String(language).trim() : '';
|
|
1387
|
+
const prefix = pos > 0 && view.state.sliceDoc(pos - 1, pos) !== '\n' ? '\n' : '';
|
|
1388
|
+
const block = `${prefix}\`\`\`${lang}\n\n\`\`\``;
|
|
1389
|
+
const cursor = pos + prefix.length + 3 + lang.length + 1;
|
|
1390
|
+
view.dispatch({
|
|
1391
|
+
changes: { from: pos, insert: block },
|
|
1392
|
+
selection: { anchor: cursor },
|
|
1393
|
+
userEvent: 'input.wysiwyg.codeblock',
|
|
1394
|
+
});
|
|
1395
|
+
return true;
|
|
1396
|
+
}
|
|
1397
|
+
// #endregion INITIAL_CURSOR
|
|
1398
|
+
|
|
794
1399
|
// #region CREATE
|
|
795
1400
|
/**
|
|
796
1401
|
* Create a standalone markdown editor
|
|
@@ -825,6 +1430,7 @@ function create(target, options = {}) {
|
|
|
825
1430
|
const dark = config.appearance.dark;
|
|
826
1431
|
const placeholderText = config.appearance.placeholder;
|
|
827
1432
|
const readonly = config.appearance.readonly;
|
|
1433
|
+
const spellcheck = config.appearance.spellcheck;
|
|
828
1434
|
const userName = config.user.name;
|
|
829
1435
|
const userColor = config.user.color;
|
|
830
1436
|
const userType = config.user.type;
|
|
@@ -839,6 +1445,11 @@ function create(target, options = {}) {
|
|
|
839
1445
|
awarenessUI = true,
|
|
840
1446
|
} = options;
|
|
841
1447
|
|
|
1448
|
+
const linkedTableHostContext = {
|
|
1449
|
+
projectRoot: options.projectRoot || null,
|
|
1450
|
+
documentPath: options.documentPath || null,
|
|
1451
|
+
};
|
|
1452
|
+
|
|
842
1453
|
// Runtimes from normalized config
|
|
843
1454
|
const runtimes = {};
|
|
844
1455
|
for (const [name, rtConfig] of Object.entries(config.runtimes)) {
|
|
@@ -891,10 +1502,16 @@ function create(target, options = {}) {
|
|
|
891
1502
|
|
|
892
1503
|
// Always read initial content from Yjs (source of truth)
|
|
893
1504
|
const initialContent = yText.toString();
|
|
1505
|
+
const initialDocumentTemplate = normalizeDocumentTemplate(options.documentTemplate || defaultDocumentTemplate);
|
|
894
1506
|
const themeCompartment = new Compartment();
|
|
1507
|
+
const documentTemplateCompartment = new Compartment();
|
|
895
1508
|
const readonlyCompartment = new Compartment();
|
|
896
1509
|
const keymapCompartment = new Compartment();
|
|
897
1510
|
const projectFilesCompartment = new Compartment();
|
|
1511
|
+
const sectionControlsCompartment = new Compartment();
|
|
1512
|
+
const sourceModeCompartment = new Compartment();
|
|
1513
|
+
const wysiwygModeCompartment = new Compartment();
|
|
1514
|
+
const invisiblesCompartment = new Compartment();
|
|
898
1515
|
|
|
899
1516
|
// Create UndoManager for undo/redo tracking
|
|
900
1517
|
// We create it ourselves so we can listen to stack changes
|
|
@@ -933,7 +1550,7 @@ function create(target, options = {}) {
|
|
|
933
1550
|
// Use explicit theme if set, otherwise auto-select based on dark mode
|
|
934
1551
|
const resolveThemeName = (theme, isDarkMode) => {
|
|
935
1552
|
if (theme) return theme;
|
|
936
|
-
return
|
|
1553
|
+
return 'plain-light';
|
|
937
1554
|
};
|
|
938
1555
|
const initialThemeName = resolveThemeName(config.appearance?.theme, isDark);
|
|
939
1556
|
|
|
@@ -959,8 +1576,11 @@ function create(target, options = {}) {
|
|
|
959
1576
|
documentTheme,
|
|
960
1577
|
codeBlockBackground, // Add gray background to code blocks
|
|
961
1578
|
codeBlockStyles,
|
|
1579
|
+
// Spellcheck: enable browser-native spellcheck on prose, disable in code
|
|
1580
|
+
...(spellcheck !== false ? createSpellcheckExtensions() : []),
|
|
962
1581
|
EditorView.lineWrapping, // Always wrap markdown text
|
|
963
1582
|
themeCompartment.of(initialCMTheme),
|
|
1583
|
+
documentTemplateCompartment.of(createDocumentTemplateExtension(initialDocumentTemplate)),
|
|
964
1584
|
readonlyCompartment.of(readonly ? EditorState.readOnly.of(true) : []),
|
|
965
1585
|
placeholderText ? placeholder(placeholderText) : [],
|
|
966
1586
|
// Yjs collaboration - y-codemirror.next handles sync and undo
|
|
@@ -972,13 +1592,26 @@ function create(target, options = {}) {
|
|
|
972
1592
|
// Initially empty, configured after api is created
|
|
973
1593
|
keymapCompartment.of([]),
|
|
974
1594
|
outputWidgetPlugin, // ANSI output rendering
|
|
1595
|
+
...createInlineEditingExtensions(),
|
|
975
1596
|
lineHeightTracker, // ViewPlugin: tracks line height for spacer calculations
|
|
1597
|
+
linkedTableMarkdownState,
|
|
976
1598
|
blockDecorations, // StateField for tables, display math (multi-line)
|
|
977
1599
|
markdownRenderer, // ViewPlugin for everything else (inline)
|
|
1600
|
+
pageViewPagination, // ViewPlugin: page-view spacers at page boundaries
|
|
1601
|
+
...createWysiwygExtensions(),
|
|
1602
|
+
...commentSyntaxModule.createCommentSyntaxExtension(),
|
|
978
1603
|
// Wiki-link completion - just the facet for project files
|
|
979
1604
|
// The actual completion is provided by runtime-lsp (via additionalSources)
|
|
980
1605
|
// or by a standalone autocompletion added below if no runtime providers exist
|
|
981
1606
|
projectFilesCompartment.of(projectFilesFacet.of([])),
|
|
1607
|
+
// Section controls are configured after API creation
|
|
1608
|
+
sectionControlsCompartment.of([]),
|
|
1609
|
+
// Source mode (show all raw markdown syntax)
|
|
1610
|
+
sourceModeCompartment.of(sourceModeFacet.of(false)),
|
|
1611
|
+
// WYSIWYG mode (fully rendered, syntax-protected editing)
|
|
1612
|
+
wysiwygModeCompartment.of(wysiwygModeFacet.of(false)),
|
|
1613
|
+
// Invisible characters (whitespace visualization)
|
|
1614
|
+
invisiblesCompartment.of([]),
|
|
982
1615
|
];
|
|
983
1616
|
|
|
984
1617
|
// Inject markdown styles
|
|
@@ -1048,6 +1681,7 @@ function create(target, options = {}) {
|
|
|
1048
1681
|
|
|
1049
1682
|
// Event handlers
|
|
1050
1683
|
const changeHandlers = [];
|
|
1684
|
+
const selectionHandlers = [];
|
|
1051
1685
|
const saveHandlers = [];
|
|
1052
1686
|
const frontmatterTitleCommitHandlers = [];
|
|
1053
1687
|
const viewSourceHandlers = [];
|
|
@@ -1175,6 +1809,12 @@ function create(target, options = {}) {
|
|
|
1175
1809
|
});
|
|
1176
1810
|
runtimeLspExtensions.push(completionExt);
|
|
1177
1811
|
|
|
1812
|
+
const signatureHelpExt = createRuntimeSignatureHelpExtension({
|
|
1813
|
+
providers: runtimeLspProviders,
|
|
1814
|
+
getContent: () => view.state.doc.toString(),
|
|
1815
|
+
});
|
|
1816
|
+
runtimeLspExtensions.push(signatureHelpExt);
|
|
1817
|
+
|
|
1178
1818
|
// Add extensions to the view
|
|
1179
1819
|
view.dispatch({
|
|
1180
1820
|
effects: StateEffect.appendConfig.of(runtimeLspExtensions),
|
|
@@ -1241,6 +1881,7 @@ function create(target, options = {}) {
|
|
|
1241
1881
|
// Runtime
|
|
1242
1882
|
registry,
|
|
1243
1883
|
execution: null, // Set below
|
|
1884
|
+
linkedTables: null, // Set below
|
|
1244
1885
|
|
|
1245
1886
|
// Runtime LSP (hover, completions, variables)
|
|
1246
1887
|
runtimeLspProviders,
|
|
@@ -1266,6 +1907,36 @@ function create(target, options = {}) {
|
|
|
1266
1907
|
return yText;
|
|
1267
1908
|
},
|
|
1268
1909
|
|
|
1910
|
+
getLinkedTableHostContext() {
|
|
1911
|
+
return {
|
|
1912
|
+
...linkedTableHostContext,
|
|
1913
|
+
...(this.linkedTables?.getHostContext?.() || {}),
|
|
1914
|
+
};
|
|
1915
|
+
},
|
|
1916
|
+
|
|
1917
|
+
setLinkedTableHostContext(context = {}) {
|
|
1918
|
+
if (context.projectRoot !== undefined) linkedTableHostContext.projectRoot = context.projectRoot;
|
|
1919
|
+
if (context.documentPath !== undefined) linkedTableHostContext.documentPath = context.documentPath;
|
|
1920
|
+
this.linkedTables?.setHostContext?.(linkedTableHostContext);
|
|
1921
|
+
return this.getLinkedTableHostContext();
|
|
1922
|
+
},
|
|
1923
|
+
|
|
1924
|
+
canImportLinkedTableFromHost(hostApi) {
|
|
1925
|
+
return canImportLinkedTableFromHost(hostApi);
|
|
1926
|
+
},
|
|
1927
|
+
|
|
1928
|
+
insertLinkedTableBlock(blockMarkdown, options = {}) {
|
|
1929
|
+
return insertLinkedTableBlock(this, blockMarkdown, options);
|
|
1930
|
+
},
|
|
1931
|
+
|
|
1932
|
+
async importLinkedTableFromHost(sourceFilePath, options = {}) {
|
|
1933
|
+
return importLinkedTableFromHost(this, {
|
|
1934
|
+
...this.getLinkedTableHostContext(),
|
|
1935
|
+
...options,
|
|
1936
|
+
sourceFilePath,
|
|
1937
|
+
});
|
|
1938
|
+
},
|
|
1939
|
+
|
|
1269
1940
|
setContent(text) {
|
|
1270
1941
|
view.dispatch({
|
|
1271
1942
|
changes: { from: 0, to: view.state.doc.length, insert: text }
|
|
@@ -1291,6 +1962,82 @@ function create(target, options = {}) {
|
|
|
1291
1962
|
});
|
|
1292
1963
|
},
|
|
1293
1964
|
|
|
1965
|
+
// ===========================================================================
|
|
1966
|
+
// WYSIWYG Editing Helpers
|
|
1967
|
+
// ===========================================================================
|
|
1968
|
+
|
|
1969
|
+
toggleBold() {
|
|
1970
|
+
return toggleInlineMark(view, 'bold');
|
|
1971
|
+
},
|
|
1972
|
+
|
|
1973
|
+
toggleItalic() {
|
|
1974
|
+
return toggleInlineMark(view, 'italic');
|
|
1975
|
+
},
|
|
1976
|
+
|
|
1977
|
+
toggleUnderline() {
|
|
1978
|
+
return toggleInlineMark(view, 'underline');
|
|
1979
|
+
},
|
|
1980
|
+
|
|
1981
|
+
toggleStrikethrough() {
|
|
1982
|
+
return toggleInlineMark(view, 'strike');
|
|
1983
|
+
},
|
|
1984
|
+
|
|
1985
|
+
toggleInlineCode() {
|
|
1986
|
+
return toggleInlineMark(view, 'code');
|
|
1987
|
+
},
|
|
1988
|
+
|
|
1989
|
+
setBlockType(type, options = {}) {
|
|
1990
|
+
return setCurrentBlockType(view, type, options);
|
|
1991
|
+
},
|
|
1992
|
+
|
|
1993
|
+
insertLink(url, text = null) {
|
|
1994
|
+
return insertLinkAtSelection(view, url, text);
|
|
1995
|
+
},
|
|
1996
|
+
|
|
1997
|
+
insertCodeBlock(language = '') {
|
|
1998
|
+
return insertCodeBlockAtCursor(view, language);
|
|
1999
|
+
},
|
|
2000
|
+
|
|
2001
|
+
/**
|
|
2002
|
+
* Delete the fenced code block surrounding the cursor.
|
|
2003
|
+
* @returns {boolean} true if a code block was found and deleted
|
|
2004
|
+
*/
|
|
2005
|
+
deleteCodeBlock() {
|
|
2006
|
+
const pos = view.state.selection.main.head;
|
|
2007
|
+
const fence = findFencedCodeAt(view.state, pos);
|
|
2008
|
+
if (!fence) return false;
|
|
2009
|
+
const doc = view.state.doc;
|
|
2010
|
+
let delFrom = fence.from;
|
|
2011
|
+
let delTo = Math.min(fence.to, doc.length);
|
|
2012
|
+
if (delFrom > 0 && doc.sliceString(delFrom - 1, delFrom) === '\n') delFrom--;
|
|
2013
|
+
if (delTo < doc.length && doc.sliceString(delTo, delTo + 1) === '\n') delTo++;
|
|
2014
|
+
view.dispatch({
|
|
2015
|
+
changes: { from: delFrom, to: delTo, insert: '' },
|
|
2016
|
+
userEvent: 'delete.wysiwyg.delete-codeblock',
|
|
2017
|
+
});
|
|
2018
|
+
return true;
|
|
2019
|
+
},
|
|
2020
|
+
|
|
2021
|
+
insertImage(url, alt = 'image') {
|
|
2022
|
+
return insertImageAtSelection(view, url, alt);
|
|
2023
|
+
},
|
|
2024
|
+
|
|
2025
|
+
getCurrentBlockType() {
|
|
2026
|
+
return getCurrentBlockTypeInfo(view);
|
|
2027
|
+
},
|
|
2028
|
+
|
|
2029
|
+
getSelectionFormatting() {
|
|
2030
|
+
return getSelectionFormattingInfo(view);
|
|
2031
|
+
},
|
|
2032
|
+
|
|
2033
|
+
onSelectionChange(callback) {
|
|
2034
|
+
selectionHandlers.push(callback);
|
|
2035
|
+
return () => {
|
|
2036
|
+
const idx = selectionHandlers.indexOf(callback);
|
|
2037
|
+
if (idx >= 0) selectionHandlers.splice(idx, 1);
|
|
2038
|
+
};
|
|
2039
|
+
},
|
|
2040
|
+
|
|
1294
2041
|
// ===========================================================================
|
|
1295
2042
|
// Streaming Writer
|
|
1296
2043
|
// ===========================================================================
|
|
@@ -1337,6 +2084,218 @@ function create(target, options = {}) {
|
|
|
1337
2084
|
return getThemeNames();
|
|
1338
2085
|
},
|
|
1339
2086
|
|
|
2087
|
+
/**
|
|
2088
|
+
* Apply a semantic document template to the editor content surface.
|
|
2089
|
+
* This is separate from the app/editor chrome theme.
|
|
2090
|
+
*
|
|
2091
|
+
* @param {object} template
|
|
2092
|
+
* @returns {object} normalized template
|
|
2093
|
+
*/
|
|
2094
|
+
setDocumentTemplate(template) {
|
|
2095
|
+
const next = normalizeDocumentTemplate(template || defaultDocumentTemplate);
|
|
2096
|
+
this._documentTemplate = cloneDocumentTemplate(next);
|
|
2097
|
+
this._documentTemplateName = next.name || 'Untitled Template';
|
|
2098
|
+
view.dispatch({
|
|
2099
|
+
effects: documentTemplateCompartment.reconfigure(
|
|
2100
|
+
createDocumentTemplateExtension(next)
|
|
2101
|
+
),
|
|
2102
|
+
});
|
|
2103
|
+
return this.getDocumentTemplate();
|
|
2104
|
+
},
|
|
2105
|
+
|
|
2106
|
+
getDocumentTemplate() {
|
|
2107
|
+
return cloneDocumentTemplate(this._documentTemplate || initialDocumentTemplate);
|
|
2108
|
+
},
|
|
2109
|
+
|
|
2110
|
+
getDocumentTemplateName() {
|
|
2111
|
+
return this._documentTemplateName || this._documentTemplate?.name || initialDocumentTemplate.name;
|
|
2112
|
+
},
|
|
2113
|
+
|
|
2114
|
+
getDocumentTemplatePresets() {
|
|
2115
|
+
return documentTemplatePresets.map(cloneDocumentTemplate);
|
|
2116
|
+
},
|
|
2117
|
+
|
|
2118
|
+
compileDocumentTemplate(template = null) {
|
|
2119
|
+
return compileDocumentTemplateCSS(template || this.getDocumentTemplate());
|
|
2120
|
+
},
|
|
2121
|
+
|
|
2122
|
+
serializeDocumentTemplate(template = null, scope = '.markdown-body') {
|
|
2123
|
+
return serializeDocumentTemplateToCss(template || this.getDocumentTemplate(), scope);
|
|
2124
|
+
},
|
|
2125
|
+
|
|
2126
|
+
/**
|
|
2127
|
+
* Serialize the current document template to Pandoc YAML metadata.
|
|
2128
|
+
* @param {object} [template]
|
|
2129
|
+
* @returns {string} YAML string
|
|
2130
|
+
*/
|
|
2131
|
+
serializeDocumentTemplatePandoc(template = null) {
|
|
2132
|
+
return serializeDocumentTemplateToPandocYaml(template || this.getDocumentTemplate());
|
|
2133
|
+
},
|
|
2134
|
+
|
|
2135
|
+
/**
|
|
2136
|
+
* Serialize the current document template to a LaTeX preamble.
|
|
2137
|
+
* @param {object} [template]
|
|
2138
|
+
* @returns {string} LaTeX commands
|
|
2139
|
+
*/
|
|
2140
|
+
serializeDocumentTemplateLatex(template = null) {
|
|
2141
|
+
return serializeDocumentTemplateToLatexPreamble(template || this.getDocumentTemplate());
|
|
2142
|
+
},
|
|
2143
|
+
|
|
2144
|
+
/**
|
|
2145
|
+
* Serialize the current document template to a standalone HTML wrapper.
|
|
2146
|
+
* @param {object} [template]
|
|
2147
|
+
* @param {object} [options]
|
|
2148
|
+
* @returns {string} HTML document string
|
|
2149
|
+
*/
|
|
2150
|
+
serializeDocumentTemplateHtml(template = null, options = {}) {
|
|
2151
|
+
return serializeDocumentTemplateToHtml(template || this.getDocumentTemplate(), options);
|
|
2152
|
+
},
|
|
2153
|
+
|
|
2154
|
+
/**
|
|
2155
|
+
* Get a Word style mapping for the current document template.
|
|
2156
|
+
* @param {object} [template]
|
|
2157
|
+
* @returns {object}
|
|
2158
|
+
*/
|
|
2159
|
+
getDocumentTemplateWordStyleMap(template = null) {
|
|
2160
|
+
return serializeDocumentTemplateToWordStyleMap(template || this.getDocumentTemplate());
|
|
2161
|
+
},
|
|
2162
|
+
|
|
2163
|
+
/**
|
|
2164
|
+
* Generate a recommended Pandoc command for the current document template.
|
|
2165
|
+
* @param {object} options - { format, input, output, referenceDoc, preambleFile }
|
|
2166
|
+
* @returns {string}
|
|
2167
|
+
*/
|
|
2168
|
+
buildPandocCommand(options = {}) {
|
|
2169
|
+
return buildPandocCommand(this.getDocumentTemplate(), options);
|
|
2170
|
+
},
|
|
2171
|
+
|
|
2172
|
+
bindDocumentTemplate(name) {
|
|
2173
|
+
const result = updateFrontmatterField(this.getContent(), 'template', name);
|
|
2174
|
+
if (!result) return false;
|
|
2175
|
+
view.dispatch({
|
|
2176
|
+
changes: result.changes,
|
|
2177
|
+
userEvent: 'input.document-template-binding',
|
|
2178
|
+
});
|
|
2179
|
+
return true;
|
|
2180
|
+
},
|
|
2181
|
+
|
|
2182
|
+
/**
|
|
2183
|
+
* Update section controls configuration.
|
|
2184
|
+
* @param {{enabled?: boolean, showAi?: boolean, showFormatting?: boolean, mode?: 'full' | 'dots-hover' | 'dots-click'}} updates
|
|
2185
|
+
*/
|
|
2186
|
+
setSectionControls(updates = {}) {
|
|
2187
|
+
this.config.sectionControls = {
|
|
2188
|
+
...this.config.sectionControls,
|
|
2189
|
+
...updates,
|
|
2190
|
+
};
|
|
2191
|
+
},
|
|
2192
|
+
|
|
2193
|
+
/**
|
|
2194
|
+
* Get current section controls configuration.
|
|
2195
|
+
* @returns {{enabled: boolean, showAi: boolean, showFormatting: boolean, mode: string}}
|
|
2196
|
+
*/
|
|
2197
|
+
getSectionControls() {
|
|
2198
|
+
return { ...(this.config.sectionControls || {}) };
|
|
2199
|
+
},
|
|
2200
|
+
|
|
2201
|
+
// ===========================================================================
|
|
2202
|
+
// Source Mode, WYSIWYG Mode & Invisible Characters
|
|
2203
|
+
// ===========================================================================
|
|
2204
|
+
|
|
2205
|
+
/** @private */
|
|
2206
|
+
_sourceMode: false,
|
|
2207
|
+
/** @private */
|
|
2208
|
+
_wysiwygMode: false,
|
|
2209
|
+
/** @private */
|
|
2210
|
+
_showInvisibles: false,
|
|
2211
|
+
/** @private */
|
|
2212
|
+
_documentTemplate: cloneDocumentTemplate(initialDocumentTemplate),
|
|
2213
|
+
/** @private */
|
|
2214
|
+
_documentTemplateName: initialDocumentTemplate.name || 'Default',
|
|
2215
|
+
|
|
2216
|
+
/**
|
|
2217
|
+
* Toggle source mode (show all raw markdown syntax).
|
|
2218
|
+
* Mutually exclusive with WYSIWYG mode.
|
|
2219
|
+
*
|
|
2220
|
+
* @param {boolean} [value] - true=on, false=off. Omit to toggle.
|
|
2221
|
+
* @returns {boolean} The new state
|
|
2222
|
+
*/
|
|
2223
|
+
setSourceMode(value) {
|
|
2224
|
+
const newValue = value !== undefined ? !!value : !this._sourceMode;
|
|
2225
|
+
this._sourceMode = newValue;
|
|
2226
|
+
if (newValue) this._wysiwygMode = false;
|
|
2227
|
+
view.dispatch({
|
|
2228
|
+
effects: [
|
|
2229
|
+
sourceModeCompartment.reconfigure(sourceModeFacet.of(newValue)),
|
|
2230
|
+
wysiwygModeCompartment.reconfigure(wysiwygModeFacet.of(false)),
|
|
2231
|
+
],
|
|
2232
|
+
});
|
|
2233
|
+
return newValue;
|
|
2234
|
+
},
|
|
2235
|
+
|
|
2236
|
+
/**
|
|
2237
|
+
* Get current source mode state.
|
|
2238
|
+
* @returns {boolean}
|
|
2239
|
+
*/
|
|
2240
|
+
getSourceMode() {
|
|
2241
|
+
return this._sourceMode;
|
|
2242
|
+
},
|
|
2243
|
+
|
|
2244
|
+
/**
|
|
2245
|
+
* Toggle WYSIWYG mode (fully rendered, syntax-protected editing).
|
|
2246
|
+
* Mutually exclusive with source mode.
|
|
2247
|
+
*
|
|
2248
|
+
* @param {boolean} [value] - true=on, false=off. Omit to toggle.
|
|
2249
|
+
* @returns {boolean} The new state
|
|
2250
|
+
*/
|
|
2251
|
+
setWysiwygMode(value) {
|
|
2252
|
+
const newValue = value !== undefined ? !!value : !this._wysiwygMode;
|
|
2253
|
+
this._wysiwygMode = newValue;
|
|
2254
|
+
if (newValue) this._sourceMode = false;
|
|
2255
|
+
view.dispatch({
|
|
2256
|
+
effects: [
|
|
2257
|
+
wysiwygModeCompartment.reconfigure(wysiwygModeFacet.of(newValue)),
|
|
2258
|
+
sourceModeCompartment.reconfigure(sourceModeFacet.of(false)),
|
|
2259
|
+
],
|
|
2260
|
+
});
|
|
2261
|
+
return newValue;
|
|
2262
|
+
},
|
|
2263
|
+
|
|
2264
|
+
/**
|
|
2265
|
+
* Get current WYSIWYG mode state.
|
|
2266
|
+
* @returns {boolean}
|
|
2267
|
+
*/
|
|
2268
|
+
getWysiwygMode() {
|
|
2269
|
+
return this._wysiwygMode;
|
|
2270
|
+
},
|
|
2271
|
+
|
|
2272
|
+
/**
|
|
2273
|
+
* Toggle invisible characters (spaces, tabs, newlines).
|
|
2274
|
+
* When enabled, spaces are shown as dots, tabs as arrows, and
|
|
2275
|
+
* newlines as ¶ symbols.
|
|
2276
|
+
*
|
|
2277
|
+
* @param {boolean} [value] - true=on, false=off. Omit to toggle.
|
|
2278
|
+
* @returns {boolean} The new state
|
|
2279
|
+
*/
|
|
2280
|
+
setShowInvisibles(value) {
|
|
2281
|
+
const newValue = value !== undefined ? !!value : !this._showInvisibles;
|
|
2282
|
+
this._showInvisibles = newValue;
|
|
2283
|
+
view.dispatch({
|
|
2284
|
+
effects: invisiblesCompartment.reconfigure(
|
|
2285
|
+
newValue ? createInvisiblesExtension() : []
|
|
2286
|
+
),
|
|
2287
|
+
});
|
|
2288
|
+
return newValue;
|
|
2289
|
+
},
|
|
2290
|
+
|
|
2291
|
+
/**
|
|
2292
|
+
* Get current invisible characters state.
|
|
2293
|
+
* @returns {boolean}
|
|
2294
|
+
*/
|
|
2295
|
+
getShowInvisibles() {
|
|
2296
|
+
return this._showInvisibles;
|
|
2297
|
+
},
|
|
2298
|
+
|
|
1340
2299
|
// ===========================================================================
|
|
1341
2300
|
// Wiki-link completion
|
|
1342
2301
|
// ===========================================================================
|
|
@@ -1981,14 +2940,13 @@ function create(target, options = {}) {
|
|
|
1981
2940
|
},
|
|
1982
2941
|
|
|
1983
2942
|
/**
|
|
1984
|
-
*
|
|
2943
|
+
* Get source code for symbol at cursor position, without emitting UI callbacks.
|
|
1985
2944
|
* Calls inspect with detail=2 to get full source code.
|
|
1986
|
-
* Triggers registered onViewSource callbacks.
|
|
1987
2945
|
*
|
|
1988
2946
|
* @param {number} [pos] - Position (defaults to cursor)
|
|
1989
2947
|
* @returns {Promise<{found: boolean, name?: string, sourceCode?: string, file?: string, ...}|null>}
|
|
1990
2948
|
*/
|
|
1991
|
-
async
|
|
2949
|
+
async getSourceInfo(pos) {
|
|
1992
2950
|
const position = pos ?? view.state.selection.main.head;
|
|
1993
2951
|
const content = this.getContent();
|
|
1994
2952
|
const cell = getCellAtCursor(content, position);
|
|
@@ -2003,7 +2961,19 @@ function create(target, options = {}) {
|
|
|
2003
2961
|
if (!provider) return null;
|
|
2004
2962
|
|
|
2005
2963
|
const offset = position - cell.codeStart;
|
|
2006
|
-
|
|
2964
|
+
return provider.inspect(cell.code, offset, cell.language, { detail: 2 });
|
|
2965
|
+
},
|
|
2966
|
+
|
|
2967
|
+
/**
|
|
2968
|
+
* View source code for symbol at cursor position.
|
|
2969
|
+
* Calls inspect with detail=2 to get full source code.
|
|
2970
|
+
* Triggers registered onViewSource callbacks.
|
|
2971
|
+
*
|
|
2972
|
+
* @param {number} [pos] - Position (defaults to cursor)
|
|
2973
|
+
* @returns {Promise<{found: boolean, name?: string, sourceCode?: string, file?: string, ...}|null>}
|
|
2974
|
+
*/
|
|
2975
|
+
async viewSource(pos) {
|
|
2976
|
+
const result = await this.getSourceInfo(pos);
|
|
2007
2977
|
|
|
2008
2978
|
// Trigger callbacks if we got a result
|
|
2009
2979
|
if (result && result.found) {
|
|
@@ -2204,6 +3174,9 @@ function create(target, options = {}) {
|
|
|
2204
3174
|
|
|
2205
3175
|
destroy() {
|
|
2206
3176
|
this.execution.cancelAll();
|
|
3177
|
+
if (this.linkedTables?.destroy) {
|
|
3178
|
+
this.linkedTables.destroy();
|
|
3179
|
+
}
|
|
2207
3180
|
if (cellControls) {
|
|
2208
3181
|
cellControls.destroy();
|
|
2209
3182
|
}
|
|
@@ -2268,6 +3241,13 @@ function create(target, options = {}) {
|
|
|
2268
3241
|
// Create execution manager
|
|
2269
3242
|
api.execution = createExecutionManager(api, registry);
|
|
2270
3243
|
|
|
3244
|
+
// Create linked-table action/job controller
|
|
3245
|
+
api.linkedTables = createLinkedTableController({
|
|
3246
|
+
editor: api,
|
|
3247
|
+
projectRoot: linkedTableHostContext.projectRoot,
|
|
3248
|
+
documentPath: linkedTableHostContext.documentPath,
|
|
3249
|
+
});
|
|
3250
|
+
|
|
2271
3251
|
// Configure keymap now that api is ready
|
|
2272
3252
|
// Merge user keybindings with defaults
|
|
2273
3253
|
const userKeybindings = options.keymap || {};
|
|
@@ -2303,6 +3283,16 @@ function create(target, options = {}) {
|
|
|
2303
3283
|
return { ...currentKeybindings };
|
|
2304
3284
|
};
|
|
2305
3285
|
|
|
3286
|
+
// Initialize section controls now that API exists
|
|
3287
|
+
const applySectionControlsConfig = () => {
|
|
3288
|
+
const options = reactiveConfig.sectionControls || {};
|
|
3289
|
+
const extension = options.enabled === false ? [] : sectionControls(api, options);
|
|
3290
|
+
view.dispatch({
|
|
3291
|
+
effects: sectionControlsCompartment.reconfigure(extension),
|
|
3292
|
+
});
|
|
3293
|
+
};
|
|
3294
|
+
applySectionControlsConfig();
|
|
3295
|
+
|
|
2306
3296
|
// Wire execution events to awareness (so execution badges work automatically)
|
|
2307
3297
|
// This makes the runtime appear as a collaborator executing code
|
|
2308
3298
|
if (awarenessSystem) {
|
|
@@ -2458,6 +3448,11 @@ function create(target, options = {}) {
|
|
|
2458
3448
|
});
|
|
2459
3449
|
|
|
2460
3450
|
reactiveConfig._subscribe(configHandler);
|
|
3451
|
+
reactiveConfig._subscribe((event) => {
|
|
3452
|
+
if (event.path[0] === 'sectionControls') {
|
|
3453
|
+
applySectionControlsConfig();
|
|
3454
|
+
}
|
|
3455
|
+
});
|
|
2461
3456
|
|
|
2462
3457
|
// =========================================================================
|
|
2463
3458
|
// UPDATE DOCUMENT STATE
|
|
@@ -2490,6 +3485,21 @@ function create(target, options = {}) {
|
|
|
2490
3485
|
stateManager.setDirty(true);
|
|
2491
3486
|
updateDocumentState();
|
|
2492
3487
|
}
|
|
3488
|
+
|
|
3489
|
+
if (update.docChanged || update.selectionSet) {
|
|
3490
|
+
const payload = {
|
|
3491
|
+
selection: update.state.selection.main,
|
|
3492
|
+
block: getCurrentBlockTypeInfo(update.view),
|
|
3493
|
+
formatting: getSelectionFormattingInfo(update.view),
|
|
3494
|
+
};
|
|
3495
|
+
selectionHandlers.forEach(fn => {
|
|
3496
|
+
try {
|
|
3497
|
+
fn(payload);
|
|
3498
|
+
} catch (err) {
|
|
3499
|
+
console.warn('[mrmd] selection change handler failed:', err);
|
|
3500
|
+
}
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
2493
3503
|
});
|
|
2494
3504
|
|
|
2495
3505
|
// Add update listener extension
|
|
@@ -2996,22 +4006,6 @@ const cellControlsExports = {
|
|
|
2996
4006
|
};
|
|
2997
4007
|
// #endregion CELL_CONTROLS_EXPORTS
|
|
2998
4008
|
|
|
2999
|
-
// #region RUNTIME_CODELENS_EXPORTS
|
|
3000
|
-
const runtimeCodeLensExports = {
|
|
3001
|
-
createExtensions: createRuntimeCodeLensExtensions,
|
|
3002
|
-
rebuild: rebuildRuntimeCodeLens,
|
|
3003
|
-
facet: runtimeCodeLensFacet,
|
|
3004
|
-
plugin: runtimeCodeLensPlugin,
|
|
3005
|
-
rebuildEffect: rebuildRuntimeCodeLensEffect,
|
|
3006
|
-
injectStyles: injectRuntimeCodeLensStyles,
|
|
3007
|
-
removeStyles: removeRuntimeCodeLensStyles,
|
|
3008
|
-
findBlocks: findRuntimeBlocks,
|
|
3009
|
-
findYamlConfigBlocks,
|
|
3010
|
-
findSessionFrontmatter,
|
|
3011
|
-
Widget: RuntimeCodeLensWidget,
|
|
3012
|
-
};
|
|
3013
|
-
// #endregion RUNTIME_CODELENS_EXPORTS
|
|
3014
|
-
|
|
3015
4009
|
// #region RUNTIME_LSP_EXPORTS
|
|
3016
4010
|
const runtimeLspExports = {
|
|
3017
4011
|
// Adapters
|
|
@@ -3039,6 +4033,16 @@ const markdownExports = {
|
|
|
3039
4033
|
// Asset resolver facet (for Electron/desktop apps)
|
|
3040
4034
|
assetResolverFacet,
|
|
3041
4035
|
|
|
4036
|
+
// Mode facets
|
|
4037
|
+
sourceModeFacet,
|
|
4038
|
+
wysiwygModeFacet,
|
|
4039
|
+
createInlineEditingExtensions,
|
|
4040
|
+
createWysiwygExtensions,
|
|
4041
|
+
toggleInlineFormat,
|
|
4042
|
+
toggleInlineMark,
|
|
4043
|
+
getSelectionFormattingState,
|
|
4044
|
+
findFencedCodeAt,
|
|
4045
|
+
|
|
3042
4046
|
// Styles
|
|
3043
4047
|
markdownStyles,
|
|
3044
4048
|
injectMarkdownStyles,
|
|
@@ -3054,6 +4058,28 @@ const markdownExports = {
|
|
|
3054
4058
|
isTableDelimiter,
|
|
3055
4059
|
generateTableId,
|
|
3056
4060
|
AlertTitleWidget,
|
|
4061
|
+
|
|
4062
|
+
// Page view pagination
|
|
4063
|
+
pageViewPagination,
|
|
4064
|
+
};
|
|
4065
|
+
|
|
4066
|
+
const documentTemplateExports = {
|
|
4067
|
+
defaultDocumentTemplate,
|
|
4068
|
+
documentTemplatePresets,
|
|
4069
|
+
normalizeDocumentTemplate,
|
|
4070
|
+
cloneDocumentTemplate,
|
|
4071
|
+
createDocumentTemplateExtension,
|
|
4072
|
+
compileDocumentTemplateCSS,
|
|
4073
|
+
serializeDocumentTemplateToCss,
|
|
4074
|
+
findDocumentTemplatePreset,
|
|
4075
|
+
// Multi-format export
|
|
4076
|
+
resolveFontForExport,
|
|
4077
|
+
serializeDocumentTemplateToPandocMeta,
|
|
4078
|
+
serializeDocumentTemplateToPandocYaml,
|
|
4079
|
+
serializeDocumentTemplateToLatexPreamble,
|
|
4080
|
+
serializeDocumentTemplateToHtml,
|
|
4081
|
+
serializeDocumentTemplateToWordStyleMap,
|
|
4082
|
+
buildPandocCommand,
|
|
3057
4083
|
};
|
|
3058
4084
|
// #endregion MARKDOWN_EXPORTS
|
|
3059
4085
|
|
|
@@ -3076,6 +4102,7 @@ const mrmd = {
|
|
|
3076
4102
|
create,
|
|
3077
4103
|
drive,
|
|
3078
4104
|
runtime,
|
|
4105
|
+
findInitialCursorPosition,
|
|
3079
4106
|
yjs,
|
|
3080
4107
|
codemirror,
|
|
3081
4108
|
terminal,
|
|
@@ -3087,14 +4114,22 @@ const mrmd = {
|
|
|
3087
4114
|
stateUtils: stateExports,
|
|
3088
4115
|
// Cell controls (run buttons, queue, status)
|
|
3089
4116
|
cellControls: cellControlsExports,
|
|
3090
|
-
// Runtime CodeLens (inline session controls above yaml config blocks)
|
|
3091
|
-
runtimeCodeLens: runtimeCodeLensExports,
|
|
3092
4117
|
// Runtime LSP (hover, completions, variables)
|
|
3093
4118
|
runtimeLsp: runtimeLspExports,
|
|
3094
4119
|
// Wiki-link completion ([[internal-links]])
|
|
3095
4120
|
wikiLink: wikiLinkExports,
|
|
4121
|
+
// Linked tables
|
|
4122
|
+
linkedTables,
|
|
3096
4123
|
// Markdown rendering (blur→render, focus→source)
|
|
3097
4124
|
markdown: markdownExports,
|
|
4125
|
+
// Frontmatter utilities
|
|
4126
|
+
frontmatter: {
|
|
4127
|
+
parseFrontmatter,
|
|
4128
|
+
readFrontmatterValue,
|
|
4129
|
+
updateFrontmatterField,
|
|
4130
|
+
},
|
|
4131
|
+
// Document templates (semantic content styling)
|
|
4132
|
+
documentTemplates: documentTemplateExports,
|
|
3098
4133
|
// Shell (status bar, file management, studio layout)
|
|
3099
4134
|
shell: shellModule,
|
|
3100
4135
|
// AI Integration (decorations, state, widgets)
|
|
@@ -3134,11 +4169,11 @@ export {
|
|
|
3134
4169
|
create,
|
|
3135
4170
|
drive,
|
|
3136
4171
|
runtime,
|
|
4172
|
+
findInitialCursorPosition,
|
|
3137
4173
|
yjs,
|
|
3138
4174
|
codemirror,
|
|
3139
4175
|
terminal,
|
|
3140
4176
|
awarenessExports as awareness,
|
|
3141
|
-
runtimeCodeLensExports as runtimeCodeLens,
|
|
3142
4177
|
RuntimeRegistry,
|
|
3143
4178
|
createRuntimeRegistry,
|
|
3144
4179
|
createJavaScriptRuntime,
|
|
@@ -3222,23 +4257,26 @@ export {
|
|
|
3222
4257
|
cellControlsExports,
|
|
3223
4258
|
createCellControls,
|
|
3224
4259
|
CellControlsSystem,
|
|
3225
|
-
// Runtime CodeLens exports
|
|
3226
|
-
runtimeCodeLensExports,
|
|
3227
|
-
createRuntimeCodeLensExtensions,
|
|
3228
|
-
rebuildRuntimeCodeLens,
|
|
3229
|
-
runtimeCodeLensFacet,
|
|
3230
|
-
runtimeCodeLensPlugin,
|
|
3231
|
-
rebuildRuntimeCodeLensEffect,
|
|
3232
|
-
injectRuntimeCodeLensStyles,
|
|
3233
|
-
removeRuntimeCodeLensStyles,
|
|
3234
|
-
findRuntimeBlocks,
|
|
3235
|
-
findYamlConfigBlocks,
|
|
3236
|
-
findSessionFrontmatter,
|
|
3237
|
-
RuntimeCodeLensWidget,
|
|
3238
4260
|
// Monitor coordination exports
|
|
3239
4261
|
MonitorCoordination,
|
|
3240
4262
|
EXECUTION_STATUS,
|
|
3241
4263
|
createMonitorCoordination,
|
|
4264
|
+
// Linked table exports
|
|
4265
|
+
linkedTables as linkedTablesModule,
|
|
4266
|
+
LINKED_TABLE_EVENT,
|
|
4267
|
+
dispatchLinkedTableAction,
|
|
4268
|
+
openLinkedTableWorkspace,
|
|
4269
|
+
canImportLinkedTableFromHost,
|
|
4270
|
+
normalizeLinkedTableBlockInsertion,
|
|
4271
|
+
insertLinkedTableBlock,
|
|
4272
|
+
importLinkedTableFromHost,
|
|
4273
|
+
TableJobsClient,
|
|
4274
|
+
TABLE_JOB_STATUS,
|
|
4275
|
+
createTableJobsClient,
|
|
4276
|
+
LinkedTableController,
|
|
4277
|
+
createLinkedTableController,
|
|
4278
|
+
createLinkedTableBlockAnchor,
|
|
4279
|
+
resolveLinkedTableBlockAnchor,
|
|
3242
4280
|
// Markdown rendering exports
|
|
3243
4281
|
markdownExports,
|
|
3244
4282
|
markdownRendering as markdown,
|
|
@@ -3256,6 +4294,33 @@ export {
|
|
|
3256
4294
|
isTableDelimiter,
|
|
3257
4295
|
generateTableId,
|
|
3258
4296
|
AlertTitleWidget,
|
|
4297
|
+
// Document template exports
|
|
4298
|
+
documentTemplateExports,
|
|
4299
|
+
defaultDocumentTemplate,
|
|
4300
|
+
documentTemplatePresets,
|
|
4301
|
+
normalizeDocumentTemplate,
|
|
4302
|
+
cloneDocumentTemplate,
|
|
4303
|
+
createDocumentTemplateExtension,
|
|
4304
|
+
compileDocumentTemplateCSS,
|
|
4305
|
+
serializeDocumentTemplateToCss,
|
|
4306
|
+
findDocumentTemplatePreset,
|
|
4307
|
+
resolveFontForExport,
|
|
4308
|
+
serializeDocumentTemplateToPandocMeta,
|
|
4309
|
+
serializeDocumentTemplateToPandocYaml,
|
|
4310
|
+
serializeDocumentTemplateToLatexPreamble,
|
|
4311
|
+
serializeDocumentTemplateToHtml,
|
|
4312
|
+
serializeDocumentTemplateToWordStyleMap,
|
|
4313
|
+
buildPandocCommand,
|
|
4314
|
+
// Spellcheck exports
|
|
4315
|
+
createSpellcheckExtensions,
|
|
4316
|
+
// Grammar exports
|
|
4317
|
+
createLanguageToolDiagnosticsExtension,
|
|
4318
|
+
collectVisibleProseFragments,
|
|
4319
|
+
forceLanguageToolRefresh,
|
|
4320
|
+
refreshLanguageToolDiagnostics,
|
|
4321
|
+
applyFirstLanguageToolSuggestion,
|
|
4322
|
+
getLanguageToolSuggestionMenu,
|
|
4323
|
+
applyLanguageToolSuggestionAt,
|
|
3259
4324
|
// Wiki-link completion exports
|
|
3260
4325
|
wikiLinkExports,
|
|
3261
4326
|
projectFilesFacet,
|
|
@@ -3272,5 +4337,5 @@ export const { createStudio, OrchestratorClient, Drive, createDrive, ShellStateM
|
|
|
3272
4337
|
|
|
3273
4338
|
// Document language detection and frontmatter updater
|
|
3274
4339
|
export { getDocumentLanguages, getLanguageDisplay, isExecutableLanguage } from './document-languages.js';
|
|
3275
|
-
export { parseFrontmatter,
|
|
4340
|
+
export { parseFrontmatter, readFrontmatterSession, getEffectiveSessionConfig, readFrontmatterValue, updateFrontmatterField } from './frontmatter-updater.js';
|
|
3276
4341
|
// #endregion EXPORTS
|