@@ -9,10 +9,13 @@ import { Config } from "../config/config"
99import { mergeDeep } from "remeda"
1010import { Instance } from "../project/instance"
1111import { Process } from "../util/process"
12+ import { InstanceContext } from "@/effect/instance-context"
13+ import { Effect , Layer , ServiceMap } from "effect"
14+ import { runPromiseInstance } from "@/effect/runtime"
1215
13- export namespace Format {
14- const log = Log . create ( { service : "format" } )
16+ const log = Log . create ( { service : "format" } )
1517
18+ export namespace Format {
1619 export const Status = z
1720 . object ( {
1821 name : z . string ( ) ,
@@ -24,113 +27,135 @@ export namespace Format {
2427 } )
2528 export type Status = z . infer < typeof Status >
2629
27- const state = Instance . state ( async ( ) => {
28- const cache : Record < string , string [ ] | false > = { }
29- const cfg = await Config . get ( )
30+ export async function init ( ) {
31+ return runPromiseInstance ( FormatService . use ( ( s ) => s . init ( ) ) )
32+ }
33+
34+ export async function status ( ) {
35+ return runPromiseInstance ( FormatService . use ( ( s ) => s . status ( ) ) )
36+ }
37+ }
38+
39+ export namespace FormatService {
40+ export interface Service {
41+ readonly init : ( ) => Effect . Effect < void >
42+ readonly status : ( ) => Effect . Effect < Format . Status [ ] >
43+ }
44+ }
45+
46+ export class FormatService extends ServiceMap . Service < FormatService , FormatService . Service > ( ) ( "@opencode/Format" ) {
47+ static readonly layer = Layer . effect (
48+ FormatService ,
49+ Effect . gen ( function * ( ) {
50+ const instance = yield * InstanceContext
51+
52+ const enabled : Record < string , boolean > = { }
53+ const formatters : Record < string , Formatter . Info > = { }
3054
31- const formatters : Record < string , Formatter . Info > = { }
32- if ( cfg . formatter === false ) {
33- log . info ( "all formatters are disabled" )
34- return {
35- cache,
36- formatters,
55+ const cfg = yield * Effect . promise ( ( ) => Config . get ( ) )
56+
57+ if ( cfg . formatter !== false ) {
58+ for ( const item of Object . values ( Formatter ) ) {
59+ formatters [ item . name ] = item
60+ }
61+ for ( const [ name , item ] of Object . entries ( cfg . formatter ?? { } ) ) {
62+ if ( item . disabled ) {
63+ delete formatters [ name ]
64+ continue
65+ }
66+ const result = mergeDeep ( formatters [ name ] ?? { } , {
67+ command : [ ] ,
68+ extensions : [ ] ,
69+ ...item ,
70+ } ) as Formatter . Info
71+
72+ if ( result . command . length === 0 ) continue
73+
74+ result . enabled = async ( ) => true
75+ result . name = name
76+ formatters [ name ] = result
77+ }
78+ } else {
79+ log . info ( "all formatters are disabled" )
3780 }
38- }
39-
40- for ( const item of Object . values ( Formatter ) ) {
41- formatters [ item . name ] = item
42- }
43- for ( const [ name , item ] of Object . entries ( cfg . formatter ?? { } ) ) {
44- if ( item . disabled ) {
45- delete formatters [ name ]
46- continue
81+
82+ async function isEnabled ( item : Formatter . Info ) {
83+ let status = enabled [ item . name ]
84+ if ( status === undefined ) {
85+ status = await item . enabled ( )
86+ enabled [ item . name ] = status
87+ }
88+ return status
4789 }
48- const result : Formatter . Info = mergeDeep ( formatters [ name ] ?? { } , {
49- extensions : [ ] ,
50- ...item ,
51- } )
5290
53- result . enabled = async ( ) => item . command ?? false
54- result . name = name
55- formatters [ name ] = result
56- }
57-
58- return {
59- cache,
60- formatters,
61- }
62- } )
63-
64- async function resolveCommand ( item : Formatter . Info ) {
65- const s = await state ( )
66- let command = s . cache [ item . name ]
67- if ( command === undefined ) {
68- log . info ( "resolving command" , { name : item . name } )
69- command = await item . enabled ( )
70- s . cache [ item . name ] = command
71- }
72- return command
73- }
91+ async function getFormatter ( ext : string ) {
92+ const result = [ ]
93+ for ( const item of Object . values ( formatters ) ) {
94+ log . info ( "checking" , { name : item . name , ext } )
95+ if ( ! item . extensions . includes ( ext ) ) continue
96+ if ( ! ( await isEnabled ( item ) ) ) continue
97+ log . info ( "enabled" , { name : item . name , ext } )
98+ result . push ( item )
99+ }
100+ return result
101+ }
74102
75- async function getFormatter ( ext : string ) {
76- const formatters = await state ( ) . then ( ( x ) => x . formatters )
77- const result : { info : Formatter . Info ; command : string [ ] } [ ] = [ ]
78- for ( const item of Object . values ( formatters ) ) {
79- if ( ! item . extensions . includes ( ext ) ) continue
80- const command = await resolveCommand ( item )
81- if ( ! command ) continue
82- log . info ( "enabled" , { name : item . name , ext } )
83- result . push ( { info : item , command } )
84- }
85- return result
86- }
103+ const unsubscribe = Bus . subscribe (
104+ File . Event . Edited ,
105+ Instance . bind ( async ( payload ) => {
106+ const file = payload . properties . file
107+ log . info ( "formatting" , { file } )
108+ const ext = path . extname ( file )
87109
88- export async function status ( ) {
89- const s = await state ( )
90- const result : Status [ ] = [ ]
91- for ( const formatter of Object . values ( s . formatters ) ) {
92- const command = await resolveCommand ( formatter )
93- result . push ( {
94- name : formatter . name ,
95- extensions : formatter . extensions ,
96- enabled : ! ! command ,
97- } )
98- }
99- return result
100- }
110+ for ( const item of await getFormatter ( ext ) ) {
111+ log . info ( "running" , { command : item . command } )
112+ try {
113+ const proc = Process . spawn (
114+ item . command . map ( ( x ) => x . replace ( "$FILE" , file ) ) ,
115+ {
116+ cwd : instance . directory ,
117+ env : { ...process . env , ...item . environment } ,
118+ stdout : "ignore" ,
119+ stderr : "ignore" ,
120+ } ,
121+ )
122+ const exit = await proc . exited
123+ if ( exit !== 0 )
124+ log . error ( "failed" , {
125+ command : item . command ,
126+ ...item . environment ,
127+ } )
128+ } catch ( error ) {
129+ log . error ( "failed to format file" , {
130+ error,
131+ command : item . command ,
132+ ...item . environment ,
133+ file,
134+ } )
135+ }
136+ }
137+ } ) ,
138+ )
101139
102- export function init ( ) {
103- log . info ( "init" )
104- Bus . subscribe ( File . Event . Edited , async ( payload ) => {
105- const file = payload . properties . file
106- log . info ( "formatting" , { file } )
107- const ext = path . extname ( file )
108-
109- for ( const { info, command } of await getFormatter ( ext ) ) {
110- const replaced = command . map ( ( x ) => x . replace ( "$FILE" , file ) )
111- log . info ( "running" , { replaced } )
112- try {
113- const proc = Process . spawn ( replaced , {
114- cwd : Instance . directory ,
115- env : { ...process . env , ...info . environment } ,
116- stdout : "ignore" ,
117- stderr : "ignore" ,
118- } )
119- const exit = await proc . exited
120- if ( exit !== 0 )
121- log . error ( "failed" , {
122- command,
123- ...info . environment ,
124- } )
125- } catch ( error ) {
126- log . error ( "failed to format file" , {
127- error,
128- command,
129- ...info . environment ,
130- file,
140+ yield * Effect . addFinalizer ( ( ) => Effect . sync ( unsubscribe ) )
141+ log . info ( "init" )
142+
143+ const init = Effect . fn ( "FormatService.init" ) ( function * ( ) { } )
144+
145+ const status = Effect . fn ( "FormatService.status" ) ( function * ( ) {
146+ const result : Format . Status [ ] = [ ]
147+ for ( const formatter of Object . values ( formatters ) ) {
148+ const isOn = yield * Effect . promise ( ( ) => isEnabled ( formatter ) )
149+ result . push ( {
150+ name : formatter . name ,
151+ extensions : formatter . extensions ,
152+ enabled : isOn ,
131153 } )
132154 }
133- }
134- } )
135- }
155+ return result
156+ } )
157+
158+ return FormatService . of ( { init, status } )
159+ } ) ,
160+ )
136161}
0 commit comments