@@ -14,6 +14,9 @@ import DataGrid from 'react-data-grid';
14
14
import * as utils from './utils' ;
15
15
import 'react-data-grid/lib/styles.css' ;
16
16
import './Editor.css' ;
17
+ import { deflate , inflate } from "pako" ;
18
+ import { encode , decode } from '@msgpack/msgpack' ;
19
+ import { bufferToBase64 , base64ToBuffer } from '../../webR/utils' ;
17
20
18
21
const language = new Compartment ( ) ;
19
22
const tabSize = new Compartment ( ) ;
@@ -44,6 +47,23 @@ export type EditorFile = EditorBase & {
44
47
scrollLeft ?: number ;
45
48
} ;
46
49
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
+
47
67
export type EditorItem = EditorData | EditorHtml | EditorFile ;
48
68
49
69
export function FileTabs ( {
@@ -146,6 +166,44 @@ export function Editor({
146
166
retrieveCompletions : RFunction ;
147
167
} > ( null ) ;
148
168
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
+
149
207
React . useEffect ( ( ) => {
150
208
let shelter : Shelter | null = null ;
151
209
@@ -160,6 +218,15 @@ export function Editor({
160
218
completeToken : await shelter . evalR ( 'utils:::.completeToken' ) as RFunction ,
161
219
retrieveCompletions : await shelter . evalR ( 'utils:::.retrieveCompletions' ) as RFunction ,
162
220
} ;
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 ( / ( c o d e ) = ( .* ) / ) ;
225
+ if ( shareHash && shareHash [ 1 ] === 'code' ) {
226
+ const items = await shareDataToEditorItems ( shareHash [ 2 ] ) ;
227
+ void filesInterface . refreshFilesystem ( ) ;
228
+ setFiles ( items ) ;
229
+ }
163
230
} ) ;
164
231
165
232
return function cleanup ( ) {
@@ -321,24 +388,24 @@ export function Editor({
321
388
} ) ;
322
389
} , [ syncActiveFileState , editorView ] ) ;
323
390
324
- const saveFile = React . useCallback ( ( ) => {
391
+ const saveFile = React . useCallback ( async ( ) => {
325
392
if ( ! editorView || activeFile . type !== "text" || activeFile . readOnly ) {
326
393
return ;
327
394
}
328
395
329
396
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
+ }
342
409
} , [ syncActiveFileState , editorView ] ) ;
343
410
344
411
React . useEffect ( ( ) => {
@@ -378,6 +445,21 @@ export function Editor({
378
445
} ;
379
446
} , [ ] ) ;
380
447
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
+
381
463
/*
382
464
* Register this component with the files interface so that when it comes to
383
465
* opening files they are displayed in this codemirror instance.
0 commit comments