formatter.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. import { readableStreamToText } from "bun"
  2. import { BunProc } from "../bun"
  3. import { Instance } from "../project/instance"
  4. import { Filesystem } from "../util/filesystem"
  5. import { Flag } from "@/flag/flag"
  6. export interface Info {
  7. name: string
  8. command: string[]
  9. environment?: Record<string, string>
  10. extensions: string[]
  11. enabled(): Promise<boolean>
  12. }
  13. export const gofmt: Info = {
  14. name: "gofmt",
  15. command: ["gofmt", "-w", "$FILE"],
  16. extensions: [".go"],
  17. async enabled() {
  18. return Bun.which("gofmt") !== null
  19. },
  20. }
  21. export const mix: Info = {
  22. name: "mix",
  23. command: ["mix", "format", "$FILE"],
  24. extensions: [".ex", ".exs", ".eex", ".heex", ".leex", ".neex", ".sface"],
  25. async enabled() {
  26. return Bun.which("mix") !== null
  27. },
  28. }
  29. export const prettier: Info = {
  30. name: "prettier",
  31. command: [BunProc.which(), "x", "prettier", "--write", "$FILE"],
  32. environment: {
  33. BUN_BE_BUN: "1",
  34. },
  35. extensions: [
  36. ".js",
  37. ".jsx",
  38. ".mjs",
  39. ".cjs",
  40. ".ts",
  41. ".tsx",
  42. ".mts",
  43. ".cts",
  44. ".html",
  45. ".htm",
  46. ".css",
  47. ".scss",
  48. ".sass",
  49. ".less",
  50. ".vue",
  51. ".svelte",
  52. ".json",
  53. ".jsonc",
  54. ".yaml",
  55. ".yml",
  56. ".toml",
  57. ".xml",
  58. ".md",
  59. ".mdx",
  60. ".graphql",
  61. ".gql",
  62. ],
  63. async enabled() {
  64. const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree)
  65. for (const item of items) {
  66. const json = await Bun.file(item).json()
  67. if (json.dependencies?.prettier) return true
  68. if (json.devDependencies?.prettier) return true
  69. }
  70. return false
  71. },
  72. }
  73. export const oxfmt: Info = {
  74. name: "oxfmt",
  75. command: [BunProc.which(), "x", "oxfmt", "$FILE"],
  76. environment: {
  77. BUN_BE_BUN: "1",
  78. },
  79. extensions: [".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts"],
  80. async enabled() {
  81. if (!Flag.OPENCODE_EXPERIMENTAL_OXFMT) return false
  82. const items = await Filesystem.findUp("package.json", Instance.directory, Instance.worktree)
  83. for (const item of items) {
  84. const json = await Bun.file(item).json()
  85. if (json.dependencies?.oxfmt) return true
  86. if (json.devDependencies?.oxfmt) return true
  87. }
  88. return false
  89. },
  90. }
  91. export const biome: Info = {
  92. name: "biome",
  93. command: [BunProc.which(), "x", "@biomejs/biome", "check", "--write", "$FILE"],
  94. environment: {
  95. BUN_BE_BUN: "1",
  96. },
  97. extensions: [
  98. ".js",
  99. ".jsx",
  100. ".mjs",
  101. ".cjs",
  102. ".ts",
  103. ".tsx",
  104. ".mts",
  105. ".cts",
  106. ".html",
  107. ".htm",
  108. ".css",
  109. ".scss",
  110. ".sass",
  111. ".less",
  112. ".vue",
  113. ".svelte",
  114. ".json",
  115. ".jsonc",
  116. ".yaml",
  117. ".yml",
  118. ".toml",
  119. ".xml",
  120. ".md",
  121. ".mdx",
  122. ".graphql",
  123. ".gql",
  124. ],
  125. async enabled() {
  126. const configs = ["biome.json", "biome.jsonc"]
  127. for (const config of configs) {
  128. const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
  129. if (found.length > 0) {
  130. return true
  131. }
  132. }
  133. return false
  134. },
  135. }
  136. export const zig: Info = {
  137. name: "zig",
  138. command: ["zig", "fmt", "$FILE"],
  139. extensions: [".zig", ".zon"],
  140. async enabled() {
  141. return Bun.which("zig") !== null
  142. },
  143. }
  144. export const clang: Info = {
  145. name: "clang-format",
  146. command: ["clang-format", "-i", "$FILE"],
  147. extensions: [".c", ".cc", ".cpp", ".cxx", ".c++", ".h", ".hh", ".hpp", ".hxx", ".h++", ".ino", ".C", ".H"],
  148. async enabled() {
  149. const items = await Filesystem.findUp(".clang-format", Instance.directory, Instance.worktree)
  150. return items.length > 0
  151. },
  152. }
  153. export const ktlint: Info = {
  154. name: "ktlint",
  155. command: ["ktlint", "-F", "$FILE"],
  156. extensions: [".kt", ".kts"],
  157. async enabled() {
  158. return Bun.which("ktlint") !== null
  159. },
  160. }
  161. export const ruff: Info = {
  162. name: "ruff",
  163. command: ["ruff", "format", "$FILE"],
  164. extensions: [".py", ".pyi"],
  165. async enabled() {
  166. if (!Bun.which("ruff")) return false
  167. const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
  168. for (const config of configs) {
  169. const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
  170. if (found.length > 0) {
  171. if (config === "pyproject.toml") {
  172. const content = await Bun.file(found[0]).text()
  173. if (content.includes("[tool.ruff]")) return true
  174. } else {
  175. return true
  176. }
  177. }
  178. }
  179. const deps = ["requirements.txt", "pyproject.toml", "Pipfile"]
  180. for (const dep of deps) {
  181. const found = await Filesystem.findUp(dep, Instance.directory, Instance.worktree)
  182. if (found.length > 0) {
  183. const content = await Bun.file(found[0]).text()
  184. if (content.includes("ruff")) return true
  185. }
  186. }
  187. return false
  188. },
  189. }
  190. export const rlang: Info = {
  191. name: "air",
  192. command: ["air", "format", "$FILE"],
  193. extensions: [".R"],
  194. async enabled() {
  195. const airPath = Bun.which("air")
  196. if (airPath == null) return false
  197. try {
  198. const proc = Bun.spawn(["air", "--help"], {
  199. stdout: "pipe",
  200. stderr: "pipe",
  201. })
  202. await proc.exited
  203. const output = await readableStreamToText(proc.stdout)
  204. // Check for "Air: An R language server and formatter"
  205. const firstLine = output.split("\n")[0]
  206. const hasR = firstLine.includes("R language")
  207. const hasFormatter = firstLine.includes("formatter")
  208. return hasR && hasFormatter
  209. } catch (error) {
  210. return false
  211. }
  212. },
  213. }
  214. export const uvformat: Info = {
  215. name: "uv",
  216. command: ["uv", "format", "--", "$FILE"],
  217. extensions: [".py", ".pyi"],
  218. async enabled() {
  219. if (await ruff.enabled()) return false
  220. if (Bun.which("uv") !== null) {
  221. const proc = Bun.spawn(["uv", "format", "--help"], { stderr: "pipe", stdout: "pipe" })
  222. const code = await proc.exited
  223. return code === 0
  224. }
  225. return false
  226. },
  227. }
  228. export const rubocop: Info = {
  229. name: "rubocop",
  230. command: ["rubocop", "--autocorrect", "$FILE"],
  231. extensions: [".rb", ".rake", ".gemspec", ".ru"],
  232. async enabled() {
  233. return Bun.which("rubocop") !== null
  234. },
  235. }
  236. export const standardrb: Info = {
  237. name: "standardrb",
  238. command: ["standardrb", "--fix", "$FILE"],
  239. extensions: [".rb", ".rake", ".gemspec", ".ru"],
  240. async enabled() {
  241. return Bun.which("standardrb") !== null
  242. },
  243. }
  244. export const htmlbeautifier: Info = {
  245. name: "htmlbeautifier",
  246. command: ["htmlbeautifier", "$FILE"],
  247. extensions: [".erb", ".html.erb"],
  248. async enabled() {
  249. return Bun.which("htmlbeautifier") !== null
  250. },
  251. }
  252. export const dart: Info = {
  253. name: "dart",
  254. command: ["dart", "format", "$FILE"],
  255. extensions: [".dart"],
  256. async enabled() {
  257. return Bun.which("dart") !== null
  258. },
  259. }
  260. export const ocamlformat: Info = {
  261. name: "ocamlformat",
  262. command: ["ocamlformat", "-i", "$FILE"],
  263. extensions: [".ml", ".mli"],
  264. async enabled() {
  265. if (!Bun.which("ocamlformat")) return false
  266. const items = await Filesystem.findUp(".ocamlformat", Instance.directory, Instance.worktree)
  267. return items.length > 0
  268. },
  269. }
  270. export const terraform: Info = {
  271. name: "terraform",
  272. command: ["terraform", "fmt", "$FILE"],
  273. extensions: [".tf", ".tfvars"],
  274. async enabled() {
  275. return Bun.which("terraform") !== null
  276. },
  277. }
  278. export const latexindent: Info = {
  279. name: "latexindent",
  280. command: ["latexindent", "-w", "-s", "$FILE"],
  281. extensions: [".tex"],
  282. async enabled() {
  283. return Bun.which("latexindent") !== null
  284. },
  285. }
  286. export const gleam: Info = {
  287. name: "gleam",
  288. command: ["gleam", "format", "$FILE"],
  289. extensions: [".gleam"],
  290. async enabled() {
  291. return Bun.which("gleam") !== null
  292. },
  293. }
  294. export const shfmt: Info = {
  295. name: "shfmt",
  296. command: ["shfmt", "-w", "$FILE"],
  297. extensions: [".sh", ".bash"],
  298. async enabled() {
  299. return Bun.which("shfmt") !== null
  300. },
  301. }
  302. export const nixfmt: Info = {
  303. name: "nixfmt",
  304. command: ["nixfmt", "$FILE"],
  305. extensions: [".nix"],
  306. async enabled() {
  307. return Bun.which("nixfmt") !== null
  308. },
  309. }
  310. export const rustfmt: Info = {
  311. name: "rustfmt",
  312. command: ["rustfmt", "$FILE"],
  313. extensions: [".rs"],
  314. async enabled() {
  315. return Bun.which("rustfmt") !== null
  316. },
  317. }
  318. export const pint: Info = {
  319. name: "pint",
  320. command: ["./vendor/bin/pint", "$FILE"],
  321. extensions: [".php"],
  322. async enabled() {
  323. const items = await Filesystem.findUp("composer.json", Instance.directory, Instance.worktree)
  324. for (const item of items) {
  325. const json = await Bun.file(item).json()
  326. if (json.require?.["laravel/pint"]) return true
  327. if (json["require-dev"]?.["laravel/pint"]) return true
  328. }
  329. return false
  330. },
  331. }