chore: initialize qiming workspace repository
This commit is contained in:
403
qimingcode/packages/opencode/src/format/formatter.ts
Normal file
403
qimingcode/packages/opencode/src/format/formatter.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
import { Npm } from "@opencode-ai/core/npm"
|
||||
import type { InstanceContext } from "../project/instance"
|
||||
import { Filesystem } from "../util"
|
||||
import { Process } from "../util"
|
||||
import { which } from "../util/which"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
|
||||
export interface Context extends Pick<InstanceContext, "directory" | "worktree"> {}
|
||||
|
||||
export interface Info {
|
||||
name: string
|
||||
environment?: Record<string, string>
|
||||
extensions: string[]
|
||||
enabled(context: Context): Promise<string[] | false>
|
||||
}
|
||||
|
||||
export const gofmt: Info = {
|
||||
name: "gofmt",
|
||||
extensions: [".go"],
|
||||
async enabled() {
|
||||
const match = which("gofmt")
|
||||
if (!match) return false
|
||||
return [match, "-w", "$FILE"]
|
||||
},
|
||||
}
|
||||
|
||||
export const mix: Info = {
|
||||
name: "mix",
|
||||
extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"],
|
||||
async enabled() {
|
||||
const match = which("mix")
|
||||
if (!match) return false
|
||||
return [match, "format", "$FILE"]
|
||||
},
|
||||
}
|
||||
|
||||
export const prettier: Info = {
|
||||
name: "prettier",
|
||||
environment: {
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
extensions: [
|
||||
".js",
|
||||
".jsx",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".ts",
|
||||
".tsx",
|
||||
".mts",
|
||||
".cts",
|
||||
".html",
|
||||
".htm",
|
||||
".css",
|
||||
".scss",
|
||||
".sass",
|
||||
".less",
|
||||
".vue",
|
||||
".svelte",
|
||||
".json",
|
||||
".jsonc",
|
||||
".yaml",
|
||||
".yml",
|
||||
".toml",
|
||||
".xml",
|
||||
".md",
|
||||
".mdx",
|
||||
".graphql",
|
||||
".gql",
|
||||
],
|
||||
async enabled(context) {
|
||||
const items = await Filesystem.findUp("package.json", context.directory, context.worktree)
|
||||
for (const item of items) {
|
||||
const json = await Filesystem.readJson<{
|
||||
dependencies?: Record<string, string>
|
||||
devDependencies?: Record<string, string>
|
||||
}>(item)
|
||||
if (json.dependencies?.prettier || json.devDependencies?.prettier) {
|
||||
const bin = await Npm.which("prettier")
|
||||
if (bin) return [bin, "--write", "$FILE"]
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
export const oxfmt: Info = {
|
||||
name: "oxfmt",
|
||||
environment: {
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"],
|
||||
async enabled(context) {
|
||||
if (!Flag.OPENCODE_EXPERIMENTAL_OXFMT) return false
|
||||
const items = await Filesystem.findUp("package.json", context.directory, context.worktree)
|
||||
for (const item of items) {
|
||||
const json = await Filesystem.readJson<{
|
||||
dependencies?: Record<string, string>
|
||||
devDependencies?: Record<string, string>
|
||||
}>(item)
|
||||
if (json.dependencies?.oxfmt || json.devDependencies?.oxfmt) {
|
||||
const bin = await Npm.which("oxfmt")
|
||||
if (bin) return [bin, "$FILE"]
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
export const biome: Info = {
|
||||
name: "biome",
|
||||
environment: {
|
||||
BUN_BE_BUN: "1",
|
||||
},
|
||||
extensions: [
|
||||
".js",
|
||||
".jsx",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".ts",
|
||||
".tsx",
|
||||
".mts",
|
||||
".cts",
|
||||
".html",
|
||||
".htm",
|
||||
".css",
|
||||
".scss",
|
||||
".sass",
|
||||
".less",
|
||||
".vue",
|
||||
".svelte",
|
||||
".json",
|
||||
".jsonc",
|
||||
".yaml",
|
||||
".yml",
|
||||
".toml",
|
||||
".xml",
|
||||
".md",
|
||||
".mdx",
|
||||
".graphql",
|
||||
".gql",
|
||||
],
|
||||
async enabled(context) {
|
||||
const configs = ["biome.json", "biome.jsonc"]
|
||||
for (const config of configs) {
|
||||
const found = await Filesystem.findUp(config, context.directory, context.worktree)
|
||||
if (found.length > 0) {
|
||||
const bin = await Npm.which("@biomejs/biome")
|
||||
if (bin) return [bin, "format", "--write", "$FILE"]
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
export const zig: Info = {
|
||||
name: "zig",
|
||||
extensions: [".zig", ".zon"],
|
||||
async enabled() {
|
||||
const match = which("zig")
|
||||
if (!match) return false
|
||||
return [match, "fmt", "$FILE"]
|
||||
},
|
||||
}
|
||||
|
||||
export const clang: Info = {
|
||||
name: "clang-format",
|
||||
extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"],
|
||||
async enabled(context) {
|
||||
const items = await Filesystem.findUp(".clang-format", context.directory, context.worktree)
|
||||
if (items.length > 0) {
|
||||
const match = which("clang-format")
|
||||
if (match) return [match, "-i", "$FILE"]
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
export const ktlint: Info = {
|
||||
name: "ktlint",
|
||||
extensions: [".kt", ".kts"],
|
||||
async enabled() {
|
||||
const match = which("ktlint")
|
||||
if (!match) return false
|
||||
return [match, "-F", "$FILE"]
|
||||
},
|
||||
}
|
||||
|
||||
export const ruff: Info = {
|
||||
name: "ruff",
|
||||
extensions: [".py", ".pyi"],
|
||||
async enabled(context) {
|
||||
if (!which("ruff")) return false
|
||||
const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
|
||||
for (const config of configs) {
|
||||
const found = await Filesystem.findUp(config, context.directory, context.worktree)
|
||||
if (found.length > 0) {
|
||||
if (config === "pyproject.toml") {
|
||||
const content = await Filesystem.readText(found[0])
|
||||
if (content.includes("[tool.ruff]")) return ["ruff", "format", "$FILE"]
|
||||
} else {
|
||||
return ["ruff", "format", "$FILE"]
|
||||
}
|
||||
}
|
||||
}
|
||||
const deps = ["requirements.txt", "pyproject.toml", "Pipfile"]
|
||||
for (const dep of deps) {
|
||||
const found = await Filesystem.findUp(dep, context.directory, context.worktree)
|
||||
if (found.length > 0) {
|
||||
const content = await Filesystem.readText(found[0])
|
||||
if (content.includes("ruff")) return ["ruff", "format", "$FILE"]
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
export const rlang: Info = {
|
||||
name: "air",
|
||||
extensions: [".R"],
|
||||
async enabled() {
|
||||
const air = which("air")
|
||||
if (air == null) return false
|
||||
|
||||
const output = await Process.text([air, "--help"], { nothrow: true })
|
||||
|
||||
// Check for "Air: An R language server and formatter"
|
||||
const firstLine = output.text.split("\n")[0]
|
||||
const hasR = firstLine.includes("R language")
|
||||
const hasFormatter = firstLine.includes("formatter")
|
||||
if (output.code === 0 && hasR && hasFormatter) return [air, "format", "$FILE"]
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
export const uvformat: Info = {
|
||||
name: "uv",
|
||||
extensions: [".py", ".pyi"],
|
||||
async enabled(context) {
|
||||
if (await ruff.enabled(context)) return false
|
||||
const uv = which("uv")
|
||||
if (uv == null) return false
|
||||
const output = await Process.run([uv, "format", "--help"], { nothrow: true })
|
||||
if (output.code === 0) return [uv, "format", "--", "$FILE"]
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
export const rubocop: Info = {
|
||||
name: "rubocop",
|
||||
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
||||
async enabled() {
|
||||
const match = which("rubocop")
|
||||
if (!match) return false
|
||||
return [match, "--autocorrect", "$FILE"]
|
||||
},
|
||||
}
|
||||
|
||||
export const standardrb: Info = {
|
||||
name: "standardrb",
|
||||
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
||||
async enabled() {
|
||||
const match = which("standardrb")
|
||||
if (!match) return false
|
||||
return [match, "--fix", "$FILE"]
|
||||
},
|
||||
}
|
||||
|
||||
export const htmlbeautifier: Info = {
|
||||
name: "htmlbeautifier",
|
||||
extensions: [".erb", ".html.erb"],
|
||||
async enabled() {
|
||||
const match = which("htmlbeautifier")
|
||||
if (!match) return false
|
||||
return [match, "$FILE"]
|
||||
},
|
||||
}
|
||||
|
||||
export const dart: Info = {
|
||||
name: "dart",
|
||||
extensions: [".dart"],
|
||||
async enabled() {
|
||||
const match = which("dart")
|
||||
if (!match) return false
|
||||
return [match, "format", "$FILE"]
|
||||
},
|
||||
}
|
||||
|
||||
export const ocamlformat: Info = {
|
||||
name: "ocamlformat",
|
||||
extensions: [".ml", ".mli"],
|
||||
async enabled(context) {
|
||||
if (!which("ocamlformat")) return false
|
||||
const items = await Filesystem.findUp(".ocamlformat", context.directory, context.worktree)
|
||||
if (items.length > 0) return ["ocamlformat", "-i", "$FILE"]
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
export const terraform: Info = {
|
||||
name: "terraform",
|
||||
extensions: [".tf", ".tfvars"],
|
||||
async enabled() {
|
||||
const match = which("terraform")
|
||||
if (!match) return false
|
||||
return [match, "fmt", "$FILE"]
|
||||
},
|
||||
}
|
||||
|
||||
export const latexindent: Info = {
|
||||
name: "latexindent",
|
||||
extensions: [".tex"],
|
||||
async enabled() {
|
||||
const match = which("latexindent")
|
||||
if (!match) return false
|
||||
return [match, "-w", "-s", "$FILE"]
|
||||
},
|
||||
}
|
||||
|
||||
export const gleam: Info = {
|
||||
name: "gleam",
|
||||
extensions: [".gleam"],
|
||||
async enabled() {
|
||||
const match = which("gleam")
|
||||
if (!match) return false
|
||||
return [match, "format", "$FILE"]
|
||||
},
|
||||
}
|
||||
|
||||
export const shfmt: Info = {
|
||||
name: "shfmt",
|
||||
extensions: [".sh", ".bash"],
|
||||
async enabled() {
|
||||
const match = which("shfmt")
|
||||
if (!match) return false
|
||||
return [match, "-w", "$FILE"]
|
||||
},
|
||||
}
|
||||
|
||||
export const nixfmt: Info = {
|
||||
name: "nixfmt",
|
||||
extensions: [".nix"],
|
||||
async enabled() {
|
||||
const match = which("nixfmt")
|
||||
if (!match) return false
|
||||
return [match, "$FILE"]
|
||||
},
|
||||
}
|
||||
|
||||
export const rustfmt: Info = {
|
||||
name: "rustfmt",
|
||||
extensions: [".rs"],
|
||||
async enabled() {
|
||||
const match = which("rustfmt")
|
||||
if (!match) return false
|
||||
return [match, "$FILE"]
|
||||
},
|
||||
}
|
||||
|
||||
export const pint: Info = {
|
||||
name: "pint",
|
||||
extensions: [".php"],
|
||||
async enabled(context) {
|
||||
const items = await Filesystem.findUp("composer.json", context.directory, context.worktree)
|
||||
for (const item of items) {
|
||||
const json = await Filesystem.readJson<{
|
||||
require?: Record<string, string>
|
||||
"require-dev"?: Record<string, string>
|
||||
}>(item)
|
||||
if (json.require?.["laravel/pint"] || json["require-dev"]?.["laravel/pint"]) return ["./vendor/bin/pint", "$FILE"]
|
||||
}
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
export const ormolu: Info = {
|
||||
name: "ormolu",
|
||||
extensions: [".hs"],
|
||||
async enabled() {
|
||||
const match = which("ormolu")
|
||||
if (!match) return false
|
||||
return [match, "-i", "$FILE"]
|
||||
},
|
||||
}
|
||||
|
||||
export const cljfmt: Info = {
|
||||
name: "cljfmt",
|
||||
extensions: [".clj", ".cljs", ".cljc", ".edn"],
|
||||
async enabled() {
|
||||
const match = which("cljfmt")
|
||||
if (!match) return false
|
||||
return [match, "fix", "--quiet", "$FILE"]
|
||||
},
|
||||
}
|
||||
|
||||
export const dfmt: Info = {
|
||||
name: "dfmt",
|
||||
extensions: [".d"],
|
||||
async enabled() {
|
||||
const match = which("dfmt")
|
||||
if (!match) return false
|
||||
return [match, "-i", "$FILE"]
|
||||
},
|
||||
}
|
||||
207
qimingcode/packages/opencode/src/format/index.ts
Normal file
207
qimingcode/packages/opencode/src/format/index.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import { Effect, Layer, Context, Schema } from "effect"
|
||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { InstanceState } from "@/effect"
|
||||
import path from "path"
|
||||
import { mergeDeep } from "remeda"
|
||||
import { Config } from "../config"
|
||||
import { Log } from "../util"
|
||||
import * as Formatter from "./formatter"
|
||||
import { zod } from "@/util/effect-zod"
|
||||
import { withStatics } from "@/util/schema"
|
||||
|
||||
const log = Log.create({ service: "format" })
|
||||
|
||||
export const Status = Schema.Struct({
|
||||
name: Schema.String,
|
||||
extensions: Schema.Array(Schema.String),
|
||||
enabled: Schema.Boolean,
|
||||
})
|
||||
.annotate({ identifier: "FormatterStatus" })
|
||||
.pipe(withStatics((s) => ({ zod: zod(s) })))
|
||||
export type Status = Schema.Schema.Type<typeof Status>
|
||||
|
||||
export interface Interface {
|
||||
readonly init: () => Effect.Effect<void>
|
||||
readonly status: () => Effect.Effect<Status[]>
|
||||
readonly file: (filepath: string) => Effect.Effect<boolean>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/Format") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const config = yield* Config.Service
|
||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||
|
||||
const state = yield* InstanceState.make(
|
||||
Effect.fn("Format.state")(function* (ctx) {
|
||||
const commands: Record<string, string[] | false> = {}
|
||||
const formatters: Record<string, Formatter.Info> = {}
|
||||
|
||||
async function getCommand(item: Formatter.Info) {
|
||||
let cmd = commands[item.name]
|
||||
if (cmd === false || cmd === undefined) {
|
||||
cmd = await item.enabled(ctx)
|
||||
commands[item.name] = cmd
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
async function isEnabled(item: Formatter.Info) {
|
||||
const cmd = await getCommand(item)
|
||||
return cmd !== false
|
||||
}
|
||||
|
||||
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 cmd = await getCommand(item)
|
||||
if (cmd) {
|
||||
log.info("enabled", { name: item.name, ext })
|
||||
}
|
||||
return {
|
||||
item,
|
||||
cmd,
|
||||
}
|
||||
}),
|
||||
)
|
||||
return checks
|
||||
.filter((x): x is { item: Formatter.Info; cmd: string[] } => x.cmd !== false)
|
||||
.map((x) => ({ item: x.item, cmd: x.cmd }))
|
||||
}
|
||||
|
||||
function formatFile(filepath: string) {
|
||||
return Effect.gen(function* () {
|
||||
log.info("formatting", { file: filepath })
|
||||
const formatters = yield* Effect.promise(() => getFormatter(path.extname(filepath)))
|
||||
|
||||
if (!formatters.length) return false
|
||||
|
||||
for (const { item, cmd } of formatters) {
|
||||
log.info("running", { command: cmd })
|
||||
const replaced = cmd.map((x) => x.replace("$FILE", filepath))
|
||||
const dir = yield* InstanceState.directory
|
||||
const code = yield* spawner
|
||||
.spawn(
|
||||
ChildProcess.make(replaced[0]!, replaced.slice(1), {
|
||||
cwd: dir,
|
||||
env: item.environment,
|
||||
extendEnv: true,
|
||||
}),
|
||||
)
|
||||
.pipe(
|
||||
Effect.flatMap((handle) => handle.exitCode),
|
||||
Effect.scoped,
|
||||
Effect.catch(() =>
|
||||
Effect.sync(() => {
|
||||
log.error("failed to format file", {
|
||||
error: "spawn failed",
|
||||
command: cmd,
|
||||
...item.environment,
|
||||
file: filepath,
|
||||
})
|
||||
return ChildProcessSpawner.ExitCode(1)
|
||||
}),
|
||||
),
|
||||
)
|
||||
if (code !== 0) {
|
||||
log.error("failed", {
|
||||
command: cmd,
|
||||
...item.environment,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
const cfg = yield* config.get()
|
||||
|
||||
if (!cfg.formatter) {
|
||||
log.info("all formatters are disabled")
|
||||
log.info("init")
|
||||
return {
|
||||
formatters,
|
||||
isEnabled,
|
||||
formatFile,
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of Object.values(Formatter)) {
|
||||
formatters[item.name] = item
|
||||
}
|
||||
|
||||
if (cfg.formatter !== true) {
|
||||
for (const [name, item] of Object.entries(cfg.formatter)) {
|
||||
const builtIn = Formatter[name as keyof typeof Formatter]
|
||||
|
||||
// Ruff and uv are both the same formatter, so disabling either should disable both.
|
||||
if (["ruff", "uv"].includes(name) && (cfg.formatter.ruff?.disabled || cfg.formatter.uv?.disabled)) {
|
||||
// TODO combine formatters so shared backends like Ruff/uv don't need linked disable handling here.
|
||||
delete formatters.ruff
|
||||
delete formatters.uv
|
||||
continue
|
||||
}
|
||||
if (item.disabled) {
|
||||
delete formatters[name]
|
||||
continue
|
||||
}
|
||||
const info = mergeDeep(builtIn ?? { extensions: [] }, item)
|
||||
|
||||
formatters[name] = {
|
||||
...info,
|
||||
name,
|
||||
extensions: info.extensions ?? [],
|
||||
enabled: builtIn && !info.command ? builtIn.enabled : async (_context) => info.command ?? false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("init")
|
||||
|
||||
return {
|
||||
formatters,
|
||||
isEnabled,
|
||||
formatFile,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
const file = Effect.fn("Format.file")(function* (filepath: string) {
|
||||
const { formatFile } = yield* InstanceState.get(state)
|
||||
return yield* formatFile(filepath)
|
||||
})
|
||||
|
||||
return Service.of({ init, status, file })
|
||||
}),
|
||||
)
|
||||
|
||||
export const defaultLayer = layer.pipe(
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(CrossSpawnSpawner.defaultLayer),
|
||||
)
|
||||
|
||||
export * as Format from "."
|
||||
Reference in New Issue
Block a user