github.com/xyproto/orbiton/v2@v2.65.12-0.20240516144430-e10a419274ec/format.go (about) 1 package main 2 3 import ( 4 "encoding/json" 5 "errors" 6 "os" 7 "os/exec" 8 "path/filepath" 9 "strconv" 10 "strings" 11 12 "github.com/xyproto/autoimport" 13 "github.com/xyproto/files" 14 "github.com/xyproto/mode" 15 "github.com/xyproto/vt100" 16 "github.com/yosssi/gohtml" 17 ) 18 19 // FormatMap maps from format command to file extensions 20 type FormatMap map[*exec.Cmd][]string 21 22 var formatMap FormatMap 23 24 // GetFormatMap will return a map from format command to file extensions. 25 // It is done this way to only initialize the map once, but not at the time when the program starts. 26 func GetFormatMap() FormatMap { 27 if formatMap == nil { 28 formatMap = FormatMap{ 29 exec.Command("clang-format", "-fallback-style=WebKit", "-style=file", "-i", "--"): {".c", ".c++", ".cc", ".cpp", ".cxx", ".h", ".h++", ".hpp"}, 30 exec.Command("astyle", "--mode=cs"): {".cs"}, 31 exec.Command("crystal", "tool", "format"): {".cr"}, 32 exec.Command("prettier", "--tab-width", "2", "-w"): {".css"}, 33 exec.Command("dart", "format"): {".dart"}, 34 exec.Command("goimports", "-w", "--"): {".go"}, 35 exec.Command("brittany", "--write-mode=inplace"): {".hs"}, 36 exec.Command("google-java-format", "-a", "-i"): {".java"}, 37 exec.Command("prettier", "--tab-width", "4", "-w"): {".js", ".ts"}, 38 exec.Command("just", "--unstable", "--fmt", "-f"): {".just", ".justfile", "justfile"}, 39 exec.Command("ktlint", "-F"): {".kt", ".kts"}, 40 exec.Command("lua-format", "-i", "--no-keep-simple-function-one-line", "--column-limit=120", "--indent-width=2", "--no-use-tab"): {".lua"}, 41 exec.Command("ocamlformat"): {".ml"}, 42 exec.Command("/usr/bin/vendor_perl/perltidy", "-se", "-b", "-i=2", "-ole=unix", "-bt=2", "-pt=2", "-sbt=2", "-ce"): {".pl"}, 43 exec.Command("black"): {".py"}, 44 exec.Command("rustfmt"): {".rs"}, 45 exec.Command("scalafmt"): {".scala"}, 46 exec.Command("shfmt", "-s", "-w", "-i", "2", "-bn", "-ci", "-sr", "-kp"): {".bash", ".sh", "APKBUILD", "PKGBUILD"}, 47 exec.Command("v", "fmt"): {".v"}, 48 exec.Command("tidy", "-w", "80", "-q", "-i", "-utf8", "--show-errors", "0", "--show-warnings", "no", "--tidy-mark", "no", "-xml", "-m"): {".xml"}, 49 exec.Command("zig", "fmt"): {".zig"}, 50 } 51 } 52 return formatMap 53 } 54 55 // Using exec.Cmd instead of *exec.Cmd is on purpose, to get a new cmd.stdout and cmd.stdin every time. 56 func (e *Editor) formatWithUtility(c *vt100.Canvas, tty *vt100.TTY, status *StatusBar, cmd exec.Cmd, extOrBaseFilename string) error { 57 if files.Which(cmd.Path) == "" { // Does the formatting tool even exist? 58 return errors.New(cmd.Path + " is missing") 59 } 60 61 tempFirstName := "o" 62 if e.mode == mode.Kotlin { 63 tempFirstName = "O" 64 } 65 66 if f, err := os.CreateTemp(tempDir, tempFirstName+".*"+extOrBaseFilename); err == nil { 67 // no error, everything is fine 68 tempFilename := f.Name() 69 defer os.Remove(tempFilename) 70 defer f.Close() 71 72 // TODO: Implement e.SaveAs 73 oldFilename := e.filename 74 e.filename = tempFilename 75 err := e.Save(c, tty) 76 e.filename = oldFilename 77 78 if err == nil { 79 // Add the filename of the temporary file to the command 80 cmd.Args = append(cmd.Args, tempFilename) 81 82 // Save the command in a temporary file 83 saveCommand(&cmd) 84 85 // Format the temporary file 86 output, err := cmd.CombinedOutput() 87 88 // Ignore errors if the command is "tidy" and tidy exists 89 ignoreErrors := strings.HasSuffix(cmd.Path, "tidy") && files.Which("tidy") != "" 90 91 // Perl may place executables in /usr/bin/vendor_perl 92 if e.mode == mode.Perl { 93 // Use perltidy from the PATH if /usr/bin/vendor_perl/perltidy does not exists 94 if cmd.Path == "/usr/bin/vendor_perl/perltidy" && !files.Exists("/usr/bin/vendor_perl/perltidy") { 95 perltidyPath := files.Which("perltidy") 96 if perltidyPath == "" { 97 return errors.New("perltidy is missing") 98 } 99 cmd.Path = perltidyPath 100 } 101 } 102 103 if err != nil && !ignoreErrors { 104 // Only grab the first error message 105 errorMessage := strings.TrimSpace(string(output)) 106 if errorMessage == "" && err != nil { 107 errorMessage = err.Error() 108 } 109 if strings.Count(errorMessage, "\n") > 0 { 110 errorMessage = strings.TrimSpace(strings.SplitN(errorMessage, "\n", 2)[0]) 111 } 112 var retErr error 113 if errorMessage == "" { 114 retErr = errors.New("failed to format code") 115 } else { 116 retErr = errors.New("failed to format code: " + errorMessage) 117 } 118 if strings.Count(errorMessage, ":") >= 3 { 119 fields := strings.Split(errorMessage, ":") 120 // Go To Y:X, if available 121 var foundY int 122 if y, err := strconv.Atoi(fields[1]); err == nil { // no error 123 foundY = y - 1 124 e.redraw, _ = e.GoTo(LineIndex(foundY), c, status) 125 foundX := -1 126 if x, err := strconv.Atoi(fields[2]); err == nil { // no error 127 foundX = x - 1 128 } 129 if foundX != -1 { 130 tabs := strings.Count(e.Line(LineIndex(foundY)), "\t") 131 e.pos.sx = foundX + (tabs * (e.indentation.PerTab - 1)) 132 e.Center(c) 133 } 134 } 135 e.redrawCursor = true 136 } 137 return retErr 138 } 139 140 if _, err := e.Load(c, tty, FilenameOrData{tempFilename, []byte{}, 0, false}); err != nil { 141 return err 142 } 143 // Mark the data as changed, despite just having loaded a file 144 e.changed = true 145 e.redrawCursor = true 146 } 147 // Try to close the file. f.Close() checks if f is nil before closing. 148 e.redraw = true 149 e.redrawCursor = true 150 } 151 return nil 152 } 153 154 // formatJSON can format the given JSON data 155 func formatJSON(data []byte, jsonFormatToggle *bool, indentationPerTab int) ([]byte, error) { 156 var v interface{} 157 err := json.Unmarshal(data, &v) 158 if err != nil { 159 return nil, err 160 } 161 // Format the JSON bytes, first without indentation and then 162 // with indentation. 163 var indentedJSON []byte 164 if *jsonFormatToggle { 165 indentedJSON, err = json.Marshal(v) 166 *jsonFormatToggle = false 167 } else { 168 indentationString := strings.Repeat(" ", indentationPerTab) 169 indentedJSON, err = json.MarshalIndent(v, "", indentationString) 170 *jsonFormatToggle = true 171 } 172 if err != nil { 173 return nil, err 174 } 175 return indentedJSON, nil 176 } 177 178 // formatHTML can format the given HTML data 179 func formatHTML(data []byte) ([]byte, error) { 180 return gohtml.FormatBytes(data), nil 181 } 182 183 // organizeImports can fix, sort and organize imports for Kotlin and for Java 184 func organizeImports(data []byte, onlyJava, removeExistingImports, deGlob bool) []byte { 185 ima, err := autoimport.New(onlyJava, removeExistingImports, deGlob) 186 if err != nil { 187 return data // no change 188 } 189 const verbose = false 190 newData, err := ima.FixImports(data, verbose) 191 if err != nil { 192 return data // no change 193 } 194 return newData 195 } 196 197 func (e *Editor) formatCode(c *vt100.Canvas, tty *vt100.TTY, status *StatusBar, jsonFormatToggle *bool) { 198 199 // Format JSON 200 if e.mode == mode.JSON { 201 data, err := formatJSON([]byte(e.String()), jsonFormatToggle, e.indentation.PerTab) 202 if err != nil { 203 status.ClearAll(c) 204 status.SetErrorAfterRedraw(err) 205 return 206 } 207 e.LoadBytes(data) 208 e.redraw = true 209 return 210 } 211 212 // Format HTML 213 if e.mode == mode.HTML { 214 data, err := formatHTML([]byte(e.String())) 215 if err != nil { 216 status.ClearAll(c) 217 status.SetErrorAfterRedraw(err) 218 return 219 } 220 e.LoadBytes(data) 221 e.redraw = true 222 return 223 } 224 225 // Format /etc/fstab files 226 if baseFilename := filepath.Base(e.filename); baseFilename == "fstab" { 227 const spaces = 2 228 e.LoadBytes(formatFstab([]byte(e.String()), spaces)) 229 e.redraw = true 230 return 231 } 232 233 // Organize Java or Kotlin imports 234 if e.mode == mode.Java || e.mode == mode.Kotlin { 235 const removeExistingImports = false 236 const deGlobImports = true 237 e.LoadBytes(organizeImports([]byte(e.String()), e.mode == mode.Java, removeExistingImports, deGlobImports)) 238 e.redraw = true 239 // Do not return, since there is more formatting to be done 240 } 241 242 // Not in git mode, format Go or C++ code with goimports or clang-format 243 244 OUT: 245 for cmd, extensions := range GetFormatMap() { 246 for _, ext := range extensions { 247 if strings.HasSuffix(e.filename, ext) { 248 // Format a specific file instead of the current directory if "go.mod" is missing 249 if sourceFilename, err := filepath.Abs(e.filename); e.mode == mode.Go && err == nil { 250 sourceDir := filepath.Dir(sourceFilename) 251 if !files.IsFile(filepath.Join(sourceDir, "go.mod")) { 252 cmd.Args = append(cmd.Args, sourceFilename) 253 } 254 } 255 if err := e.formatWithUtility(c, tty, status, *cmd, ext); err != nil { 256 status.ClearAll(c) 257 status.SetMessage(err.Error()) 258 status.Show(c, e) 259 break OUT 260 } 261 break OUT 262 } 263 } 264 } 265 }