19 "github.com/blang/semver"
20 runewidth "github.com/mattn/go-runewidth"
24 // These variables should be set by the linker when compiling
26 // Version is the version number or commit hash
27 Version = "0.0.0-unknown"
28 // SemVersion is the Semantic version
29 SemVersion semver.Version
30 // CommitHash is the commit this version was built on
31 CommitHash = "Unknown"
32 // CompileDate is the date this binary was compiled on
33 CompileDate = "Unknown"
36 // FakeCursor is used to disable the terminal cursor and have micro
37 // draw its own (enabled for windows consoles where the cursor is slow)
40 // Stdout is a buffer that is written to stdout when micro closes
46 SemVersion, err = semver.Make(Version)
48 fmt.Println("Invalid version: ", Version, err)
51 _, wt := os.LookupEnv("WT_SESSION")
52 if runtime.GOOS == "windows" && !wt {
55 Stdout = new(bytes.Buffer)
58 // SliceEnd returns a byte slice where the index is a rune index
59 // Slices off the start of the slice
60 func SliceEnd(slc []byte, index int) []byte {
66 return slc[totalSize:]
69 _, _, size := DecodeCharacter(slc[totalSize:])
74 return slc[totalSize:]
77 // SliceEndStr is the same as SliceEnd but for strings
78 func SliceEndStr(str string, index int) string {
84 return str[totalSize:]
87 _, _, size := DecodeCharacterInString(str[totalSize:])
92 return str[totalSize:]
95 // SliceStart returns a byte slice where the index is a rune index
96 // Slices off the end of the slice
97 func SliceStart(slc []byte, index int) []byte {
101 for totalSize < len {
103 return slc[:totalSize]
106 _, _, size := DecodeCharacter(slc[totalSize:])
111 return slc[:totalSize]
114 // SliceStartStr is the same as SliceStart but for strings
115 func SliceStartStr(str string, index int) string {
119 for totalSize < len {
121 return str[:totalSize]
124 _, _, size := DecodeCharacterInString(str[totalSize:])
129 return str[:totalSize]
132 // SliceVisualEnd will take a byte slice and slice off the start
133 // up to a given visual index. If the index is in the middle of a
134 // rune the number of visual columns into the rune will be returned
135 // It will also return the char pos of the first character of the slice
136 func SliceVisualEnd(b []byte, n, tabsize int) ([]byte, int, int) {
140 r, _, size := DecodeCharacter(b)
145 ts := tabsize - (width % tabsize)
148 w = runewidth.RuneWidth(r)
151 return b, n - width, i
157 return b, n - width, i
160 // Abs is a simple absolute value function for ints
161 func Abs(n int) int {
168 // StringWidth returns the visual width of a byte array indexed from 0 to n (rune index)
169 // with a given tabsize
170 func StringWidth(b []byte, n, tabsize int) int {
177 r, _, size := DecodeCharacter(b)
182 ts := tabsize - (width % tabsize)
185 width += runewidth.RuneWidth(r)
197 // Min takes the min of two ints
198 func Min(a, b int) int {
205 // Max takes the max of two ints
206 func Max(a, b int) int {
213 // FSize gets the size of a file
214 func FSize(f *os.File) int64 {
219 // IsWordChar returns whether or not the string is a 'word character'
220 // Word characters are defined as numbers, letters, or '_'
221 func IsWordChar(r rune) bool {
222 return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_'
225 // Spaces returns a string with n spaces
226 func Spaces(n int) string {
227 return strings.Repeat(" ", n)
230 // IsSpaces checks if a given string is only spaces
231 func IsSpaces(str []byte) bool {
232 for _, c := range str {
241 // IsSpacesOrTabs checks if a given string contains only spaces and tabs
242 func IsSpacesOrTabs(str []byte) bool {
243 for _, c := range str {
244 if c != ' ' && c != '\t' {
252 // IsWhitespace returns true if the given rune is a space, tab, or newline
253 func IsWhitespace(c rune) bool {
254 return unicode.IsSpace(c)
257 // IsBytesWhitespace returns true if the given bytes are all whitespace
258 func IsBytesWhitespace(b []byte) bool {
259 for _, c := range b {
260 if !IsWhitespace(rune(c)) {
267 // RunePos returns the rune index of a given byte index
268 // Make sure the byte index is not between code points
269 func RunePos(b []byte, i int) int {
270 return CharacterCount(b[:i])
273 // MakeRelative will attempt to make a relative path between path and base
274 func MakeRelative(path, base string) (string, error) {
276 rel, err := filepath.Rel(base, path)
285 // ReplaceHome takes a path as input and replaces ~ at the start of the path with the user's
286 // home directory. Does nothing if the path does not start with '~'.
287 func ReplaceHome(path string) (string, error) {
288 if !strings.HasPrefix(path, "~") {
292 var userData *user.User
295 homeString := strings.Split(path, "/")[0]
296 if homeString == "~" {
297 userData, err = user.Current()
299 return "", errors.New("Could not find user: " + err.Error())
302 userData, err = user.Lookup(homeString[1:])
304 return "", errors.New("Could not find user: " + err.Error())
308 home := userData.HomeDir
310 return strings.Replace(path, homeString, home, 1), nil
313 // GetPathAndCursorPosition returns a filename without everything following a `:`
314 // This is used for opening files like util.go:10:5 to specify a line and column
315 // Special cases like Windows Absolute path (C:\myfile.txt:10:5) are handled correctly.
316 func GetPathAndCursorPosition(path string) (string, []string) {
317 re := regexp.MustCompile(`([\s\S]+?)(?::(\d+))(?::(\d+))?`)
318 match := re.FindStringSubmatch(path)
319 // no lines/columns were specified in the path, return just the path with no cursor location
322 } else if match[len(match)-1] != "" {
323 // if the last capture group match isn't empty then both line and column were provided
324 return match[1], match[2:]
326 // if it was empty, then only a line was provided, so default to column 0
327 return match[1], []string{match[2], "0"}
330 // GetModTime returns the last modification time for a given file
331 func GetModTime(path string) (time.Time, error) {
332 info, err := os.Stat(path)
334 return time.Now(), err
336 return info.ModTime(), nil
339 // EscapePath replaces every path separator in a given path with a %
340 func EscapePath(path string) string {
341 path = filepath.ToSlash(path)
342 if runtime.GOOS == "windows" {
343 // ':' is not valid in a path name on Windows but is ok on Unix
344 path = strings.ReplaceAll(path, ":", "%")
346 return strings.ReplaceAll(path, "/", "%")
349 // GetLeadingWhitespace returns the leading whitespace of the given byte array
350 func GetLeadingWhitespace(b []byte) []byte {
353 r, _, size := DecodeCharacter(b)
354 if r == ' ' || r == '\t' {
355 ws = append(ws, byte(r))
365 // IntOpt turns a float64 setting to an int
366 func IntOpt(opt interface{}) int {
367 return int(opt.(float64))
370 // GetCharPosInLine gets the char position of a visual x y
371 // coordinate (this is necessary because tabs are 1 char but
373 func GetCharPosInLine(b []byte, visualPos int, tabsize int) int {
374 // Scan rune by rune until we exceed the visual width that we are
375 // looking for. Then we can return the character position we have found
377 width := 0 // string visual width
379 r, _, size := DecodeCharacter(b)
384 ts := tabsize - (width % tabsize)
387 width += runewidth.RuneWidth(r)
390 if width >= visualPos {
391 if width == visualPos {
402 // ParseBool is almost exactly like strconv.ParseBool, except it also accepts 'on' and 'off'
403 // as 'true' and 'false' respectively
404 func ParseBool(str string) (bool, error) {
411 return strconv.ParseBool(str)
414 // Clamp clamps a value between min and max
415 func Clamp(val, min, max int) int {
418 } else if val > max {
424 // IsNonAlphaNumeric returns if the rune is not a number of letter or underscore.
425 func IsNonAlphaNumeric(c rune) bool {
426 return !unicode.IsLetter(c) && !unicode.IsNumber(c) && c != '_'
429 // IsAutocomplete returns whether a character should begin an autocompletion.
430 func IsAutocomplete(c rune) bool {
431 return c == '.' || !IsNonAlphaNumeric(c)
434 // ParseSpecial replaces escaped ts with '\t'.
435 func ParseSpecial(s string) string {
436 return strings.ReplaceAll(s, "\\t", "\t")
439 // String converts a byte array to a string (for lua plugins)
440 func String(s []byte) string {
444 // Unzip unzips a file to given folder
445 func Unzip(src, dest string) error {
446 r, err := zip.OpenReader(src)
452 os.MkdirAll(dest, 0755)
454 // Closure to address file descriptors issue with all the deferred .Close() methods
455 extractAndWriteFile := func(f *zip.File) error {
462 path := filepath.Join(dest, f.Name)
464 // Check for ZipSlip (Directory traversal)
465 if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) {
466 return fmt.Errorf("illegal file path: %s", path)
469 if f.FileInfo().IsDir() {
470 os.MkdirAll(path, f.Mode())
472 os.MkdirAll(filepath.Dir(path), f.Mode())
473 f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
479 _, err = io.Copy(f, rc)
487 for _, f := range r.File {
488 err := extractAndWriteFile(f)