Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add diagram source code view, JSON integration, and read-only mode for diagram preview #381

Merged
merged 11 commits into from
Feb 18, 2025
Merged
2 changes: 1 addition & 1 deletion packages/ds-ext/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
* Create a *.ds file (must have a content)
40 changes: 34 additions & 6 deletions packages/ds-ext/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -31,22 +57,24 @@
"Data Story"
],
"extensions": [
".diagram.json",
".ds"
],
"filenamePatterns": [
"*.ds"
],
"configuration": "./language-configuration.json"
}
],
"customEditors": [
{
"viewType": "ds-ext.diagramEditor",
"displayName": "Diagram Editor",
"language": "diagramJson",
"priority": "default",
"selector": [
{
"filenamePattern": "*.diagram.json"
},
{
"filenamePattern": "*.ds"
"filenamePattern": "*.ds",
"language": "diagramJson"
}
]
}
Expand Down
12 changes: 10 additions & 2 deletions packages/ds-ext/src/DiagramDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export class DiagramDocument implements vscode.CustomDocument {
return new DiagramDocument(uri, fileData);
}

private _onDidChange = new vscode.EventEmitter<vscode.CustomDocumentEditEvent<DiagramDocument>>();
readonly onDidChange = this._onDidChange.event;

private constructor(
public readonly uri: vscode.Uri,
private documentData: Uint8Array,
Expand All @@ -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
Expand Down Expand Up @@ -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
});
}
}
}
21 changes: 18 additions & 3 deletions packages/ds-ext/src/DiagramEditorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import { loadConfig } from './loadConfig';
import { DataStoryConfig } from './DataStoryConfig';

export class DiagramEditorProvider implements vscode.CustomEditorProvider<DiagramDocument> {
private readonly _onDidChangeCustomDocument = new vscode.EventEmitter<vscode.CustomDocumentEditEvent<DiagramDocument>>();
public readonly onDidChangeCustomDocument = this._onDidChangeCustomDocument.event;
public readonly onDidChangeCustomDocument = new vscode.EventEmitter<vscode.CustomDocumentEditEvent<DiagramDocument>>().event;
private inputObserverController!: InputObserverController;
private observerStorage!: ObserverStorage;
private config: DataStoryConfig;
private contentMap = new Map<string, DiagramDocument>();

constructor(private readonly context: vscode.ExtensionContext) {
this.config = loadConfig(this.context);
Expand Down Expand Up @@ -58,6 +58,10 @@ export class DiagramEditorProvider implements vscode.CustomEditorProvider<Diagra
this.inputObserverController = new InputObserverController(this.observerStorage);
}

/**
* openCustomDocument is called when the first time an editor for a given resource is opened.
* When multiple instances of the editor are opened or closed, the OpenCustomDocument method won't be re-invoked.
*/
async openCustomDocument(
uri: vscode.Uri,
_openContext: vscode.CustomDocumentOpenContext,
Expand All @@ -66,7 +70,10 @@ export class DiagramEditorProvider implements vscode.CustomEditorProvider<Diagra
// Initialize storage with diagram ID from the file name
const diagramId = path.basename(uri.fsPath);
await this.initializeStorage(diagramId);
return DiagramDocument.create(uri);

const diagramDocument = await DiagramDocument.create(uri);
this.contentMap.set(uri.toString(), diagramDocument);
return diagramDocument;
}

resolveCustomEditor(
Expand Down Expand Up @@ -152,6 +159,14 @@ export class DiagramEditorProvider implements vscode.CustomEditorProvider<Diagra
`;
}

provideDiagramContent(uri: vscode.Uri): DiagramDocument {
const document = this.contentMap.get(uri.toString());
if (!document) {
throw new Error('Could not find document');
}

return document;
}
// Save changes to the document
saveCustomDocument(document: DiagramDocument, cancellation: vscode.CancellationToken): Thenable<void> {
return document.save();
Expand Down
24 changes: 24 additions & 0 deletions packages/ds-ext/src/JsonReadonlyProvider.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>();
readonly onDidChangeEmitter = new vscode.EventEmitter<vscode.Uri>();
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();
}
}
2 changes: 1 addition & 1 deletion packages/ds-ext/src/commands/createDemosDirectory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
66 changes: 64 additions & 2 deletions packages/ds-ext/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -52,4 +113,5 @@ export function activate(context: vscode.ExtensionContext) {

export function deactivate(context: any) {
diagramEditorProvider.dispose();
}
jsonReadonlyProvider?.dispose();
}
1 change: 0 additions & 1 deletion packages/ds-ext/themes/file-icon-theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
}
},
"fileExtensions": {
"diagram.json": "diagramIcon",
"ds": "diagramIcon"
},
"fileNames": {},
Expand Down
4 changes: 4 additions & 0 deletions packages/ds-ext/themes/preview-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions packages/ds-ext/themes/preview-white.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const defaultImport = (): Promise<Diagram> => {
// 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
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/DataStory/hooks/useEscapeKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>) {
export function useEscapeKey(handleClose: () => void, flowRef?: React.RefObject<HTMLDivElement | null>) {
const handleEscKey = useCallback((event: KeyboardEvent) => {
if (event.key === KEY_NAME_ESC) {
handleClose();
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/DropDown/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/Node/table/TableCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const formatTooltipContent = (content: unknown) => {
}
}

export function TableCell(props: { tableRef: React.RefObject<HTMLTableElement>, content?: unknown }): JSX.Element {
export function TableCell(props: { tableRef: React.RefObject<HTMLTableElement | null>, content?: unknown }) {
const { content = '', tableRef } = props;
const [showTooltip, setShowTooltip] = useState(false);

Expand Down