1package main 2 3import ( 4 "encoding/json" 5 "errors" 6 "io/ioutil" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "strconv" 11 "strings" 12 13 "github.com/xyproto/env" 14 "github.com/xyproto/mode" 15 "github.com/xyproto/vt100" 16) 17 18// Map from formatting command to a list of file extensions 19var format = map[*exec.Cmd][]string{ 20 exec.Command("goimports", "-w", "--"): {".go"}, 21 exec.Command("clang-format", "-fallback-style=WebKit", "-style=file", "-i", "--"): {".cpp", ".cc", ".cxx", ".h", ".hpp", ".c++", ".h++", ".c"}, 22 exec.Command("zig", "fmt"): {".zig"}, 23 exec.Command("v", "fmt"): {".v"}, 24 exec.Command("rustfmt"): {".rs"}, 25 exec.Command("brittany", "--write-mode=inplace"): {".hs"}, 26 exec.Command("autopep8", "-i", "--max-line-length", "120"): {".py"}, 27 exec.Command("ocamlformat"): {".ml"}, 28 exec.Command("crystal", "tool", "format"): {".cr"}, 29 exec.Command("ktlint", "-F"): {".kt", ".kts"}, 30 exec.Command("google-java-format", "-i"): {".java"}, 31 exec.Command("scalafmt"): {".scala"}, 32 exec.Command("astyle", "--mode=cs"): {".cs"}, 33 exec.Command("prettier", "--tab-width", "4", "-w"): {".js", ".ts"}, 34 exec.Command("prettier", "--tab-width", "2", "-w"): {".css"}, 35 exec.Command("lua-format", "-i", "--no-keep-simple-function-one-line", "--column-limit=120", "--indent-width=2", "--no-use-tab"): {".lua"}, 36 exec.Command("tidy", "-w", "80", "-q", "-i", "-utf8", "--show-errors", "0", "--show-warnings", "no", "--tidy-mark", "no", "-xml", "-m"): {".xml"}, 37 exec.Command("tidy", "-w", "120", "-q", "-i", "-utf8", "--show-errors", "0", "--show-warnings", "no", "--tidy-mark", "no", "--hide-endtags", "yes", "--force-output", "yes", "-ashtml", "-omit", "no", "-xml", "no", "-m", "-c"): {".html", ".htm"}, 38} 39 40// Using exec.Cmd instead of *exec.Cmd is on purpose, to get a new cmd.stdout and cmd.stdin every time. 41func (e *Editor) formatWithUtility(c *vt100.Canvas, tty *vt100.TTY, status *StatusBar, cmd exec.Cmd, extOrBaseFilename string) error { 42 if which(cmd.Path) == "" { // Does the formatting tool even exist? 43 return errors.New(cmd.Path + " is missing") 44 } 45 46 utilityName := filepath.Base(cmd.Path) 47 status.SetMessage("Calling " + utilityName) 48 status.Show(c, e) 49 50 // Use the temporary directory defined in TMPDIR, with fallback to /tmp 51 tempdir := env.Str("TMPDIR", "/tmp") 52 53 if f, err := ioutil.TempFile(tempdir, "o.*"+extOrBaseFilename); err == nil { 54 // no error, everything is fine 55 tempFilename := f.Name() 56 defer os.Remove(tempFilename) 57 defer f.Close() 58 59 // TODO: Implement e.SaveAs 60 oldFilename := e.filename 61 e.filename = tempFilename 62 err := e.Save(c, tty) 63 e.filename = oldFilename 64 65 if err == nil { 66 // Add the filename of the temporary file to the command 67 cmd.Args = append(cmd.Args, tempFilename) 68 69 // Save the command in a temporary file 70 saveCommand(&cmd) 71 72 // Format the temporary file 73 output, err := cmd.CombinedOutput() 74 75 // Ignore errors if the command is "tidy" and tidy exists 76 ignoreErrors := strings.HasSuffix(cmd.Path, "tidy") && which("tidy") != "" 77 78 if err != nil && !ignoreErrors { 79 // Only grab the first error message 80 errorMessage := strings.TrimSpace(string(output)) 81 if errorMessage == "" && err != nil { 82 errorMessage = err.Error() 83 } 84 if strings.Count(errorMessage, "\n") > 0 { 85 errorMessage = strings.TrimSpace(strings.SplitN(errorMessage, "\n", 2)[0]) 86 } 87 var retErr error 88 if errorMessage == "" { 89 retErr = errors.New("failed to format code") 90 } else { 91 retErr = errors.New("failed to format code: " + errorMessage) 92 } 93 if strings.Count(errorMessage, ":") >= 3 { 94 fields := strings.Split(errorMessage, ":") 95 // Go To Y:X, if available 96 var foundY int 97 if y, err := strconv.Atoi(fields[1]); err == nil { // no error 98 foundY = y - 1 99 e.redraw = e.GoTo(LineIndex(foundY), c, status) 100 foundX := -1 101 if x, err := strconv.Atoi(fields[2]); err == nil { // no error 102 foundX = x - 1 103 } 104 if foundX != -1 { 105 tabs := strings.Count(e.Line(LineIndex(foundY)), "\t") 106 e.pos.sx = foundX + (tabs * (e.tabsSpaces.PerTab - 1)) 107 e.Center(c) 108 } 109 } 110 e.redrawCursor = true 111 } 112 return retErr 113 } 114 115 if _, err := e.Load(c, tty, tempFilename); err != nil { 116 return err 117 } 118 // Mark the data as changed, despite just having loaded a file 119 e.changed = true 120 e.redrawCursor = true 121 } 122 // Try to close the file. f.Close() checks if f is nil before closing. 123 e.redraw = true 124 e.redrawCursor = true 125 } 126 return nil 127} 128 129func (e *Editor) formatCode(c *vt100.Canvas, tty *vt100.TTY, status *StatusBar, jsonFormatToggle *bool) { 130 131 // Format JSON 132 if e.mode == mode.JSON { 133 // TODO: Find a JSON formatter that does not need a JavaScript package like otto 134 var v interface{} 135 136 err := json.Unmarshal([]byte(e.String()), &v) 137 if err != nil { 138 status.ClearAll(c) 139 status.SetErrorMessage(err.Error()) 140 status.Show(c, e) 141 return 142 } 143 144 // Format the JSON bytes, first without indentation and then 145 // with indentation. 146 var indentedJSON []byte 147 if *jsonFormatToggle { 148 indentedJSON, err = json.Marshal(v) 149 *jsonFormatToggle = false 150 } else { 151 indentationString := strings.Repeat(" ", e.tabsSpaces.PerTab) 152 indentedJSON, err = json.MarshalIndent(v, "", indentationString) 153 *jsonFormatToggle = true 154 } 155 if err != nil { 156 status.ClearAll(c) 157 status.SetErrorMessage(err.Error()) 158 status.Show(c, e) 159 return 160 } 161 162 e.LoadBytes(indentedJSON) 163 e.redraw = true 164 return 165 } 166 167 baseFilename := filepath.Base(e.filename) 168 if baseFilename == "fstab" { 169 cmd := exec.Command("fstabfmt", "-i") 170 if which(cmd.Path) == "" { // Does the formatting tool even exist? 171 status.ClearAll(c) 172 status.SetErrorMessage(cmd.Path + " is missing") 173 status.Show(c, e) 174 return 175 } 176 if err := e.formatWithUtility(c, tty, status, *cmd, baseFilename); err != nil { 177 status.ClearAll(c) 178 status.SetMessage(err.Error()) 179 status.Show(c, e) 180 } 181 return 182 } 183 184 // Not in git mode, format Go or C++ code with goimports or clang-format 185 186OUT: 187 for cmd, extensions := range format { 188 for _, ext := range extensions { 189 if strings.HasSuffix(e.filename, ext) { 190 if err := e.formatWithUtility(c, tty, status, *cmd, ext); err != nil { 191 status.ClearAll(c) 192 status.SetMessage(err.Error()) 193 status.Show(c, e) 194 } 195 break OUT 196 } 197 } 198 } 199 200} 201