formatter.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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: [
  129. ".c",
  130. ".cc",
  131. ".cpp",
  132. ".cxx",
  133. ".c++",
  134. ".h",
  135. ".hh",
  136. ".hpp",
  137. ".hxx",
  138. ".h++",
  139. ".ino",
  140. ".C",
  141. ".H",
  142. ],
  143. async enabled() {
  144. const items = await Filesystem.findUp(".clang-format", Instance.directory, Instance.worktree)
  145. return items.length > 0
  146. },
  147. }
  148. export const ktlint: Info = {
  149. name: "ktlint",
  150. command: ["ktlint", "-F", "$FILE"],
  151. extensions: [".kt", ".kts"],
  152. async enabled() {
  153. return Bun.which("ktlint") !== null
  154. },
  155. }
  156. export const ruff: Info = {
  157. name: "ruff",
  158. command: ["ruff", "format", "$FILE"],
  159. extensions: [".py", ".pyi"],
  160. async enabled() {
  161. if (!Bun.which("ruff")) return false
  162. const configs = ["pyproject.toml", "ruff.toml", ".ruff.toml"]
  163. for (const config of configs) {
  164. const found = await Filesystem.findUp(config, Instance.directory, Instance.worktree)
  165. if (found.length > 0) {
  166. if (config === "pyproject.toml") {
  167. const content = await Bun.file(found[0]).text()
  168. if (content.includes("[tool.ruff]")) return true
  169. } else {
  170. return true
  171. }
  172. }
  173. }
  174. const deps = ["requirements.txt", "pyproject.toml", "Pipfile"]
  175. for (const dep of deps) {
  176. const found = await Filesystem.findUp(dep, Instance.directory, Instance.worktree)
  177. if (found.length > 0) {
  178. const content = await Bun.file(found[0]).text()
  179. if (content.includes("ruff")) return true
  180. }
  181. }
  182. return false
  183. },
  184. }
  185. export const rlang: Info = {
  186. name: "air",
  187. command: ["air", "format", "$FILE"],
  188. extensions: [".R"],
  189. async enabled() {
  190. const airPath = Bun.which("air")
  191. if (airPath == null) return false
  192. try {
  193. const proc = Bun.spawn(["air", "--help"], {
  194. stdout: "pipe",
  195. stderr: "pipe",
  196. })
  197. await proc.exited
  198. const output = await readableStreamToText(proc.stdout)
  199. // Check for "Air: An R language server and formatter"
  200. const firstLine = output.split("\n")[0]
  201. const hasR = firstLine.includes("R language")
  202. const hasFormatter = firstLine.includes("formatter")
  203. return hasR && hasFormatter
  204. } catch (error) {
  205. return false
  206. }
  207. },
  208. }
  209. export const uvformat: Info = {
  210. name: "uv format",
  211. command: ["uv", "format", "--", "$FILE"],
  212. extensions: [".py", ".pyi"],
  213. async enabled() {
  214. if (await ruff.enabled()) return false
  215. if (Bun.which("uv") !== null) {
  216. const proc = Bun.spawn(["uv", "format", "--help"], { stderr: "pipe", stdout: "pipe" })
  217. const code = await proc.exited
  218. return code === 0
  219. }
  220. return false
  221. },
  222. }
  223. export const rubocop: Info = {
  224. name: "rubocop",
  225. command: ["rubocop", "--autocorrect", "$FILE"],
  226. extensions: [".rb", ".rake", ".gemspec", ".ru"],
  227. async enabled() {
  228. return Bun.which("rubocop") !== null
  229. },
  230. }
  231. export const standardrb: Info = {
  232. name: "standardrb",
  233. command: ["standardrb", "--fix", "$FILE"],
  234. extensions: [".rb", ".rake", ".gemspec", ".ru"],
  235. async enabled() {
  236. return Bun.which("standardrb") !== null
  237. },
  238. }
  239. export const htmlbeautifier: Info = {
  240. name: "htmlbeautifier",
  241. command: ["htmlbeautifier", "$FILE"],
  242. extensions: [".erb", ".html.erb"],
  243. async enabled() {
  244. return Bun.which("htmlbeautifier") !== null
  245. },
  246. }