@codingame/monaco-vscode-markdown-language-features-default-extension 36.2.6 → 37.0.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.
Files changed (126) hide show
  1. package/index.js +237 -23
  2. package/package.json +2 -2
  3. package/resources/abnfDiagram-VCTEODGH-MFZBS7QV.js +1 -0
  4. package/resources/architecture-7GRP2DOG-KMESPJ5A.js +1 -0
  5. package/resources/architectureDiagram-5GKGNRK7-MTTZPDNG.js +36 -0
  6. package/resources/blockDiagram-NRAW4CY4-UDRMVB5I.js +129 -0
  7. package/resources/c4Diagram-UCG6FXSJ-CNUYFC6N.js +38 -0
  8. package/resources/chunk-2KBWIZHB.js +14 -0
  9. package/resources/chunk-3CEAP2V5.js +1 -0
  10. package/resources/chunk-43RBMH4Y.js +1 -0
  11. package/resources/chunk-4AUIAGYT.js +1 -0
  12. package/resources/chunk-4PRM3W7S.js +88 -0
  13. package/resources/chunk-5DJUZA4F.js +1 -0
  14. package/resources/chunk-6LQNQPBM.js +1 -0
  15. package/resources/chunk-A3DHMJKP.js +1 -0
  16. package/resources/chunk-ATDVZUJ2.js +231 -0
  17. package/resources/chunk-CBQW2LLN.js +1 -0
  18. package/resources/chunk-CCQNES6G.js +1 -0
  19. package/resources/chunk-CD5RYVNW.js +1 -0
  20. package/resources/chunk-CQFFNO72.js +1 -0
  21. package/resources/chunk-EWILYK3H.js +1 -0
  22. package/resources/chunk-EWVD6AUZ.js +15 -0
  23. package/resources/chunk-EXFMYL6U.js +1 -0
  24. package/resources/chunk-FH3BATM5.js +1 -0
  25. package/resources/chunk-FK4OKHO7.js +178 -0
  26. package/resources/chunk-FZLE27CN.js +1 -0
  27. package/resources/chunk-HRJ5UBVC.js +1 -0
  28. package/resources/chunk-IEMKBRIG.js +339 -0
  29. package/resources/chunk-IRNO36C3.js +1 -0
  30. package/resources/chunk-J4SQIVOP.js +1 -0
  31. package/resources/chunk-JAORNH2M.js +2 -0
  32. package/resources/chunk-JBQZ5UCE.js +1 -0
  33. package/resources/chunk-JFI5BOLY.js +1 -0
  34. package/resources/chunk-JXCTOEX6.js +1 -0
  35. package/resources/chunk-K2JFJOPP.js +1 -0
  36. package/resources/chunk-LK6KML2S.js +10 -0
  37. package/resources/chunk-MNHOAW4R.js +3 -0
  38. package/resources/chunk-NLCJ5AKJ.js +132 -0
  39. package/resources/chunk-OOJM4CTU.js +1 -0
  40. package/resources/chunk-R7CM4BQN.js +2 -0
  41. package/resources/chunk-RBL2QVA3.js +1 -0
  42. package/resources/chunk-SNGMH3JW.js +2 -0
  43. package/resources/chunk-TGD3YKTU.js +32 -0
  44. package/resources/chunk-TGYBLZFN.js +1 -0
  45. package/resources/chunk-U3E5RSXE.js +62 -0
  46. package/resources/chunk-U7TCJVVP.js +1 -0
  47. package/resources/chunk-WSHCWW4Q.js +2 -0
  48. package/resources/chunk-WVYKIHVG.js +1 -0
  49. package/resources/chunk-XCMS6RVA.js +1 -0
  50. package/resources/chunk-XM54GAH3.js +206 -0
  51. package/resources/chunk-YICKNY6Y.js +1 -0
  52. package/resources/chunk-YNKYB66X.js +257 -0
  53. package/resources/chunk-ZMBQGVIF.js +1 -0
  54. package/resources/chunk-ZNREFWKG.js +1 -0
  55. package/resources/chunk-ZU7JKVUR.js +168 -0
  56. package/resources/chunk-ZYRITDEB.js +72 -0
  57. package/resources/classDiagram-DTDB5LWJ-TBJLBGTZ.js +1 -0
  58. package/resources/codicon-R6HEZFPR.ttf +0 -0
  59. package/resources/cose-bilkent-JH36ORCC-7TCRTQWC.js +1 -0
  60. package/resources/cynefin-OW5HDTMX-QMEUXFXO.js +1 -0
  61. package/resources/cynefinDiagram-5FMLGOSQ-J745EYTI.js +62 -0
  62. package/resources/dagre-3AP2YEHR-CUFPL4A5.js +4 -0
  63. package/resources/diagram-S7CK7UJ4-SC5AFKXH.js +30 -0
  64. package/resources/diagram-UQ7AKVKN-3O7WQJXE.js +41 -0
  65. package/resources/diagram-VSXAHHWV-SN3VCCS5.js +3 -0
  66. package/resources/diagram-VX7I27RA-7PQJWMN5.js +24 -0
  67. package/resources/diagram-Z3DM3KII-QZ45TJFK.js +24 -0
  68. package/resources/ebnfDiagram-PWID7BFC-CBJPGG56.js +1 -0
  69. package/resources/editor.css.txt +1 -1
  70. package/resources/editor.js +137 -3431
  71. package/resources/editor.ts +587 -46
  72. package/resources/erDiagram-SSCWMZ5O-TUKYG42P.js +99 -0
  73. package/resources/eventmodeling-NTZA5JFV-MGOPW7BD.js +1 -0
  74. package/resources/extension.js +179 -172
  75. package/resources/extension.js.map +4 -4
  76. package/resources/flowDiagram-A5DVABFB-M6K5X26V.js +1 -0
  77. package/resources/ganttDiagram-EL5Y4UJY-JCSKUU2V.js +292 -0
  78. package/resources/gitGraph-4MIJSDKK-ITDCTFJT.js +1 -0
  79. package/resources/gitGraphDiagram-WWUBYQGX-RXRB5JA2.js +106 -0
  80. package/resources/index.js +3 -3
  81. package/resources/index2.js +17 -17
  82. package/resources/info-A6RAGUB7-VXIEHQAI.js +1 -0
  83. package/resources/infoDiagram-RXCK75RN-2T6V4JS2.js +2 -0
  84. package/resources/ishikawaDiagram-5VMMS53U-6WC4IGXZ.js +70 -0
  85. package/resources/journeyDiagram-EYS64GPL-PZQYT6UF.js +139 -0
  86. package/resources/kanban-definition-3QL26DDD-VYBV345J.js +89 -0
  87. package/resources/katex-QJ7O5CJ5.js +1 -0
  88. package/resources/linkPresentationProvider.ts +179 -0
  89. package/resources/markdown.css.txt +39 -0
  90. package/resources/markdownEditor.css.txt +13 -0
  91. package/resources/mermaid.core-S5KCHHL2.js +18 -0
  92. package/resources/mindmap-definition-FBJOCRG2-H3I5F4WB.js +96 -0
  93. package/resources/package.json +1 -1
  94. package/resources/package.nls.json +1 -1
  95. package/resources/package.schema.json +1 -1
  96. package/resources/packet-AYTQ26CC-WLFKOXIZ.js +1 -0
  97. package/resources/pegDiagram-XKGWAZYB-HUV4U5RU.js +1 -0
  98. package/resources/pie-WAS4IAKB-GZIMD664.js +1 -0
  99. package/resources/pieDiagram-E7YTZNPT-XOJEVCEO.js +39 -0
  100. package/resources/pre.js +1 -1
  101. package/resources/preview-dark.svg +1 -1
  102. package/resources/preview-light.svg +1 -1
  103. package/resources/quadrantDiagram-AXDQQJYC-YPPZYAGP.js +7 -0
  104. package/resources/radar-RG4KPBEZ-URG5LVGS.js +1 -0
  105. package/resources/railroad-74A4TZTK-3ZMXCKLP.js +1 -0
  106. package/resources/railroad-abnf-HS5TGJTU-LBR5MV33.js +1 -0
  107. package/resources/railroad-ebnf-LZEXJU2U-YEVF3EJR.js +1 -0
  108. package/resources/railroad-peg-WCYAUIDC-CV774YSK.js +1 -0
  109. package/resources/railroadDiagram-O6MQD6OU-YZF7JNDT.js +1 -0
  110. package/resources/requirementDiagram-EFPCY7ZU-PZ7BYTDZ.js +84 -0
  111. package/resources/sankeyDiagram-P5KCCOFB-3P5DWREA.js +40 -0
  112. package/resources/sequenceDiagram-WJ2MYXX4-O52K5NPU.js +162 -0
  113. package/resources/sizeCapture-X5ZJPWSS-PRFE7SKK.js +1 -0
  114. package/resources/stateDiagram-HBIQ2CUA-KDPYCQOT.js +1 -0
  115. package/resources/stateDiagram-v2-4QOOHH4V-QBQE56JB.js +1 -0
  116. package/resources/swimlanes-XN3QIQJK-YCLJ3KIH.js +1 -0
  117. package/resources/swimlanesDiagram-VK2B7HYN-RS7VRH6T.js +8 -0
  118. package/resources/syntaxHighlighter.ts +2 -2
  119. package/resources/timeline-definition-24CTP7MA-GQ3MCHCF.js +120 -0
  120. package/resources/treeView-Q6P3EWNA-UEXUSAGC.js +1 -0
  121. package/resources/treemap-WGGIJYW6-NFXJPDAT.js +1 -0
  122. package/resources/tsconfig.json +1 -1
  123. package/resources/vennDiagram-4TSXK5OY-PNMXQJF6.js +34 -0
  124. package/resources/wardley-WFR3VGLG-MQ6DEZYA.js +1 -0
  125. package/resources/wardleyDiagram-VM6X3IG4-46KJTB57.js +78 -0
  126. package/resources/xychartDiagram-S5SC5T6Z-NSN5W7VL.js +7 -0
