]> git.lizzy.rs Git - micro.git/blobdiff - cmd/micro/buffer.go
Merge pull request #507 from NicolaiSoeborg/master
[micro.git] / cmd / micro / buffer.go
index 0879c0ec1f39c6b27c06b47d26b380c9a4fdc037..fdcc37ad2d43cf3f8e71773c43d749b174f48165 100644 (file)
 package main
 
 import (
-       "crypto/md5"
-       "github.com/vinzmay/go-rope"
+       "bytes"
+       "encoding/gob"
+       "io"
        "io/ioutil"
+       "os"
+       "os/exec"
+       "os/signal"
+       "path/filepath"
+       "regexp"
+       "strconv"
        "strings"
+       "time"
+       "unicode/utf8"
+
+       "github.com/mitchellh/go-homedir"
 )
 
 // Buffer stores the text for files that are loaded into the text editor
 // It uses a rope to efficiently store the string and contains some
 // simple functions for saving and wrapper functions for modifying the rope
 type Buffer struct {
-       // Stores the text of the buffer
-       r *rope.Rope
+       // The eventhandler for undo/redo
+       *EventHandler
+       // This stores all the text in the buffer as an array of lines
+       *LineArray
+
+       Cursor Cursor
 
        // Path to the file on disk
-       path string
+       Path string
+       // Absolute path to the file on disk
+       AbsPath string
        // Name of the buffer on the status line
        name string
 
-       // This is the text stored every time the buffer is saved to check if the buffer is modified
-       savedText           [16]byte
-       netInsertions       int
-       dirtySinceLastCheck bool
+       // Whether or not the buffer has been modified since it was opened
+       IsModified bool
+
+       // Stores the last modification time of the file the buffer is pointing to
+       ModTime time.Time
 
-       // Provide efficient and easy access to text and lines so the rope String does not
-       // need to be constantly recalculated
-       // These variables are updated in the update() function
-       text  string
-       lines []string
+       NumLines int
 
        // Syntax highlighting rules
        rules []SyntaxRule
-       // The buffer's filetype
-       filetype string
+
+       // Buffer local settings
+       Settings map[string]interface{}
+}
+
+// The SerializedBuffer holds the types that get serialized when a buffer is saved
+// These are used for the savecursor and saveundo options
+type SerializedBuffer struct {
+       EventHandler *EventHandler
+       Cursor       Cursor
+       ModTime      time.Time
 }
 
