import { UnknownRecord } from '@tldraw/tlstore' import { isEqual } from 'lodash' import fetch from 'node-fetch' import * as vscode from 'vscode' import { TLDrawDocument } from './TldrawDocument' import { loadFile } from './file' // @ts-ignore import type { VscodeMessage } from '../../messages' import { nicelog } from './utils' export const GlobalStateKeys = { ShowV1FileOpenWarning: 'showV1fileOpenWarning', UserId: 'userId', } export class WebViewMessageHandler { multiplayerOmitKeys = /^(user_presence:|camera:|user:|user_document:|instance:)/ newDocumentOmitKeys = /^(user_presence:|camera:|user:|user_document:|instance:|document:|page:)/ constructor( private document: TLDrawDocument, private webviewPanel: vscode.WebviewPanel, private context: vscode.ExtensionContext, private userId: unknown, private assetSrc: string ) {} isLoaded = false firstChangeDone = false handle = async (e: VscodeMessage) => { if (!this.document) return switch (e.type) { case 'vscode:ready-to-receive-file': { // Send the initial document content to bootstrap the Tldraw/Tldraw component. this.webviewPanel.webview.postMessage({ type: 'vscode:opened-file', data: { fileContents: JSON.stringify(this.document.documentData), uri: this.document.uri.toString(), userId: this.userId, assetSrc: this.assetSrc, isDarkMode: this.document.isBlankDocument && (vscode.window.activeColorTheme.kind === 2 || vscode.window.activeColorTheme.kind === 3), }, } as VscodeMessage) break } case 'vscode:open-window': { vscode.env.openExternal(vscode.Uri.parse(e.data.url)) break } case 'vscode:undo': { vscode.commands.executeCommand('undo') break } case 'vscode:redo': { vscode.commands.executeCommand('redo') break } case 'vscode:refresh-page': { vscode.commands.executeCommand('workbench.action.reloadWindow') break } case 'vscode:hard-reset': { await this.document.loadBlankDocument() vscode.commands.executeCommand('workbench.action.reloadWindow') break } case 'vscode:bookmark/request': { const url = e.data.url fetch('https://www.tldraw.com/api/bookmark', { method: 'POST', headers: { 'Content-Type': 'application/json', // We can fake the origin here because we're in node.js origin: 'https://www.tldraw.com', }, body: JSON.stringify({ url, }), }) .then((resp) => { return resp.json() }) .then((json: any) => { this.webviewPanel.webview.postMessage({ type: 'vscode:bookmark/response', uuid: e.uuid, data: { url, title: json.title, description: json.description, image: json.image, }, }) }) .catch((error: any) => { this.webviewPanel.webview.postMessage({ type: 'vscode:bookmark/error', data: { error: error.toString(), }, }) }) break } case 'vscode:editor-loaded': { this.isLoaded = true break } case 'vscode:v1-file-opened': { const showV1FileOpenWarning = this.context.globalState.get( GlobalStateKeys.ShowV1FileOpenWarning ) if (!showV1FileOpenWarning) return const { backup, open, description, dontAskAgain } = e.data vscode.window .showInformationMessage(description, open, dontAskAgain, backup) .then((result) => { if (result === backup) { this.document.v1Backup(e.data.backupSaved, e.data.backupFailed) } else if (result === dontAskAgain) { this.context.globalState.update(GlobalStateKeys.ShowV1FileOpenWarning, false) } }) break } case 'vscode:editor-updated': { if (!this.isLoaded) return if (!this.firstChangeDone) { this.firstChangeDone = true return } const raw = e.data.fileContents if (!raw) return // The event will contain the new TDFile as JSON. const nextFile = loadFile(raw) const existingDoc = this.document.documentData let isSame = false if (existingDoc?.records?.length > 0) { const oldDoc = this.omit(existingDoc.records, this.multiplayerOmitKeys) const newDoc = this.omit(nextFile.records, this.multiplayerOmitKeys) isSame = isEqual(oldDoc, newDoc) } else { const newDoc = this.omit(nextFile.records, this.newDocumentOmitKeys) isSame = isEqual(newDoc, []) } if (!isSame) { this.document.makeEdit(nextFile) } break } case 'vscode:hide-v1-file-open-warning': { this.context.globalState.update(GlobalStateKeys.ShowV1FileOpenWarning, false) break } case 'vscode:cancel-v1-migrate': { vscode.commands.executeCommand('workbench.action.closeActiveEditor') break } } } private omit = (records: UnknownRecord[], keys: RegExp) => { return records.filter((record) => { return !record.id.match(keys) }) } findDiff(oldDoc: Record, newDoc: Record) { const newRecords = Object.values(newDoc) const oldRecords = Object.values(oldDoc) for (const oldRecord of oldRecords) { const newRecord = newRecords.find((r: any) => r.id === oldRecord.id) if (!newRecord) { nicelog('record missing in new doc', oldRecord) continue } else { if (!isEqual(oldRecord, newRecord)) { nicelog('record different', oldRecord, newRecord) continue } } } for (const newRecord of newRecords) { const oldRecord = oldRecords.find((r: any) => r.id === newRecord.id) if (!oldRecord) { nicelog('record missing in oldDoc doc', newRecord) continue } else { if (!isEqual(newRecord, oldRecord)) { nicelog('record different', newRecord, oldRecord) continue } } } } }