Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/mermaid-chat-features/src/editorManager.ts
5223 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
import * as vscode from 'vscode';
6
import { generateUuid } from './util/uuid';
7
import { MermaidWebviewManager } from './webviewManager';
8
import { escapeHtmlText } from './util/html';
9
import { Disposable } from './util/dispose';
10
11
export const mermaidEditorViewType = 'vscode.chat-mermaid-features.preview';
12
13
interface MermaidPreviewState {
14
readonly webviewId: string;
15
readonly mermaidSource: string;
16
}
17
18
/**
19
* Manages mermaid diagram editor panels, ensuring only one editor per diagram.
20
*/
21
export class MermaidEditorManager extends Disposable implements vscode.WebviewPanelSerializer {
22
23
private readonly _previews = new Map<string, MermaidPreview>();
24
25
constructor(
26
private readonly _extensionUri: vscode.Uri,
27
private readonly _webviewManager: MermaidWebviewManager
28
) {
29
super();
30
31
this._register(vscode.window.registerWebviewPanelSerializer(mermaidEditorViewType, this));
32
}
33
34
/**
35
* Opens a preview for the given diagram
36
*
37
* If a preview already exists for this diagram, it will be revealed instead of creating a new one.
38
*/
39
public openPreview(mermaidSource: string, title?: string): void {
40
const webviewId = getWebviewId(mermaidSource);
41
const existingPreview = this._previews.get(webviewId);
42
if (existingPreview) {
43
existingPreview.reveal();
44
return;
45
}
46
47
const preview = MermaidPreview.create(
48
webviewId,
49
mermaidSource,
50
title,
51
this._extensionUri,
52
this._webviewManager,
53
vscode.ViewColumn.Active);
54
55
this._registerPreview(preview);
56
}
57
58
public async deserializeWebviewPanel(
59
webviewPanel: vscode.WebviewPanel,
60
state: MermaidPreviewState
61
): Promise<void> {
62
if (!state?.mermaidSource) {
63
webviewPanel.webview.html = this._getErrorHtml();
64
return;
65
}
66
67
const webviewId = getWebviewId(state.mermaidSource);
68
69
const preview = MermaidPreview.revive(
70
webviewPanel,
71
webviewId,
72
state.mermaidSource,
73
this._extensionUri,
74
this._webviewManager
75
);
76
77
this._registerPreview(preview);
78
}
79
80
private _registerPreview(preview: MermaidPreview): void {
81
this._previews.set(preview.diagramId, preview);
82
83
preview.onDispose(() => {
84
this._previews.delete(preview.diagramId);
85
});
86
}
87
88
private _getErrorHtml(): string {
89
return /* html */`<!DOCTYPE html>
90
<html lang="en">
91
<head>
92
<meta charset="UTF-8">
93
<meta name="viewport" content="width=device-width, initial-scale=1.0">
94
<title>Mermaid Preview</title>
95
<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
96
<style>
97
body {
98
display: flex;
99
justify-content: center;
100
align-items: center;
101
height: 100vh;
102
margin: 0;
103
}
104
</style>
105
</head>
106
<body>
107
<p>An unexpected error occurred while restoring the Mermaid preview.</p>
108
</body>
109
</html>`;
110
}
111
112
public override dispose(): void {
113
super.dispose();
114
115
for (const preview of this._previews.values()) {
116
preview.dispose();
117
}
118
this._previews.clear();
119
}
120
}
121
122
class MermaidPreview extends Disposable {
123
124
private readonly _onDisposeEmitter = this._register(new vscode.EventEmitter<void>());
125
public readonly onDispose = this._onDisposeEmitter.event;
126
127
public static create(
128
diagramId: string,
129
mermaidSource: string,
130
title: string | undefined,
131
extensionUri: vscode.Uri,
132
webviewManager: MermaidWebviewManager,
133
viewColumn: vscode.ViewColumn
134
): MermaidPreview {
135
const webviewPanel = vscode.window.createWebviewPanel(
136
mermaidEditorViewType,
137
title ?? vscode.l10n.t('Mermaid Diagram'),
138
viewColumn,
139
{
140
retainContextWhenHidden: false,
141
}
142
);
143
144
return new MermaidPreview(webviewPanel, diagramId, mermaidSource, extensionUri, webviewManager);
145
}
146
147
public static revive(
148
webviewPanel: vscode.WebviewPanel,
149
diagramId: string,
150
mermaidSource: string,
151
extensionUri: vscode.Uri,
152
webviewManager: MermaidWebviewManager
153
): MermaidPreview {
154
return new MermaidPreview(webviewPanel, diagramId, mermaidSource, extensionUri, webviewManager);
155
}
156
157
private constructor(
158
private readonly _webviewPanel: vscode.WebviewPanel,
159
public readonly diagramId: string,
160
private readonly _mermaidSource: string,
161
private readonly _extensionUri: vscode.Uri,
162
private readonly _webviewManager: MermaidWebviewManager
163
) {
164
super();
165
166
this._webviewPanel.iconPath = new vscode.ThemeIcon('graph');
167
168
this._webviewPanel.webview.options = {
169
enableScripts: true,
170
localResourceRoots: [
171
vscode.Uri.joinPath(this._extensionUri, 'chat-webview-out')
172
],
173
};
174
175
this._webviewPanel.webview.html = this._getHtml();
176
177
// Register with the webview manager
178
this._register(this._webviewManager.registerWebview(this.diagramId, this._webviewPanel.webview, this._mermaidSource, undefined, 'editor'));
179
180
this._register(this._webviewPanel.onDidChangeViewState(e => {
181
if (e.webviewPanel.active) {
182
this._webviewManager.setActiveWebview(this.diagramId);
183
}
184
}));
185
186
this._register(this._webviewPanel.onDidDispose(() => {
187
this._onDisposeEmitter.fire();
188
this.dispose();
189
}));
190
}
191
192
public reveal(): void {
193
this._webviewPanel.reveal();
194
}
195
196
public override dispose() {
197
this._onDisposeEmitter.fire();
198
199
super.dispose();
200
201
this._webviewPanel.dispose();
202
}
203
204
private _getHtml(): string {
205
const nonce = generateUuid();
206
207
const mediaRoot = vscode.Uri.joinPath(this._extensionUri, 'chat-webview-out');
208
const scriptUri = this._webviewPanel.webview.asWebviewUri(
209
vscode.Uri.joinPath(mediaRoot, 'index-editor.js')
210
);
211
const codiconsUri = this._webviewPanel.webview.asWebviewUri(
212
vscode.Uri.joinPath(mediaRoot, 'codicon.css')
213
);
214
215
return /* html */`<!DOCTYPE html>
216
<html lang="en">
217
<head>
218
<meta charset="UTF-8">
219
<meta name="viewport" content="width=device-width, initial-scale=1.0">
220
<title>Mermaid Diagram</title>
221
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'nonce-${nonce}'; style-src ${this._webviewPanel.webview.cspSource} 'unsafe-inline'; font-src data:;" />
222
<link rel="stylesheet" type="text/css" href="${codiconsUri}">
223
<style>
224
html, body {
225
margin: 0;
226
padding: 0;
227
height: 100%;
228
width: 100%;
229
overflow: hidden;
230
}
231
.mermaid {
232
visibility: hidden;
233
}
234
.mermaid.rendered {
235
visibility: visible;
236
}
237
.mermaid-wrapper {
238
height: 100%;
239
width: 100%;
240
}
241
.zoom-controls {
242
position: absolute;
243
top: 8px;
244
right: 8px;
245
display: flex;
246
gap: 2px;
247
z-index: 100;
248
background: var(--vscode-editorWidget-background);
249
border: 1px solid var(--vscode-editorWidget-border);
250
border-radius: 6px;
251
padding: 3px;
252
}
253
.zoom-controls button {
254
display: flex;
255
align-items: center;
256
justify-content: center;
257
width: 26px;
258
height: 26px;
259
background: transparent;
260
color: var(--vscode-icon-foreground);
261
border: none;
262
border-radius: 4px;
263
cursor: pointer;
264
}
265
.zoom-controls button:hover {
266
background: var(--vscode-toolbar-hoverBackground);
267
}
268
</style>
269
</head>
270
<body data-vscode-context='${JSON.stringify({ preventDefaultContextMenuItems: true, mermaidWebviewId: this.diagramId })}' data-vscode-mermaid-webview-id="${this.diagramId}">
271
<div class="zoom-controls">
272
<button class="zoom-out-btn" title="${vscode.l10n.t('Zoom Out')}"><i class="codicon codicon-zoom-out"></i></button>
273
<button class="zoom-in-btn" title="${vscode.l10n.t('Zoom In')}"><i class="codicon codicon-zoom-in"></i></button>
274
<button class="zoom-reset-btn" title="${vscode.l10n.t('Reset Zoom')}"><i class="codicon codicon-screen-normal"></i></button>
275
</div>
276
<pre class="mermaid">
277
${escapeHtmlText(this._mermaidSource)}
278
</pre>
279
<script type="module" nonce="${nonce}" src="${scriptUri}"></script>
280
</body>
281
</html>`;
282
}
283
}
284
285
286
/**
287
* Generates a unique ID for a diagram based on its content.
288
* This ensures the same diagram content always gets the same ID.
289
*/
290
function getWebviewId(source: string): string {
291
// Simple hash function for generating a content-based ID
292
let hash = 0;
293
for (let i = 0; i < source.length; i++) {
294
const char = source.charCodeAt(i);
295
hash = ((hash << 5) - hash) + char;
296
hash = hash & hash; // Convert to 32-bit integer
297
}
298
return Math.abs(hash).toString(16);
299
}
300
301