9fans.net/go@v0.0.5/acme/acmego/main.go (about)

     1  // Copyright 2014 The Go Authors.  All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Acmego watches acme for .go files being written.
     6  //
     7  // Usage:
     8  //
     9  //	acmego [-f]
    10  //
    11  // Each time a .go file is written, acmego checks whether the
    12  // import block needs adjustment. If so, it makes the changes
    13  // in the window body but does not write the file.
    14  // It depends on “goimports” being installed.
    15  //
    16  // If the -f option is given, reformats the Go source file body
    17  // as well as updating the imports. It also watches for other
    18  // known extensions and runs their formatters if found in
    19  // the executable path.
    20  //
    21  // The other known extensions and formatters are:
    22  //
    23  //	.rs - rustfmt
    24  //	.py - yapf
    25  //
    26  package main
    27  
    28  import (
    29  	"bytes"
    30  	"flag"
    31  	"fmt"
    32  	"io/ioutil"
    33  	"log"
    34  	"os"
    35  	"os/exec"
    36  	"strconv"
    37  	"strings"
    38  	"unicode/utf8"
    39  
    40  	"9fans.net/go/acme"
    41  )
    42  
    43  var gofmt = flag.Bool("f", false, "format the entire file after Put")
    44  
    45  var formatters = map[string][]string{
    46  	".go": []string{"goimports"},
    47  }
    48  
    49  // Non-Go formatters (only loaded with -f option).
    50  var otherFormatters = map[string][]string{
    51  	".rs": []string{"rustfmt", "--emit", "stdout"},
    52  	".py": []string{"yapf"},
    53  }
    54  
    55  func main() {
    56  	flag.Parse()
    57  	if *gofmt {
    58  		for suffix, formatter := range otherFormatters {
    59  			formatters[suffix] = formatter
    60  		}
    61  	}
    62  	l, err := acme.Log()
    63  	if err != nil {
    64  		log.Fatal(err)
    65  	}
    66  
    67  	for {
    68  		event, err := l.Read()
    69  		if err != nil {
    70  			log.Fatal(err)
    71  		}
    72  		if event.Name == "" || event.Op != "put" {
    73  			continue
    74  		}
    75  		for suffix, formatter := range formatters {
    76  			if strings.HasSuffix(event.Name, suffix) {
    77  				reformat(event.ID, event.Name, formatter)
    78  				break
    79  			}
    80  		}
    81  	}
    82  }
    83  
    84  func reformat(id int, name string, formatter []string) {
    85  	w, err := acme.Open(id, nil)
    86  	if err != nil {
    87  		log.Print(err)
    88  		return
    89  	}
    90  	defer w.CloseFiles()
    91  
    92  	old, err := ioutil.ReadFile(name)
    93  	if err != nil {
    94  		//log.Print(err)
    95  		return
    96  	}
    97  
    98  	exe, err := exec.LookPath(formatter[0])
    99  	if err != nil {
   100  		// Formatter not installed.
   101  		return
   102  	}
   103  
   104  	new, err := exec.Command(exe, append(formatter[1:], name)...).CombinedOutput()
   105  	if err != nil {
   106  		if strings.Contains(string(new), "fatal error") {
   107  			fmt.Fprintf(os.Stderr, "goimports %s: %v\n%s", name, err, new)
   108  		} else {
   109  			fmt.Fprintf(os.Stderr, "%s", new)
   110  		}
   111  		return
   112  	}
   113  
   114  	if bytes.Equal(old, new) {
   115  		return
   116  	}
   117  
   118  	if !*gofmt {
   119  		oldTop, err := readImports(bytes.NewReader(old), true)
   120  		if err != nil {
   121  			//log.Print(err)
   122  			return
   123  		}
   124  		newTop, err := readImports(bytes.NewReader(new), true)
   125  		if err != nil {
   126  			//log.Print(err)
   127  			return
   128  		}
   129  		if bytes.Equal(oldTop, newTop) {
   130  			return
   131  		}
   132  		w.Addr("0,#%d", utf8.RuneCount(oldTop))
   133  		w.Write("data", newTop)
   134  		return
   135  	}
   136  
   137  	f, err := ioutil.TempFile("", "acmego")
   138  	if err != nil {
   139  		log.Print(err)
   140  		return
   141  	}
   142  	if _, err := f.Write(new); err != nil {
   143  		log.Print(err)
   144  		return
   145  	}
   146  	tmp := f.Name()
   147  	f.Close()
   148  	defer os.Remove(tmp)
   149  
   150  	diff, _ := exec.Command("9", "diff", name, tmp).CombinedOutput()
   151  
   152  	latest, err := w.ReadAll("body")
   153  	if err != nil {
   154  		log.Print(err)
   155  		return
   156  	}
   157  	if !bytes.Equal(old, latest) {
   158  		log.Printf("skipped update to %s: window modified since Put\n", name)
   159  		return
   160  	}
   161  
   162  	w.Write("ctl", []byte("mark"))
   163  	w.Write("ctl", []byte("nomark"))
   164  	diffLines := strings.Split(string(diff), "\n")
   165  	for i := len(diffLines) - 1; i >= 0; i-- {
   166  		line := diffLines[i]
   167  		if line == "" {
   168  			continue
   169  		}
   170  		if line[0] == '<' || line[0] == '-' || line[0] == '>' {
   171  			continue
   172  		}
   173  		j := 0
   174  		for j < len(line) && line[j] != 'a' && line[j] != 'c' && line[j] != 'd' {
   175  			j++
   176  		}
   177  		if j >= len(line) {
   178  			log.Printf("cannot parse diff line: %q", line)
   179  			break
   180  		}
   181  		oldStart, oldEnd := parseSpan(line[:j])
   182  		newStart, newEnd := parseSpan(line[j+1:])
   183  		if newStart == 0 || (oldStart == 0 && line[j] != 'a') {
   184  			continue
   185  		}
   186  		switch line[j] {
   187  		case 'a':
   188  			err := w.Addr("%d+#0", oldStart)
   189  			if err != nil {
   190  				log.Print(err)
   191  				break
   192  			}
   193  			w.Write("data", findLines(new, newStart, newEnd))
   194  		case 'c':
   195  			err := w.Addr("%d,%d", oldStart, oldEnd)
   196  			if err != nil {
   197  				log.Print(err)
   198  				break
   199  			}
   200  			w.Write("data", findLines(new, newStart, newEnd))
   201  		case 'd':
   202  			err := w.Addr("%d,%d", oldStart, oldEnd)
   203  			if err != nil {
   204  				log.Print(err)
   205  				break
   206  			}
   207  			w.Write("data", nil)
   208  		}
   209  	}
   210  	if !bytes.HasSuffix(old, nlBytes) && bytes.HasSuffix(new, nlBytes) {
   211  		// plan9port diff doesn't report a difference if there's a mismatch in the
   212  		// final newline, so add one if needed.
   213  		if err := w.Addr("$"); err != nil {
   214  			log.Print(err)
   215  			return
   216  		}
   217  		w.Write("data", nlBytes)
   218  	}
   219  }
   220  
   221  var nlBytes = []byte("\n")
   222  
   223  func parseSpan(text string) (start, end int) {
   224  	i := strings.Index(text, ",")
   225  	if i < 0 {
   226  		n, err := strconv.Atoi(text)
   227  		if err != nil {
   228  			log.Printf("cannot parse span %q", text)
   229  			return 0, 0
   230  		}
   231  		return n, n
   232  	}
   233  	start, err1 := strconv.Atoi(text[:i])
   234  	end, err2 := strconv.Atoi(text[i+1:])
   235  	if err1 != nil || err2 != nil {
   236  		log.Printf("cannot parse span %q", text)
   237  		return 0, 0
   238  	}
   239  	return start, end
   240  }
   241  
   242  func findLines(text []byte, start, end int) []byte {
   243  	i := 0
   244  
   245  	start--
   246  	for ; i < len(text) && start > 0; i++ {
   247  		if text[i] == '\n' {
   248  			start--
   249  			end--
   250  		}
   251  	}
   252  	startByte := i
   253  	for ; i < len(text) && end > 0; i++ {
   254  		if text[i] == '\n' {
   255  			end--
   256  		}
   257  	}
   258  	endByte := i
   259  	return text[startByte:endByte]
   260  }