14 "github.com/go-errors/errors"
15 "github.com/mattn/go-runewidth"
19 // Util.go is a collection of utility functions that are used throughout
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)
28 // Convert byte array to rune array
29 func toRunes(b []byte) []rune {
30 runes := make([]rune, 0, utf8.RuneCount(b))
33 r, size := utf8.DecodeRune(b)
34 runes = append(runes, r)
42 func sliceStart(slc []byte, index int) []byte {
48 return slc[totalSize:]
51 _, size := utf8.DecodeRune(slc[totalSize:])
56 return slc[totalSize:]
59 func sliceEnd(slc []byte, index int) []byte {
65 return slc[:totalSize]
68 _, size := utf8.DecodeRune(slc[totalSize:])
73 return slc[:totalSize]
76 // NumOccurrences counts the number of occurrences of a byte in a string
77 func NumOccurrences(s string, c byte) int {
79 for i := 0; i < len(s); i++ {
87 // Spaces returns a string with n spaces
88 func Spaces(n int) string {
89 return strings.Repeat(" ", n)
92 // Min takes the min of two ints
93 func Min(a, b int) int {
100 // Max takes the max of two ints
101 func Max(a, b int) int {
108 // FSize gets the size of a file
109 func FSize(f *os.File) int64 {
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 {
124 return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_')
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'
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) {
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 {
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:])
157 // MakeRelative will attempt to make a relative path between path and base
158 func MakeRelative(path, base string) (string, error) {
160 rel, err := filepath.Rel(base, path)
169 // GetLeadingWhitespace returns the leading whitespace of the given string
170 func GetLeadingWhitespace(str string) string {
172 for _, c := range str {
173 if c == ' ' || c == '\t' {
182 // IsSpaces checks if a given string is only spaces
183 func IsSpaces(str []byte) bool {
184 for _, c := range str {
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' {
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) {
213 return strconv.ParseBool(str)
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)
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)
227 return time.Now(), false
229 return info.ModTime(), true
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)
236 for _, ch := range str {
239 ts := tabsize - (lineIdx % tabsize)
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 {
256 for _, ch := range str {
259 w = tabsize - (lineIdx % tabsize)
261 w = runewidth.RuneWidth(ch)
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])
281 func lcs(a, b string) string {
286 for i, r := range arunes {
287 if i >= len(brunes) {
299 // CommonSubstring gets a common substring among the inputs
300 func CommonSubstring(arr ...string) string {
303 for _, str := range arr[1:] {
304 commonStr = lcs(commonStr, str)
310 // Abs is a simple absolute value function for ints
311 func Abs(n int) int {
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()
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).")
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, "~") {
335 var userData *user.User
338 homeString := strings.Split(path, "/")[0]
339 if homeString == "~" {
340 userData, err = user.Current()
342 messenger.Error("Could not find user: ", err)
345 userData, err = user.Lookup(homeString[1:])
347 if messenger != nil {
348 messenger.Error("Could not find user: ", err)
350 TermMessage("Could not find user: ", err)
356 home := userData.HomeDir
358 return strings.Replace(path, homeString, home, 1)
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
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:]
374 // if it was empty, then only a line was provided, so default to column 0
375 return match[1], []string{match[2], "0"}
378 func ParseCursorLocation(cursorPositions []string) (Loc, error) {
379 startpos := Loc{0, 0}
382 // if no positions are available exit early
383 if cursorPositions == nil {
384 return startpos, errors.New("No cursor positions were provided.")
387 startpos.Y, err = strconv.Atoi(cursorPositions[0])
389 messenger.Error("Error parsing cursor position: ", err)
391 if len(cursorPositions) > 1 {
392 startpos.X, err = strconv.Atoi(cursorPositions[1])
394 messenger.Error("Error parsing cursor position: ", err)