Skip to content

Commit

Permalink
Add menu command to move or rename document
Browse files Browse the repository at this point in the history
  • Loading branch information
wedaly committed Dec 24, 2023
1 parent 6e7022e commit 62637c7
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ Menu Commands
| quit | q |
| force quit | q! |
| new document | |
| move or rename document | |
| save document | s, w |
| save document and quit | sq, wq, x |
| force save document | s!, w! |
Expand Down
9 changes: 9 additions & 0 deletions input/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,15 @@ func ShowNewDocumentTextField(s *state.EditorState) {
})
}

func ShowMoveOrRenameDocumentTextField(s *state.EditorState) {
state.AbortIfUnsavedChanges(s, state.DefaultUnsavedChangesAbortMsg, func(s *state.EditorState) {
state.ShowTextField(s,
"Move/rename document file path:",
state.RenameDocument,
file.AutocompleteDirectory)
})
}

func AppendRuneToTextField(r rune) Action {
return func(s *state.EditorState) {
state.AppendRuneToTextField(s, r)
Expand Down
4 changes: 4 additions & 0 deletions input/menu.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ func menuItems(ctx Context) []menu.Item {
Name: "new document",
Action: ShowNewDocumentTextField,
},
{
Name: "move or rename document",
Action: ShowMoveOrRenameDocumentTextField,
},
{
Name: "save document",
Aliases: []string{"s", "w"},
Expand Down
31 changes: 31 additions & 0 deletions state/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,37 @@ func NewDocument(state *EditorState, path string) error {
return nil
}

// RenameDocument moves a document to a different file path.
// Returns an error if the file already exists or the directory doesn't exist.
func RenameDocument(state *EditorState, newPath string) error {
// Validate that we can create a file at the new path.
// This isn't 100% reliable, since some other process could create a file
// at the target path between this check and the rename below, but it at least
// reduces the risk of overwriting another file.
err := file.ValidateCreate(newPath)
if err != nil {
return err
}

// Move the file on disk. Ignore fs.ErrNotExist which can happen if
// the file was never saved to the old path.
//
// The rename won't trigger a reload of the old document because:
// 1. file.Watcher's check loop ignores fs.ErrNotExist.
// 2. LoadDocument below starts a new file watcher, so the main event loop
// won't check the old file.Watcher's changed channel anyway.
oldPath := state.fileWatcher.Path()
err = os.Rename(oldPath, newPath)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return err
}

// Load the document at the new path, retaining the original cursor position.
cursorPos := state.documentBuffer.cursor.position
LoadDocument(state, newPath, false, func(_ LocatorParams) uint64 { return cursorPos })
return nil
}

// LoadDocument loads a file into the editor.
func LoadDocument(state *EditorState, path string, requireExists bool, cursorLoc Locator) {
timelineState := currentTimelineState(state)
Expand Down
46 changes: 46 additions & 0 deletions state/document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,3 +554,49 @@ func TestNewDocumentFileAlreadyExists(t *testing.T) {
err := NewDocument(state, path)
assert.ErrorContains(t, err, "File already exists")
}

func TestRenameDocument(t *testing.T) {
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "before.txt")
_, err := os.Create(path)
require.NoError(t, err)

state := NewEditorState(100, 100, nil, nil)
defer state.fileWatcher.Stop()
LoadDocument(state, path, true, startOfDocLocator)

newPath := filepath.Join(filepath.Dir(path), "renamed.txt")
err = RenameDocument(state, newPath)
require.NoError(t, err)
assert.Equal(t, newPath, state.FileWatcher().Path())
}

func TestRenameDocumentSrcFileNotSaved(t *testing.T) {
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "test.txt")

state := NewEditorState(100, 100, nil, nil)
defer state.fileWatcher.Stop()
LoadDocument(state, path, true, startOfDocLocator)

newPath := filepath.Join(filepath.Dir(path), "renamed.txt")
err := RenameDocument(state, newPath)
require.NoError(t, err)
assert.Equal(t, newPath, state.FileWatcher().Path())
}

func TestRenameDocumentDestFileAlreadyExists(t *testing.T) {
tmpDir := t.TempDir()
path := filepath.Join(tmpDir, "test.txt")

state := NewEditorState(100, 100, nil, nil)
defer state.fileWatcher.Stop()
LoadDocument(state, path, true, startOfDocLocator)

newPath := filepath.Join(filepath.Dir(path), "renamed.txt")
_, err := os.Create(newPath)
require.NoError(t, err)

err = RenameDocument(state, newPath)
assert.ErrorContains(t, err, "File already exists")
}

0 comments on commit 62637c7

Please sign in to comment.