Skip to content

Commit ae4ad8f

Browse files
committed
REPL: Update share code in URL as files are modified
1 parent b183267 commit ae4ad8f

File tree

2 files changed

+105
-13
lines changed

2 files changed

+105
-13
lines changed

src/repl/components/Editor.tsx

Lines changed: 95 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import DataGrid from 'react-data-grid';
1414
import * as utils from './utils';
1515
import 'react-data-grid/lib/styles.css';
1616
import './Editor.css';
17+
import { deflate, inflate } from "pako";
18+
import { encode, decode } from '@msgpack/msgpack';
19+
import { bufferToBase64, base64ToBuffer } from '../../webR/utils';
1720

1821
const language = new Compartment();
1922
const tabSize = new Compartment();
@@ -44,6 +47,23 @@ export type EditorFile = EditorBase & {
4447
scrollLeft?: number;
4548
};
4649

50+
export interface ShareItem {
51+
name: string;
52+
path: string;
53+
data: Uint8Array;
54+
}
55+
56+
export function isShareItems(files: any): files is ShareItem[] {
57+
return Array.isArray(files) && files.every((file) =>
58+
'name' in file &&
59+
typeof file.name === 'string' &&
60+
'path' in file &&
61+
typeof file.path === 'string' &&
62+
'data' in file &&
63+
file.data instanceof Uint8Array
64+
)
65+
}
66+
4767
export type EditorItem = EditorData | EditorHtml | EditorFile;
4868

4969
export function FileTabs({
@@ -146,6 +166,44 @@ export function Editor({
146166
retrieveCompletions: RFunction;
147167
}>(null);
148168

169+
const editorToShareData = async (files: EditorItem[]): Promise<string> => {
170+
const shareFiles: ShareItem[] = await Promise.all(
171+
files.filter((file): file is EditorFile => file.type === "text" && !file.readOnly)
172+
.map(async (file) => ({
173+
name: file.name,
174+
path: file.path,
175+
data: await webR.FS.readFile(file.path)
176+
}))
177+
);
178+
const compressed = deflate(encode(shareFiles));
179+
return bufferToBase64(compressed);
180+
};
181+
182+
const shareDataToEditorItems = async (data: string): Promise<EditorItem[]> => {
183+
const buffer = base64ToBuffer(data);
184+
const items = decode(inflate(buffer));
185+
if (!isShareItems(items)) {
186+
throw new Error("Provided URL data is not a valid set of share files.");
187+
}
188+
189+
void Promise.all(items.map(async ({ path, data }) => await webR.FS.writeFile(path, data)));
190+
return items.map((file) => {
191+
const extensions = file.name.toLowerCase().endsWith('.r') ? scriptExtensions : editorExtensions;
192+
const state = EditorState.create({
193+
doc: new TextDecoder().decode(file.data),
194+
extensions
195+
});
196+
return {
197+
name: file.name,
198+
readOnly: false,
199+
path: file.path,
200+
type: "text",
201+
dirty: false,
202+
editorState: state,
203+
};
204+
});
205+
}
206+
149207
React.useEffect(() => {
150208
let shelter: Shelter | null = null;
151209

@@ -160,6 +218,15 @@ export function Editor({
160218
completeToken: await shelter.evalR('utils:::.completeToken') as RFunction,
161219
retrieveCompletions: await shelter.evalR('utils:::.retrieveCompletions') as RFunction,
162220
};
221+
222+
// Load files from URL, if a share code has been provided
223+
const url = new URL(window.location.href);
224+
const shareHash = url.hash.match(/(code)=(.*)/);
225+
if (shareHash && shareHash[1] === 'code') {
226+
const items = await shareDataToEditorItems(shareHash[2]);
227+
void filesInterface.refreshFilesystem();
228+
setFiles(items);
229+
}
163230
});
164231

165232
return function cleanup() {
@@ -321,24 +388,24 @@ export function Editor({
321388
});
322389
}, [syncActiveFileState, editorView]);
323390

324-
const saveFile = React.useCallback(() => {
391+
const saveFile = React.useCallback(async () => {
325392
if (!editorView || activeFile.type !== "text" || activeFile.readOnly) {
326393
return;
327394
}
328395

329396
syncActiveFileState();
330-
const code = editorView.state.doc.toString();
331-
const data = new TextEncoder().encode(code);
332-
333-
webR.FS.writeFile(activeFile.path, data)
334-
.then(() => setFileDirty(false))
335-
.then(() => {
336-
void filesInterface.refreshFilesystem();
337-
}, (reason) => {
338-
setFileDirty(true)
339-
console.error(reason);
340-
throw new Error(`Can't save editor contents. See the JavaScript console for details.`);
341-
})
397+
const content = editorView.state.doc.toString();
398+
const data = new TextEncoder().encode(content);
399+
400+
try {
401+
await webR.FS.writeFile(activeFile.path, data);
402+
void filesInterface.refreshFilesystem();
403+
setFileDirty(false);
404+
} catch (err) {
405+
setFileDirty(true)
406+
console.error(err);
407+
throw new Error(`Can't save editor contents. See the JavaScript console for details.`);
408+
}
342409
}, [syncActiveFileState, editorView]);
343410

344411
React.useEffect(() => {
@@ -378,6 +445,21 @@ export function Editor({
378445
};
379446
}, []);
380447

448+
449+
/*
450+
* Update the share URL as active files are saved
451+
*/
452+
React.useEffect(() => {
453+
const shouldUpdate = files.filter((file): file is EditorFile => file.type === 'text').every((file) => !file.dirty);
454+
if (files.length > 0 && shouldUpdate) {
455+
editorToShareData(files).then((shareData) => {
456+
const url = new URL(window.location.href);
457+
url.hash = `code=${shareData}`;
458+
window.history.pushState({}, '', url.toString());
459+
});
460+
}
461+
}, [files]);
462+
381463
/*
382464
* Register this component with the files interface so that when it comes to
383465
* opening files they are displayed in this codemirror instance.

src/webR/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,13 @@ export function bufferToBase64(buffer: ArrayBuffer) {
159159
}
160160
return window.btoa(binary);
161161
}
162+
163+
// From https://stackoverflow.com/a/21797381
164+
export function base64ToBuffer(base64: string) {
165+
var binaryString = window.atob(base64);
166+
var bytes = new Uint8Array(binaryString.length);
167+
for (var i = 0; i < binaryString.length; i++) {
168+
bytes[i] = binaryString.charCodeAt(i);
169+
}
170+
return bytes.buffer;
171+
}

0 commit comments

Comments
 (0)