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  }