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