formatter.ts 6.8 KB

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