mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-04 13:45:31 +00:00
104 lines
3.4 KiB
TypeScript
104 lines
3.4 KiB
TypeScript
export * as Formatter from "./formatter.js"
|
|
|
|
import { Context, Effect, Layer } from "effect"
|
|
import { ChildProcess } from "effect/unstable/process"
|
|
import path from "path"
|
|
import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
|
|
import { AppProcess } from "@opencode-ai/util/process"
|
|
import { Location } from "./location.js"
|
|
import type { Info } from "./formatter/builtins.js"
|
|
import { State } from "./state.js"
|
|
|
|
type Data = {
|
|
formatters: Info[]
|
|
}
|
|
|
|
export type Editor = {
|
|
set: (formatter: Info) => void
|
|
remove: (name: string) => void
|
|
}
|
|
|
|
export interface Interface extends State.Transformable<Editor> {
|
|
readonly file: (filepath: string) => Effect.Effect<boolean>
|
|
}
|
|
|
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Formatter") {}
|
|
|
|
const layer = Layer.effect(
|
|
Service,
|
|
Effect.gen(function* () {
|
|
const location = yield* Location.Service
|
|
const processes = yield* AppProcess.Service
|
|
const commands = new WeakMap<Info, string[] | false>()
|
|
const state = State.create<Data, Editor>({
|
|
name: "formatter",
|
|
initial: () => ({ formatters: [] }),
|
|
editor: (editor) => ({
|
|
set: (formatter) => {
|
|
const index = editor.formatters.findIndex((item) => item.name === formatter.name)
|
|
if (index === -1) editor.formatters.push(formatter)
|
|
else editor.formatters[index] = formatter
|
|
},
|
|
remove: (name) => {
|
|
editor.formatters = editor.formatters.filter((formatter) => formatter.name !== name)
|
|
},
|
|
}),
|
|
})
|
|
|
|
const command = Effect.fnUntraced(function* (formatter: Info) {
|
|
const cached = commands.get(formatter)
|
|
if (cached !== undefined) return cached
|
|
const result = yield* formatter.enabled
|
|
if (result !== false) commands.set(formatter, result)
|
|
return result
|
|
})
|
|
|
|
const file = Effect.fn("Formatter.file")(function* (filepath: string) {
|
|
const extension = path.extname(filepath)
|
|
const matching = state.get().formatters.filter((formatter) => formatter.extensions.includes(extension))
|
|
|
|
for (const formatter of matching) {
|
|
const enabled = yield* command(formatter)
|
|
if (enabled === false) continue
|
|
const cmd = enabled.map((argument) => argument.replace("$FILE", filepath))
|
|
yield* Effect.logInfo("formatting file", { file: filepath, command: cmd })
|
|
const result = yield* processes
|
|
.run(
|
|
ChildProcess.make(cmd[0], cmd.slice(1), {
|
|
cwd: location.directory,
|
|
env: formatter.environment,
|
|
extendEnv: true,
|
|
stdin: "ignore",
|
|
stdout: "ignore",
|
|
stderr: "ignore",
|
|
}),
|
|
)
|
|
.pipe(
|
|
Effect.catch((error) =>
|
|
Effect.logError("failed to format file", {
|
|
file: filepath,
|
|
command: cmd,
|
|
error: error.message,
|
|
}).pipe(Effect.as(undefined)),
|
|
),
|
|
)
|
|
if (!result) continue
|
|
if (result.exitCode === 0) return true
|
|
yield* Effect.logError("formatter exited unsuccessfully", {
|
|
file: filepath,
|
|
command: cmd,
|
|
exitCode: result.exitCode,
|
|
})
|
|
}
|
|
return false
|
|
})
|
|
|
|
return Service.of({ transform: state.transform, reload: state.reload, file })
|
|
}),
|
|
)
|
|
|
|
export const node = makeLocationNode({
|
|
service: Service,
|
|
layer,
|
|
deps: [Location.node, AppProcess.node],
|
|
})
|