@@ -3,14 +3,17 @@
3
3
  * Licensed under the MIT License. See License.txt in the project root for license information.
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
 
6
- import { EditorController, EditorModel, EditorView, GutterMarker, OffsetRange, StringEdit, StringValue, findNodeOffsetById, taskCheckboxRange } from '@vscode/markdown-editor';
7
- import { Disposable, autorun } from '@vscode/markdown-editor/observables';
8
- import mermaid from 'mermaid';
6
+ import { AsyncClipboardStrategy, CommentModeController, CommentsModel, CommentsView, EditorController, EditorModel, EditorView, GutterMarker, OffsetRange, Selection, StringEdit, StringReplacement, StringValue, commands, findNodeOffsetById, vscodeHostKeyboardProfile, vscodeLocalKeyboardProfile, type CodeBlockAstNode, type LinkPresentationKind } from '@vscode/markdown-editor';
7
+ import { VirtualizedIframeEmbeddedEditorFactory, type IframeEmbeddedEditorHostTransport, type IframeEmbeddedEditorProvider, type IframeEmbeddedEditorProviderSelector, type ResolvedIframeEmbeddedEditor } from '@vscode/markdown-editor/web-editors';
8
+ import { Disposable, autorun, observableValue } from '@vscode/observables';
9
9
  import 'katex/dist/katex.min.css';
