Skip to content

Commit 80ef9ea

Browse files
Apply PR #17675: refactor(format): effectify FormatService as scoped service
2 parents 96e8798 + 56d5cb3 commit 80ef9ea

File tree

5 files changed

+659
-103
lines changed

5 files changed

+659
-103
lines changed

packages/opencode/src/effect/instances.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { PermissionService } from "@/permission/service"
77
import { FileWatcherService } from "@/file/watcher"
88
import { VcsService } from "@/project/vcs"
99
import { FileTimeService } from "@/file/time"
10+
import { FormatService } from "@/format"
1011
import { Instance } from "@/project/instance"
1112

1213
export { InstanceContext } from "./instance-context"
@@ -18,6 +19,7 @@ export type InstanceServices =
1819
| FileWatcherService
1920
| VcsService
2021
| FileTimeService
22+
| FormatService
2123

2224
function lookup(directory: string) {
2325
const project = Instance.project
@@ -29,6 +31,7 @@ function lookup(directory: string) {
2931
Layer.fresh(FileWatcherService.layer).pipe(Layer.orDie),
3032
Layer.fresh(VcsService.layer),
3133
Layer.fresh(FileTimeService.layer).pipe(Layer.orDie),
34+
Layer.fresh(FormatService.layer),
3235
).pipe(Layer.provide(ctx))
3336
}
3437

packages/opencode/src/format/index.ts

Lines changed: 125 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ import { Config } from "../config/config"
99
import { mergeDeep } from "remeda"
1010
import { Instance } from "../project/instance"
1111
import { 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,133 @@ 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 cache: Record<string, string[] | false> = {}
53+
const formatters: Record<string, Formatter.Info> = {}
54+
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+
extensions: [],
68+
...item,
69+
}) as Formatter.Info
70+
71+
result.enabled = async () => item.command ?? false
72+
result.name = name
73+
formatters[name] = result
74+
}
75+
} else {
76+
log.info("all formatters are disabled")
77+
}
3078

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,
79+
async function resolveCommand(item: Formatter.Info) {
80+
let command = cache[item.name]
81+
if (command === undefined) {
82+
command = await item.enabled()
83+
cache[item.name] = command
84+
}
85+
return command
3786
}
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
87+
88+
async function getFormatter(ext: string) {
89+
const result: { info: Formatter.Info; command: string[] }[] = []
90+
for (const item of Object.values(formatters)) {
91+
log.info("checking", { name: item.name, ext })
92+
if (!item.extensions.includes(ext)) continue
93+
const command = await resolveCommand(item)
94+
if (!command) continue
95+
log.info("enabled", { name: item.name, ext })
96+
result.push({ info: item, command })
97+
}
98+
return result
4799
}
48-
const result: Formatter.Info = mergeDeep(formatters[name] ?? {}, {
49-
extensions: [],
50-
...item,
51-
})
52100

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-
}
101+
const unsubscribe = Bus.subscribe(
102+
File.Event.Edited,
103+
Instance.bind(async (payload) => {
104+
const file = payload.properties.file
105+
log.info("formatting", { file })
106+
const ext = path.extname(file)
74107

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-
}
108+
for (const { info, command } of await getFormatter(ext)) {
109+
log.info("running", { command })
110+
try {
111+
const proc = Process.spawn(
112+
command.map((x) => x.replace("$FILE", file)),
113+
{
114+
cwd: instance.directory,
115+
env: { ...process.env, ...info.environment },
116+
stdout: "ignore",
117+
stderr: "ignore",
118+
},
119+
)
120+
const exit = await proc.exited
121+
if (exit !== 0)
122+
log.error("failed", {
123+
command,
124+
...info.environment,
125+
})
126+
} catch (error) {
127+
log.error("failed to format file", {
128+
error,
129+
command,
130+
...info.environment,
131+
file,
132+
})
133+
}
134+
}
135+
}),
136+
)
87137

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-
}
138+
yield* Effect.addFinalizer(() => Effect.sync(unsubscribe))
139+
log.info("init")
101140

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,
141+
const init = Effect.fn("FormatService.init")(function* () {})
142+
143+
const status = Effect.fn("FormatService.status")(function* () {
144+
const result: Format.Status[] = []
145+
for (const formatter of Object.values(formatters)) {
146+
const command = yield* Effect.promise(() => resolveCommand(formatter))
147+
result.push({
148+
name: formatter.name,
149+
extensions: formatter.extensions,
150+
enabled: !!command,
131151
})
132152
}
133-
}
134-
})
135-
}
153+
return result
154+
})
155+
156+
return FormatService.of({ init, status })
157+
}),
158+
)
136159
}

packages/opencode/src/project/bootstrap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export async function InstanceBootstrap() {
1818
Log.Default.info("bootstrapping", { directory: Instance.directory })
1919
await Plugin.init()
2020
ShareNext.init()
21-
Format.init()
21+
await Format.init()
2222
await LSP.init()
2323
await runPromiseInstance(FileWatcherService.use((service) => service.init()))
2424
File.init()

0 commit comments

Comments
 (0)