]> git.lizzy.rs Git - micro.git/blob - cmd/micro/util.go
Merge pull request #1200 from Calinou/add-systemd-timer-section
[micro.git] / cmd / micro / util.go
1 package main
2
3 import (
4         "os"
5         "os/user"
6         "path/filepath"
7         "reflect"
8         "runtime"
9         "strconv"
10         "strings"
11         "time"
12         "unicode/utf8"
13
14         "github.com/go-errors/errors"
15         "github.com/mattn/go-runewidth"
16         "regexp"
17 )
18
19 // Util.go is a collection of utility functions that are used throughout
20 // the program
21
22 // Count returns the length of a string in runes
23 // This is exactly equivalent to utf8.RuneCountInString(), just less characters
24 func Count(s string) int {
25         return utf8.RuneCountInString(s)
26 }
27
28 // Convert byte array to rune array
29 func toRunes(b []byte) []rune {
30         runes := make([]rune, 0, utf8.RuneCount(b))
31
32         for len(b) > 0 {
33                 r, size := utf8.DecodeRune(b)
34                 runes = append(runes, r)
35
36                 b = b[size:]
37         }
38
39         return runes
40 }
41
42 func sliceStart(slc []byte, index int) []byte {
43         len := len(slc)
44         i := 0
45         totalSize := 0
46         for totalSize < len {
47                 if i >= index {
48                         return slc[totalSize:]
49                 }
50
51                 _, size := utf8.DecodeRune(slc[totalSize:])
52                 totalSize += size
53                 i++
54         }
55
56         return slc[totalSize:]
57 }
58
59 func sliceEnd(slc []byte, index int) []byte {
60         len := len(slc)
61         i := 0
62         totalSize := 0
63         for totalSize < len {
64                 if i >= index {
65                         return slc[:totalSize]
66                 }
67
68                 _, size := utf8.DecodeRune(slc[totalSize:])
69                 totalSize += size
70                 i++
71         }
72
73         return slc[:totalSize]
74 }
75
76 // NumOccurrences counts the number of occurrences of a byte in a string
77 func NumOccurrences(s string, c byte) int {
78         var n int
79         for i := 0; i < len(s); i++ {
80                 if s[i] == c {
81                         n++
82                 }
83         }
84         return n
85 }
86
87 // Spaces returns a string with n spaces
88 func Spaces(n int) string {
89         return strings.Repeat(" ", n)
90 }
91
92 // Min takes the min of two ints
93 func Min(a, b int) int {
94         if a > b {
95                 return b
96         }
97         return a
98 }
99
100 // Max takes the max of two ints
101 func Max(a, b int) int {
102         if a > b {
103                 return a
104         }
105         return b
106 }
107
108 // FSize gets the size of a file
109 func FSize(f *os.File) int64 {
110         fi, _ := f.Stat()
111         // get the size
112         return fi.Size()
113 }
114
115 // IsWordChar returns whether or not the string is a 'word character'
116 // If it is a unicode character, then it does not match
117 // Word characters are defined as [A-Za-z0-9_]
118 func IsWordChar(str string) bool {
119         if len(str) > 1 {
120                 // Unicode
121                 return true
122         }
123         c := str[0]
124         return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_')
125 }
126
127 // IsWhitespace returns true if the given rune is a space, tab, or newline
128 func IsWhitespace(c rune) bool {
129         return c == ' ' || c == '\t' || c == '\n'
130 }
131
132 // IsStrWhitespace returns true if the given string is all whitespace
133 func IsStrWhitespace(str string) bool {
134         for _, c := range str {
135                 if !IsWhitespace(c) {
136                         return false
137                 }
138         }
139         return true
140 }
141
142 // Contains returns whether or not a string array contains a given string
143 func Contains(list []string, a string) bool {
144         for _, b := range list {
145                 if b == a {
146                         return true
147                 }
148         }
149         return false
150 }
151
152 // Insert makes a simple insert into a string at the given position
153 func Insert(str string, pos int, value string) string {
154         return string([]rune(str)[:pos]) + value + string([]rune(str)[pos:])
155 }
156
157 // MakeRelative will attempt to make a relative path between path and base
158 func MakeRelative(path, base string) (string, error) {
159         if len(path) > 0 {
160                 rel, err := filepath.Rel(base, path)
161                 if err != nil {
162                         return path, err
163                 }
164                 return rel, nil
165         }
166         return path, nil
167 }
168
169 // GetLeadingWhitespace returns the leading whitespace of the given string
170 func GetLeadingWhitespace(str string) string {
171         ws := ""
172         for _, c := range str {
173                 if c == ' ' || c == '\t' {
174                         ws += string(c)
175                 } else {
176                         break
177                 }
178         }
179         return ws
180 }
181
182 // IsSpaces checks if a given string is only spaces
183 func IsSpaces(str []byte) bool {
184         for _, c := range str {
185                 if c != ' ' {
186                         return false
187                 }
188         }
189
190         return true
191 }
192
193 // IsSpacesOrTabs checks if a given string contains only spaces and tabs
194 func IsSpacesOrTabs(str string) bool {
195         for _, c := range str {
196                 if c != ' ' && c != '\t' {
197                         return false
198                 }
199         }
200
201         return true
202 }
203
204 // ParseBool is almost exactly like strconv.ParseBool, except it also accepts 'on' and 'off'
205 // as 'true' and 'false' respectively
206 func ParseBool(str string) (bool, error) {
207         if str == "on" {
208                 return true, nil
209         }
210         if str == "off" {
211                 return false, nil
212         }
213         return strconv.ParseBool(str)
214 }
215
216 // EscapePath replaces every path separator in a given path with a %
217 func EscapePath(path string) string {
218         path = filepath.ToSlash(path)
219         return strings.Replace(path, "/", "%", -1)
220 }
221
222 // GetModTime returns the last modification time for a given file
223 // It also returns a boolean if there was a problem accessing the file
224 func GetModTime(path string) (time.Time, bool) {
225         info, err := os.Stat(path)
226         if err != nil {
227                 return time.Now(), false
228         }
229         return info.ModTime(), true
230 }
231
232 // StringWidth returns the width of a string where tabs count as `tabsize` width
233 func StringWidth(str string, tabsize int) int {
234         sw := runewidth.StringWidth(str)
235         lineIdx := 0
236         for _, ch := range str {
237                 switch ch {
238                 case '\t':
239                         ts := tabsize - (lineIdx % tabsize)
240                         sw += ts
241                         lineIdx += ts
242                 case '\n':
243                         lineIdx = 0
244                 default:
245                         lineIdx++
246                 }
247         }
248         return sw
249 }
250
251 // WidthOfLargeRunes searches all the runes in a string and counts up all the widths of runes
252 // that have a width larger than 1 (this also counts tabs as `tabsize` width)
253 func WidthOfLargeRunes(str string, tabsize int) int {
254         count := 0
255         lineIdx := 0
256         for _, ch := range str {
257                 var w int
258                 if ch == '\t' {
259                         w = tabsize - (lineIdx % tabsize)
260                 } else {
261                         w = runewidth.RuneWidth(ch)
262                 }
263                 if w > 1 {
264                         count += (w - 1)
265                 }
266                 if ch == '\n' {
267                         lineIdx = 0
268                 } else {
269                         lineIdx += w
270                 }
271         }
272         return count
273 }
274
275 // RunePos returns the rune index of a given byte index
276 // This could cause problems if the byte index is between code points
277 func runePos(p int, str string) int {
278         return utf8.RuneCountInString(str[:p])
279 }
280
281 func lcs(a, b string) string {
282         arunes := []rune(a)
283         brunes := []rune(b)
284
285         lcs := ""
286         for i, r := range arunes {
287                 if i >= len(brunes) {
288                         break
289                 }
290                 if r == brunes[i] {
291                         lcs += string(r)
292                 } else {
293                         break
294                 }
295         }
296         return lcs
297 }
298
299 // CommonSubstring gets a common substring among the inputs
300 func CommonSubstring(arr ...string) string {
301         commonStr := arr[0]
302
303         for _, str := range arr[1:] {
304                 commonStr = lcs(commonStr, str)
305         }
306
307         return commonStr
308 }
309
310 // Abs is a simple absolute value function for ints
311 func Abs(n int) int {
312         if n < 0 {
313                 return -n
314         }
315         return n
316 }
317
318 // FuncName returns the full name of a given function object
319 func FuncName(i interface{}) string {
320         return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
321 }
322
323 // ShortFuncName returns the name only of a given function object
324 func ShortFuncName(i interface{}) string {
325         return strings.TrimPrefix(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name(), "main.(*View).")
326 }
327
328 // ReplaceHome takes a path as input and replaces ~ at the start of the path with the user's
329 // home directory. Does nothing if the path does not start with '~'.
330 func ReplaceHome(path string) string {
331         if !strings.HasPrefix(path, "~") {
332                 return path
333         }
334
335         var userData *user.User
336         var err error
337
338         homeString := strings.Split(path, "/")[0]
339         if homeString == "~" {
340                 userData, err = user.Current()
341                 if err != nil {
342                         messenger.Error("Could not find user: ", err)
343                 }
344         } else {
345                 userData, err = user.Lookup(homeString[1:])
346                 if err != nil {
347                         if messenger != nil {
348                                 messenger.Error("Could not find user: ", err)
349                         } else {
350                                 TermMessage("Could not find user: ", err)
351                         }
352                         return ""
353                 }
354         }
355
356         home := userData.HomeDir
357
358         return strings.Replace(path, homeString, home, 1)
359 }
360
361 // GetPathAndCursorPosition returns a filename without everything following a `:`
362 // This is used for opening files like util.go:10:5 to specify a line and column
363 // Special cases like Windows Absolute path (C:\myfile.txt:10:5) are handled correctly.
364 func GetPathAndCursorPosition(path string) (string, []string) {
365         re := regexp.MustCompile(`([\s\S]+?)(?::(\d+))(?::(\d+))?`)
366         match := re.FindStringSubmatch(path)
367         // no lines/columns were specified in the path, return just the path with no cursor location
368         if len(match) == 0 {
369                 return path, nil
370         } else if match[len(match)-1] != "" {
371                 // if the last capture group match isn't empty then both line and column were provided
372                 return match[1], match[2:]
373         }
374         // if it was empty, then only a line was provided, so default to column 0
375         return match[1], []string{match[2], "0"}
376 }
377
378 func ParseCursorLocation(cursorPositions []string) (Loc, error) {
379         startpos := Loc{0, 0}
380         var err error
381
382         // if no positions are available exit early
383         if cursorPositions == nil {
384                 return startpos, errors.New("No cursor positions were provided.")
385         }
386
387         startpos.Y, err = strconv.Atoi(cursorPositions[0])
388         if err != nil {
389                 messenger.Error("Error parsing cursor position: ", err)
390         } else {
391                 if len(cursorPositions) > 1 {
392                         startpos.X, err = strconv.Atoi(cursorPositions[1])
393                         if err != nil {
394                                 messenger.Error("Error parsing cursor position: ", err)
395                         }
396                 }
397         }
398
399         return startpos, err
400 }