]> git.lizzy.rs Git - micro.git/blobdiff - internal/buffer/buffer.go
Use abspath for local glob settings
[micro.git] / internal / buffer / buffer.go
index 43b02dc4de6526d199a0982b22884b1e46b846ae..f33b24389b225425bdcf2b61503cae3e4b1e79db 100644 (file)
@@ -14,6 +14,7 @@ import (
        "strconv"
        "strings"
        "sync"
+       "sync/atomic"
        "time"
 
        luar "layeh.com/gopher-luar"
@@ -102,9 +103,7 @@ type SharedBuffer struct {
        diffLock          sync.RWMutex
        diff              map[int]DiffStatus
 
-       // counts the number of edits
-       // resets every backupTime edits
-       lastbackup time.Time
+       requestedBackup bool
 
        // ReloadDisabled allows the user to disable reloads if they
        // are viewing a file that is constantly changing
@@ -147,18 +146,20 @@ func (b *SharedBuffer) remove(start, end Loc) []byte {
 func (b *SharedBuffer) MarkModified(start, end int) {
        b.ModifiedThisFrame = true
 
-       if !b.Settings["syntax"].(bool) || b.SyntaxDef == nil {
-               return
-       }
-
        start = util.Clamp(start, 0, len(b.lines)-1)
        end = util.Clamp(end, 0, len(b.lines)-1)
 
-       l := -1
+       if b.Settings["syntax"].(bool) && b.SyntaxDef != nil {
+               l := -1
+               for i := start; i <= end; i++ {
+                       l = util.Max(b.Highlighter.ReHighlightStates(b, i), l)
+               }
+               b.Highlighter.HighlightMatches(b, start, l)
+       }
+
        for i := start; i <= end; i++ {
-               l = util.Max(b.Highlighter.ReHighlightStates(b, i), l)
+               b.LineArray.invalidateSearchMatches(i)
        }
-       b.Highlighter.HighlightMatches(b, start, l)
 }
 
 // DisableReload disables future reloads of this sharedbuffer
@@ -182,13 +183,34 @@ type DiffStatus byte
 // The syntax highlighting info must be stored with the buffer because the syntax
 // highlighter attaches information to each line of the buffer for optimization
 // purposes so it doesn't have to rehighlight everything on every update.
+// Likewise for the search highlighting.
 type Buffer struct {
        *EventHandler
        *SharedBuffer
 
+       fini        int32
        cursors     []*Cursor
        curCursor   int
        StartCursor Loc
+
+       // OptionCallback is called after a buffer option value is changed.
+       // The display module registers its OptionCallback to ensure the buffer window
+       // is properly updated when needed. This is a workaround for the fact that
+       // the buffer module cannot directly call the display's API (it would mean
+       // a circular dependency between packages).
+       OptionCallback func(option string, nativeValue interface{})
+
+       // The display module registers its own GetVisualX function for getting
+       // the correct visual x location of a cursor when softwrap is used.
+       // This is hacky. Maybe it would be better to move all the visual x logic
+       // from buffer to display, but it would require rewriting a lot of code.
+       GetVisualX func(loc Loc) int
+
+       // Last search stores the last successful search
+       LastSearch      string
+       LastSearchRegex bool
+       // HighlightSearch enables highlighting all instances of the last successful search
+       HighlightSearch bool
 }
 
 // NewBufferFromFileAtLoc opens a new buffer with a given cursor location
@@ -212,21 +234,39 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer,
                return nil, err
        }
 
-       file, err := os.Open(filename)
-       fileInfo, _ := os.Stat(filename)
+       f, err := os.OpenFile(filename, os.O_WRONLY, 0)
+       readonly := os.IsPermission(err)
+       f.Close()
 
-       if err == nil && fileInfo.IsDir() {
+       fileInfo, serr := os.Stat(filename)
+       if serr != nil && !os.IsNotExist(serr) {
+               return nil, serr
+       }
+       if serr == nil && fileInfo.IsDir() {
                return nil, errors.New("Error: " + filename + " is a directory and cannot be opened")
        }
 
-       defer file.Close()
+       file, err := os.Open(filename)
+       if err == nil {
+               defer file.Close()
+       }
 
        var buf *Buffer
-       if err != nil {
+       if os.IsNotExist(err) {
                // File does not exist -- create an empty buffer with that name
                buf = NewBufferFromString("", filename, btype)
+       } else if err != nil {
+               return nil, err
        } else {
                buf = NewBuffer(file, util.FSize(file), filename, cursorLoc, btype)
+               if buf == nil {
+                       return nil, errors.New("could not open file")
+               }
+       }
+
+       if readonly && prompt != nil {
+               prompt.Message(fmt.Sprintf("Warning: file is readonly - %s will be attempted when saving", config.GlobalSettings["sucmd"].(string)))
+               // buf.SetOptionNative("readonly", true)
        }
 
        return buf, nil
@@ -256,7 +296,10 @@ func NewBufferFromString(text, path string, btype BufType) *Buffer {
 // Places the cursor at startcursor. If startcursor is -1, -1 places the
 // cursor at an autodetected location (based on savecursor or :LINE:COL)
 func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufType) *Buffer {
-       absPath, _ := filepath.Abs(path)
+       absPath, err := filepath.Abs(path)
+       if err != nil {
+               absPath = path
+       }
 
        b := new(Buffer)
 
@@ -271,6 +314,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
                }
        }
 
+       hasBackup := false
        if !found {
                b.SharedBuffer = new(SharedBuffer)
                b.Type = btype
@@ -278,23 +322,38 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
                b.AbsPath = absPath
                b.Path = path
 
+               // this is a little messy since we need to know some settings to read
+               // the file properly, but some settings depend on the filetype, which
+               // we don't know until reading the file. We first read the settings
+               // into a local variable and then use that to determine the encoding,
+               // readonly, and fileformat necessary for reading the file and
+               // assigning the filetype.
+               settings := config.DefaultCommonSettings()
                b.Settings = config.DefaultCommonSettings()
                for k, v := range config.GlobalSettings {
                        if _, ok := config.DefaultGlobalOnlySettings[k]; !ok {
                                // make sure setting is not global-only
+                               settings[k] = v
                                b.Settings[k] = v
                        }
                }
-               config.InitLocalSettings(b.Settings, path)
+               config.InitLocalSettings(settings, absPath)
+               b.Settings["readonly"] = settings["readonly"]
+               b.Settings["filetype"] = settings["filetype"]
+               b.Settings["syntax"] = settings["syntax"]
 
-               enc, err := htmlindex.Get(b.Settings["encoding"].(string))
+               enc, err := htmlindex.Get(settings["encoding"].(string))
                if err != nil {
                        enc = unicode.UTF8
                        b.Settings["encoding"] = "utf-8"
                }
 
-               hasBackup := b.ApplyBackup(size)
+               var ok bool
+               hasBackup, ok = b.ApplyBackup(size)
 
+               if !ok {
+                       return NewBufferFromString("", "", btype)
+               }
                if !hasBackup {
                        reader := bufio.NewReader(transform.NewReader(r, enc.NewDecoder()))
 
@@ -303,7 +362,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
                        if size == 0 {
                                // for empty files, use the fileformat setting instead of
                                // autodetection
-                               switch b.Settings["fileformat"] {
+                               switch settings["fileformat"] {
                                case "unix":
                                        ff = FFUnix
                                case "dos":
@@ -340,12 +399,10 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
 
        if startcursor.X != -1 && startcursor.Y != -1 {
                b.StartCursor = startcursor
-       } else {
-               if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
-                       err := b.Unserialize()
-                       if err != nil {
-                               screen.TermMessage(err)
-                       }
+       } else if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
+               err := b.Unserialize()
+               if err != nil {
+                       screen.TermMessage(err)
                }
        }
 
@@ -356,7 +413,9 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
                if size > LargeFileThreshold {
                        // If the file is larger than LargeFileThreshold fastdirty needs to be on
                        b.Settings["fastdirty"] = true
-               } else {
+               } else if !hasBackup {
+                       // since applying a backup does not save the applied backup to disk, we should
+                       // not calculate the original hash based on the backup data
                        calcHash(b, &b.origHash)
                }
        }
@@ -395,6 +454,8 @@ func (b *Buffer) Fini() {
        if b.Type == BTStdout {
                fmt.Fprint(util.Stdout, string(b.Bytes()))
        }
+
+       atomic.StoreInt32(&(b.fini), int32(1))
 }
 
 // GetName returns the name that should be displayed in the statusline
@@ -425,7 +486,7 @@ func (b *Buffer) Insert(start Loc, text string) {
                b.EventHandler.active = b.curCursor
                b.EventHandler.Insert(start, text)
 
-               go b.Backup(true)
+               b.RequestBackup()
        }
 }
 
@@ -436,7 +497,7 @@ func (b *Buffer) Remove(start, end Loc) {
                b.EventHandler.active = b.curCursor
                b.EventHandler.Remove(start, end)
 
-               go b.Backup(true)
+               b.RequestBackup()
        }
 }
 
@@ -506,16 +567,38 @@ func (b *Buffer) RuneAt(loc Loc) rune {
                for len(line) > 0 {
                        r, _, size := util.DecodeCharacter(line)
                        line = line[size:]
-                       i++
 
                        if i == loc.X {
                                return r
                        }
+
+                       i++
                }
        }
        return '\n'
 }
 
+// WordAt returns the word around a given location in the buffer
+func (b *Buffer) WordAt(loc Loc) []byte {
+       if len(b.LineBytes(loc.Y)) == 0 || !util.IsWordChar(b.RuneAt(loc)) {
+               return []byte{}
+       }
+
+       start := loc
+       end := loc.Move(1, b)
+
+       for start.X > 0 && util.IsWordChar(b.RuneAt(start.Move(-1, b))) {
+               start.X--
+       }
+
+       lineLen := util.CharacterCount(b.LineBytes(loc.Y))
+       for end.X < lineLen && util.IsWordChar(b.RuneAt(end)) {
+               end.X++
+       }
+
+       return b.Substr(start, end)
+}
+
 // Modified returns if this buffer has been modified since
 // being opened
 func (b *Buffer) Modified() bool {
@@ -533,6 +616,22 @@ func (b *Buffer) Modified() bool {
        return buff != b.origHash
 }
 
+// Size returns the number of bytes in the current buffer
+func (b *Buffer) Size() int {
+       nb := 0
+       for i := 0; i < b.LinesNum(); i++ {
+               nb += len(b.LineBytes(i))
+
+               if i != b.LinesNum()-1 {
+                       if b.Endings == FFDos {
+                               nb++ // carriage return
+                       }
+                       nb++ // newline
+               }
+       }
+       return nb
+}
+
 // calcHash calculates md5 hash of all lines in the buffer
 func calcHash(b *Buffer, out *[md5.Size]byte) error {
        h := md5.New()
@@ -589,6 +688,9 @@ func (b *Buffer) UpdateRules() {
                }
 
                header, err = highlight.MakeHeaderYaml(data)
+               if err != nil {
+                       screen.TermMessage("Error parsing header for syntax file " + f.Name() + ": " + err.Error())
+               }
                file, err := highlight.ParseFile(data)
                if err != nil {
                        screen.TermMessage("Error parsing syntax file " + f.Name() + ": " + err.Error())
@@ -837,19 +939,18 @@ func (b *Buffer) MoveLinesUp(start int, end int) {
        }
        l := string(b.LineBytes(start - 1))
        if end == len(b.lines) {
-               b.Insert(
+               b.insert(
                        Loc{
                                util.CharacterCount(b.lines[end-1].data),
                                end - 1,
                        },
-                       "\n"+l,
-               )
-       } else {
-               b.Insert(
-                       Loc{0, end},
-                       l+"\n",
+                       []byte{'\n'},
                )
        }
+       b.Insert(
+               Loc{0, end},
+               l+"\n",
+       )
        b.Remove(
                Loc{0, start - 1},
                Loc{0, start},
@@ -858,7 +959,7 @@ func (b *Buffer) MoveLinesUp(start int, end int) {
 
 // MoveLinesDown moves the range of lines down one row
 func (b *Buffer) MoveLinesDown(start int, end int) {
-       if start < 0 || start >= end || end >= len(b.lines)-1 {
+       if start < 0 || start >= end || end >= len(b.lines) {
                return
        }
        l := string(b.LineBytes(end))
@@ -965,9 +1066,9 @@ func (b *Buffer) Retab() {
                ws := util.GetLeadingWhitespace(l)
                if len(ws) != 0 {
                        if toSpaces {
-                               ws = bytes.Replace(ws, []byte{'\t'}, bytes.Repeat([]byte{' '}, tabsize), -1)
+                               ws = bytes.ReplaceAll(ws, []byte{'\t'}, bytes.Repeat([]byte{' '}, tabsize))
                        } else {
-                               ws = bytes.Replace(ws, bytes.Repeat([]byte{' '}, tabsize), []byte{'\t'}, -1)
+                               ws = bytes.ReplaceAll(ws, bytes.Repeat([]byte{' '}, tabsize), []byte{'\t'})
                        }
                }
 
@@ -1110,6 +1211,12 @@ func (b *Buffer) DiffStatus(lineN int) DiffStatus {
        return b.diff[lineN]
 }
 
+// SearchMatch returns true if the given location is within a match of the last search.
+// It is used for search highlighting
+func (b *Buffer) SearchMatch(pos Loc) bool {
+       return b.LineArray.SearchMatch(b, pos)
+}
+
 // WriteLog writes a string to the log buffer
 func WriteLog(s string) {
        LogBuf.EventHandler.Insert(LogBuf.End(), s)