]> git.lizzy.rs Git - micro.git/blob - internal/util/util.go
Change project layout and use go.mod
[micro.git] / internal / util / util.go
1 package util
2
3 import (
4         "errors"
5         "os"
6         "os/user"
7         "path/filepath"
8         "regexp"
9         "strconv"
10         "strings"
11         "time"
12         "unicode/utf8"
13
14         runewidth "github.com/mattn/go-runewidth"
15 )
16
17 // SliceEnd returns a byte slice where the index is a rune index
18 // Slices off the start of the slice
19 func SliceEnd(slc []byte, index int) []byte {
20         len := len(slc)
21         i := 0
22         totalSize := 0
23         for totalSize < len {
24                 if i >= index {
25                         return slc[totalSize:]
26                 }
27
28                 _, size := utf8.DecodeRune(slc[totalSize:])
29                 totalSize += size
30                 i++
31         }
32
33         return slc[totalSize:]
34 }
35
36 // SliceEndStr is the same as SliceEnd but for strings
37 func SliceEndStr(str string, index int) string {
38         len := len(str)
39         i := 0
40         totalSize := 0
41         for totalSize < len {
42                 if i >= index {
43                         return str[totalSize:]
44                 }
45
46                 _, size := utf8.DecodeRuneInString(str[totalSize:])
47                 totalSize += size
48                 i++
49         }
50
51         return str[totalSize:]
52 }
53
54 // SliceStart returns a byte slice where the index is a rune index
55 // Slices off the end of the slice
56 func SliceStart(slc []byte, index int) []byte {
57         len := len(slc)
58         i := 0
59         totalSize := 0
60         for totalSize < len {
61                 if i >= index {
62                         return slc[:totalSize]
63                 }
64
65                 _, size := utf8.DecodeRune(slc[totalSize:])
66                 totalSize += size
67                 i++
68         }
69
70         return slc[:totalSize]
71 }
72
73 // SliceStartStr is the same as SliceStart but for strings
74 func SliceStartStr(str string, index int) string {
75         len := len(str)
76         i := 0
77         totalSize := 0
78         for totalSize < len {
79                 if i >= index {
80                         return str[:totalSize]
81                 }
82
83                 _, size := utf8.DecodeRuneInString(str[totalSize:])
84                 totalSize += size
85                 i++
86         }
87
88         return str[:totalSize]
89 }
90
91 // SliceVisualEnd will take a byte slice and slice off the start
92 // up to a given visual index. If the index is in the middle of a
93 // rune the number of visual columns into the rune will be returned
94 // It will also return the char pos of the first character of the slice
95 func SliceVisualEnd(b []byte, n, tabsize int) ([]byte, int, int) {
96         width := 0
97         i := 0
98         for len(b) > 0 {
99                 r, size := utf8.DecodeRune(b)
100
101                 w := 0
102                 switch r {
103                 case '\t':
104                         ts := tabsize - (width % tabsize)
105                         w = ts
106                 default:
107                         w = runewidth.RuneWidth(r)
108                 }
109                 if width+w > n {
110                         return b, n - width, i
111                 }
112                 width += w
113                 b = b[size:]
114                 i++
115         }
116         return b, n - width, i
117 }
118
119 // Abs is a simple absolute value function for ints
120 func Abs(n int) int {
121         if n < 0 {
122                 return -n
123         }
124         return n
125 }
126
127 // StringWidth returns the visual width of a byte array indexed from 0 to n (rune index)
128 // with a given tabsize
129 func StringWidth(b []byte, n, tabsize int) int {
130         if n <= 0 {
131                 return 0
132         }
133         i := 0
134         width := 0
135         for len(b) > 0 {
136                 r, size := utf8.DecodeRune(b)
137                 b = b[size:]
138
139                 switch r {
140                 case '\t':
141                         ts := tabsize - (width % tabsize)
142                         width += ts
143                 default:
144                         width += runewidth.RuneWidth(r)
145                 }
146
147                 i++
148
149                 if i == n {
150                         return width
151                 }
152         }
153         return width
154 }
155
156 // Min takes the min of two ints
157 func Min(a, b int) int {
158         if a > b {
159                 return b
160         }
161         return a
162 }
163
164 // Max takes the max of two ints
165 func Max(a, b int) int {
166         if a > b {
167                 return a
168         }
169         return b
170 }
171
172 // FSize gets the size of a file
173 func FSize(f *os.File) int64 {
174         fi, _ := f.Stat()
175         return fi.Size()
176 }
177
178 // IsWordChar returns whether or not the string is a 'word character'
179 // If it is a unicode character, then it does not match
180 // Word characters are defined as [A-Za-z0-9_]
181 func IsWordChar(r rune) bool {
182         return (r >= '0' && r <= '9') || (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r == '_')
183 }
184
185 // Spaces returns a string with n spaces
186 func Spaces(n int) string {
187         return strings.Repeat(" ", n)
188 }
189
190 // IsSpaces checks if a given string is only spaces
191 func IsSpaces(str []byte) bool {
192         for _, c := range str {
193                 if c != ' ' {
194                         return false
195                 }
196         }
197
198         return true
199 }
200
201 // IsSpacesOrTabs checks if a given string contains only spaces and tabs
202 func IsSpacesOrTabs(str []byte) bool {
203         for _, c := range str {
204                 if c != ' ' && c != '\t' {
205                         return false
206                 }
207         }
208
209         return true
210 }
211
212 // IsWhitespace returns true if the given rune is a space, tab, or newline
213 func IsWhitespace(c rune) bool {
214         return c == ' ' || c == '\t' || c == '\n'
215 }
216
217 // IsStrWhitespace returns true if the given string is all whitespace
218 func IsStrWhitespace(str string) bool {
219         // Range loop for unicode correctness
220         for _, c := range str {
221                 if !IsWhitespace(c) {
222                         return false
223                 }
224         }
225         return true
226 }
227
228 // RunePos returns the rune index of a given byte index
229 // Make sure the byte index is not between code points
230 func RunePos(b []byte, i int) int {
231         return utf8.RuneCount(b[:i])
232 }
233
234 // MakeRelative will attempt to make a relative path between path and base
235 func MakeRelative(path, base string) (string, error) {
236         if len(path) > 0 {
237                 rel, err := filepath.Rel(base, path)
238                 if err != nil {
239                         return path, err
240                 }
241                 return rel, nil
242         }
243         return path, nil
244 }
245
246 // TODO: consider changing because of snap segfault
247 // ReplaceHome takes a path as input and replaces ~ at the start of the path with the user's
248 // home directory. Does nothing if the path does not start with '~'.
249 func ReplaceHome(path string) (string, error) {
250         if !strings.HasPrefix(path, "~") {
251                 return path, nil
252         }
253
254         var userData *user.User
255         var err error
256
257         homeString := strings.Split(path, "/")[0]
258         if homeString == "~" {
259                 userData, err = user.Current()
260                 if err != nil {
261                         return "", errors.New("Could not find user: " + err.Error())
262                 }
263         } else {
264                 userData, err = user.Lookup(homeString[1:])
265                 if err != nil {
266                         return "", errors.New("Could not find user: " + err.Error())
267                 }
268         }
269
270         home := userData.HomeDir
271
272         return strings.Replace(path, homeString, home, 1), nil
273 }
274
275 // GetPathAndCursorPosition returns a filename without everything following a `:`
276 // This is used for opening files like util.go:10:5 to specify a line and column
277 // Special cases like Windows Absolute path (C:\myfile.txt:10:5) are handled correctly.
278 func GetPathAndCursorPosition(path string) (string, []string) {
279         re := regexp.MustCompile(`([\s\S]+?)(?::(\d+))(?::(\d+))?`)
280         match := re.FindStringSubmatch(path)
281         // no lines/columns were specified in the path, return just the path with no cursor location
282         if len(match) == 0 {
283                 return path, nil
284         } else if match[len(match)-1] != "" {
285                 // if the last capture group match isn't empty then both line and column were provided
286                 return match[1], match[2:]
287         }
288         // if it was empty, then only a line was provided, so default to column 0
289         return match[1], []string{match[2], "0"}
290 }
291
292 // GetModTime returns the last modification time for a given file
293 func GetModTime(path string) (time.Time, error) {
294         info, err := os.Stat(path)
295         if err != nil {
296                 return time.Now(), err
297         }
298         return info.ModTime(), nil
299 }
300
301 // EscapePath replaces every path separator in a given path with a %
302 func EscapePath(path string) string {
303         path = filepath.ToSlash(path)
304         return strings.Replace(path, "/", "%", -1)
305 }
306
307 // GetLeadingWhitespace returns the leading whitespace of the given byte array
308 func GetLeadingWhitespace(b []byte) []byte {
309         ws := []byte{}
310         for len(b) > 0 {
311                 r, size := utf8.DecodeRune(b)
312                 if r == ' ' || r == '\t' {
313                         ws = append(ws, byte(r))
314                 } else {
315                         break
316                 }
317
318                 b = b[size:]
319         }
320         return ws
321 }
322
323 // IntOpt turns a float64 setting to an int
324 func IntOpt(opt interface{}) int {
325         return int(opt.(float64))
326 }
327
328 // GetCharPosInLine gets the char position of a visual x y
329 // coordinate (this is necessary because tabs are 1 char but
330 // 4 visual spaces)
331 func GetCharPosInLine(b []byte, visualPos int, tabsize int) int {
332
333         // Scan rune by rune until we exceed the visual width that we are
334         // looking for. Then we can return the character position we have found
335         i := 0     // char pos
336         width := 0 // string visual width
337         for len(b) > 0 {
338                 r, size := utf8.DecodeRune(b)
339                 b = b[size:]
340
341                 switch r {
342                 case '\t':
343                         ts := tabsize - (width % tabsize)
344                         width += ts
345                 default:
346                         width += runewidth.RuneWidth(r)
347                 }
348
349                 if width >= visualPos {
350                         if width == visualPos {
351                                 i++
352                         }
353                         break
354                 }
355                 i++
356         }
357
358         return i
359 }
360
361 // ParseBool is almost exactly like strconv.ParseBool, except it also accepts 'on' and 'off'
362 // as 'true' and 'false' respectively
363 func ParseBool(str string) (bool, error) {
364         if str == "on" {
365                 return true, nil
366         }
367         if str == "off" {
368                 return false, nil
369         }
370         return strconv.ParseBool(str)
371 }
372
373 // Clamp clamps a value between min and max
374 func Clamp(val, min, max int) int {
375         if val < min {
376                 val = min
377         } else if val > max {
378                 val = max
379         }
380         return val
381 }