11import { BusEvent } from "@/bus/bus-event"
22import { InstanceState } from "@/effect/instance-state"
33import { makeRuntime } from "@/effect/run-service"
4+ import { AppFileSystem } from "@/filesystem"
45import { git } from "@/util/git"
56import { Effect , Layer , ServiceMap } from "effect"
67import { formatPatch , structuredPatch } from "diff"
@@ -343,6 +344,8 @@ export namespace File {
343344 export const layer = Layer . effect (
344345 Service ,
345346 Effect . gen ( function * ( ) {
347+ const appFs = yield * AppFileSystem . Service
348+
346349 const state = yield * InstanceState . make < State > (
347350 Effect . fn ( "File.state" ) ( ( ) =>
348351 Effect . succeed ( {
@@ -512,57 +515,54 @@ export namespace File {
512515 } )
513516
514517 const read = Effect . fn ( "File.read" ) ( function * ( file : string ) {
515- return yield * Effect . promise ( async ( ) : Promise < File . Content > => {
516- using _ = log . time ( "read" , { file } )
517- const full = path . join ( Instance . directory , file )
518+ using _ = log . time ( "read" , { file } )
519+ const full = path . join ( Instance . directory , file )
518520
519- if ( ! Instance . containsPath ( full ) ) {
520- throw new Error ( "Access denied: path escapes project directory" )
521- }
521+ if ( ! Instance . containsPath ( full ) ) throw new Error ( "Access denied: path escapes project directory" )
522522
523- if ( isImageByExtension ( file ) ) {
524- if ( await Filesystem . exists ( full ) ) {
525- const buffer = await Filesystem . readBytes ( full ) . catch ( ( ) => Buffer . from ( [ ] ) )
526- return {
527- type : "text" ,
528- content : buffer . toString ( "base64" ) ,
529- mimeType : getImageMimeType ( file ) ,
530- encoding : "base64" ,
531- }
523+ if ( isImageByExtension ( file ) ) {
524+ const exists = yield * appFs . existsSafe ( full )
525+ if ( exists ) {
526+ const bytes = yield * appFs . readFile ( full ) . pipe ( Effect . catch ( ( ) => Effect . succeed ( new Uint8Array ( ) ) ) )
527+ return {
528+ type : "text" as const ,
529+ content : Buffer . from ( bytes ) . toString ( "base64" ) ,
530+ mimeType : getImageMimeType ( file ) ,
531+ encoding : "base64" as const ,
532532 }
533- return { type : "text" , content : "" }
534533 }
534+ return { type : "text" as const , content : "" }
535+ }
535536
536- const knownText = isTextByExtension ( file ) || isTextByName ( file )
537+ const knownText = isTextByExtension ( file ) || isTextByName ( file )
537538
538- if ( isBinaryByExtension ( file ) && ! knownText ) {
539- return { type : "binary" , content : "" }
540- }
539+ if ( isBinaryByExtension ( file ) && ! knownText ) return { type : "binary" as const , content : "" }
541540
542- if ( ! ( await Filesystem . exists ( full ) ) ) {
543- return { type : "text" , content : "" }
544- }
541+ const exists = yield * appFs . existsSafe ( full )
542+ if ( ! exists ) return { type : "text" as const , content : "" }
545543
546- const mimeType = Filesystem . mimeType ( full )
547- const encode = knownText ? false : shouldEncode ( mimeType )
544+ const mimeType = Filesystem . mimeType ( full )
545+ const encode = knownText ? false : shouldEncode ( mimeType )
548546
549- if ( encode && ! isImage ( mimeType ) ) {
550- return { type : "binary" , content : "" , mimeType }
551- }
547+ if ( encode && ! isImage ( mimeType ) ) return { type : "binary" as const , content : "" , mimeType }
552548
553- if ( encode ) {
554- const buffer = await Filesystem . readBytes ( full ) . catch ( ( ) => Buffer . from ( [ ] ) )
555- return {
556- type : "text" ,
557- content : buffer . toString ( "base64" ) ,
558- mimeType,
559- encoding : "base64" ,
560- }
549+ if ( encode ) {
550+ const bytes = yield * appFs . readFile ( full ) . pipe ( Effect . catch ( ( ) => Effect . succeed ( new Uint8Array ( ) ) ) )
551+ return {
552+ type : "text" as const ,
553+ content : Buffer . from ( bytes ) . toString ( "base64" ) ,
554+ mimeType,
555+ encoding : "base64" as const ,
561556 }
557+ }
562558
563- const content = ( await Filesystem . readText ( full ) . catch ( ( ) => "" ) ) . trim ( )
559+ const content = yield * appFs . readFileString ( full ) . pipe (
560+ Effect . map ( ( s ) => s . trim ( ) ) ,
561+ Effect . catch ( ( ) => Effect . succeed ( "" ) ) ,
562+ )
564563
565- if ( Instance . project . vcs === "git" ) {
564+ if ( Instance . project . vcs === "git" ) {
565+ return yield * Effect . promise ( async ( ) : Promise < File . Content > => {
566566 let diff = (
567567 await git ( [ "-c" , "core.fsmonitor=false" , "diff" , "--" , file ] , { cwd : Instance . directory } )
568568 ) . text ( )
@@ -579,60 +579,51 @@ export namespace File {
579579 context : Infinity ,
580580 ignoreWhitespace : true ,
581581 } )
582- return {
583- type : "text" ,
584- content,
585- patch,
586- diff : formatPatch ( patch ) ,
587- }
582+ return { type : "text" , content, patch, diff : formatPatch ( patch ) }
588583 }
589- }
584+ return { type : "text" , content }
585+ } )
586+ }
590587
591- return { type : "text" , content }
592- } )
588+ return { type : "text" as const , content }
593589 } )
594590
595591 const list = Effect . fn ( "File.list" ) ( function * ( dir ?: string ) {
596- return yield * Effect . promise ( async ( ) => {
597- const exclude = [ ".git" , ".DS_Store" ]
598- let ignored = ( _ : string ) => false
599- if ( Instance . project . vcs === "git" ) {
600- const ig = ignore ( )
601- const gitignore = path . join ( Instance . project . worktree , ".gitignore" )
602- if ( await Filesystem . exists ( gitignore ) ) {
603- ig . add ( await Filesystem . readText ( gitignore ) )
604- }
605- const ignoreFile = path . join ( Instance . project . worktree , ".ignore" )
606- if ( await Filesystem . exists ( ignoreFile ) ) {
607- ig . add ( await Filesystem . readText ( ignoreFile ) )
608- }
609- ignored = ig . ignores . bind ( ig )
610- }
611-
612- const resolved = dir ? path . join ( Instance . directory , dir ) : Instance . directory
613- if ( ! Instance . containsPath ( resolved ) ) {
614- throw new Error ( "Access denied: path escapes project directory" )
615- }
616-
617- const nodes : File . Node [ ] = [ ]
618- for ( const entry of await fs . promises . readdir ( resolved , { withFileTypes : true } ) . catch ( ( ) => [ ] ) ) {
619- if ( exclude . includes ( entry . name ) ) continue
620- const absolute = path . join ( resolved , entry . name )
621- const file = path . relative ( Instance . directory , absolute )
622- const type = entry . isDirectory ( ) ? "directory" : "file"
623- nodes . push ( {
624- name : entry . name ,
625- path : file ,
626- absolute,
627- type,
628- ignored : ignored ( type === "directory" ? file + "/" : file ) ,
629- } )
630- }
631-
632- return nodes . sort ( ( a , b ) => {
633- if ( a . type !== b . type ) return a . type === "directory" ? - 1 : 1
634- return a . name . localeCompare ( b . name )
592+ const exclude = [ ".git" , ".DS_Store" ]
593+ let ignored = ( _ : string ) => false
594+ if ( Instance . project . vcs === "git" ) {
595+ const ig = ignore ( )
596+ const gitignore = path . join ( Instance . project . worktree , ".gitignore" )
597+ const gitignoreText = yield * appFs . readFileString ( gitignore ) . pipe ( Effect . catch ( ( ) => Effect . succeed ( "" ) ) )
598+ if ( gitignoreText ) ig . add ( gitignoreText )
599+ const ignoreFile = path . join ( Instance . project . worktree , ".ignore" )
600+ const ignoreText = yield * appFs . readFileString ( ignoreFile ) . pipe ( Effect . catch ( ( ) => Effect . succeed ( "" ) ) )
601+ if ( ignoreText ) ig . add ( ignoreText )
602+ ignored = ig . ignores . bind ( ig )
603+ }
604+
605+ const resolved = dir ? path . join ( Instance . directory , dir ) : Instance . directory
606+ if ( ! Instance . containsPath ( resolved ) ) throw new Error ( "Access denied: path escapes project directory" )
607+
608+ const entries = yield * appFs . readDirectoryEntries ( resolved ) . pipe ( Effect . orElseSucceed ( ( ) => [ ] ) )
609+
610+ const nodes : File . Node [ ] = [ ]
611+ for ( const entry of entries ) {
612+ if ( exclude . includes ( entry . name ) ) continue
613+ const absolute = path . join ( resolved , entry . name )
614+ const file = path . relative ( Instance . directory , absolute )
615+ const type = entry . type === "directory" ? "directory" : "file"
616+ nodes . push ( {
617+ name : entry . name ,
618+ path : file ,
619+ absolute,
620+ type,
621+ ignored : ignored ( type === "directory" ? file + "/" : file ) ,
635622 } )
623+ }
624+ return nodes . sort ( ( a , b ) => {
625+ if ( a . type !== b . type ) return a . type === "directory" ? - 1 : 1
626+ return a . name . localeCompare ( b . name )
636627 } )
637628 } )
638629
@@ -676,7 +667,9 @@ export namespace File {
676667 } ) ,
677668 )
678669
679- const { runPromise } = makeRuntime ( Service , layer )
670+ export const defaultLayer = layer . pipe ( Layer . provide ( AppFileSystem . defaultLayer ) )
671+
672+ const { runPromise } = makeRuntime ( Service , defaultLayer )
680673
681674 export function init ( ) {
682675 return runPromise ( ( svc ) => svc . init ( ) )
0 commit comments