diff --git a/internal/buffer/backup.go b/internal/buffer/backup.go index e9b91bcb..6f6978bd 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -2,19 +2,51 @@ package buffer import ( "io" + "log" + "os" + "time" "github.com/zyedidia/micro/internal/config" "github.com/zyedidia/micro/internal/util" "golang.org/x/text/encoding" ) +const backupMsg = `A backup was detected for this file. This likely means that micro +crashed while editing this file, or another instance of micro is currently +editing this file. + +The backup was created at %s. + +* 'recover' will apply the backup as unsaved changes to the current buffer. + When the buffer is closed, the backup will be removed. +* 'ignore' will ignore the backup, discarding its changes. The backup file + will be removed. + +Options: [r]ecover, [i]gnore: ` + // Backup saves the current buffer to ConfigDir/backups func (b *Buffer) Backup() error { if !b.Settings["backup"].(bool) { return nil } - name := config.ConfigDir + "/backups" + util.EscapePath(b.AbsPath) + sub := time.Now().Sub(b.lastbackup) + if sub < time.Duration(backup_time)*time.Millisecond { + log.Println("Backup event but not enough time has passed", sub) + return nil + } + + b.lastbackup = time.Now() + + backupdir := config.ConfigDir + "/backups/" + if _, err := os.Stat(backupdir); os.IsNotExist(err) { + os.Mkdir(backupdir, os.ModePerm) + log.Println("Creating backup dir") + } + + name := backupdir + util.EscapePath(b.AbsPath) + + log.Println("Backing up to", name) err := overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) { if len(b.lines) == 0 { @@ -43,6 +75,15 @@ func (b *Buffer) Backup() error { return err } +// RemoveBackup removes any backup file associated with this buffer +func (b *Buffer) RemoveBackup() { + if !b.Settings["backup"].(bool) { + return + } + f := config.ConfigDir + "/backups/" + util.EscapePath(b.AbsPath) + os.Remove(f) +} + // ApplyBackup applies the corresponding backup file to this buffer (if one exists) func (b *Buffer) ApplyBackup() error { return nil diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index 44f0e1e9..b85677b8 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -4,8 +4,10 @@ import ( "bytes" "crypto/md5" "errors" + "fmt" "io" "io/ioutil" + "log" "os" "path/filepath" "strconv" @@ -25,6 +27,8 @@ import ( "golang.org/x/text/transform" ) +const backup_time = 8000 + var ( OpenBuffers []*Buffer LogBuf *Buffer @@ -109,6 +113,10 @@ type Buffer struct { CurSuggestion int Messages []*Message + + // counts the number of edits + // resets every backup_time edits + lastbackup time.Time } // NewBufferFromFile opens a new buffer using the given path @@ -191,9 +199,36 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT } if !found { + choice := 1 // ignore by default + b.SharedBuffer = new(SharedBuffer) b.Type = btype - b.LineArray = NewLineArray(uint64(size), FFAuto, reader) + + if b.Settings["backup"].(bool) { + backupfile := config.ConfigDir + "/backups/" + EscapePath(absPath) + if info, err := os.Stat(backupfile); err == nil { + backup, err := os.Open(backupfile) + if err == nil { + t := info.ModTime() + msg := fmt.Sprintf(backupMsg, t.Format("Mon Jan _2 15:04 2006")) + choice = screen.TermPrompt(msg, []string{"r", "i", "recover", "ignore"}, true) + log.Println("Choice:", choice) + + if choice%2 == 0 { + // recover + b.LineArray = NewLineArray(uint64(size), FFAuto, backup) + } else if choice%2 == 1 { + // delete + os.Remove(backupfile) + } + backup.Close() + } + } + } + + if choice > 0 { + b.LineArray = NewLineArray(uint64(size), FFAuto, reader) + } b.EventHandler = NewEventHandler(b.SharedBuffer, b.cursors) } @@ -273,6 +308,7 @@ func (b *Buffer) Fini() { if !b.Modified() { b.Serialize() } + b.RemoveBackup() } // GetName returns the name that should be displayed in the statusline @@ -297,6 +333,8 @@ func (b *Buffer) Insert(start Loc, text string) { b.EventHandler.cursors = b.cursors b.EventHandler.active = b.curCursor b.EventHandler.Insert(start, text) + + go b.Backup() } } @@ -305,6 +343,8 @@ func (b *Buffer) Remove(start, end Loc) { b.EventHandler.cursors = b.cursors b.EventHandler.active = b.curCursor b.EventHandler.Remove(start, end) + + go b.Backup() } } diff --git a/internal/screen/message.go b/internal/screen/message.go index cd4abce9..1ca9141c 100644 --- a/internal/screen/message.go +++ b/internal/screen/message.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "strconv" + "strings" ) // TermMessage sends a message to the user in the terminal. This usually occurs before @@ -25,6 +26,38 @@ func TermMessage(msg ...interface{}) { TempStart(screenb) } +// TermPrompt prints a prompt and requests the user for a response +// The result is matched against a list of options and the index of +// the match is returned +// If wait is true, the prompt re-prompts until a valid option is +// chosen, otherwise if wait is false, -1 is returned for no match +func TermPrompt(prompt string, options []string, wait bool) int { + screenb := TempFini() + + idx := -1 + // same behavior as do { ... } while (wait && idx == -1) + for ok := true; ok; ok = wait && idx == -1 { + reader := bufio.NewReader(os.Stdin) + fmt.Print(prompt) + resp, _ := reader.ReadString('\n') + resp = strings.TrimSpace(resp) + + for i, opt := range options { + if resp == opt { + idx = i + } + } + + if wait && idx == -1 { + fmt.Println("\nInvalid choice.") + } + } + + TempStart(screenb) + + return idx +} + // TermError sends an error to the user in the terminal. Like TermMessage except formatted // as an error func TermError(filename string, lineNum int, err string) {