]> git.lizzy.rs Git - micro.git/blob - cmd/micro/util/util.go
More actions and window organization
[micro.git] / cmd / micro / util / util.go
1 package util
2
3 import (
4         "errors"
5         "os"
6         "os/user"
7         "path/filepath"
8         "regexp"
9         "strings"
10         "time"
11         "unicode/utf8"
12
13         runewidth "github.com/mattn/go-runewidth"
14 )
15
16 // SliceEnd returns a byte slice where the index is a rune index
17 // Slices off the start of the slice
18 func SliceEnd(slc []byte, index int) []byte {
19         len := len(slc)
20         i := 0
21         totalSize := 0
22         for totalSize < len {
23                 if i >= index {
24                         return slc[totalSize:]
25                 }
26
27                 _, size := utf8.DecodeRune(slc[totalSize:])
28                 totalSize += size
29                 i++
30         }
31
32         return slc[totalSize:]
33 }
34
35 // SliceStart returns a byte slice where the index is a rune index
36 // Slices off the end of the slice
37 func SliceStart(slc []byte, index int) []byte {
38         len := len(slc)
39         i := 0
40         totalSize := 0
41         for totalSize < len {
42                 if i >= index {
43                         return slc[:totalSize]
44                 }
45
46                 _, size := utf8.DecodeRune(slc[totalSize:])
47                 totalSize += size
48                 i++
49         }
50
51         return slc[:totalSize]
52 }
53
54 // SliceVisualEnd will take a byte slice and slice off the start
55 // up to a given visual index. If the index is in the middle of a
56 // rune the number of visual columns into the rune will be returned
57 func SliceVisualEnd(b []byte, n, tabsize int) ([]byte, int) {
58         width := 0
59         for len(b) > 0 {
60                 r, size := utf8.DecodeRune(b)
61
62                 w := 0
63                 switch r {
64                 case '\t':
65                         ts := tabsize - (width % tabsize)
66                         w = ts
67                 default:
68                         w = runewidth.RuneWidth(r)
69                 }
70                 if width+w > n {
71                         return b, n - width
72                 }
73                 width += w
74                 b = b[size:]
75         }
76         return b, width
77 }
78
79 // Abs is a simple absolute value function for ints
80 func Abs(n int) int {
81         if n < 0 {
82                 return -n
83         }
84         return n
85 }
86
87 // StringWidth returns the visual width of a byte array indexed from 0 to n (rune index)
88 // with a given tabsize
89 func StringWidth(b []byte, n, tabsize int) int {
90         i := 0
91         width := 0
92         for len(b) > 0 {
93                 r, size := utf8.DecodeRune(b)
94                 b = b[size:]
95
96                 switch r {
97                 case '\t':
98                         ts := tabsize - (width % tabsize)
99                         width += ts
100                 default:
101                         width += runewidth.RuneWidth(r)
102                 }
103
104                 i++
105
106                 if i == n {
107                         return width
108                 }
109         }
110         return width
111 }
112
113 // Min takes the min of two ints
114 func Min(a, b int) int {
115         if a > b {
116                 return b
117         }
118         return a
119 }
120
121 // Max takes the max of two ints
122 func Max(a, b int) int {
123         if a > b {
124                 return a
125         }
126         return b
127 }
128
129 // FSize gets the size of a file
130 func FSize(f *os.File) int64 {
131         fi, _ := f.Stat()
132         return fi.Size()
133 }
134
135 // IsWordChar returns whether or not the string is a 'word character'
136 // If it is a unicode character, then it does not match
137 // Word characters are defined as [A-Za-z0-9_]
138 func IsWordChar(r rune) bool {
139         return (r >= '0' && r <= '9') || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r == '_')
140 }
141
142 // IsWhitespace returns true if the given rune is a space, tab, or newline
143 func IsWhitespace(c rune) bool {
144         return c == ' ' || c == '\t' || c == '\n'
145 }
146
147 // IsStrWhitespace returns true if the given string is all whitespace
148 func IsStrWhitespace(str string) bool {
149         // Range loop for unicode correctness
150         for _, c := range str {
151                 if !IsWhitespace(c) {
152                         return false
153                 }
154         }
155         return true
156 }
157
158 // TODO: consider changing because of snap segfault
159 // ReplaceHome takes a path as input and replaces ~ at the start of the path with the user's
160 // home directory. Does nothing if the path does not start with '~'.
161 func ReplaceHome(path string) (string, error) {
162         if !strings.HasPrefix(path, "~") {
163                 return path, nil
164         }
165
166         var userData *user.User
167         var err error
168
169         homeString := strings.Split(path, "/")[0]
170         if homeString == "~" {
171                 userData, err = user.Current()
172                 if err != nil {
173                         return "", errors.New("Could not find user: " + err.Error())
174                 }
175         } else {
176                 userData, err = user.Lookup(homeString[1:])
177                 if err != nil {
178                         return "", errors.New("Could not find user: " + err.Error())
179                 }
180         }
181
182         home := userData.HomeDir
183
184         return strings.Replace(path, homeString, home, 1), nil
185 }
186
187 // GetPathAndCursorPosition returns a filename without everything following a `:`
188 // This is used for opening files like util.go:10:5 to specify a line and column
189 // Special cases like Windows Absolute path (C:\myfile.txt:10:5) are handled correctly.
190 func GetPathAndCursorPosition(path string) (string, []string) {
191         re := regexp.MustCompile(`([\s\S]+?)(?::(\d+))(?::(\d+))?`)
192         match := re.FindStringSubmatch(path)
193         // no lines/columns were specified in the path, return just the path with no cursor location
194         if len(match) == 0 {
195                 return path, nil
196         } else if match[len(match)-1] != "" {
197                 // if the last capture group match isn't empty then both line and column were provided
198                 return match[1], match[2:]
199         }
200         // if it was empty, then only a line was provided, so default to column 0
201         return match[1], []string{match[2], "0"}
202 }
203
204 // GetModTime returns the last modification time for a given file
205 func GetModTime(path string) (time.Time, error) {
206         info, err := os.Stat(path)
207         if err != nil {
208                 return time.Now(), err
209         }
210         return info.ModTime(), nil
211 }
212
213 // EscapePath replaces every path separator in a given path with a %
214 func EscapePath(path string) string {
215         path = filepath.ToSlash(path)
216         return strings.Replace(path, "/", "%", -1)
217 }