-// NewBuffer creates a new buffer from `txt` with path and name `path`
-func NewBuffer(txt, path string) *Buffer {
+func NewBufferFromString(text, path string) *Buffer {
+       return NewBuffer(strings.NewReader(text), path)
+}
+
+// NewBuffer creates a new buffer from a given reader with a given path
+func NewBuffer(reader io.Reader, path string) *Buffer {
+       if path != "" {
+               for _, tab := range tabs {
+                       for _, view := range tab.views {
+                               if view.Buf.Path == path {
+                                       return view.Buf
+                               }
+                       }
+               }
+       }
+
        b := new(Buffer)
-       if txt == "" {
-               b.r = new(rope.Rope)
-       } else {
-               b.r = rope.New(txt)
+       b.LineArray = NewLineArray(reader)
+
+       b.Settings = DefaultLocalSettings()
+       for k, v := range globalSettings {
+               if _, ok := b.Settings[k]; ok {
+                       b.Settings[k] = v
+               }
        }
-       b.path = path
-       b.name = path
-       b.savedText = md5.Sum([]byte(txt))
+
+       absPath, _ := filepath.Abs(path)
+
+       b.Path = path
+       b.AbsPath = absPath
+
+       // The last time this file was modified
+       b.ModTime, _ = GetModTime(b.Path)
+
+       b.EventHandler = NewEventHandler(b)
 
        b.Update()
+       b.FindFileType()
        b.UpdateRules()
 
+       if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
+               os.Mkdir(configDir+"/buffers/", os.ModePerm)
+       }
+
+       // 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 {
+               positions := strings.Split(*flagStartPos, ",")
+               if len(positions) == 2 {
+                       lineNum, errPos1 := strconv.Atoi(positions[0])
+                       colNum, errPos2 := strconv.Atoi(positions[1])
+                       if errPos1 == nil && errPos2 == nil {
+                               cursorStartX = colNum
+                               cursorStartY = lineNum - 1
+                               // Check to avoid line overflow
+                               if cursorStartY > b.NumLines {
+                                       cursorStartY = b.NumLines - 1
+                               } else if cursorStartY < 0 {
+                                       cursorStartY = 0
+                               }
+                               // Check to avoid column overflow
+                               if cursorStartX > len(b.Line(cursorStartY)) {
+                                       cursorStartX = len(b.Line(cursorStartY))
+                               } else if cursorStartX < 0 {
+                                       cursorStartX = 0
+                               }
+                       }
+               }
+       }
+       b.Cursor = Cursor{
+               Loc: Loc{
+                       X: cursorStartX,
+                       Y: cursorStartY,
+               },
+               buf: b,
+       }
+
+       InitLocalSettings(b)
+
+       if 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))
+               if err == nil {
+                       var buffer SerializedBuffer
+                       decoder := gob.NewDecoder(file)
+                       gob.Register(TextEvent{})
+                       err = decoder.Decode(&buffer)
+                       if err != nil {
+                               TermMessage(err.Error(), "\n", "You may want to remove the files in ~/.config/micro/buffers (these files store the information for the 'saveundo' and 'savecursor' options) if this problem persists.")
+                       }
+                       if b.Settings["savecursor"].(bool) {
+                               b.Cursor = buffer.Cursor
+                               b.Cursor.buf = b
+                               b.Cursor.Relocate()
+                       }
+
+                       if b.Settings["saveundo"].(bool) {
+                               // We should only use last time's eventhandler if the file wasn't by someone else in the meantime
+                               if b.ModTime == buffer.ModTime {
+                                       b.EventHandler = buffer.EventHandler
+                                       b.EventHandler.buf = b
+                               }
+                       }
+               }
+               file.Close()
+       }
+
        return b
 }
 
