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 }