From efe343b37c56524eb398d5b4abe5964ec0fc709b Mon Sep 17 00:00:00 2001 From: Dimitar Borislavov Tasev Date: Sun, 3 Jun 2018 22:13:03 +0100 Subject: [PATCH] Allows opening files using full path on Windows (#1126) * Now can open Windows full-path from command line arg Example that now works: micro.exe D:\myfile.txt * Now correctly retrieves the path from the input path string. Except for single-letter filenames * Fixed line/cols, need to make the code prettier * Fixed path matching with regex by @Pariador * Fixed not stripping the line/col args from file path * Added tests for ParseCursorLocation --- cmd/micro/buffer.go | 51 +++------ cmd/micro/util.go | 43 +++++++- cmd/micro/util_test.go | 236 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 290 insertions(+), 40 deletions(-) diff --git a/cmd/micro/buffer.go b/cmd/micro/buffer.go index a9de2a7a..8b95109e 100644 --- a/cmd/micro/buffer.go +++ b/cmd/micro/buffer.go @@ -77,12 +77,12 @@ type SerializedBuffer struct { ModTime time.Time } -// NewBufferFromFile opens a new buffer using the given filepath +// NewBufferFromFile opens a new buffer using the given path // It will also automatically handle `~`, and line/column with filename:l:c -// It will return an empty buffer if the filepath does not exist +// It will return an empty buffer if the path does not exist // and an error if the file is a directory func NewBufferFromFile(path string) (*Buffer, error) { - filename := GetPath(path) + filename, cursorPosition := GetPathAndCursorPosition(path) filename = ReplaceHome(filename) file, err := os.Open(filename) fileInfo, _ := os.Stat(filename) @@ -96,41 +96,22 @@ func NewBufferFromFile(path string) (*Buffer, error) { var buf *Buffer if err != nil { // File does not exist -- create an empty buffer with that name - buf = NewBufferFromString("", path) + buf = NewBufferFromString("", filename) } else { - buf = NewBuffer(file, FSize(file), path) + buf = NewBuffer(file, FSize(file), filename, cursorPosition) } return buf, nil } -// NewBufferFromString creates a new buffer containing the given -// string +// NewBufferFromString creates a new buffer containing the given string func NewBufferFromString(text, path string) *Buffer { - return NewBuffer(strings.NewReader(text), int64(len(text)), path) + return NewBuffer(strings.NewReader(text), int64(len(text)), path, []string{"0", "0"}) } // NewBuffer creates a new buffer from a given reader with a given path -func NewBuffer(reader io.Reader, size int64, path string) *Buffer { - startpos := Loc{0, 0} - startposErr := true - if strings.Contains(path, ":") { - var err error - split := strings.Split(path, ":") - path = split[0] - startpos.Y, err = strconv.Atoi(split[1]) - if err != nil { - messenger.Error("Error opening file: ", err) - } else { - startposErr = false - if len(split) > 2 { - startpos.X, err = strconv.Atoi(split[2]) - if err != nil { - messenger.Error("Error opening file: ", err) - } - } - } - } +func NewBuffer(reader io.Reader, size int64, path string, cursorPosition []string) *Buffer { + cursorLocation, cursorLocationError := ParseCursorLocation(cursorPosition) if path != "" { for _, tab := range tabs { @@ -178,15 +159,15 @@ func NewBuffer(reader io.Reader, size int64, path string) *Buffer { // Put the cursor at the first spot cursorStartX := 0 cursorStartY := 0 - // If -startpos LINE,COL was passed, use start position LINE,COL - if len(*flagStartPos) > 0 || !startposErr { + // If -cursorLocation LINE,COL was passed, use start position LINE,COL + if len(*flagStartPos) > 0 || cursorLocationError == nil { positions := strings.Split(*flagStartPos, ",") - if len(positions) == 2 || !startposErr { + if len(positions) == 2 || cursorLocationError == nil { var lineNum, colNum int var errPos1, errPos2 error - if !startposErr { - lineNum = startpos.Y - colNum = startpos.X + if cursorLocationError == nil { + lineNum = cursorLocation.Y + colNum = cursorLocation.X } else { lineNum, errPos1 = strconv.Atoi(positions[0]) colNum, errPos2 = strconv.Atoi(positions[1]) @@ -219,7 +200,7 @@ func NewBuffer(reader io.Reader, size int64, path string) *Buffer { InitLocalSettings(b) - if startposErr && len(*flagStartPos) == 0 && (b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool)) { + if cursorLocationError != nil && len(*flagStartPos) == 0 && (b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool)) { // If either savecursor or saveundo is turned on, we need to load the serialized information // from ~/.config/micro/buffers file, err := os.Open(configDir + "/buffers/" + EscapePath(b.AbsPath)) diff --git a/cmd/micro/util.go b/cmd/micro/util.go index eb8a1977..09d116a2 100644 --- a/cmd/micro/util.go +++ b/cmd/micro/util.go @@ -12,6 +12,7 @@ import ( "unicode/utf8" "github.com/mattn/go-runewidth" + "regexp" ) // Util.go is a collection of utility functions that are used throughout @@ -356,11 +357,43 @@ func ReplaceHome(path string) string { return strings.Replace(path, homeString, home, 1) } -// GetPath returns a filename without everything following a `:` +// GetPathAndCursorPosition returns a filename without everything following a `:` // This is used for opening files like util.go:10:5 to specify a line and column -func GetPath(path string) string { - if strings.Contains(path, ":") { - path = strings.Split(path, ":")[0] +// Special cases like Windows Absolute path (C:\myfile.txt:10:5) are handled correctly. +func GetPathAndCursorPosition(path string) (string, []string) { + re := regexp.MustCompile(`([\s\S]+?)(?::(\d+))(?::(\d+))?`) + match := re.FindStringSubmatch(path) + // no lines/columns were specified in the path, return just the path with cursor at 0, 0 + if len(match) == 0 { + return path, []string{"0", "0"} + } else if match[len(match)-1] != "" { + // if the last capture group match isn't empty then both line and column were provided + return match[1], match[2:] } - return path + // if it was empty, then only a line was provided, so default to column 0 + return match[1], []string{match[2], "0"} +} + +func ParseCursorLocation(cursorPositions []string) (Loc, error) { + startpos := Loc{0, 0} + var err error + + // if no positions are available exit early + if len(cursorPositions) == 0 { + return startpos, err + } + + startpos.Y, err = strconv.Atoi(cursorPositions[0]) + if err != nil { + messenger.Error("Error parsing cursor position: ", err) + } else { + if len(cursorPositions) > 1 { + startpos.X, err = strconv.Atoi(cursorPositions[1]) + if err != nil { + messenger.Error("Error parsing cursor position: ", err) + } + } + } + + return startpos, err } diff --git a/cmd/micro/util_test.go b/cmd/micro/util_test.go index 51a24e02..e62fee5f 100644 --- a/cmd/micro/util_test.go +++ b/cmd/micro/util_test.go @@ -103,3 +103,239 @@ func TestWidthOfLargeRunes(t *testing.T) { t.Error("WidthOfLargeRunes 5 Failed. Got", w) } } + +func assertEqual(t *testing.T, expected interface{}, result interface{}) { + if expected != result { + t.Fatalf("Expected: %d != Got: %d", expected, result) + } +} + +func assertTrue(t *testing.T, condition bool) { + if !condition { + t.Fatalf("Condition was not true. Got false") + } +} + +func TestGetPathRelativeWithDot(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("./myfile:10:5") + + assertEqual(t, path, "./myfile") + assertEqual(t, "10", cursorPosition[0]) + assertEqual(t, "5", cursorPosition[1]) +} +func TestGetPathRelativeWithDotWindows(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition(".\\myfile:10:5") + + assertEqual(t, path, ".\\myfile") + assertEqual(t, "10", cursorPosition[0]) + assertEqual(t, cursorPosition[1], "5") +} +func TestGetPathRelativeNoDot(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("myfile:10:5") + + assertEqual(t, path, "myfile") + assertEqual(t, "10", cursorPosition[0]) + + assertEqual(t, cursorPosition[1], "5") +} +func TestGetPathAbsoluteWindows(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("C:\\myfile:10:5") + + assertEqual(t, path, "C:\\myfile") + assertEqual(t, "10", cursorPosition[0]) + + assertEqual(t, cursorPosition[1], "5") + + path, cursorPosition = GetPathAndCursorPosition("C:/myfile:10:5") + + assertEqual(t, path, "C:/myfile") + assertEqual(t, "10", cursorPosition[0]) + + assertEqual(t, cursorPosition[1], "5") +} +func TestGetPathAbsoluteUnix(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("/home/user/myfile:10:5") + + assertEqual(t, path, "/home/user/myfile") + assertEqual(t, "10", cursorPosition[0]) + + assertEqual(t, cursorPosition[1], "5") +} + +func TestGetPathRelativeWithDotWithoutLineAndColumn(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("./myfile") + + assertEqual(t, path, "./myfile") + assertEqual(t, "0", cursorPosition[0]) + + assertEqual(t, "0", cursorPosition[1]) +} +func TestGetPathRelativeWithDotWindowsWithoutLineAndColumn(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition(".\\myfile") + + assertEqual(t, path, ".\\myfile") + assertEqual(t, "0", cursorPosition[0]) + + assertEqual(t, "0", cursorPosition[1]) +} +func TestGetPathRelativeNoDotWithoutLineAndColumn(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("myfile") + + assertEqual(t, path, "myfile") + assertEqual(t, "0", cursorPosition[0]) + + assertEqual(t, "0", cursorPosition[1]) +} +func TestGetPathAbsoluteWindowsWithoutLineAndColumn(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("C:\\myfile") + + assertEqual(t, path, "C:\\myfile") + assertEqual(t, "0", cursorPosition[0]) + + assertEqual(t, "0", cursorPosition[1]) + + path, cursorPosition = GetPathAndCursorPosition("C:/myfile") + + assertEqual(t, path, "C:/myfile") + assertEqual(t, "0", cursorPosition[0]) + + assertEqual(t, "0", cursorPosition[1]) +} +func TestGetPathAbsoluteUnixWithoutLineAndColumn(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("/home/user/myfile") + + assertEqual(t, path, "/home/user/myfile") + assertEqual(t, "0", cursorPosition[0]) + + assertEqual(t, "0", cursorPosition[1]) +} +func TestGetPathSingleLetterFileRelativePath(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("a:5:6") + + assertEqual(t, path, "a") + assertEqual(t, "5", cursorPosition[0]) + assertEqual(t, "6", cursorPosition[1]) +} +func TestGetPathSingleLetterFileAbsolutePathWindows(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("C:\\a:5:6") + + assertEqual(t, path, "C:\\a") + assertEqual(t, "5", cursorPosition[0]) + assertEqual(t, "6", cursorPosition[1]) + + path, cursorPosition = GetPathAndCursorPosition("C:/a:5:6") + + assertEqual(t, path, "C:/a") + assertEqual(t, "5", cursorPosition[0]) + assertEqual(t, "6", cursorPosition[1]) +} +func TestGetPathSingleLetterFileAbsolutePathUnix(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("/home/user/a:5:6") + + assertEqual(t, path, "/home/user/a") + assertEqual(t, "5", cursorPosition[0]) + assertEqual(t, "6", cursorPosition[1]) +} +func TestGetPathSingleLetterFileAbsolutePathWindowsWithoutLineAndColumn(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("C:\\a") + + assertEqual(t, path, "C:\\a") + assertEqual(t, "0", cursorPosition[0]) + + assertEqual(t, "0", cursorPosition[1]) + + path, cursorPosition = GetPathAndCursorPosition("C:/a") + + assertEqual(t, path, "C:/a") + assertEqual(t, "0", cursorPosition[0]) + assertEqual(t, "0", cursorPosition[1]) +} +func TestGetPathSingleLetterFileAbsolutePathUnixWithoutLineAndColumn(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("/home/user/a") + + assertEqual(t, path, "/home/user/a") + assertEqual(t, "0", cursorPosition[0]) + assertEqual(t, "0", cursorPosition[1]) +} + +// TODO test for only line without a column +func TestGetPathRelativeWithDotOnlyLine(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("./myfile:10") + + assertEqual(t, path, "./myfile") + assertEqual(t, "10", cursorPosition[0]) + assertEqual(t, "0", cursorPosition[1]) +} +func TestGetPathRelativeWithDotWindowsOnlyLine(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition(".\\myfile:10") + + assertEqual(t, path, ".\\myfile") + assertEqual(t, "10", cursorPosition[0]) + assertEqual(t, "0", cursorPosition[1]) +} +func TestGetPathRelativeNoDotOnlyLine(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("myfile:10") + + assertEqual(t, path, "myfile") + assertEqual(t, "10", cursorPosition[0]) + assertEqual(t, "0", cursorPosition[1]) +} +func TestGetPathAbsoluteWindowsOnlyLine(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("C:\\myfile:10") + + assertEqual(t, path, "C:\\myfile") + assertEqual(t, "10", cursorPosition[0]) + assertEqual(t, "0", cursorPosition[1]) + + path, cursorPosition = GetPathAndCursorPosition("C:/myfile:10") + + assertEqual(t, path, "C:/myfile") + assertEqual(t, "10", cursorPosition[0]) + assertEqual(t, "0", cursorPosition[1]) +} +func TestGetPathAbsoluteUnixOnlyLine(t *testing.T) { + path, cursorPosition := GetPathAndCursorPosition("/home/user/myfile:10") + + assertEqual(t, path, "/home/user/myfile") + assertEqual(t, "10", cursorPosition[0]) + assertEqual(t, "0", cursorPosition[1]) +} +func TestParseCursorLocationOneArg(t *testing.T) { + location, err := ParseCursorLocation([]string{"3"}) + + assertEqual(t, 3, location.Y) + assertEqual(t, 0, location.X) + assertEqual(t, nil, err) +} +func TestParseCursorLocationTwoArgs(t *testing.T) { + location, err := ParseCursorLocation([]string{"3", "15"}) + + assertEqual(t, 3, location.Y) + assertEqual(t, 15, location.X) + assertEqual(t, nil, err) +} +func TestParseCursorLocationNoArgs(t *testing.T) { + location, err := ParseCursorLocation([]string{}) + // the expected result is the start position - 0, 0 + assertEqual(t, 0, location.Y) + assertEqual(t, 0, location.X) + assertEqual(t, nil, err) +} +func TestParseCursorLocationFirstArgNotValidNumber(t *testing.T) { + // the messenger is necessary as ParseCursorLocation + // puts a message in it on error + messenger = new(Messenger) + _, err := ParseCursorLocation([]string{"apples", "1"}) + // the expected result is the start position - 0, 0 + assertTrue(t, messenger.hasMessage) + assertTrue(t, err != nil) +} +func TestParseCursorLocationSecondArgNotValidNumber(t *testing.T) { + // the messenger is necessary as ParseCursorLocation + // puts a message in it on error + messenger = new(Messenger) + _, err := ParseCursorLocation([]string{"1", "apples"}) + // the expected result is the start position - 0, 0 + assertTrue(t, messenger.hasMessage) + assertTrue(t, err != nil) +}