+func (b *Buffer) GetName() string {
+       if b.name == "" {
+               if b.Path == "" {
+                       return "No name"
+               }
+               return b.Path
+       }
+       return b.name
+}
+
 // UpdateRules updates the syntax rules and filetype for this buffer
 // This is called when the colorscheme changes
 func (b *Buffer) UpdateRules() {
-       b.rules, b.filetype = GetRules(b)
+       b.rules = GetRules(b)
+}
+
+// FindFileType identifies this buffer's filetype based on the extension or header
+func (b *Buffer) FindFileType() {
+       b.Settings["filetype"] = FindFileType(b)
+}
+
+// FileType returns the buffer's filetype
+func (b *Buffer) FileType() string {
+       return b.Settings["filetype"].(string)
+}
+
+// IndentString returns a string representing one level of indentation
+func (b *Buffer) IndentString() string {
+       if b.Settings["tabstospaces"].(bool) {
+               return Spaces(int(b.Settings["tabsize"].(float64)))
+       }
+       return "\t"
+}
+
+// CheckModTime makes sure that the file this buffer points to hasn't been updated
+// by an external program since it was last read
+// If it has, we ask the user if they would like to reload the file
+func (b *Buffer) CheckModTime() {
+       modTime, ok := GetModTime(b.Path)
+       if ok {
+               if modTime != b.ModTime {
+                       choice, canceled := messenger.YesNoPrompt("The file has changed since it was last read. Reload file? (y,n)")
+                       messenger.Reset()
+                       messenger.Clear()
+                       if !choice || canceled {
+                               // Don't load new changes -- do nothing
+                               b.ModTime, _ = GetModTime(b.Path)
+                       } else {
+                               // Load new changes
+                               b.ReOpen()
+                       }
+               }
+       }
+}
+
+// ReOpen reloads the current buffer from disk
+func (b *Buffer) ReOpen() {
+       data, err := ioutil.ReadFile(b.Path)
+       txt := string(data)
+
+       if err != nil {
+               messenger.Error(err.Error())
+               return
+       }
+       b.EventHandler.ApplyDiff(txt)
+
+       b.ModTime, _ = GetModTime(b.Path)
+       b.IsModified = false
+       b.Update()
+       b.Cursor.Relocate()
 }
 
 // Update fetches the string from the rope and updates the `text` and `lines` in the buffer
 func (b *Buffer) Update() {
-       if b.r.Len() == 0 {
-               b.text = ""
-       } else {
-               b.text = b.r.String()
-       }
-       b.lines = strings.Split(b.text, "\n")
+       b.NumLines = len(b.lines)
 }
 
 // Save saves the buffer to its default path
 func (b *Buffer) Save() error {
-       return b.SaveAs(b.path)
+       return b.SaveAs(b.Path)
+}
+
+// SaveWithSudo saves the buffer to the default path with sudo
+func (b *Buffer) SaveWithSudo() error {
+       return b.SaveAsWithSudo(b.Path)
+}
+
+// Serialize serializes the buffer to configDir/buffers
+func (b *Buffer) Serialize() error {
+       if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
+               file, err := os.Create(configDir + "/buffers/" + EscapePath(b.AbsPath))
+               if err == nil {
+                       enc := gob.NewEncoder(file)
+                       gob.Register(TextEvent{})
+                       err = enc.Encode(SerializedBuffer{
+                               b.EventHandler,
+                               b.Cursor,
+                               b.ModTime,
+                       })
+               }
+               file.Close()
+               return err
+       }
+       return nil
 }
 
 // SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
 func (b *Buffer) SaveAs(filename string) error {
+       b.FindFileType()
        b.UpdateRules()
-       data := []byte(b.text)
+       dir, _ := homedir.Dir()
+       b.Path = strings.Replace(filename, "~", dir, 1)
+       if b.Settings["rmtrailingws"].(bool) {
+               r, _ := regexp.Compile(`[ \t]+$`)
+               for lineNum, line := range b.Lines(0, b.NumLines) {
+                       indices := r.FindStringIndex(line)
+                       if indices == nil {
+                               continue
+                       }
+                       startLoc := Loc{indices[0], lineNum}
+                       b.deleteToEnd(startLoc)
+               }
+               b.Cursor.Relocate()
+       }
+       if b.Settings["eofnewline"].(bool) {
+               end := b.End()
+               if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
+                       b.Insert(end, "\n")
+               }
+       }
+       str := b.String()
+       data := []byte(str)
        err := ioutil.WriteFile(filename, data, 0644)
        if err == nil {
-               b.savedText = md5.Sum(data)
-               b.netInsertions = 0
+               b.IsModified = false
+               b.ModTime, _ = GetModTime(filename)
+               return b.Serialize()
        }
+       b.ModTime, _ = GetModTime(filename)
        return err
 }
 
