diff --git a/packages/ds-ext/README.md b/packages/ds-ext/README.md index 8a4154f66..f3175cae6 100644 --- a/packages/ds-ext/README.md +++ b/packages/ds-ext/README.md @@ -5,4 +5,4 @@ * In "Run and Debug" tab, click "Run Extension" * This opens a new instance of VS Code * With the new instance, open a directory -* Create a *.diagram.json file (must have a content) \ No newline at end of file +* Create a *.ds file (must have a content) \ No newline at end of file diff --git a/packages/ds-ext/package.json b/packages/ds-ext/package.json index 7b3e73738..0b1bb6dfa 100644 --- a/packages/ds-ext/package.json +++ b/packages/ds-ext/package.json @@ -20,9 +20,35 @@ "commands": [ { "command": "ds-ext.createDemos", - "title": "DataStory: Create Demo Diagrams" + "title": "Create Demo Diagrams", + "category": "DataStory" + }, + { + "command": "ds-ext.showDiagramPreview", + "title": "Show Diagram Preview11", + "category": "DataStory", + "icon": { + "light": "./themes/preview-dark.svg", + "dark": "./themes/preview-white.svg" + } } ], + "menus": { + "editor/title": [ + { + "command": "ds-ext.showDiagramPreview", + "when": "resourceLangId == 'diagramJson'", + "group": "navigation" + } + ], + "view/title": [ + { + "command": "ds-ext.showDiagramPreview", + "when": "resourceLangId == 'diagramJson'", + "group": "navigation" + } + ] + }, "languages": [ { "id": "diagramJson", @@ -31,9 +57,11 @@ "Data Story" ], "extensions": [ - ".diagram.json", ".ds" ], + "filenamePatterns": [ + "*.ds" + ], "configuration": "./language-configuration.json" } ], @@ -41,12 +69,12 @@ { "viewType": "ds-ext.diagramEditor", "displayName": "Diagram Editor", + "language": "diagramJson", + "priority": "default", "selector": [ { - "filenamePattern": "*.diagram.json" - }, - { - "filenamePattern": "*.ds" + "filenamePattern": "*.ds", + "language": "diagramJson" } ] } diff --git a/packages/ds-ext/src/DiagramDocument.ts b/packages/ds-ext/src/DiagramDocument.ts index e6b0b7c59..a8c075691 100644 --- a/packages/ds-ext/src/DiagramDocument.ts +++ b/packages/ds-ext/src/DiagramDocument.ts @@ -9,6 +9,9 @@ export class DiagramDocument implements vscode.CustomDocument { return new DiagramDocument(uri, fileData); } + private _onDidChange = new vscode.EventEmitter>(); + readonly onDidChange = this._onDidChange.event; + private constructor( public readonly uri: vscode.Uri, private documentData: Uint8Array, @@ -19,7 +22,7 @@ export class DiagramDocument implements vscode.CustomDocument { } dispose(): void { - // Clean up resources here if needed + this._onDidChange.dispose(); } // Save the document to the file system @@ -50,5 +53,10 @@ export class DiagramDocument implements vscode.CustomDocument { // A method to update the document data update(newData: Uint8Array) { this.documentData = newData; + this._onDidChange.fire({ + document: this, + undo: () => {}, // TODO: Implement proper undo if needed + redo: () => {}, // TODO: Implement proper redo if needed + }); } -} \ No newline at end of file +} diff --git a/packages/ds-ext/src/DiagramEditorProvider.ts b/packages/ds-ext/src/DiagramEditorProvider.ts index dcf392974..c86cea4af 100644 --- a/packages/ds-ext/src/DiagramEditorProvider.ts +++ b/packages/ds-ext/src/DiagramEditorProvider.ts @@ -21,11 +21,11 @@ import { loadConfig } from './loadConfig'; import { DataStoryConfig } from './DataStoryConfig'; export class DiagramEditorProvider implements vscode.CustomEditorProvider { - private readonly _onDidChangeCustomDocument = new vscode.EventEmitter>(); - public readonly onDidChangeCustomDocument = this._onDidChangeCustomDocument.event; + public readonly onDidChangeCustomDocument = new vscode.EventEmitter>().event; private inputObserverController!: InputObserverController; private observerStorage!: ObserverStorage; private config: DataStoryConfig; + private contentMap = new Map(); constructor(private readonly context: vscode.ExtensionContext) { this.config = loadConfig(this.context); @@ -58,6 +58,10 @@ export class DiagramEditorProvider implements vscode.CustomEditorProvider { return document.save(); diff --git a/packages/ds-ext/src/JsonReadonlyProvider.ts b/packages/ds-ext/src/JsonReadonlyProvider.ts new file mode 100644 index 000000000..955a20ee0 --- /dev/null +++ b/packages/ds-ext/src/JsonReadonlyProvider.ts @@ -0,0 +1,24 @@ +import vscode from 'vscode'; + +export class JsonReadonlyProvider implements vscode.TextDocumentContentProvider { + // Store the content corresponding to each URI + private contentMap = new Map(); + readonly onDidChangeEmitter = new vscode.EventEmitter(); + readonly onDidChange = this.onDidChangeEmitter.event; + + // Update the content of the specified URI + updateContent(uri: vscode.Uri, content: string) { + this.contentMap.set(uri.toString(), content); + // Notify VS Code that the content has been updated + this.onDidChangeEmitter.fire(uri); + } + + provideTextDocumentContent(uri: vscode.Uri): string { + return this.contentMap.get(uri.toString()) || ''; + } + + dispose() { + this.contentMap.clear(); + this.onDidChangeEmitter.dispose(); + } +} \ No newline at end of file diff --git a/packages/ds-ext/src/commands/createDemosDirectory.ts b/packages/ds-ext/src/commands/createDemosDirectory.ts index 9b919d887..e3a37acdf 100644 --- a/packages/ds-ext/src/commands/createDemosDirectory.ts +++ b/packages/ds-ext/src/commands/createDemosDirectory.ts @@ -37,7 +37,7 @@ export async function createDemosDirectory() { makeDensityDatasets(path.join(demosDir, 'data', 'densities')); for (const [moduleName, demoFactory] of Object.entries(demos)) { - const filePath = path.join(demosDir, `${moduleName}.diagram.json`); + const filePath = path.join(demosDir, `${moduleName}.ds`); const demoData = await demoFactory(); fs.writeFileSync(filePath, JSON.stringify(demoData, null, 2)); } diff --git a/packages/ds-ext/src/extension.ts b/packages/ds-ext/src/extension.ts index fdb149adc..020526a72 100644 --- a/packages/ds-ext/src/extension.ts +++ b/packages/ds-ext/src/extension.ts @@ -3,20 +3,81 @@ import { DiagramEditorProvider } from './DiagramEditorProvider'; import { createDemosDirectory } from './commands/createDemosDirectory'; import path from 'path'; import * as fs from 'fs'; +import { JsonReadonlyProvider } from './JsonReadonlyProvider'; +import { DiagramDocument } from './DiagramDocument'; let diagramEditorProvider: DiagramEditorProvider; +let jsonReadonlyProvider: JsonReadonlyProvider | undefined; + +function createReadonlyUri(args: vscode.Uri): vscode.Uri { + const fileName = path.basename(args.path, '.json'); + const readOnlyUri = vscode.Uri.parse( + `json-readonly:Preview_${fileName}.json`, + ); + + return readOnlyUri; +} export function activate(context: vscode.ExtensionContext) { let disposable = vscode.commands.registerCommand('ds-ext.createDemos', async () => { await createDemosDirectory(); }); + diagramEditorProvider = new DiagramEditorProvider(context); + jsonReadonlyProvider = new JsonReadonlyProvider(); + context.subscriptions.push( + vscode.workspace.registerTextDocumentContentProvider('json-readonly', jsonReadonlyProvider), + ); + + const registerDiagramChangeAndCloseListeners = (diagramDocument: DiagramDocument, readOnlyUri: vscode.Uri): void => { + const changeSubscription = diagramDocument.onDidChange(async(diagramInfo) => { + const diagramJson = JSON.parse(new TextDecoder().decode(diagramInfo.document.data)); + + // update the content of the read-only document + jsonReadonlyProvider!.updateContent( + readOnlyUri, + JSON.stringify(diagramJson, null, 2), + ); + }); + + // stop listening when the document is closed + const closeSubscription = vscode.workspace.onDidCloseTextDocument(closedDoc => { + if (closedDoc.uri.toString() === readOnlyUri.toString()) { + changeSubscription.dispose(); + closeSubscription .dispose(); + } + }); + context.subscriptions.push(changeSubscription, closeSubscription ); + }; + + vscode.commands.registerCommand('ds-ext.showDiagramPreview', async (args: vscode.Uri) => { + const diagramDocument = diagramEditorProvider.provideDiagramContent(args); + const diagramData = diagramDocument?.data; + const dataString = JSON.stringify(JSON.parse(new TextDecoder().decode(diagramData)), null, 2); + const readOnlyUri = createReadonlyUri(args); + + jsonReadonlyProvider!.updateContent(readOnlyUri, dataString); + + // Open the document and show readonly content + const doc = await vscode.workspace.openTextDocument(readOnlyUri); + await vscode.languages.setTextDocumentLanguage(doc, 'json'); + const editor = await vscode.window.showTextDocument(doc, { + viewColumn: vscode.ViewColumn.Beside, + preview: true, + preserveFocus: true, + }); + + if (diagramDocument) { + // Listen for diagram changes and update content and stop listening when the document is closed + registerDiagramChangeAndCloseListeners(diagramDocument, readOnlyUri); + } + }); + const outputChannel = vscode.window.createOutputChannel('DS-Ext'); outputChannel.appendLine('Congratulations, your extension "ds-ext" is now active!'); outputChannel.appendLine(`ds-ext is installed at ${context.extensionPath}`); // outputChannel.show(); - diagramEditorProvider = new DiagramEditorProvider(context); context.subscriptions.push( vscode.window.registerCustomEditorProvider( 'ds-ext.diagramEditor', @@ -52,4 +113,5 @@ export function activate(context: vscode.ExtensionContext) { export function deactivate(context: any) { diagramEditorProvider.dispose(); -} \ No newline at end of file + jsonReadonlyProvider?.dispose(); +} diff --git a/packages/ds-ext/themes/file-icon-theme.json b/packages/ds-ext/themes/file-icon-theme.json index 3a798b9f6..953ab5257 100644 --- a/packages/ds-ext/themes/file-icon-theme.json +++ b/packages/ds-ext/themes/file-icon-theme.json @@ -5,7 +5,6 @@ } }, "fileExtensions": { - "diagram.json": "diagramIcon", "ds": "diagramIcon" }, "fileNames": {}, diff --git a/packages/ds-ext/themes/preview-dark.svg b/packages/ds-ext/themes/preview-dark.svg new file mode 100644 index 000000000..a5b593b6b --- /dev/null +++ b/packages/ds-ext/themes/preview-dark.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/ds-ext/themes/preview-white.svg b/packages/ds-ext/themes/preview-white.svg new file mode 100644 index 000000000..9d3a0d221 --- /dev/null +++ b/packages/ds-ext/themes/preview-white.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/ui/src/components/DataStory/controls/ImportControl.tsx b/packages/ui/src/components/DataStory/controls/ImportControl.tsx index 17f889f3f..b89978b44 100644 --- a/packages/ui/src/components/DataStory/controls/ImportControl.tsx +++ b/packages/ui/src/components/DataStory/controls/ImportControl.tsx @@ -10,7 +10,7 @@ const defaultImport = (): Promise => { // create an input element const input = document.createElement('input'); input.type = 'file'; - input.accept = '.diagram.json,.ds'; + input.accept = '.ds'; input.style.display = 'none'; // when the user selects a file diff --git a/packages/ui/src/components/DataStory/hooks/useEscapeKey.ts b/packages/ui/src/components/DataStory/hooks/useEscapeKey.ts index a0fe54553..2aef23770 100644 --- a/packages/ui/src/components/DataStory/hooks/useEscapeKey.ts +++ b/packages/ui/src/components/DataStory/hooks/useEscapeKey.ts @@ -3,7 +3,7 @@ import { useCallback, useEffect } from 'react'; const KEY_NAME_ESC = 'Escape'; const KEY_EVENT_TYPE = 'keyup'; -export function useEscapeKey(handleClose: () => void, flowRef?: React.RefObject) { +export function useEscapeKey(handleClose: () => void, flowRef?: React.RefObject) { const handleEscKey = useCallback((event: KeyboardEvent) => { if (event.key === KEY_NAME_ESC) { handleClose(); diff --git a/packages/ui/src/components/DropDown/index.tsx b/packages/ui/src/components/DropDown/index.tsx index c47cc2150..fc4b9f7a7 100644 --- a/packages/ui/src/components/DropDown/index.tsx +++ b/packages/ui/src/components/DropDown/index.tsx @@ -1,5 +1,5 @@ import '../../styles/globals.css'; -import { useCallback, useMemo, useState } from 'react'; +import { JSX, useCallback, useMemo, useState } from 'react'; import { autoUpdate, type ExtendedRefs, diff --git a/packages/ui/src/components/Node/table/TableCell.tsx b/packages/ui/src/components/Node/table/TableCell.tsx index dbd7dbb33..34e8763ad 100644 --- a/packages/ui/src/components/Node/table/TableCell.tsx +++ b/packages/ui/src/components/Node/table/TableCell.tsx @@ -27,7 +27,7 @@ const formatTooltipContent = (content: unknown) => { } } -export function TableCell(props: { tableRef: React.RefObject, content?: unknown }): JSX.Element { +export function TableCell(props: { tableRef: React.RefObject, content?: unknown }) { const { content = '', tableRef } = props; const [showTooltip, setShowTooltip] = useState(false);