1+ import { HocuspocusProvider , HocuspocusProviderWebsocket } from '@hocuspocus/provider'
12import path from 'node:path'
23import vs from 'vscode'
34
@@ -9,10 +10,19 @@ export class WorkbenchFileSystemProvider implements vs.FileSystemProvider {
910 constructor (
1011 /** Base path of the project on disk. */
1112 readonly basePath : string ,
13+ /** Absolute path to file ↦ Yjs connection for that file */
14+ // TODO: a wrapper on hocuspocusprovider with a promise for synced
15+ // then in readFile, lazily create one of these,
16+ // wait for initial sync,
17+ // then return the contents.
18+ private readonly docs : Map < string , HocuspocusProvider > ,
19+ readonly collabSock : HocuspocusProviderWebsocket ,
20+ private readonly log : vs . LogOutputChannel ,
1221 ) { }
1322
14- /** Convert a `wrkbnch:` URI to a `file:` URI. */
15- private toDiskUri ( uri : vs . Uri ) : vs . Uri {
23+ /** Convert a `wrkbnch:` URI to a `file:` URI,
24+ * ensuring that it is accessible. */
25+ private checkedToFileUri ( uri : vs . Uri ) : vs . Uri {
1626 const filePath = path . normalize ( uri . fsPath )
1727 if ( ! `${ filePath } /` . startsWith ( this . basePath ) ) {
1828 throw vs . FileSystemError . NoPermissions ( uri )
@@ -22,26 +32,52 @@ export class WorkbenchFileSystemProvider implements vs.FileSystemProvider {
2232
2333 // Operations that touch single files go through collab-server
2434
35+ // TODO: move to DocManager
36+ async ensureHpDoc ( path : string ) : Promise < HocuspocusProvider > {
37+ let doc = this . docs . get ( path )
38+ // TODO should wait on doc.synced; same deal as `EditorSessionManager`
39+ if ( doc ) return doc
40+ return new Promise ( ( resolve , reject ) => {
41+ doc = new HocuspocusProvider ( {
42+ websocketProvider : this . collabSock ,
43+ name : path ,
44+ onSynced : data => {
45+ console . log ( `[HocuspocusProvider] '${ path } ' synced ${ String ( data . state ) } ` )
46+ // this.log.trace(`[HocuspocusProvider] '${path}' synced ${String(data.state)}`)
47+ resolve ( doc ! )
48+ } ,
49+ } )
50+ setTimeout ( ( ) => {
51+ reject ( new Error ( 'timeout waiting for HocuspocusProvider' ) )
52+ } , 3_000 )
53+ this . docs . set ( path , doc )
54+ doc . attach ( )
55+ } )
56+ }
57+
2558 watch ( ) : vs . Disposable {
2659 throw new Error ( 'TODO watch' )
2760 }
2861
2962 async stat ( uri : vs . Uri ) : Promise < vs . FileStat > {
3063 // TODO: stat from Yjs
31- return vs . workspace . fs . stat ( this . toDiskUri ( uri ) )
64+ return vs . workspace . fs . stat ( this . checkedToFileUri ( uri ) )
3265 }
3366
3467 async readFile ( uri : vs . Uri ) : Promise < Uint8Array > {
35- // TODO: prefer reads from Yjs
36- return vs . workspace . fs . readFile ( this . toDiskUri ( uri ) )
68+ // TODO: only read from Yjs; ignore fs.
69+ console . log ( `reading file ${ JSON . stringify ( uri ) } ` )
70+ const doc = await this . ensureHpDoc ( this . checkedToFileUri ( uri ) . fsPath )
71+ console . log ( `read file ${ JSON . stringify ( uri ) } from Yjs` )
72+ return vs . workspace . fs . readFile ( this . checkedToFileUri ( uri ) )
3773 }
3874
3975 async writeFile (
4076 uri : vs . Uri ,
4177 content : Uint8Array ,
4278 options : { readonly create : boolean ; readonly overwrite : boolean } ,
4379 ) : Promise < void > {
44- const existed = await this . stat ( this . toDiskUri ( uri ) ) . then (
80+ const existed = await this . stat ( this . checkedToFileUri ( uri ) ) . then (
4581 ( ) => true ,
4682 ( ) => false ,
4783 )
@@ -54,21 +90,21 @@ export class WorkbenchFileSystemProvider implements vs.FileSystemProvider {
5490 // Filesystem operations go through the filesystem
5591
5692 async readDirectory ( uri : vs . Uri ) : Promise < [ string , vs . FileType ] [ ] > {
57- return vs . workspace . fs . readDirectory ( this . toDiskUri ( uri ) )
93+ return vs . workspace . fs . readDirectory ( this . checkedToFileUri ( uri ) )
5894 }
5995
6096 async createDirectory ( uri : vs . Uri ) : Promise < void > {
61- await vs . workspace . fs . createDirectory ( this . toDiskUri ( uri ) )
97+ await vs . workspace . fs . createDirectory ( this . checkedToFileUri ( uri ) )
6298 this . onDidChangeFileEmitter . fire ( [ { type : vs . FileChangeType . Created , uri } ] )
6399 }
64100
65101 async delete ( uri : vs . Uri , options : { readonly recursive : boolean } ) : Promise < void > {
66- await vs . workspace . fs . delete ( this . toDiskUri ( uri ) , options )
102+ await vs . workspace . fs . delete ( this . checkedToFileUri ( uri ) , options )
67103 this . onDidChangeFileEmitter . fire ( [ { type : vs . FileChangeType . Deleted , uri } ] )
68104 }
69105
70106 async rename ( oldUri : vs . Uri , newUri : vs . Uri , options : { readonly overwrite : boolean } ) : Promise < void > {
71- await vs . workspace . fs . rename ( this . toDiskUri ( oldUri ) , this . toDiskUri ( newUri ) , options )
107+ await vs . workspace . fs . rename ( this . checkedToFileUri ( oldUri ) , this . checkedToFileUri ( newUri ) , options )
72108 this . onDidChangeFileEmitter . fire ( [
73109 { type : vs . FileChangeType . Deleted , uri : oldUri } ,
74110 { type : vs . FileChangeType . Created , uri : newUri } ,
0 commit comments