]> git.lizzy.rs Git - micro.git/blob - internal/util/util.go
Improve comments
[micro.git] / internal / util / util.go
1 package util
2
3 import (
4         "archive/zip"
5         "bytes"
6         "errors"
7         "fmt"
8         "io"
9         "os"
10         "os/user"
11         "path/filepath"
12         "regexp"
13         "runtime"
14         "strconv"
15         "strings"
16         "time"
17         "unicode"
18
19         "github.com/blang/semver"
20         runewidth "github.com/mattn/go-runewidth"
21 )
22
23 var (
24         // These variables should be set by the linker when compiling
25
26         // Version is the version number or commit hash
27         Version = "0.0.0-unknown"
28         // SemVersion is the Semantic version
29         SemVersion semver.Version
30         // CommitHash is the commit this version was built on
31         CommitHash = "Unknown"
32         // CompileDate is the date this binary was compiled on
33         CompileDate = "Unknown"
34         // Debug logging
35         Debug = "OFF"
36         // FakeCursor is used to disable the terminal cursor and have micro
37         // draw its own (enabled for windows consoles where the cursor is slow)
38         FakeCursor = false
39
40         // Stdout is a buffer that is written to stdout when micro closes
41         Stdout *bytes.Buffer
42 )
43
44 func init() {
45         var err error
46         SemVersion, err = semver.Make(Version)
47         if err != nil {
48                 fmt.Println("Invalid version: ", Version, err)
49         }
50
51         _, wt := os.LookupEnv("WT_SESSION")
52         if runtime.GOOS == "windows" && !wt {
53                 FakeCursor = true
54         }
55         Stdout = new(bytes.Buffer)
56 }
57
58 // SliceEnd returns a byte slice where the index is a rune index
59 // Slices off the start of the slice
60 func SliceEnd(slc []byte, index int) []byte {
61         len := len(slc)
62         i := 0
63         totalSize := 0
64         for totalSize < len {
65                 if i >= index {
66                         return slc[totalSize:]
67                 }
68
69                 _, _, size := DecodeCharacter(slc[totalSize:])
70                 totalSize += size
71                 i++
72         }
73
74         return slc[totalSize:]
75 }
76
77 // SliceEndStr is the same as SliceEnd but for strings
78 func SliceEndStr(str string, index int) string {
79         len := len(str)
80         i := 0
81         totalSize := 0
82         for totalSize < len {
83                 if i >= index {
84                         return str[totalSize:]
85                 }
86
87                 _, _, size := DecodeCharacterInString(str[totalSize:])
88                 totalSize += size
89                 i++
90         }
91
92         return str[totalSize:]
93 }
94
95 // SliceStart returns a byte slice where the index is a rune index
96 // Slices off the end of the slice
97 func SliceStart(slc []byte, index int) []byte {
98         len := len(slc)
99         i := 0
100         totalSize := 0
101         for totalSize < len {
102                 if i >= index {
103                         return slc[:totalSize]
104                 }
105
106                 _, _, size := DecodeCharacter(slc[totalSize:])
107                 totalSize += size
108                 i++
109         }
110
111         return slc[:totalSize]
112 }
113
114 // SliceStartStr is the same as SliceStart but for strings
115 func SliceStartStr(str string, index int) string {
116         len := len(str)
117         i := 0
118         totalSize := 0
119         for totalSize < len {
120                 if i >= index {
121                         return str[:totalSize]
122                 }
123
124                 _, _, size := DecodeCharacterInString(str[totalSize:])
125                 totalSize += size
126                 i++
127         }
128
129         return str[:totalSize]
130 }
131
132 // SliceVisualEnd will take a byte slice and slice off the start
133 // up to a given visual index. If the index is in the middle of a
134 // rune the number of visual columns into the rune will be returned
135 // It will also return the char pos of the first character of the slice
136 func SliceVisualEnd(b []byte, n, tabsize int) ([]byte, int, int) {
137         width := 0
138         i := 0
139         for len(b) > 0 {
140                 r, _, size := DecodeCharacter(b)
141
142                 w := 0
143                 switch r {
144                 case '\t':
145                         ts := tabsize - (width % tabsize)
146                         w = ts
147                 default:
148                         w = runewidth.RuneWidth(r)
149                 }
150                 if width+w > n {
151                         return b, n - width, i
152                 }
153                 width += w
154                 b = b[size:]
155                 i++
156         }
157         return b, n - width, i
158 }
159
160 // Abs is a simple absolute value function for ints
161 func Abs(n int) int {
162         if n < 0 {
163                 return -n
164         }
165         return n
166 }
167
168 // StringWidth returns the visual width of a byte array indexed from 0 to n (rune index)
169 // with a given tabsize
170 func StringWidth(b []byte, n, tabsize int) int {
171         if n <= 0 {
172                 return 0
173         }
174         i := 0
175         width := 0
176         for len(b) > 0 {
177                 r, _, size := DecodeCharacter(b)
178                 b = b[size:]
179
180                 switch r {
181                 case '\t':
182                         ts := tabsize - (width % tabsize)
183                         width += ts
184                 default:
185                         width += runewidth.RuneWidth(r)
186                 }
187
188                 i++
189
190                 if i == n {
191                         return width
192                 }
193         }
194         return width
195 }
196
197 // Min takes the min of two ints
198 func Min(a, b int) int {
199         if a > b {
200                 return b
201         }
202         return a
203 }
204
205 // Max takes the max of two ints
206 func Max(a, b int) int {
207         if a > b {
208                 return a
209         }
210         return b
211 }
212
213 // FSize gets the size of a file
214 func FSize(f *os.File) int64 {
215         fi, _ := f.Stat()
216         return fi.Size()
217 }
218
219 // IsWordChar returns whether or not the string is a 'word character'
220 // Word characters are defined as numbers, letters, or '_'
221 func IsWordChar(r rune) bool {
222         return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_'
223 }
224
225 // Spaces returns a string with n spaces
226 func Spaces(n int) string {
227         return strings.Repeat(" ", n)
228 }
229
230 // IsSpaces checks if a given string is only spaces
231 func IsSpaces(str []byte) bool {
232         for _, c := range str {
233                 if c != ' ' {
234                         return false
235                 }
236         }
237
238         return true
239 }
240
241 // IsSpacesOrTabs checks if a given string contains only spaces and tabs
242 func IsSpacesOrTabs(str []byte) bool {
243         for _, c := range str {
244                 if c != ' ' && c != '\t' {
245                         return false
246                 }
247         }
248
249         return true
250 }
251
252 // IsWhitespace returns true if the given rune is a space, tab, or newline
253 func IsWhitespace(c rune) bool {
254         return unicode.IsSpace(c)
255 }
256
257 // IsBytesWhitespace returns true if the given bytes are all whitespace
258 func IsBytesWhitespace(b []byte) bool {
259         for _, c := range b {
260                 if !IsWhitespace(rune(c)) {
261                         return false
262                 }
263         }
264         return true
265 }
266
267 // RunePos returns the rune index of a given byte index
268 // Make sure the byte index is not between code points
269 func RunePos(b []byte, i int) int {
270         return CharacterCount(b[:i])
271 }
272
273 // MakeRelative will attempt to make a relative path between path and base
274 func MakeRelative(path, base string) (string, error) {
275         if len(path) > 0 {
276                 rel, err := filepath.Rel(base, path)
277                 if err != nil {
278                         return path, err
279                 }
280                 return rel, nil
281         }
282         return path, nil
283 }
284
285 // ReplaceHome takes a path as input and replaces ~ at the start of the path with the user's
286 // home directory. Does nothing if the path does not start with '~'.
287 func ReplaceHome(path string) (string, error) {
288         if !strings.HasPrefix(path, "~") {
289                 return path, nil
290         }
291
292         var userData *user.User
293         var err error
294
295         homeString := strings.Split(path, "/")[0]
296         if homeString == "~" {
297                 userData, err = user.Current()
298                 if err != nil {
299                         return "", errors.New("Could not find user: " + err.Error())
300                 }
301         } else {
302                 userData, err = user.Lookup(homeString[1:])
303                 if err != nil {
304                         return "", errors.New("Could not find user: " + err.Error())
305                 }
306         }
307
308         home := userData.HomeDir
309
310         return strings.Replace(path, homeString, home, 1), nil
311 }
312
313 // GetPathAndCursorPosition returns a filename without everything following a `:`
314 // This is used for opening files like util.go:10:5 to specify a line and column
315 // Special cases like Windows Absolute path (C:\myfile.txt:10:5) are handled correctly.
316 func GetPathAndCursorPosition(path string) (string, []string) {
317         re := regexp.MustCompile(`([\s\S]+?)(?::(\d+))(?::(\d+))?`)
318         match := re.FindStringSubmatch(path)
319         // no lines/columns were specified in the path, return just the path with no cursor location
320         if len(match) == 0 {
321                 return path, nil
322         } else if match[len(match)-1] != "" {
323                 // if the last capture group match isn't empty then both line and column were provided
324                 return match[1], match[2:]
325         }
326         // if it was empty, then only a line was provided, so default to column 0
327         return match[1], []string{match[2], "0"}
328 }
329
330 // GetModTime returns the last modification time for a given file
331 func GetModTime(path string) (time.Time, error) {
332         info, err := os.Stat(path)
333         if err != nil {
334                 return time.Now(), err
335         }
336         return info.ModTime(), nil
337 }
338
339 // EscapePath replaces every path separator in a given path with a %
340 func EscapePath(path string) string {
341         path = filepath.ToSlash(path)
342         if runtime.GOOS == "windows" {
343                 // ':' is not valid in a path name on Windows but is ok on Unix
344                 path = strings.ReplaceAll(path, ":", "%")
345         }
346         return strings.ReplaceAll(path, "/", "%")
347 }
348
349 // GetLeadingWhitespace returns the leading whitespace of the given byte array
350 func GetLeadingWhitespace(b []byte) []byte {
351         ws := []byte{}
352         for len(b) > 0 {
353                 r, _, size := DecodeCharacter(b)
354                 if r == ' ' || r == '\t' {
355                         ws = append(ws, byte(r))
356                 } else {
357                         break
358                 }
359
360                 b = b[size:]
361         }
362         return ws
363 }
364
365 // IntOpt turns a float64 setting to an int
366 func IntOpt(opt interface{}) int {
367         return int(opt.(float64))
368 }
369
370 // GetCharPosInLine gets the char position of a visual x y
371 // coordinate (this is necessary because tabs are 1 char but
372 // 4 visual spaces)
373 func GetCharPosInLine(b []byte, visualPos int, tabsize int) int {
374         // Scan rune by rune until we exceed the visual width that we are
375         // looking for. Then we can return the character position we have found
376         i := 0     // char pos
377         width := 0 // string visual width
378         for len(b) > 0 {
379                 r, _, size := DecodeCharacter(b)
380                 b = b[size:]
381
382                 switch r {
383                 case '\t':
384                         ts := tabsize - (width % tabsize)
385                         width += ts
386                 default:
387                         width += runewidth.RuneWidth(r)
388                 }
389
390                 if width >= visualPos {
391                         if width == visualPos {
392                                 i++
393                         }
394                         break
395                 }
396                 i++
397         }
398
399         return i
400 }
401
402 // ParseBool is almost exactly like strconv.ParseBool, except it also accepts 'on' and 'off'
403 // as 'true' and 'false' respectively
404 func ParseBool(str string) (bool, error) {
405         if str == "on" {
406                 return true, nil
407         }
408         if str == "off" {
409                 return false, nil
410         }
411         return strconv.ParseBool(str)
412 }
413
414 // Clamp clamps a value between min and max
415 func Clamp(val, min, max int) int {
416         if val < min {
417                 val = min
418         } else if val > max {
419                 val = max
420         }
421         return val
422 }
423
424 // IsNonAlphaNumeric returns if the rune is not a number of letter or underscore.
425 func IsNonAlphaNumeric(c rune) bool {
426         return !unicode.IsLetter(c) && !unicode.IsNumber(c) && c != '_'
427 }
428
429 // IsAutocomplete returns whether a character should begin an autocompletion.
430 func IsAutocomplete(c rune) bool {
431         return c == '.' || !IsNonAlphaNumeric(c)
432 }
433
434 // ParseSpecial replaces escaped ts with '\t'.
435 func ParseSpecial(s string) string {
436         return strings.ReplaceAll(s, "\\t", "\t")
437 }
438
439 // String converts a byte array to a string (for lua plugins)
440 func String(s []byte) string {
441         return string(s)
442 }
443
444 // Unzip unzips a file to given folder
445 func Unzip(src, dest string) error {
446         r, err := zip.OpenReader(src)
447         if err != nil {
448                 return err
449         }
450         defer r.Close()
451
452         os.MkdirAll(dest, 0755)
453
454         // Closure to address file descriptors issue with all the deferred .Close() methods
455         extractAndWriteFile := func(f *zip.File) error {
456                 rc, err := f.Open()
457                 if err != nil {
458                         return err
459                 }
460                 defer rc.Close()
461
462                 path := filepath.Join(dest, f.Name)
463
464                 // Check for ZipSlip (Directory traversal)
465                 if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) {
466                         return fmt.Errorf("illegal file path: %s", path)
467                 }
468
469                 if f.FileInfo().IsDir() {
470                         os.MkdirAll(path, f.Mode())
471                 } else {
472                         os.MkdirAll(filepath.Dir(path), f.Mode())
473                         f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
474                         if err != nil {
475                                 return err
476                         }
477                         defer f.Close()
478
479                         _, err = io.Copy(f, rc)
480                         if err != nil {
481                                 return err
482                         }
483                 }
484                 return nil
485         }
486
487         for _, f := range r.File {
488                 err := extractAndWriteFile(f)
489                 if err != nil {
490                         return err
491                 }
492         }
493
494         return nil
495 }