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