import { Effect, Layer, ServiceMap } from "effect" import { InstanceState } from "@/effect/instance-state" import { makeRunPromise } from "@/effect/run-service" import path from "path" import { mergeDeep } from "remeda" import z from "zod" import { Bus } from "../bus" import { Config } from "../config/config" import { File } from "../file" import { Instance } from "../project/instance" import { Process } from "../util/process" import { Log } from "../util/log" import * as Formatter from "./formatter" export namespace Format { const log = Log.create({ service: "format" }) export const Status = z .object({ name: z.string(), extensions: z.string().array(), enabled: z.boolean(), }) .meta({ ref: "FormatterStatus", }) export type Status = z.infer export interface Interface { readonly init: () => Effect.Effect readonly status: () => Effect.Effect } export class Service extends ServiceMap.Service()("@opencode/Format") {} export const layer = Layer.effect( Service, Effect.gen(function* () { const state = yield* InstanceState.make( Effect.fn("Format.state")(function* (_ctx) { const enabled: Record = {} const formatters: Record = {} const cfg = yield* Effect.promise(() => Config.get()) if (cfg.formatter !== false) { for (const item of Object.values(Formatter)) { formatters[item.name] = item } for (const [name, item] of Object.entries(cfg.formatter ?? {})) { if (item.disabled) { delete formatters[name] continue } const info = mergeDeep(formatters[name] ?? {}, { command: [], extensions: [], ...item, }) if (info.command.length === 0) continue formatters[name] = { ...info, name, enabled: async () => true, } } } else { log.info("all formatters are disabled") } async function isEnabled(item: Formatter.Info) { let status = enabled[item.name] if (status === undefined) { status = await item.enabled() enabled[item.name] = status } return status } async function getFormatter(ext: string) { const matching = Object.values(formatters).filter((item) => item.extensions.includes(ext)) const checks = await Promise.all( matching.map(async (item) => { log.info("checking", { name: item.name, ext }) const on = await isEnabled(item) if (on) { log.info("enabled", { name: item.name, ext }) } return { item, enabled: on, } }), ) return checks.filter((x) => x.enabled).map((x) => x.item) } yield* Effect.acquireRelease( Effect.sync(() => Bus.subscribe( File.Event.Edited, Instance.bind(async (payload) => { const file = payload.properties.file log.info("formatting", { file }) const ext = path.extname(file) for (const item of await getFormatter(ext)) { log.info("running", { command: item.command }) try { const proc = Process.spawn( item.command.map((x) => x.replace("$FILE", file)), { cwd: Instance.directory, env: { ...process.env, ...item.environment }, stdout: "ignore", stderr: "ignore", }, ) const exit = await proc.exited if (exit !== 0) { log.error("failed", { command: item.command, ...item.environment, }) } } catch (error) { log.error("failed to format file", { error, command: item.command, ...item.environment, file, }) } } }), ), ), (unsubscribe) => Effect.sync(unsubscribe), ) log.info("init") return { formatters, isEnabled, } }), ) const init = Effect.fn("Format.init")(function* () { yield* InstanceState.get(state) }) const status = Effect.fn("Format.status")(function* () { const { formatters, isEnabled } = yield* InstanceState.get(state) const result: Status[] = [] for (const formatter of Object.values(formatters)) { const isOn = yield* Effect.promise(() => isEnabled(formatter)) result.push({ name: formatter.name, extensions: formatter.extensions, enabled: isOn, }) } return result }) return Service.of({ init, status }) }), ) const runPromise = makeRunPromise(Service, layer) export async function init() { return runPromise((s) => s.init()) } export async function status() { return runPromise((s) => s.status()) } }