Path: blob/main/extensions/mermaid-chat-features/src/editorManager.ts
5223 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import * as vscode from 'vscode';5import { generateUuid } from './util/uuid';6import { MermaidWebviewManager } from './webviewManager';7import { escapeHtmlText } from './util/html';8import { Disposable } from './util/dispose';910export const mermaidEditorViewType = 'vscode.chat-mermaid-features.preview';1112interface MermaidPreviewState {13readonly webviewId: string;14readonly mermaidSource: string;15}1617/**18* Manages mermaid diagram editor panels, ensuring only one editor per diagram.19*/20export class MermaidEditorManager extends Disposable implements vscode.WebviewPanelSerializer {2122private readonly _previews = new Map<string, MermaidPreview>();2324constructor(25private readonly _extensionUri: vscode.Uri,26private readonly _webviewManager: MermaidWebviewManager27) {28super();2930this._register(vscode.window.registerWebviewPanelSerializer(mermaidEditorViewType, this));31}3233/**34* Opens a preview for the given diagram35*36* If a preview already exists for this diagram, it will be revealed instead of creating a new one.37*/38public openPreview(mermaidSource: string, title?: string): void {39const webviewId = getWebviewId(mermaidSource);40const existingPreview = this._previews.get(webviewId);41if (existingPreview) {42existingPreview.reveal();43return;44}4546const preview = MermaidPreview.create(47webviewId,48mermaidSource,49title,50this._extensionUri,51this._webviewManager,52vscode.ViewColumn.Active);5354this._registerPreview(preview);55}5657public async deserializeWebviewPanel(58webviewPanel: vscode.WebviewPanel,59state: MermaidPreviewState60): Promise<void> {61if (!state?.mermaidSource) {62webviewPanel.webview.html = this._getErrorHtml();63return;64}6566const webviewId = getWebviewId(state.mermaidSource);6768const preview = MermaidPreview.revive(69webviewPanel,70webviewId,71state.mermaidSource,72this._extensionUri,73this._webviewManager74);7576this._registerPreview(preview);77}7879private _registerPreview(preview: MermaidPreview): void {80this._previews.set(preview.diagramId, preview);8182preview.onDispose(() => {83this._previews.delete(preview.diagramId);84});85}8687private _getErrorHtml(): string {88return /* html */`<!DOCTYPE html>89<html lang="en">90<head>91<meta charset="UTF-8">92<meta name="viewport" content="width=device-width, initial-scale=1.0">93<title>Mermaid Preview</title>94<meta http-equiv="Content-Security-Policy" content="default-src 'none';">95<style>96body {97display: flex;98justify-content: center;99align-items: center;100height: 100vh;101margin: 0;102}103</style>104</head>105<body>106<p>An unexpected error occurred while restoring the Mermaid preview.</p>107</body>108</html>`;109}110111public override dispose(): void {112super.dispose();113114for (const preview of this._previews.values()) {115preview.dispose();116}117this._previews.clear();118}119}120121class MermaidPreview extends Disposable {122123private readonly _onDisposeEmitter = this._register(new vscode.EventEmitter<void>());124public readonly onDispose = this._onDisposeEmitter.event;125126public static create(127diagramId: string,128mermaidSource: string,129title: string | undefined,130extensionUri: vscode.Uri,131webviewManager: MermaidWebviewManager,132viewColumn: vscode.ViewColumn133): MermaidPreview {134const webviewPanel = vscode.window.createWebviewPanel(135mermaidEditorViewType,136title ?? vscode.l10n.t('Mermaid Diagram'),137viewColumn,138{139retainContextWhenHidden: false,140}141);142143return new MermaidPreview(webviewPanel, diagramId, mermaidSource, extensionUri, webviewManager);144}145146public static revive(147webviewPanel: vscode.WebviewPanel,148diagramId: string,149mermaidSource: string,150extensionUri: vscode.Uri,151webviewManager: MermaidWebviewManager152): MermaidPreview {153return new MermaidPreview(webviewPanel, diagramId, mermaidSource, extensionUri, webviewManager);154}155156private constructor(157private readonly _webviewPanel: vscode.WebviewPanel,158public readonly diagramId: string,159private readonly _mermaidSource: string,160private readonly _extensionUri: vscode.Uri,161private readonly _webviewManager: MermaidWebviewManager162) {163super();164165this._webviewPanel.iconPath = new vscode.ThemeIcon('graph');166167this._webviewPanel.webview.options = {168enableScripts: true,169localResourceRoots: [170vscode.Uri.joinPath(this._extensionUri, 'chat-webview-out')171],172};173174this._webviewPanel.webview.html = this._getHtml();175176// Register with the webview manager177this._register(this._webviewManager.registerWebview(this.diagramId, this._webviewPanel.webview, this._mermaidSource, undefined, 'editor'));178179this._register(this._webviewPanel.onDidChangeViewState(e => {180if (e.webviewPanel.active) {181this._webviewManager.setActiveWebview(this.diagramId);182}183}));184185this._register(this._webviewPanel.onDidDispose(() => {186this._onDisposeEmitter.fire();187this.dispose();188}));189}190191public reveal(): void {192this._webviewPanel.reveal();193}194195public override dispose() {196this._onDisposeEmitter.fire();197198super.dispose();199200this._webviewPanel.dispose();201}202203private _getHtml(): string {204const nonce = generateUuid();205206const mediaRoot = vscode.Uri.joinPath(this._extensionUri, 'chat-webview-out');207const scriptUri = this._webviewPanel.webview.asWebviewUri(208vscode.Uri.joinPath(mediaRoot, 'index-editor.js')209);210const codiconsUri = this._webviewPanel.webview.asWebviewUri(211vscode.Uri.joinPath(mediaRoot, 'codicon.css')212);213214return /* html */`<!DOCTYPE html>215<html lang="en">216<head>217<meta charset="UTF-8">218<meta name="viewport" content="width=device-width, initial-scale=1.0">219<title>Mermaid Diagram</title>220<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:;" />221<link rel="stylesheet" type="text/css" href="${codiconsUri}">222<style>223html, body {224margin: 0;225padding: 0;226height: 100%;227width: 100%;228overflow: hidden;229}230.mermaid {231visibility: hidden;232}233.mermaid.rendered {234visibility: visible;235}236.mermaid-wrapper {237height: 100%;238width: 100%;239}240.zoom-controls {241position: absolute;242top: 8px;243right: 8px;244display: flex;245gap: 2px;246z-index: 100;247background: var(--vscode-editorWidget-background);248border: 1px solid var(--vscode-editorWidget-border);249border-radius: 6px;250padding: 3px;251}252.zoom-controls button {253display: flex;254align-items: center;255justify-content: center;256width: 26px;257height: 26px;258background: transparent;259color: var(--vscode-icon-foreground);260border: none;261border-radius: 4px;262cursor: pointer;263}264.zoom-controls button:hover {265background: var(--vscode-toolbar-hoverBackground);266}267</style>268</head>269<body data-vscode-context='${JSON.stringify({ preventDefaultContextMenuItems: true, mermaidWebviewId: this.diagramId })}' data-vscode-mermaid-webview-id="${this.diagramId}">270<div class="zoom-controls">271<button class="zoom-out-btn" title="${vscode.l10n.t('Zoom Out')}"><i class="codicon codicon-zoom-out"></i></button>272<button class="zoom-in-btn" title="${vscode.l10n.t('Zoom In')}"><i class="codicon codicon-zoom-in"></i></button>273<button class="zoom-reset-btn" title="${vscode.l10n.t('Reset Zoom')}"><i class="codicon codicon-screen-normal"></i></button>274</div>275<pre class="mermaid">276${escapeHtmlText(this._mermaidSource)}277</pre>278<script type="module" nonce="${nonce}" src="${scriptUri}"></script>279</body>280</html>`;281}282}283284285/**286* Generates a unique ID for a diagram based on its content.287* This ensures the same diagram content always gets the same ID.288*/289function getWebviewId(source: string): string {290// Simple hash function for generating a content-based ID291let hash = 0;292for (let i = 0; i < source.length; i++) {293const char = source.charCodeAt(i);294hash = ((hash << 5) - hash) + char;295hash = hash & hash; // Convert to 32-bit integer296}297return Math.abs(hash).toString(16);298}299300301