-// IsDirty returns whether or not the buffer has been modified compared to the one on disk
-func (b *Buffer) IsDirty() bool {
-       if !b.dirtySinceLastCheck {
-               return false
+// SaveAsWithSudo is the same as SaveAs except it uses a neat trick
+// with tee to use sudo so the user doesn't have to reopen micro with sudo
+func (b *Buffer) SaveAsWithSudo(filename string) error {
+       b.FindFileType()
+       b.UpdateRules()
+       b.Path = filename
+
+       // The user may have already used sudo in which case we won't need the password
+       // It's a bit nicer for them if they don't have to enter the password every time
+       _, err := RunShellCommand("sudo -v")
+       needPassword := err != nil
+
+       // If we need the password, we have to close the screen and ask using the shell
+       if needPassword {
+               // Shut down the screen because we're going to interact directly with the shell
+               screen.Fini()
+               screen = nil
        }
-       if b.netInsertions == 0 {
-               isDirty := b.savedText != md5.Sum([]byte(b.text))
-               b.dirtySinceLastCheck = isDirty
-               return isDirty
+
+       // Set up everything for the command
+       cmd := exec.Command("sudo", "tee", filename)
+       cmd.Stdin = bytes.NewBufferString(b.String())
+
+       // This is a trap for Ctrl-C so that it doesn't kill micro
+       // Instead we trap Ctrl-C to kill the program we're running
+       c := make(chan os.Signal, 1)
+       signal.Notify(c, os.Interrupt)
+       go func() {
+               for range c {
+                       cmd.Process.Kill()
+               }
+       }()
+
+       // Start the command
+       cmd.Start()
+       err = cmd.Wait()
+
+       // If we needed the password, we closed the screen, so we have to initialize it again
+       if needPassword {
+               // Start the screen back up
+               InitScreen()
        }
-       return true
+       if err == nil {
+               b.IsModified = false
+               b.ModTime, _ = GetModTime(filename)
+               b.Serialize()
+       }
+       return err
 }
 
-func (b *Buffer) Lines() []string {
-       return strings.Split(b.text, "\n")
+func (b *Buffer) insert(pos Loc, value []byte) {
+       b.IsModified = true
+       b.LineArray.insert(pos, value)
+       b.Update()
 }
-
-// Insert a string into the rope
-func (b *Buffer) Insert(idx int, value string) {
-       b.dirtySinceLastCheck = true
-       b.netInsertions += len(value)
-       b.r = b.r.Insert(idx, value)
+func (b *Buffer) remove(start, end Loc) string {
+       b.IsModified = true
+       sub := b.LineArray.remove(start, end)
        b.Update()
+       return sub
+}
+func (b *Buffer) deleteToEnd(start Loc) {
+       b.IsModified = true
+       b.LineArray.DeleteToEnd(start)
+       b.Update()
+}
+
+// Start returns the location of the first character in the buffer
+func (b *Buffer) Start() Loc {
+       return Loc{0, 0}
 }
 
-// Remove a slice of the rope from start to end (exclusive)
-// Returns the string that was removed
-func (b *Buffer) Remove(start, end int) string {
-       b.dirtySinceLastCheck = true
-       b.netInsertions -= end - start
-       if start < 0 {
-               start = 0
+// End returns the location of the last character in the buffer
+func (b *Buffer) End() Loc {
+       return Loc{utf8.RuneCount(b.lines[b.NumLines-1]), b.NumLines - 1}
+}
+
+// RuneAt returns the rune at a given location in the buffer
+func (b *Buffer) RuneAt(loc Loc) rune {
+       line := []rune(b.Line(loc.Y))
+       if len(line) > 0 {
+               return line[loc.X]
        }
-       if end > b.Len() {
-               end = b.Len()
+       return '\n'
+}
+
+// Line returns a single line
+func (b *Buffer) Line(n int) string {
+       if n >= len(b.lines) {
+               return ""
        }
-       removed := b.text[start:end]
-       // The rope implenentation I am using wants indicies starting at 1 instead of 0
-       start++
-       end++
-       b.r = b.r.Delete(start, end-start)
-       b.Update()
-       return removed
+       return string(b.lines[n])
+}
+
+// Lines returns an array of strings containing the lines from start to end
+func (b *Buffer) Lines(start, end int) []string {
+       lines := b.lines[start:end]
+       var slice []string
+       for _, line := range lines {
+               slice = append(slice, string(line))
+       }
+       return slice
 }
 
 // Len gives the length of the buffer
 func (b *Buffer) Len() int {
-       return b.r.Len()
+       return Count(b.String())
+}
+
+// MoveLinesUp moves the range of lines up one row
+func (b *Buffer) MoveLinesUp(start int, end int) {
+       // 0 < start < end <= len(b.lines)
+       if start < 1 || start >= end || end > len(b.lines) {
+               return // what to do? FIXME
+       }
+       if end == len(b.lines) {
+               b.Insert(
+                       Loc{
+                               utf8.RuneCount(b.lines[end-1]),
+                               end - 1,
+                       },
+                       "\n"+b.Line(start-1),
+               )
+       } else {
+               b.Insert(
+                       Loc{0, end},
+                       b.Line(start-1)+"\n",
+               )
+       }
+       b.Remove(
+               Loc{0, start - 1},
+               Loc{0, start},
+       )
+}
+
+// MoveLinesDown moves the range of lines down one row
+func (b *Buffer) MoveLinesDown(start int, end int) {
+       // 0 <= start < end < len(b.lines)
+       // if end == len(b.lines), we can't do anything here because the
+       // last line is unaccessible, FIXME
+       if start < 0 || start >= end || end >= len(b.lines)-1 {
+               return // what to do? FIXME
+       }
+       b.Insert(
+               Loc{0, start},
+               b.Line(end)+"\n",
+       )
+       end++
+       b.Remove(
+               Loc{0, end},
+               Loc{0, end + 1},
+       )
 }