Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/app/src/custom-elements.d.ts
2 changes: 1 addition & 1 deletion packages/enterprise/src/custom-elements.d.ts
1 change: 1 addition & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,7 @@ export namespace Config {
z.object({
disabled: z.boolean().optional(),
command: z.array(z.string()).optional(),
args: z.array(z.string()).optional(),
environment: z.record(z.string(), z.string()).optional(),
extensions: z.array(z.string()).optional(),
}),
Expand Down
43 changes: 43 additions & 0 deletions packages/opencode/src/format/formatter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import path from "path"
import { text } from "node:stream/consumers"
import { BunProc } from "../bun"
import { Instance } from "../project/instance"
Expand All @@ -9,11 +10,18 @@ import { Flag } from "@/flag/flag"
export interface Info {
name: string
command: string[]
args?: string[]
environment?: Record<string, string>
setup?(file: string): Promise<Setup | undefined>
extensions: string[]
enabled(): Promise<boolean>
}

export interface Setup {
directory?: string
placeholders?: Record<string, string>
}

export const gofmt: Info = {
name: "gofmt",
command: ["gofmt", "-w", "$FILE"],
Expand Down Expand Up @@ -350,6 +358,41 @@ export const rustfmt: Info = {
},
}

export const dotnetformat: Info = {
name: "dotnet-format",
command: ["dotnet", "format", "$TARGET", "--include", "$RELATIVE_FILE"],
async setup(file) {
let current = path.dirname(file)

while (true) {
for (const pattern of ["*.slnx", "*.sln", "*.csproj", "*.fsproj", "*.vbproj"]) {
const glob = new Bun.Glob(pattern)
const matches = Array.from(glob.scanSync({ cwd: current, absolute: true })).sort()
if (matches[0]) {
const root = path.dirname(matches[0])
const rel = path.relative(root, file)
if (!rel || rel.startsWith("..")) return
return {
directory: root,
placeholders: {
$TARGET: path.basename(matches[0]),
$RELATIVE_FILE: rel.split(path.sep).join("/"),
},
}
}
}

const parent = path.dirname(current)
if (parent === current) break
current = parent
}
},
extensions: [".cs", ".vb", ".fs", ".fsi", ".fsx", ".fsscript"],
async enabled() {
return which("dotnet") !== null
},
}

export const pint: Info = {
name: "pint",
command: ["./vendor/bin/pint", "$FILE"],
Expand Down
48 changes: 36 additions & 12 deletions packages/opencode/src/format/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,18 @@ export namespace Format {
delete formatters[name]
continue
}
const result: Formatter.Info = mergeDeep(formatters[name] ?? {}, {
command: [],
extensions: [],
...item,
})
const base = formatters[name]
const merged = mergeDeep(base ?? { command: [], extensions: [] }, item)
const result: Formatter.Info = {
...merged,
name,
command: merged.command ?? [],
extensions: merged.extensions ?? [],
enabled: base?.enabled ?? (async () => true),
}

if (result.command.length === 0) continue

result.enabled = async () => true
result.name = name
formatters[name] = result
}

Expand Down Expand Up @@ -109,12 +111,32 @@ export namespace Format {
const ext = path.extname(file)

for (const item of await getFormatter(ext)) {
log.info("running", { command: item.command })
let command = item.command
try {
const cfg = await item.setup?.(file)
const placeholders: Record<string, string> = {
$FILE: file,
...(cfg?.placeholders ?? {}),
}
const cwd = cfg?.directory ?? path.dirname(file)
command = item.command.map((part) => {
let result = part
for (const [key, value] of Object.entries(placeholders).toSorted((a, b) => b[0].length - a[0].length)) {
result = result.replaceAll(key, value)
}
return result
})
if (command.some((part) => /\$[A-Z][A-Z0-9_]*/.test(part))) {
continue
}
command.push(...(item.args ?? []))

log.info("running", { command: command })

const proc = Process.spawn(
item.command.map((x) => x.replace("$FILE", file)),
command,
{
cwd: Instance.directory,
cwd,
env: { ...process.env, ...item.environment },
stdout: "ignore",
stderr: "ignore",
Expand All @@ -123,13 +145,15 @@ export namespace Format {
const exit = await proc.exited
if (exit !== 0)
log.error("failed", {
command: item.command,
command,
cwd,
exit,
...item.environment,
})
} catch (error) {
log.error("failed to format file", {
error,
command: item.command,
command,
...item.environment,
file,
})
Expand Down
6 changes: 4 additions & 2 deletions packages/opencode/src/util/which.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import whichPkg from "which"

export function which(cmd: string, env?: NodeJS.ProcessEnv) {
const path = env?.PATH ?? env?.Path ?? process.env.PATH ?? process.env.Path
const pathExt = env?.PATHEXT ?? env?.PathExt ?? process.env.PATHEXT ?? process.env.PathExt
const result = whichPkg.sync(cmd, {
nothrow: true,
path: env?.PATH ?? env?.Path ?? process.env.PATH ?? process.env.Path,
pathExt: env?.PATHEXT ?? env?.PathExt ?? process.env.PATHEXT ?? process.env.PathExt,
path,
pathExt
})
return typeof result === "string" ? result : null
}
3 changes: 3 additions & 0 deletions packages/web/src/content/docs/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,9 @@ You can configure code formatters through the `formatter` option.
{
"$schema": "https://opencode.ai/config.json",
"formatter": {
"dotnet-format": {
"args": ["--severity", "info"]
},
"prettier": {
"disabled": true
},
Expand Down
4 changes: 4 additions & 0 deletions packages/web/src/content/docs/formatters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ OpenCode comes with several built-in formatters for popular languages and framew
| cljfmt | .clj, .cljs, .cljc, .edn | `cljfmt` command available |
| dart | .dart | `dart` command available |
| dfmt | .d | `dfmt` command available |
| dotnet-format | .cs, .vb, .fs, .fsi, .fsx, .fsscript | `dotnet` command available and a nearest `.slnx`, `.sln`, `.csproj`, `.fsproj`, or `.vbproj` |
| gleam | .gleam | `gleam` command available |
| gofmt | .go | `gofmt` command available |
| htmlbeautifier | .erb, .html.erb | `htmlbeautifier` command available |
Expand Down Expand Up @@ -71,6 +72,7 @@ Each formatter configuration supports the following:

| Property | Type | Description |
| ------------- | -------- | ------------------------------------------------------- |
| `args` | string[] | Additional arguments appended to the formatter command |
| `disabled` | boolean | Set this to `true` to disable the formatter |
| `command` | string[] | The command to run for formatting |
| `environment` | object | Environment variables to set when running the formatter |
Expand Down Expand Up @@ -130,3 +132,5 @@ You can override the built-in formatters or add new ones by specifying the comma
```

The **`$FILE` placeholder** in the command will be replaced with the path to the file being formatted.

The `args` property is appended after the formatter command. Use it when you want to tweak a built-in formatter without replacing its full command. If you need complete control over argument order, override `command` instead.
Loading