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