10
10
  import '@vscode/markdown-editor/editor.css';
11
- import '@vscode/markdown-editor/themes/vscode.css';
11
+ import '@vscode/markdown-editor/themes/vscode-default.css';
12
+ import '@vscode/markdown-editor/commentInput.css';
13
+ import '@vscode/markdown-editor/commentWidget.css';
12
14
  import './markdownEditor.css';
13
15
  import { WebviewSyntaxHighlighter } from './syntaxHighlighter';
16
+ import { WebviewLinkPresentationProvider } from './linkPresentationProvider';
14
17
 
15
18
  interface VsCodeApi {
16
19
  postMessage(message: unknown): void;
@@ -20,40 +23,179 @@ interface VsCodeApi {
20
23
 
21
24
  declare function acquireVsCodeApi(): VsCodeApi;
22
25
 
26
+ /**
27
+ * The editor's view state, persisted as webview state (`getState`/`setState`) so
28
+ * the scroll and cursor position are restored when the webview is reloaded or the
29
+ * custom editor is re-created (e.g. after switching sessions and back).
30
+ */
31
+ interface PersistedViewState {
32
+ scrollTop?: number;
33
+ selection?: { anchor: number; active: number };
34
+ }
35
+
36
+ interface CodeBlockEditorProviderDefinition {
37
+ readonly id: string;
38
+ readonly selector: IframeEmbeddedEditorProviderSelector;
39
+ readonly source: { readonly kind: 'static'; readonly descriptor: ResolvedIframeEmbeddedEditor } | { readonly kind: 'exportApi' };
40
+ }
41
+
42
+ interface InitialState {
43
+ readonly content: string;
44
+ readonly documentVersion: number;
45
+ readonly readonly: boolean;
46
+ readonly richLinksEnabled: boolean;
47
+ readonly linkPresentationRules: readonly { id: string; source: string; flags: string; kind: LinkPresentationKind }[];
48
+ }
49
+
50
+ class CodeBlockEditorHostTransport implements IframeEmbeddedEditorHostTransport {
51
+ readonly #listeners = new Set<(message: unknown) => void>();
52
+ readonly #pendingMessages: unknown[] = [];
53
+ readonly #postMessage: (message: unknown) => void;
54
+ readonly #onDispose: () => void;
55
+ #activated = false;
56
+ #disposed = false;
57
+
58
+ readonly onMessage: IframeEmbeddedEditorHostTransport['onMessage'] = (listener: (message: unknown) => void) => {
59
+ if (this.#disposed) {
60
+ throw new Error('Code block editor host transport is disposed');
61
+ }
62
+ this.#listeners.add(listener);
63
+ if (!this.#activated) {
64
+ this.#activated = true;
65
+ for (const message of this.#pendingMessages.splice(0)) {
66
+ listener(message);
67
+ }
68
+ }
69
+ return { dispose: () => this.#listeners.delete(listener) };
70
+ };
71
+
72
+ constructor(
73
+ readonly runtimeId: string,
74
+ postMessage: (message: unknown) => void,
75
+ onDispose: () => void,
76
+ ) {
77
+ this.#postMessage = postMessage;
78
+ this.#onDispose = onDispose;
79
+ }
80
+
81
+ sendMessage(message: unknown): void {
82
+ if (this.#disposed) {
83
+ throw new Error('Code block editor host transport is disposed');
84
+ }
85
+ this.#postMessage(message);
86
+ }
87
+
88
+ acceptMessage(message: unknown): void {
89
+ if (this.#disposed) {
90
+ return;
91
+ }
92
+ if (!this.#activated) {
93
+ this.#pendingMessages.push(message);
94
+ return;
95
+ }
96
+ for (const listener of this.#listeners) {
97
+ listener(message);
98
+ }
99
+ }
100
+
101
+ dispose(): void {
102
+ if (this.#disposed) {
103
+ return;
104
+ }
105
+ this.#disposed = true;
106
+ this.#pendingMessages.length = 0;
107
+ this.#listeners.clear();
108
+ this.#onDispose();
109
+ }
110
+ }
111
+
23
112
  class Editor extends Disposable {
24
113
  readonly model = new EditorModel();
25
114
  isUpdatingFromExtension = false;
115
+ #isUpdatingComments = false;
26
116
  #mermaidCounter = 0;
27
- #initialized = false;
117
+ #codeBlockEditorProviders: readonly CodeBlockEditorProviderDefinition[] = [];
118
+ #nextCodeBlockEditorRequestId = 1;
119
+ #nextCodeBlockEditorRuntimeId = 1;
120
+ readonly #codeBlockEditorRequests = new Map<number, (descriptor: ResolvedIframeEmbeddedEditor | undefined) => void>();
121
+ readonly #codeBlockEditorHostTransports = new Map<string, CodeBlockEditorHostTransport>();
122
+ #controller: EditorController | undefined;
123
+ #view: EditorView | undefined;
124
+ #embeddedCodeEditorFactory: VirtualizedIframeEmbeddedEditorFactory | undefined;
28
125
 
126
+ readonly #comments = new CommentsModel();
127
+ #commentsView: CommentsView | undefined;
128
+ /** Whether the workbench feedback store currently accepts new comments for this resource. */
129
+ readonly #acceptsComments = observableValue<boolean>('acceptsComments', false);
130
+ // the message secret allows to distinguish vscode sending us a message vs a nested iframe
131
+ readonly #messageSecret: string;
29
132
  readonly #vscode = acquireVsCodeApi();
30
133
  readonly #syntaxHighlighter = new WebviewSyntaxHighlighter((message) => this.#vscode.postMessage(message));
134
+ readonly #linkPresentationProvider: WebviewLinkPresentationProvider | undefined;
31
135
 
32
- constructor(host: HTMLElement) {
136
+ constructor(host: HTMLElement, initialState: InitialState) {
33
137
  super();
34
138
 
35
- mermaid.initialize({ startOnLoad: false, theme: 'default' });
139
+ const messageSecret = document.querySelector<HTMLMetaElement>('meta[name="vscode-markdown-editor-message-secret"]')?.content;
140
+ if (!messageSecret) {
141
+ throw new Error('Missing Markdown editor message secret');
142
+ }
143
+ this.#messageSecret = messageSecret;
144
+ this.#linkPresentationProvider = initialState.richLinksEnabled
145
+ ? this._register(new WebviewLinkPresentationProvider(
146
+ initialState.linkPresentationRules,
147
+ message => this.#vscode.postMessage(message),
148
+ ))
149
+ : undefined;
150
+
151
+ this.model.sourceText.set(new StringValue(initialState.content), undefined);
152
+ this.model.readonlyMode.set(initialState.readonly, undefined);
36
153
 
37
154
  window.addEventListener('message', (event) => {
38
155
  const message = event.data;
156
+ if (!message || typeof message !== 'object' || message.messageSecret !== this.#messageSecret) {
157
+ return;
158
+ }
39
159
  if (this.#syntaxHighlighter.handleMessage(message)) {
40
160
  return;
41
161
  }
162
+ if (this.#linkPresentationProvider?.handleMessage(message)) {
163
+ return;
164
+ }
42
165
  switch (message.type) {
43
- case 'init': {
44
- if (!this.#initialized) {
45
- this.#initialized = true;
46
- this.#createView(host, !!message.readonly);
47
- this.model.sourceText.set(new StringValue(message.content), undefined);
48
- }
49
- break;
50
- }
51
166
  case 'update': {
167
+ // `replaceSourceText` (not `sourceText.set`) applies authoritative host
168
+ // text: it maps the selection through the change and clears stale
169
+ // pending-paragraph state, so the caret stays valid after an undo shrinks
170
+ // the document. The guard stops this echoing back as a user edit.
52
171
  this.isUpdatingFromExtension = true;
53
- this.model.sourceText.set(new StringValue(message.content), undefined);
172
+ this.model.replaceSourceText(new StringValue(message.content));
54
173
  this.isUpdatingFromExtension = false;
55
174
  break;
56
175
  }
176
+ case 'codeBlockEditorProviders': {
177
+ const providers = readCodeBlockEditorProviderDefinitions(message.codeBlockEditorProviders);
178
+ this.#codeBlockEditorProviders = providers;
179
+ this.#embeddedCodeEditorFactory?.updateProviders(this.#createIframeProviders(providers));
180
+ break;
181
+ }
182
+ case 'resolvedCodeBlockEditor': {
183
+ if (typeof message.requestId !== 'number') {
184
+ break;
185
+ }
186
+ const resolve = this.#codeBlockEditorRequests.get(message.requestId);
187
+ if (resolve) {
188
+ this.#codeBlockEditorRequests.delete(message.requestId);
189
+ resolve(readResolvedCodeBlockEditor(message.descriptor));
190
+ }
191
+ break;
192
+ }
193
+ case 'codeBlockEditorHostTransportMessage': {
194
+ if (typeof message.runtimeId === 'string') {
195
+ this.#codeBlockEditorHostTransports.get(message.runtimeId)?.acceptMessage(message.message);
196
+ }
197
+ break;
198
+ }
57
199
  case 'gutterMarkers': {
58
200
  const markers: GutterMarker[] = message.markers.map((marker: { start: number; endExclusive: number; type: GutterMarker['type'] }) => ({
59
201
  range: OffsetRange.fromTo(marker.start, marker.endExclusive),
@@ -62,33 +204,92 @@ class Editor extends Disposable {
62
204
  this.model.gutterMarkers.set(markers, undefined);
63
205
  break;
64
206
  }
207
+ case 'comments': {
208
+ this.#isUpdatingComments = true;
209
+ this.#comments.set(message.comments.map((comment: { id: string; start: number; endExclusive: number; body: string; author?: string }) => ({
210
+ id: comment.id,
211
+ range: OffsetRange.fromTo(comment.start, comment.endExclusive),
212
+ body: comment.body,
213
+ author: comment.author,
214
+ })));
215
+ this.#isUpdatingComments = false;
216
+ this.#acceptsComments.set(!!message.acceptsComments, undefined);
217
+ break;
218
+ }
219
+ case 'revealComment': {
220
+ this.#commentsView?.revealComment(message.id);
221
+ break;
222
+ }
223
+ case 'command': {
224
+ const command = commands.find(command => command.id === message.command);
225
+ if (command) {
226
+ this.#controller?.executeCommand(command);
227
+ }
228
+ break;
229
+ }
65
230
  }
66
231
  });
67
232
 
68
- this.#vscode.postMessage({ type: 'ready' });
233
+ this.#createView(host, initialState.content);
234
+ this.#vscode.postMessage({ type: 'ready', documentVersion: initialState.documentVersion });
235
+ this._register({
236
+ dispose: () => {
237
+ for (const resolve of this.#codeBlockEditorRequests.values()) {
238
+ resolve(undefined);
239
+ }
240
+ this.#codeBlockEditorRequests.clear();
241
+ for (const transport of Array.from(this.#codeBlockEditorHostTransports.values())) {
242
+ transport.dispose();
243
+ }
244
+ },
245
+ });
69
246
  }
70
247
 
71
- #createView(host: HTMLElement, readonly: boolean): void {
248
+ #createView(host: HTMLElement, content: string): void {
72
249
  const model = this.model;
250
+ const scriptNonce = document.querySelector<HTMLMetaElement>('meta[name="vscode-markdown-editor-script-nonce"]')?.content;
251
+ const iframeBootstrapUrl = new URL(location.href);
252
+ // Nested frames must use the empty webview bootstrap, not the restricted index.html entrypoint.
253
+ iframeBootstrapUrl.pathname = '/fake.html';
254
+ const embeddedCodeEditorFactory = this._register(new VirtualizedIframeEmbeddedEditorFactory({
255
+ providers: this.#createIframeProviders(this.#codeBlockEditorProviders),
256
+ scriptNonce,
257
+ themeCss: () => `:root { ${document.documentElement.getAttribute('style') ?? ''} }`,
258
+ iframeBootstrapUrl: iframeBootstrapUrl.href,
259
+ onAmbiguous: (language, providers) => this.#vscode.postMessage({
260
+ type: 'codeBlockEditorDiagnostic',
261
+ message: `Ambiguous providers for ${language}: ${providers.map(provider => provider.id).join(', ')}`,
262
+ }),
263
+ onDidChange: () => this.#view?.refreshEmbeddedCodeEditors(),
264
+ }));
265
+ this.#embeddedCodeEditorFactory = embeddedCodeEditorFactory;
266
+ // The scroll + cursor position last persisted for this document, captured
267
+ // before any listener below can overwrite it, so it survives the editor being
268
+ // re-created (e.g. after a session switch).
269
+ const savedViewState = this.#getViewState();
73
270
 
74
271
  const view = this._register(new EditorView(model, {
75
- classNames: ['md-theme-vscode'],
272
+ classNames: ['md-theme-vscode-default'],
76
273
  syntaxHighlighter: this.#syntaxHighlighter,
77
- onToggleCheckbox: (item, newChecked) => {
78
- if (readonly) {
79
- return;
80
- }
274
+ linkPresentationProvider: this.#linkPresentationProvider,
275
+ embeddedCodeEditorFactory,
276
+ onEmbeddedCodeEditorEdit: (block: CodeBlockAstNode, contentEdit: StringEdit) => {
81
277
  const doc = model.document.get();
82
- const itemOffset = findNodeOffsetById(doc, item);
83
- if (itemOffset === undefined) { return; }
84
- const range = taskCheckboxRange(item);
85
- if (!range) { return; }
86
- model.applyEdit(
87
- StringEdit.replace(
88
- range.delta(itemOffset),
89
- newChecked ? '[x]' : '[ ]'
90
- )
91
- );
278
+ const blockOffset = findNodeOffsetById(doc, block);
279
+ if (blockOffset === undefined) { return; }
280
+ const contentStart = blockOffset + block.codeOffset;
281
+ model.applyEdit(new StringEdit(
282
+ contentEdit.replacements.map(replacement => StringReplacement.replace(
283
+ replacement.replaceRange.delta(contentStart),
284
+ replacement.newText,
285
+ )),
286
+ ));
287
+ },
288
+ onOpenLink: url => {
289
+ this.#vscode.postMessage({ type: 'openLink', href: url });
290
+ },
291
+ onToggleCheckbox: (item, newChecked) => {
292
+ model.setTaskCheckboxChecked(item, newChecked);
92
293
  },
93
294
  renderCustomCodeBlock: (language, content) => {
94
295
  if (language !== 'mermaid') {
@@ -96,33 +297,373 @@ class Editor extends Disposable {
96
297
  }
97
298
  const div = document.createElement('div');
98
299
  div.className = 'md-mermaid';
300
+ div.textContent = content;
301
+ div.setAttribute('aria-busy', 'true');
99
302
  const id = `mermaid-${this.#mermaidCounter++}`;
100
- mermaid
101
- .render(id, content)
303
+ loadMermaid()
304
+ .then(mermaid => mermaid.render(id, content))
102
305
  .then(({ svg }) => {
103
306
  div.innerHTML = svg;
307
+ div.setAttribute('aria-busy', 'false');
104
308
  })
105
- .catch(() => {
309
+ .catch(error => {
106
310
  div.textContent = content;
311
+ div.setAttribute('aria-busy', 'false');
312
+ this.#vscode.postMessage({
313
+ type: 'codeBlockEditorDiagnostic',
314
+ message: `Failed to render Mermaid diagram: ${error instanceof Error ? error.message : String(error)}`,
315
+ });
107
316
  });
108
317
  return div;
109
318
  },
110
319
  }));
320
+ this.#view = view;
111
321
 
112
- this._register(new EditorController(model, view));
322
+ // Wire history chords (undo/redo) to the extension so they run against the
323
+ // backing TextDocument's own undo stack. `record` is deliberately omitted:
324
+ // the TextDocument owns the history, and a second local stack would drift
325
+ // from the Edit menu, dirty state and hot exit.
326
+ this.#controller = this._register(new EditorController(model, view, {
327
+ clipboardStrategy: new AsyncClipboardStrategy(),
328
+ keyboardProfile: vscodeLocalKeyboardProfile,
329
+ forwardedKeyboardProfile: vscodeHostKeyboardProfile,
330
+ historyStrategy: {
331
+ undo: () => this.#vscode.postMessage({ type: 'history', command: 'undo' }),
332
+ redo: () => this.#vscode.postMessage({ type: 'history', command: 'redo' }),
333
+ },
334
+ }));
335
+ let lastEditorFocus: boolean | undefined;
336
+ const postEditorFocus = (): void => {
337
+ const focused = document.hasFocus() && document.activeElement === view.element;
338
+ if (focused === lastEditorFocus) {
339
+ return;
340
+ }
341
+ lastEditorFocus = focused;
342
+ this.#vscode.postMessage({ type: 'editorFocusChanged', focused });
343
+ };
344
+ const onFocusOut = (): void => queueMicrotask(postEditorFocus);
345
+ document.addEventListener('focusin', postEditorFocus);
346
+ document.addEventListener('focusout', onFocusOut);
347
+ window.addEventListener('focus', postEditorFocus);
348
+ window.addEventListener('blur', postEditorFocus);
349
+ this._register({
350
+ dispose: () => {
351
+ document.removeEventListener('focusin', postEditorFocus);
352
+ document.removeEventListener('focusout', onFocusOut);
353
+ window.removeEventListener('focus', postEditorFocus);
354
+ window.removeEventListener('blur', postEditorFocus);
355
+ },
356
+ });
113
357
  host.appendChild(view.element);
358
+ postEditorFocus();
359
+
360
+ // Render comments as the VS Code V2 markdown cards. The card colours come
361
+ this.#commentsView = this._register(new CommentsView(this.#comments, view));
362
+ // The comment input (the gdocs-style "add a comment" affordance) is only
363
+ // useful when the workbench feedback store will actually accept the comment;
364
+ // otherwise submitting is a no-op. Mount the controller only while the
365
+ // resource is in scope for a session, and tear it down when it leaves scope.
366
+ let commentController: CommentModeController | undefined;
367
+ this._register(autorun((reader) => {
368
+ const accepts = reader.readObservable(this.#acceptsComments);
369
+ if (accepts && !commentController) {
370
+ commentController = new CommentModeController(model, view, {
371
+ onSubmit: ({ text, range }) => {
372
+ this.#vscode.postMessage({ type: 'addComment', start: range.start, endExclusive: range.endExclusive, text });
373
+ },
374
+ });
375
+ } else if (!accepts && commentController) {
376
+ commentController.dispose();
377
+ commentController = undefined;
378
+ }
379
+ }));
380
+ this._register({ dispose: () => commentController?.dispose() });
114
381
 
115
- if (!readonly) {
116
- let firstTime = true;
117
- this._register(autorun((reader) => {
118
- const text = reader.readObservable(this.model.sourceText).value;
119
- if (!this.isUpdatingFromExtension && !firstTime) {
120
- this.#vscode.postMessage({ type: 'edit', content: text });
382
+ // The comment card's delete button mutates the local CommentsModel
383
+ // directly. Mirror those removals back to the extension so the shared
384
+ // store (and the code editor) stay in sync. Removals coming from an
385
+ // extension-driven update set `#isUpdatingComments`, so they are not
386
+ // echoed back.
387
+ let knownCommentIds = new Set(this.#comments.comments.get().map(comment => comment.id));
388
+ this._register(autorun((reader) => {
389
+ const currentIds = new Set(reader.readObservable(this.#comments.comments).map(comment => comment.id));
390
+ if (!this.#isUpdatingComments) {
391
+ for (const id of knownCommentIds) {
392
+ if (!currentIds.has(id)) {
393
+ this.#vscode.postMessage({ type: 'deleteComment', id });
394
+ }
121
395
  }
122
- firstTime = false;
123
- }));
396
+ }
397
+ knownCommentIds = currentIds;
398
+ }));
399
+
400
+ if (savedViewState.selection) {
401
+ const max = content.length;
402
+ const anchor = Math.min(savedViewState.selection.anchor, max);
403
+ const active = Math.min(savedViewState.selection.active, max);
404
+ model.selection.set(new Selection(anchor, active), undefined);
124
405
  }
406
+
407
+ // Persist scroll as webview state (throttled to a frame). Registered after the
408
+ // restore above so it never clobbers the values we are about to restore.
409
+ let scrollSaveScheduled = false;
410
+ const saveScroll = (): void => {
411
+ scrollSaveScheduled = false;
412
+ this.#patchViewState({ scrollTop: host.scrollTop });
413
+ };
414
+ const onScroll = (): void => {
415
+ if (scrollSaveScheduled) { return; }
416
+ scrollSaveScheduled = true;
417
+ requestAnimationFrame(saveScroll);
418
+ };
419
+ host.addEventListener('scroll', onScroll, { passive: true });
420
+ this._register({ dispose: () => host.removeEventListener('scroll', onScroll) });
421
+
422
+ // Flush the latest scroll synchronously before the webview is hidden or torn
423
+ // down, since the frame-throttled save above may not have run yet.
424
+ const onHide = (): void => {
425
+ if (document.visibilityState === 'hidden') {
426
+ this.#patchViewState({ scrollTop: host.scrollTop });
427
+ }
428
+ };
429
+ document.addEventListener('visibilitychange', onHide);
430
+ window.addEventListener('pagehide', saveScroll);
431
+ this._register({ dispose: () => { document.removeEventListener('visibilitychange', onHide); window.removeEventListener('pagehide', saveScroll); } });
432
+
433
+ // Persist the cursor whenever it moves.
434
+ this._register(autorun((reader) => {
435
+ const sel = reader.readObservable(this.model.selection);
436
+ this.#patchViewState({ selection: sel ? { anchor: sel.anchor, active: sel.active } : undefined });
437
+ }));
438
+
439
+ // Persist the edit/read-only mode as the global default whenever the lock
440
+ // toggle flips it, so the next Markdown editor opens in the same mode. The
441
+ // initial (restored) value is skipped so opening an editor doesn't re-write it.
442
+ let firstReadonly = true;
443
+ this._register(autorun((reader) => {
444
+ const isReadonly = reader.readObservable(this.model.readonlyMode);
445
+ if (!firstReadonly) {
446
+ this.#vscode.postMessage({ type: 'setReadonly', readonly: isReadonly });
447
+ }
448
+ firstReadonly = false;
449
+ }));
450
+
451
+ // Forward user edits to the extension. Edits are ignored by the model while
452
+ // read-only, so this is a no-op in that mode; keeping it always registered
453
+ // means unlocking a read-only editor immediately resumes edit forwarding.
454
+ let previousText = this.model.sourceText.get().value;
455
+ this._register(autorun((reader) => {
456
+ const text = reader.readObservable(this.model.sourceText).value;
457
+ if (!this.isUpdatingFromExtension && text !== previousText) {
458
+ this.#vscode.postMessage({ type: 'edit', ...computeTextEdit(previousText, text) });
459
+ }
460
+ previousText = text;
461
+ }));
462
+
463
+ // Restore scroll last: content height settles over a few frames (async parse,
464
+ // syntax highlighting, mermaid), so re-apply until it sticks.
465
+ // TODO@copilot: Consider using a more robust method for restoring scroll position, e.g. by waiting for the editor to stabilize
466
+ this.#restoreScroll(host, savedViewState.scrollTop);
467
+ }
468
+
469
+ #createIframeProviders(definitions: readonly CodeBlockEditorProviderDefinition[]): readonly IframeEmbeddedEditorProvider[] {
470
+ return definitions.map(definition => ({
471
+ id: definition.id,
472
+ selector: definition.selector,
473
+ createHostTransport: runtimeKey => this.#createCodeBlockEditorHostTransport(definition.id, runtimeKey),
474
+ resolve: definition.source.kind === 'static'
475
+ ? async () => definition.source.kind === 'static' ? definition.source.descriptor : undefined
476
+ : language => this.#resolveCodeBlockEditor(definition.id, language),
477
+ }));
478
+ }
479
+
480
+ #createCodeBlockEditorHostTransport(providerId: string, runtimeKey: string): CodeBlockEditorHostTransport {
481
+ const runtimeId = `${providerId}:${this.#nextCodeBlockEditorRuntimeId++}`;
482
+ const transport = new CodeBlockEditorHostTransport(
483
+ runtimeId,
484
+ message => this.#vscode.postMessage({
485
+ type: 'codeBlockEditorHostTransportMessage',
486
+ runtimeId,
487
+ message,
488
+ }),
489
+ () => {
490
+ this.#codeBlockEditorHostTransports.delete(runtimeId);
491
+ this.#vscode.postMessage({
492
+ type: 'disposeCodeBlockEditorHostTransport',
493
+ runtimeId,
494
+ });
495
+ },
496
+ );
497
+ this.#codeBlockEditorHostTransports.set(runtimeId, transport);
498
+ this.#vscode.postMessage({
499
+ type: 'createCodeBlockEditorHostTransport',
500
+ runtimeId,
501
+ providerId,
502
+ runtimeKey,
503
+ });
504
+ return transport;
505
+ }
506
+
507
+ #resolveCodeBlockEditor(providerId: string, language: string): Promise<ResolvedIframeEmbeddedEditor | undefined> {
508
+ const requestId = this.#nextCodeBlockEditorRequestId++;
509
+ return new Promise(resolve => {
510
+ this.#codeBlockEditorRequests.set(requestId, resolve);
511
+ this.#vscode.postMessage({
512
+ type: 'resolveCodeBlockEditor',
513
+ requestId,
514
+ providerId,
515
+ language,
516
+ });
517
+ });
518
+ }
519
+
520
+ #getViewState(): PersistedViewState {
521
+ return (this.#vscode.getState() as PersistedViewState | undefined) ?? {};
522
+ }
523
+
524
+ #patchViewState(patch: PersistedViewState): void {
525
+ this.#vscode.setState({ ...this.#getViewState(), ...patch });
526
+ }
527
+
528
+ #restoreScroll(host: HTMLElement, scrollTop: number | undefined): void {
529
+ if (typeof scrollTop !== 'number' || scrollTop <= 0) {
530
+ return;
531
+ }
532
+ let tries = 0;
533
+ const apply = (): void => {
534
+ host.scrollTop = scrollTop;
535
+ if (++tries < 6 && Math.abs(host.scrollTop - scrollTop) > 1) {
536
+ requestAnimationFrame(apply);
537
+ }
538
+ };
539
+ requestAnimationFrame(apply);
540
+ }
541
+ }
542
+
543
+ let mermaidPromise: Promise<(typeof import('mermaid'))['default']> | undefined;
544
+
545
+ function loadMermaid(): Promise<(typeof import('mermaid'))['default']> {
546
+ if (!mermaidPromise) {
547
+ mermaidPromise = import('mermaid').then(module => {
548
+ module.default.initialize({ startOnLoad: false, theme: 'default' });
549
+ return module.default;
550
+ });
551
+ }
552
+ return mermaidPromise;
553
+ }
554
+
555
+ function readInitialState(): InitialState {
556
+ const element = document.getElementById('vscode-markdown-editor-initial-state');
557
+ if (!(element instanceof HTMLMetaElement)) {
558
+ throw new Error('Markdown editor initial state was not found.');
559
+ }
560
+ element.remove();
561
+ const value: unknown = JSON.parse(decodeURIComponent(element.content));
562
+ if (!isInitialState(value)) {
563
+ throw new Error('Markdown editor initial state is invalid.');
564
+ }
565
+ return value;
566
+ }
567
+
568
+ function isInitialState(value: unknown): value is InitialState {
569
+ if (!value || typeof value !== 'object') {
570
+ return false;
571
+ }
572
+ const candidate = value as Record<string, unknown>;
573
+ return typeof candidate.content === 'string'
574
+ && typeof candidate.documentVersion === 'number'
575
+ && typeof candidate.readonly === 'boolean'
576
+ && typeof candidate.richLinksEnabled === 'boolean'
577
+ && Array.isArray(candidate.linkPresentationRules);
578
+ }
579
+
580
+ function readCodeBlockEditorProviderDefinitions(value: unknown): readonly CodeBlockEditorProviderDefinition[] {
581
+ if (!Array.isArray(value)) {
582
+ return [];
583
+ }
584
+ const values: unknown[] = value;
585
+ return values.filter(isCodeBlockEditorProviderDefinition);
586
+ }
587
+
588
+ function isCodeBlockEditorProviderDefinition(value: unknown): value is CodeBlockEditorProviderDefinition {
589
+ if (typeof value !== 'object' || value === null) {
590
+ return false;
591
+ }
592
+ const candidate = value as Record<string, unknown>;
593
+ return typeof candidate.id === 'string'
594
+ && isCodeBlockEditorSelector(candidate.selector)
595
+ && isCodeBlockEditorSource(candidate.source);
596
+ }
597
+
598
+ function isCodeBlockEditorSelector(value: unknown): value is IframeEmbeddedEditorProviderSelector {
599
+ if (!value || typeof value !== 'object') {
600
+ return false;
125
601
  }
602
+ const selector = value as Record<string, unknown>;
603
+ return (typeof selector.language === 'string' && selector.languagePrefix === undefined)
604
+ || (selector.language === undefined && typeof selector.languagePrefix === 'string');
605
+ }
606
+
607
+ function isCodeBlockEditorSource(value: unknown): value is CodeBlockEditorProviderDefinition['source'] {
608
+ if (!value || typeof value !== 'object') {
609
+ return false;
610
+ }
611
+ const source = value as Record<string, unknown>;
612
+ return source.kind === 'exportApi'
613
+ || (source.kind === 'static' && readResolvedCodeBlockEditor(source.descriptor) !== undefined);
614
+ }
615
+
616
+ function readResolvedCodeBlockEditor(value: unknown): ResolvedIframeEmbeddedEditor | undefined {
617
+ if (!value || typeof value !== 'object') {
618
+ return undefined;
619
+ }
620
+ const descriptor = value as Record<string, unknown>;
621
+ if (
622
+ typeof descriptor.html !== 'string'
623
+ || typeof descriptor.runtimeKey !== 'string'
624
+ || descriptor.runtimeKey.length === 0
625
+ || (descriptor.resourceBaseUrl !== undefined && typeof descriptor.resourceBaseUrl !== 'string')
626
+ || (descriptor.hostTransport !== undefined && typeof descriptor.hostTransport !== 'boolean')
627
+ || (descriptor.contentType !== 'text' && descriptor.contentType !== 'json')
628
+ || (descriptor.cacheKey !== undefined && typeof descriptor.cacheKey !== 'string')
629
+ || (descriptor.initialHeight !== undefined && (typeof descriptor.initialHeight !== 'number' || !Number.isFinite(descriptor.initialHeight) || descriptor.initialHeight <= 0))
630
+ || !isResolvedSandbox(descriptor.sandbox)
631
+ ) {
632
+ return undefined;
633
+ }
634
+ return descriptor as unknown as ResolvedIframeEmbeddedEditor;
635
+ }
636
+
637
+ function isResolvedSandbox(value: unknown): boolean {
638
+ if (value === undefined) {
639
+ return true;
640
+ }
641
+ if (!value || typeof value !== 'object') {
642
+ return false;
643
+ }
644
+ const sandbox = value as Record<string, unknown>;
645
+ return ['forms', 'downloads', 'pointerLock', 'clipboardWrite']
646
+ .every(key => sandbox[key] === undefined || typeof sandbox[key] === 'boolean');
647
+ }
648
+
649
+ function computeTextEdit(previousText: string, text: string): { start: number; endExclusive: number; text: string } {
650
+ let start = 0;
651
+ while (start < previousText.length && start < text.length && previousText.charCodeAt(start) === text.charCodeAt(start)) {
652
+ start++;
653
+ }
654
+
655
+ let previousEnd = previousText.length;
656
+ let end = text.length;
657
+ while (previousEnd > start && end > start && previousText.charCodeAt(previousEnd - 1) === text.charCodeAt(end - 1)) {
658
+ previousEnd--;
659
+ end--;
660
+ }
661
+
662
+ return {
663
+ start,
664
+ endExclusive: previousEnd,
665
+ text: text.slice(start, end),
666
+ };
126
667
  }
127
668
 
128
- new Editor(document.getElementById('editor')!);
669
+ new Editor(document.getElementById('editor')!, readInitialState());